Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a6ccdcc
feat(desktop): ship GUI plugins through ./gui-experimental exports
Hona Oct 26, 2026
1cdb39a
docs(plugin): sync the GUI page with the SDK after the beta merge
Hona Oct 26, 2026
0aaad58
fix(app): reject a plugin's own main calls once the plugin goes away
Hona Oct 26, 2026
8b9f112
feat(plugin): add composer pills for GUI plugins
Hona Oct 26, 2026
6718850
fix(desktop): restore non-ASCII text the rebase mangled in built-in p…
Hona Oct 26, 2026
f4bcefe
test(app): read the browser's own + menu item in the pane restore story
Hona Oct 26, 2026
c05dbfd
test(desktop): close plugin storage state stores before their databases
Hona Oct 26, 2026
6a106ae
docs: keep the Build intro to extend, run and embed
Hona Oct 26, 2026
fb22f08
fix(app): name the TUI plugin's id in the Settings > Plugins cli.json…
Hona Oct 26, 2026
73d8b2f
refactor(plugin): keep Layout.command and the workspace slot built-in…
Hona Oct 26, 2026
8d6023b
fix(desktop): settle the remaining GUI plugin review findings
Hona Oct 26, 2026
4713f57
refactor(plugin): name a main server endpoint's same-machine flag sam…
Hona Oct 26, 2026
f7746ff
fix(core): keep TUI-only plugins out of the server's plugin list
Hona Oct 26, 2026
e85beae
fix(plugin): treat a local plugin's unreadable package.json as missing
Hona Oct 26, 2026
8d8b8f1
chore(sdk): regenerate client types
Hona Oct 26, 2026
0068402
docs: shorten the desktop plugins section
Hona Oct 26, 2026
abc70ca
refactor(app): finish renaming GUI extensions to plugins
Hona Oct 26, 2026
33a3670
chore(plugin): match desktop's Electron version
Hona Oct 26, 2026
ba8cd61
docs(plugin): correct GUI and plugin docs against the code
Hona Oct 26, 2026
8211530
fix(desktop): show a plugin's cleared failure at once
Hona Oct 26, 2026
cbaf2e4
fix(app): keep a failed window plugin stopped until Reload
Hona Oct 26, 2026
659754b
docs(plugin): correct GUI SDK TSDoc against the hosts
Hona Oct 26, 2026
7b61de8
refactor(plugin): drop the unused server endpoint username
Hona Oct 26, 2026
46e2f18
fix(app): keep migrating the summary.toggle keybind
Hona Oct 26, 2026
33998fb
fix(stories): key built-in plugin stories by folder
Hona Oct 26, 2026
57c9b96
docs(plugin): use a public Ipc in SDK examples
Hona Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Typecheck (TypeScript 7.1)
run: bun run typecheck:tsc

# Every GUI package file (app, desktop, gui-extensions, ui, session-ui) a pull request adds or edits must be free of
# Every GUI package file (app, desktop, gui-plugins, the plugin package's GUI SDK, ui, session-ui) a pull request adds or edits must be free of
# oxlint problems, warn-level rules (anti-slop) included. Other packages are not affected.
- name: Lint changed files
if: github.event_name == 'pull_request'
Expand Down
2 changes: 1 addition & 1 deletion .opencode/skills/app-e2e-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Most of these rarely reproduce on an idle local machine.
- **Use the harness's signals.** `holdRoute(...).arrived` resolves when the request reaches the route, and `transport.send` resolves after delivery. `setupTimeline(...).waitForPart` resolves once a part renders (`timeline.ts:328`).
- **Decide which side is wrong.** Three of eight investigated e2e flakes were product bugs, each since fixed in the product:
- a stale scroll offset restored when a scroll settles (`src/session/timeline/observe-element-offset.ts`);
- a video double-click that also entered native fullscreen (`packages/gui-extensions/src/file/artifact-view.tsx:617`);
- a video double-click that also entered native fullscreen (`packages/gui-plugins/src/file/artifact-view.tsx:617`);
- shortcuts dropped while a dialog closed.

If a user could hit the interleaving, fix the product and keep the assertion.
Expand Down
10 changes: 5 additions & 5 deletions .opencode/skills/component-tests/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: component-tests
description: Use when writing, changing, debugging, or reviewing the Playwright component tests that mount stories. That covers specs in packages/app/component-tests or packages/session-ui/component-tests, `*.fixture.tsx` files there or beside source (such as packages/gui-extensions/src/browser/panel.fixture.tsx), the stories they mount, and the shared harness in packages/stories (playwright/config.ts, playwright/story.ts, vite.config.ts, src/frame.tsx, src/index-plugin.ts, mocks). Also use when a component test is flaky, passed on retry, or fails with "Execution context was destroyed", "Failed to fetch dynamically imported module", or `#story-root` hidden. Covers mount args, source modules imported by URL, Vite dependency pre-bundling and reloads, a cold vs warm story server, mobile drawers, geometry, and checks before pushing.
description: Use when writing, changing, debugging, or reviewing the Playwright component tests that mount stories. That covers specs in packages/app/component-tests or packages/session-ui/component-tests, `*.fixture.tsx` files there or beside source (such as packages/gui-plugins/src/browser/panel.fixture.tsx), the stories they mount, and the shared harness in packages/stories (playwright/config.ts, playwright/story.ts, vite.config.ts, src/frame.tsx, src/index-plugin.ts, mocks). Also use when a component test is flaky, passed on retry, or fails with "Execution context was destroyed", "Failed to fetch dynamically imported module", or `#story-root` hidden. Covers mount args, source modules imported by URL, Vite dependency pre-bundling and reloads, a cold vs warm story server, mobile drawers, geometry, and checks before pushing.
---

# Component tests
Expand All @@ -15,7 +15,7 @@ Read these first rather than repeating them:

## Stories and `mount`

- **Location.** Put the story next to the production component under `src/` of `ui`, `session-ui`, `app` or `gui-extensions`. The story server indexes `*.stories.tsx` files in those four trees (`packages/stories/vite.config.ts`, `src/index-plugin.ts`), reading each file's `export default { title, id }` and its `export const` stories from source.
- **Location.** Put the story next to the production component under `src/` of `ui`, `session-ui`, `app` or `gui-plugins`. The story server indexes `*.stories.tsx` files in those four trees (`packages/stories/vite.config.ts`, `src/index-plugin.ts`), reading each file's `export default { title, id }` and its `export const` stories from source.
- **Story ID.** It is the meta `id` if one is set (`packages/session-ui/src/components/session-review.stories.tsx:25`). Otherwise it is the kebab-cased `title`, then `--`, then the kebab-cased export. Check it at `http://127.0.0.1:6006/?id=<id>` on a running story server (`bun --cwd packages/stories dev`). A wrong ID renders nothing (the page throws `No story has the id …`), so `mount` waits out its full 30 s root wait (`story.ts:39`) and then fails.
- **URL args.** `mount(id, { args, globals })` writes them into the URL as `key:value` pairs (`story.ts:20-36`). The frame reads every one: a value becomes a boolean or number when the story's own default for that key is one, `!true`/`!false`/`!null`/`!undefined` keep Storybook's meaning, and anything else stays text.
- Give every arg a typed default. With `args: { split: false }` (`session-review.stories.tsx:129`), `split:false` arrives as a boolean.
Expand All @@ -29,7 +29,7 @@ Read these first rather than repeating them:

Use these when the contract is not visible in a story, such as construction counts, unmount races or cache state.

- **Fixture file.** Put a `*.fixture.tsx` file in `component-tests/` or beside the source it drives (`packages/gui-extensions/src/browser/panel.fixture.tsx`). It exports a `mountX()` that renders into a host with a `data-testid` (`packages/session-ui/component-tests/basic-tool.fixture.tsx:7-46`). Never end the name in `.spec` or `.test`.
- **Fixture file.** Put a `*.fixture.tsx` file in `component-tests/` or beside the source it drives (`packages/gui-plugins/src/browser/panel.fixture.tsx`). It exports a `mountX()` that renders into a host with a `data-testid` (`packages/session-ui/component-tests/basic-tool.fixture.tsx:7-46`). Never end the name in `.spec` or `.test`.
- **Mount it in three steps.**
1. Mount a light story first, so that the app styles load.
2. Import the fixture and call it inside `page.evaluate`.
Expand All @@ -40,7 +40,7 @@ Use these when the contract is not visible in a story, such as construction coun
- **Module URLs.** Build each one with `sourceURL(new URL("<relative path with extension>", import.meta.url))` (`story.ts:10`), written as a string literal in the spec (`browser-pane-restore.spec.ts:6-24`). Any other spelling either fails to load or loads a second module instance whose contexts the page never provides; a hand-written `/@fs//...` is the documented second-instance case (`story.ts:6-9`). The literal also matters for pre-bundling (next section).
- **Aliases.** A module imported by URL is the real file. App code that reaches the same module through a `vite.config.ts` alias gets the mock (`browser-pane-restore.spec.ts:95-96`). Decide which of the two the contract needs.
- **Spec imports.** Import source modules into the spec with `import type` only (`browser-pane-restore.spec.ts:2-4`). A value import runs browser code in the Node runner.
- **Async order.** Let the test decide the order through deferreds the fixture exposes (`mountExtensionHost`, `packages/app/component-tests/extension-host.fixture.tsx:135`). Await the outcome they lead to (`extension-host.spec.ts:46-77`), or poll in-page state with `until` (`extension-host.fixture.tsx:124-132`). Do not copy the fixed in-page `setTimeout` waits that other cases in `extension-host.spec.ts` (`:38`, `:88-90`, `:108-119`) use to prove a negative.
- **Async order.** Let the test decide the order through deferreds the fixture exposes (`mountPluginHost`, `packages/app/component-tests/plugin-host.fixture.tsx:141`). Await the outcome they lead to (`plugin-host.spec.ts:38-69`), or poll in-page state with `until` (`plugin-host.fixture.tsx:131-138`). Do not copy the fixed in-page `setTimeout` waits that other cases in `plugin-host.spec.ts` (`:30`, `:80-82`, `:100-111`) use to prove a negative.

## Vite pre-bundling and reloads

Expand Down Expand Up @@ -102,7 +102,7 @@ From the owning package (`packages/session-ui` or `packages/app`):
- Do not skip on Windows, and do not raise `config.ts` timeouts, retries or the `mount` wait.
- If one test does genuinely expensive work, call `story.setTimeout(...)` in that test, with a comment that names the work.
6. **Confirm CI ran the component legs.** E2E never runs on pushes to dev (`check-test.yml:28`), so no post-merge run catches a regression.
- The `affected` gate filters on `@opencode/app` and `@opencode/stories` (`check-test.yml:60-61`). `ui`, `session-ui` and `gui-extensions` count through app's dependencies. `workflow_dispatch` forces the legs on (`check-test.yml:56-59`).
- The `affected` gate filters on `@opencode/app` and `@opencode/stories` (`check-test.yml:60-61`). `ui`, `session-ui` and `gui-plugins` count through app's dependencies. `workflow_dispatch` forces the legs on (`check-test.yml:56-59`).
- `check-test.yml` is listed in `globalDependencies` (`turbo.json:8`), so editing it marks every package affected.
- The required `e2e (linux)` and `e2e (windows)` checks also pass when no leg was needed (`check-test.yml:329-352`). Look for `e2e (<os>, session-ui-components)` and `e2e (<os>, app-components)`.
- If CI skipped them, report your local cold run in the PR.
Expand Down
2 changes: 1 addition & 1 deletion .opencode/skills/opencode-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ opencode debug heap-dump

It discovers the already-running service (never starts or replaces one), calls `POST /api/debug/heap-dump`, waits until the file is complete, and prints its server-local path. Pass `--server <url>` (with `OPENCODE_PASSWORD`) for a server started with `serve`, and `--directory <dir>` to write somewhere other than the server's log directory (the server creates it; a server that predates the option ignores it and the CLI warns). A 404 means the running server predates the endpoint.

For desktop memory, run **Capture heap snapshots** from the desktop command palette (`packages/gui-extensions/src/debug/main.ts`). It writes `~/Downloads/opencode-heap-<time>/` with the desktop's own (local) server, which writes straight into the folder, every app window renderer, and the Electron main process. Heap snapshots only hold GC-managed objects, so it also records Chromium's memory-infra dump (`memory-infra.json`, open in Perfetto) and summarizes it per process and allocator in `memory.json` (MiB: `malloc`, `partition_alloc`, `cc/tile_memory`, `gpu`, `v8`, …), next to each window's DOM size and decoded images and the server's `process.memoryUsage()`. `manifest.json` (PIDs, `app.getAppMetrics()` before and after) is written last. Start with `memory.json` when a process is larger than its heap snapshot. In a dev desktop build you can drive it over CDP (`OPENCODE_DESKTOP_REMOTE_DEBUGGING_PORT`, default 9222): press `Control+Shift+P`, type `heap snapshots`, press Enter.
For desktop memory, run **Capture heap snapshots** from the desktop command palette (`packages/gui-plugins/src/debug/main.ts`). It writes `~/Downloads/opencode-heap-<time>/` with the desktop's own (local) server, which writes straight into the folder, every app window renderer, and the Electron main process. Heap snapshots only hold GC-managed objects, so it also records Chromium's memory-infra dump (`memory-infra.json`, open in Perfetto) and summarizes it per process and allocator in `memory.json` (MiB: `malloc`, `partition_alloc`, `cc/tile_memory`, `gpu`, `v8`, …), next to each window's DOM size and decoded images and the server's `process.memoryUsage()`. `manifest.json` (PIDs, `app.getAppMetrics()` before and after) is written last. Start with `memory.json` when a process is larger than its heap snapshot. In a dev desktop build you can drive it over CDP (`OPENCODE_DESKTOP_REMOTE_DEBUGGING_PORT`, default 9222): press `Control+Shift+P`, type `heap snapshots`, press Enter.

The Unix `SIGUSR1` listener in `packages/cli/src/heap.ts` shares the same writer (`packages/util/src/heap-snapshot.ts`) and is still useful for a process that is not an HTTP server, such as a TUI client:

Expand Down
10 changes: 5 additions & 5 deletions .opencode/skills/unit-tests/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: unit-tests
description: Use when writing, reviewing, speeding up, or fixing a flaky `bun test` unit test in any package (core, cli, server, ai, codemode, util, client, sdk, plugin, http-recorder, merman, app src and test-browser, desktop, gui-extensions, ui, session-ui, latex, ...), except OpenTUI renderer tests (see tui-tests). Also use when a test "timed out after 5000ms" on Windows CI, fails only on Windows, when the unit job is slow, when adding cli subprocess or service tests, or when adding a package's test task to turbo.json. Covers controlling time instead of sleeping, per-test timeouts, process and git spawn cost on Windows, Windows-only failures, temp dirs and process state, leaks between files, and reading CI timeouts.
description: Use when writing, reviewing, speeding up, or fixing a flaky `bun test` unit test in any package (core, cli, server, ai, codemode, util, client, sdk, plugin, http-recorder, merman, app src and test-browser, desktop, gui-plugins, ui, session-ui, latex, ...), except OpenTUI renderer tests (see tui-tests). Also use when a test "timed out after 5000ms" on Windows CI, fails only on Windows, when the unit job is slow, when adding cli subprocess or service tests, or when adding a package's test task to turbo.json. Covers controlling time instead of sleeping, per-test timeouts, process and git spawn cost on Windows, Windows-only failures, temp dirs and process state, leaks between files, and reading CI timeouts.
---

# Fast, Reliable Unit Tests
Expand All @@ -10,7 +10,7 @@ CI runs `bun turbo test --affected --concurrency=3` on 4-vCPU Linux and Windows
Read these first; this skill does not repeat them:

- Root `AGENTS.md`: the Testing and Style Guide sections.
- `packages/app/AGENTS.md` Tests (`packages/desktop` and `packages/gui-extensions` follow it): one owner per contract, and no production seams for tests.
- `packages/app/AGENTS.md` Tests (`packages/desktop` and `packages/gui-plugins` follow it): one owner per contract, and no production seams for tests.
- `packages/ai/AGENTS.md`: cassettes. The `effect` skill: `it.effect` and `it.live`.

Sibling skills: `tui-tests` (OpenTUI renderer tests in tui and cli), `app-e2e-tests` (Playwright in `packages/app/e2e`), `component-tests` (Playwright on stories, `packages/stories`). This skill owns cli subprocess and service tests.
Expand All @@ -19,7 +19,7 @@ Sibling skills: `tui-tests` (OpenTUI renderer tests in tui and cli), `app-e2e-te

- Bun's default per-test timeout is 5000ms. Check the package's `test` script:
- 30s: ai, cli, desktop, http-recorder, latex, merman, simulation, theme and tui pass `--timeout 30000`; core's `script/test.ts` passes `--timeout=30000`.
- 5s: client, httpapi-codegen and sdk pass `--timeout 5000`; app, codemode, function, gui-extensions, plugin, plugin-browser, script, server, session-ui, ui and util use the default.
- 5s: client, httpapi-codegen and sdk pass `--timeout 5000`; app, codemode, function, gui-plugins, plugin, plugin-browser, script, server, session-ui, ui and util use the default.
- Size Windows risk by counting spawns, not by local wall time.
- npm.test.ts "installs a Git package from an npm ::path: subdirectory at a branch" takes 0.4s locally and hit 5000ms on Windows CI. Each Npm.add Git case makes about 75 git process events: 3 `clone --recurse-submodules`, which run the git-submodule shell script.
- Assume roughly 40ms per Git-for-Windows spawn (estimated from an earlier git.test run on Windows CI: ~24s for 564 spawns). Some core file groups run 5-9x slower on Windows than locally.
Expand Down Expand Up @@ -64,7 +64,7 @@ To avoid adding more:
- Shorten a production duration through an option, not by waiting it out. Precedent: real product options (`Flock.Options` in `packages/util/src/flock.ts:41-47`, `idleTimeout`/`flushInterval` in `packages/client/src/solid/connection.ts`) and client's hidden seam `withEnsureTiming` (`packages/client/src/service-timing.ts:31`, used by `packages/client/test/fixture/service-timing.ts`).
- For a spawned process the value must reach the child, as `timeoutMs` does through the worker payload at `packages/core/test/util/flock.test.ts:50-58`.
- server-process.ts has no override today, so its 15s tests cannot be shortened from the test side.
- app, desktop and gui-extensions forbid such seams (their AGENTS.md Tests sections); core, cli, util, client and tui have no such ban.
- app, desktop and gui-plugins forbid such seams (their AGENTS.md Tests sections); core, cli, util, client and tui have no such ban.
- Retry timing belongs in core under TestClock. `packages/server/test/fetch.test.ts:363-408` spends about 5.8s of server's 16s in real 200ms bind retries (`packages/core/src/plugin/provider/openai.ts:21`). The fixed ports 1455/1457 are the contract there; new server tests cover routing and error mapping only. Elsewhere bind port 0 (`packages/app/src/runtime/server/global-sync/bootstrap.test.ts:81`).

## Do not assert wall-clock speed
Expand Down Expand Up @@ -105,7 +105,7 @@ To avoid adding more:
- Do not use `mock.module`; it leaks across files.
- It is why app's scripts pass `--isolate`: app unit went from 2.4s to 9.9s locally, and unit plus browser from 5.3s to 12-20s on CI.
- `packages/cli/test/mini-config.test.ts:25` and `packages/core/test/plugin/provider-gitlab.test.ts:74` already leak in non-isolated suites; do not copy them.
- In Effect packages, provide the dependency through a test layer. In app, desktop and gui-extensions, drive the real module and entry point, and inject only through a parameter the product already has.
- In Effect packages, provide the dependency through a test layer. In app, desktop and gui-plugins, drive the real module and entry point, and inject only through a parameter the product already has.
- Never add `--isolate` or bare `--parallel` (which implies `--isolate`) to a test script: ai went from 7.6s to 26.3s with `--parallel=2`.
- `--parallel=N --no-isolate` keeps the shared registry and measured 0.47-0.68x serial wall time at about the same CPU in core. It also exposed load-sensitive tests (browser-idle and integration's `eventually`, both since fixed).
- Adopting it is a runner decision for maintainers once CI passes on both OSes, not a fix for one slow test.
Expand Down
Loading
Loading