Skip to content

fix(v2): register pty tools, slash commands and exit notifications via the V2 plugin SDK - #61

Merged
shekohex merged 5 commits into
shekohex:mainfrom
lenucksi:fix/v2-tool-command-registration
Sep 19, 2026
Merged

shekohex merged 5 commits into
shekohex:mainfrom
lenucksi:fix/v2-tool-command-registration

Conversation

@lenucksi

@lenucksi lenucksi commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Problem

The V2 entry point (src/v2/index.ts) was nearly inert on real OpenCode v2 hosts:

  1. ToolsptyTools (src/v2/tools.ts) was exported but never passed to ctx.tool.transform, so pty_spawn/pty_write/pty_read/pty_list/pty_kill never reached OpenCode. Issue Plan OpenCode V2 plugin API support #55 explicitly listed "Register PTY tools through the V2 tool transform/registration surface" as part of the port.
  2. CommandsregisterV2Commands called draft.update(name, cb), but OpenCode v2's CommandEditor only exposes add(definition). The guard typeof draft.update === 'function' therefore always short-circuited, and pty-open-background-spy / pty-show-server-url never appeared in the / menu.
  3. Exit notificationscreateV2Adapter() was called without options, so installHostAdapter never installed a notifier and the fallback NotificationManager had no client: <pty_exited> was silently never sent. Agents that were told to wait for it (per spawn.txt) simply stopped — indistinguishable from "the model decided not to respond" ([Bug]: exit notification delivery failures are silently swallowed — agent never wakes, no diagnostic #54).

The existing tests did not catch any of this: they mocked a draft that happened to implement update, only asserted module-level existence of ptyTools, and never exercised the notifier.

Fix

  • src/v2/tools.ts: add registerV2Tools(draft), adapting the V1 tool definitions ({ description, args, execute }) to V2 Tool.Info ({ name, input, description, execute }). The args Zod shape is converted to a JSON Schema via Zod v4's toJSONSchema, matching the plugin documentation.
  • src/v2/commands.ts: registerV2Commands now uses draft.add({ name, description, execute }).
  • src/v2/index.ts: setup calls ctx.tool.transform(registerV2Tools); the adapter now receives a V2SessionNotifier built from the plugin context's session domain (ctx.session.prompt).
  • src/v2/notifier.ts (new): exit notifications are delivered as user prompts with a deterministic message id (pty_<id>_exited), so admission.reconcile makes repeated deliveries idempotent. opencode v2 resolves the run with the session's current model (session/runner/model.ts), so the user's model selection is preserved — the V1 setAgentModel clobber tracked in [Bug]: pty exit notification omits model, clobbers user's session model selection (falls back to agent's configured model) #57 does not exist on the V2 path. Delivery failures are logged instead of swallowed ([Bug]: exit notification delivery failures are silently swallowed — agent never wakes, no diagnostic #54).
  • src/plugin/pty/notification-manager.ts: buildExitNotification is exported so V1 and V2 notifiers share the message format.
  • src/v2/types.ts: ctx.session is typed from the official V2 SDK (@opencode/plugin); the hand-rolled context loses its any-ish index signature for the domains we use.
  • Dependencies modernized: added @opencode/plugin@^2.0.10 (the V2 plugin SDK) and bumped @opencode-ai/plugin/@opencode-ai/sdk ^1.3.13^1.18 (V1 entry only; ToolResult is now string | { output }, the affected test was adjusted).
  • Tests now assert real registration via mocked add() (what OpenCode actually provides) for tools and commands, plus live notification delivery and the no-session-domain fallback.

Verification

  • bun run typecheck — clean
  • bun test test/opencode-v2-live.test.ts test/pty-spawn-echo.test.ts test/notification-manager.test.ts — pass
  • bun run lint, bun run format — clean
  • Full bun test — same 4 pre-existing failures as on the base commit (npm-pack asset tests + two timing-sensitive tests), no new failures
  • Manually verified against OpenCode v2.0.8 (@opencode-ai/plugin@0.0.0-beta-19271): all five pty_* tools visible to the agent, both slash commands in the / menu, /pty-open-background-spy starts the web UI with live sessions. The notification path is validated in the live test against the real PTY lifecycle (shimmed ctx.session.prompt recorder); ctx.session itself is source-confirmed in OpenCode v2.0.9's plugin host (packages/core/src/plugin/host.ts builds a context whose session domain exposes prompt/get/switchModel/wait/…).

Refs #55, #54, #57.

The V2 setup registered neither tools nor commands with OpenCode:

- Tools: `ptyTools` was exported but never passed to `ctx.tool.transform`,
  so no `pty_*` tool ever reached OpenCode. Issue shekohex#55 explicitly asked to
  "register PTY tools through the V2 tool transform/registration surface".
- Commands: `registerV2Commands` called `draft.update(name, cb)`, but
  OpenCode v2's CommandEditor exposes `add(definition)` only. The guard
  `typeof draft.update === 'function'` therefore always short-circuited and
  the slash commands never appeared in the "/" menu.

Adapts the V1 tool definitions ({ description, args, execute }) to V2
Tool.Info ({ name, input, description, execute }), registers them through
ctx.tool.transform, and switches commands to draft.add() with an execute
handler. Tests now assert real registration (mocking add()) instead of
mere module-level existence.
opencode v2's Tool.Info expects a JSON Schema for `input` (see the
plugins docs), not a Zod schema. Convert the V1 Zod args via Zod v4's
`toJSONSchema` instead of passing the Zod object through.
@shekohex

Copy link
Copy Markdown
Owner

CI needs fixing, overall looks good.

Would be nice if you can post a small video of it.

The PR CI (test (format), Devenv CI) failed because biome wants the
definition.execute(...) call on a single line.
@lenucksi

Copy link
Copy Markdown
Contributor Author

CI needs fixing, overall looks good.
Should probably be fixed.

Would be nice if you can post a small video of it.
Can offer you a screenshot of the tools working, the rest is your tool:

image

…-ai/* 1.18

- add @opencode/plugin@^2.0.10 (V2 plugin SDK: Plugin.Context with the
  session domain, used to type ctx.session in src/v2)
- bump @opencode-ai/plugin + @opencode-ai/sdk from ^1.3.13 to ^1.18
  (kept for the V1 entry point); ToolResult is now string | { output },
  adjusted the spawn integration test accordingly
The V2 entry point never wired a SessionNotifier: createV2Adapter() was
called without options, so installHostAdapter skipped setNotifier and the
fallback NotificationManager had no client - <pty_exited> was silently
never sent, leaving agents that were told to wait for it stuck.

- src/v2/notifier.ts: V2SessionNotifier delivers through the plugin
  context's session domain (ctx.session.prompt) with a deterministic
  message id (pty_<id>_exited) so admission.reconcile makes repeated
  deliveries idempotent. opencode v2 resolves the run with the session's
  current model, so the V1 setAgentModel clobber (shekohex#57) does not apply.
- src/v2/index.ts: build the notifier from ctx in setup, guarded by
  typeof ctx.session?.prompt; hosts without the session domain get a
  visible warning instead of dead silence (shekohex#54)
- src/plugin/pty/notification-manager.ts: export buildExitNotification
  so both notifiers share the message format
- tests: live integration covers notification delivery + the no-session
  fallback
@lenucksi lenucksi changed the title fix(v2): register pty tools and slash commands via editor add() fix(v2): register pty tools, slash commands and exit notifications via the V2 plugin SDK Sep 19, 2026
@lenucksi

Copy link
Copy Markdown
Contributor Author

Quick heads-up on the dependency swap: the second commit migrates the V2 typing surface from the old @opencode-ai/* 1.x (plus the beta-19271 surface) to the modern V2 SDK @opencode/plugin@2.0.10 — the plugin context's session domain (prompt, get, switchModel, wait, …) is now checked against the real published types instead of being hand-rolled. @opencode-ai/plugin/@opencode-ai/sdk stay at ^1.18 but only for the V1 entry point, which keeps working unchanged (ToolResult is string | { output } now — one test adapted).

@opencode/plugin is imported type-only, so the published bundle gains no new runtime dependencies.

Happy to rebase/split if you'd prefer the dep bump as its own PR alongside #64 — just let me know. 🙂

@shekohex
shekohex merged commit 9be5126 into shekohex:main Sep 19, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants