* feat(opencode): restore the slash commands on OpenCode 2 OpenCode's V2 plugin API gained native command execution upstream (anomalyco/opencode issue #2185, PR #44765): ctx.command.transform lets a plugin add a command whose execute callback fully owns the invocation. That shape currently ships on the beta and dev dist-tags of @opencode-ai/plugin while next and latest still carry the older context, so the capability is duck-typed at runtime and never imported. On a host that exposes it the V2 adapter registers /plannotator-review, /plannotator-annotate and /plannotator-last and runs the same handleCliCommand machinery OpenCode 1 uses, passing the raw argument tail straight through to the CLI. On a host without it nothing new is registered and behavior is byte-identical to before. Also wires ctx.session.switchAgent (same API generation, same probe) so an agent switch chosen in the review UI is applied instead of only warned about, and accepts both agent.list() response shapes: the HTTP client types it as a { location, data } envelope while the in-process plugin domain answers with a bare array, where reading .data threw and silently emptied the agent list. The shared command stubs get model-mediated fallback bodies for OpenCode 2 hosts on the stale channels. They carry no shell interpolation on purpose: OpenCode 1 evaluates a template's !`...` before the V1 plugin's command.execute.before hook can clear the parts, so a bang template there would launch a second Plannotator session on every OC1 invocation. A source level test pins that. AI-assisted (Claude) under maintainer direction. * fix(opencode): probe the command draft and reclaim the names from the stubs Review found the capability probe was wrong in the direction that matters. ctx.command.transform exists on pre-#44765 hosts too: our own pinned @opencode-ai/plugin@0.0.0-next-16775 declares CommandDraft as { list, get, update, remove } with no add. The probe therefore returned true on next and latest, draft.add was undefined, and because transforms are stored and replayed the TypeError landed in the batched reload flush and aborted it before commit, plausibly taking every command registration on the host down with it. Capability is now read from the draft handed to the callback, which is the only witness, and the registration call is wrapped so no transform rejection can fail plugin setup. The stubs also shadowed the native definitions on new hosts. Command definitions land in a name-keyed map where add is Map.set, transforms replay in registration order, and OpenCode's own ConfigCommandPlugin activates in the post group after package plugins while scanning the exact directory the installer writes the three stubs to. A setup-time registration is therefore always overwritten on a normal install. The plugin now re-registers the same transform once activation settles, so its definitions are last in the replay order, and calls ctx.command.reload() explicitly because a late registration only adds its reload to the already-flushed boot batch. Ownership is read back from ctx.command.list() by description, which is why the native descriptions and the stub frontmatter are deliberately distinct. If the reclaim cannot run the stubs keep the names and the commands still work through their fallback bodies. Also: a failing switchAgent no longer costs the reviewer their feedback on the command path, feedback is delivered as "queue" rather than replaying the invocation's admission mode minutes later when a steer would land mid-turn, and the agent-list comment no longer asserts a bare-array response that could not be reproduced upstream (accepting both shapes is still right, since reading .data blindly throws into a catch that degrades silently). Tests: the real old-host draft shape registers nothing and throws nothing, the shadowing contest is modelled against upstream's replay semantics, the OpenCode 1 parts-clearing invariant is pinned for all three commands in both plan-agent and manual mode now that the stubs carry real instructions, and the V2 smoke asserts the plugin did not activate as failed and that all three commands resolve. The smoke now also installs the stubs into its sandbox config dir so the contest actually happens there. scripts/opencode2-native-commands-smoke.sh runs the same smoke against a dev-channel build with native commands required; CI cannot, because it pins a next build. AI-assisted (Claude) under maintainer direction. * fix(opencode): keep the reclaim ticking and stop an unbuilt checkout failing setup The reclaim ended the loop when the draft-probe flag read false, but that flag only flips when the transform replays, which under boot batching is the flush after every plugin has loaded. Plannotator loads before the post-group config plugins, so the first tick legitimately reads false and the loop exited for good: the reclaim was inert in exactly the shape production has. The tick is skipped now instead, with a test that flips the flag between ticks. The V1 entry called resolveBundledHtmlPath synchronously during plugin construction, outside the .catch that was there to absorb a missing asset, so an unbuilt checkout threw out of construction before any code path that needs the HTML. The Test workflow runs bun test with no build step, so the new OpenCode 1 interception tests failed there. Both preloads are guarded; the lazy getters still raise a clear error if something actually needs the file. The smoke's failed-plugin guard read entry.state.status, but Plugin.Info carries status and error at the top level, so a failed activation slipped through. Reads the top level first and keeps the nested one as a fallback. Comment corrections: State.batch clears its active flag before flushing, so a late transform registration materializes on its own; the explicit reload() is redundant-but-defensive rather than required. The reclaim schedule is a list of deltas the loop awaits in turn, so the ticks land near 0.3s, 1.5s, 5.5s and 15.5s, not at the raw numbers. AI-assisted (Claude) under maintainer direction.
@plannotator/opencode
Annotate plans. Not in the terminal.
Interactive Plan Review for OpenCode. Select the exact parts of the plan you want to change—mark for deletion, add a comment, or suggest a replacement. Feedback flows back to your agent automatically.
Obsidian users can auto-save approved plans to Obsidian as well. See details
Watch Demo
|
Install
OpenCode 2 beta
Install OpenCode 2 from npm's next tag, then add Plannotator to the V2 plugins field:
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
{
"package": "@plannotator/opencode@latest",
"options": {
"workflow": "plan-agent",
"planningAgents": ["plan"]
}
}
]
}
Restart OpenCode 2 and verify that plannotator appears in opencode2 plugin list.
OpenCode 2 support is experimental while its plugin API is in beta. The core submit_plan review flow works everywhere. Two newer capabilities depend on which plugin API your OpenCode build ships with, and Plannotator detects both at runtime rather than requiring a particular channel:
- Slash commands. Native command execution landed upstream in
@opencode-ai/plugin(anomalyco/opencode issue #2185, PR #44765) and currently ships on thebetaanddevdist-tags; thenextandlatesttags still carry the older API. Capability is detected from the command draft OpenCode hands the plugin, not from the plugin API's shape:ctx.command.transformexists on both generations, and only the newer draft hasadd. On a host that has it, Plannotator registers/plannotator-review,/plannotator-annotate, and/plannotator-lastitself and runs the same machinery OpenCode 1 uses, so your raw arguments reach the CLI unchanged and nothing is routed through the model. On an older host it registers nothing and the commands run from their markdown definitions, which ask the agent to run theplannotatorCLI and relay its output; that path works but costs a model turn and depends on the agent following the instruction. - Command precedence. OpenCode activates its own config-command loader after package plugins, and the last definition to claim a name wins, so the markdown stubs the installer writes to
~/.config/opencode/commandswould otherwise shadow the native definitions on every normal install. Plannotator re-registers the three names shortly after startup so its own definitions are the ones that run. If that reclaim cannot run, the stubs keep the names and the commands still work through the model-mediated fallback. - Agent switching.
ctx.session.switchAgentarrived with the same plugin API generation. On a host that exposes it, an agent switch chosen in the review UI is applied to the session. On an older host the plan is still approved and a warning is written to the server log; switch tobuildmanually before implementation. - Abort signal. V2 tool execution still exposes no abort signal. Cancelling a turn cannot stop a running review server or CLI child immediately.
- TUI toasts. OpenCode 2 has a TUI plugin entry point, but it is separate from the server plugin Plannotator registers, so session URLs are written to the server output rather than shown as a toast. Remote sessions should read the URL from the OpenCode log.
OpenCode 1
Add to your opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@plannotator/opencode@latest"]
}
Restart OpenCode. By default, the submit_plan tool is available to OpenCode's plan agent, not to build or other primary agents.
OpenCode 1 slash commands: Run the install script to get
/plannotator-review,/plannotator-annotate, and/plannotator-last:curl -fsSL https://plannotator.ai/install.sh | bashThis also clears any cached plugin versions.
Workflow Modes
The examples below use the OpenCode 1 config shape. OpenCode 2 places the same option keys under the plugin entry's options object shown above. In V2, manual registers no tool, so it leaves only the slash commands: useful on a host with native command execution, inactive on one without it.
plan-agent(default):submit_planis available to OpenCode's built-inplanagent plus any extra agents listed inplanningAgents. This keeps Plannotator integrated with OpenCode plan mode without nudgingbuildto call it.manual:submit_planis not registered. Use/plannotator-last,/plannotator-annotate, and/plannotator-reviewwhen you want Plannotator.user-managed:submit_planis registered but no prompts or agent permissions are modified. You manage which agents can callsubmit_planvia OpenCode's native agent configuration.all-agents: legacy broad behavior. Primary agents can see and callsubmit_plan.
Default config:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@plannotator/opencode@latest", {
"workflow": "plan-agent",
"planningAgents": ["plan"]
}]
]
}
Runtime selection is automatic. In Bun-hosted OpenCode, Plannotator uses the embedded server bundled with the plugin. In Node-hosted or wrapped OpenCode environments, the plugin falls back to the installed plannotator CLI and sends the result back through OpenCode. You can force the fallback while debugging:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@plannotator/opencode@latest", {
"runtime": "cli"
}]
]
}
If you use other OpenCode plugins, keep everything in one plugin array and attach Plannotator's options directly to the Plannotator entry:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@plannotator/opencode@latest", {
"workflow": "plan-agent",
"planningAgents": ["plan", "sisyphus"]
}],
"@tarquinen/opencode-dcp@latest",
"octto",
"oh-my-opencode-slim"
]
}
Do not put { "workflow": "plan-agent" } as its own item in the plugin array. OpenCode plugin entries must be either a plugin string or a two-item array like [pluginName, options].
Restore the old broad behavior:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@plannotator/opencode@latest", {
"workflow": "all-agents"
}]
]
}
Use commands only:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@plannotator/opencode@latest", {
"workflow": "manual"
}]
]
}
Register the tool but manage prompts and permissions yourself:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@plannotator/opencode@latest", {
"workflow": "user-managed"
}]
]
}
How It Works
- The configured planning agent calls
submit_plan→ Plannotator opens in your browser - Select text → annotate (delete, replace, comment)
- Approve → Agent proceeds with implementation
- Request changes → Annotations sent back as structured feedback
Features
- Visual annotations: Select text, choose an action, see feedback in the sidebar
- Local by default: Plans, annotations, drafts, history, and configuration stay local. Every app load checks GitHub for updates without sending plan content, and there is currently no opt-out setting; URL annotation, hosted PR review, AI, sharing, and Workspaces use the network when selected.
- Legacy link sharing: Small markdown shares use compressed, unencrypted URL fragments. Larger and raw HTML shares can use client-encrypted short links. Workspaces is the primary direction for team sharing.
- Plan Diff: See what changed when the agent revises a plan after feedback
- Annotate last message: Run
/plannotator-lastto annotate the agent's most recent response - Annotate files, folders, and URLs: Run
/plannotator-annotatewhen you want manual review of an artifact - Obsidian integration: Auto-save approved plans to your vault with frontmatter and tags
Environment Variables
| Variable | Description |
|---|---|
PLANNOTATOR_REMOTE |
Set to 1 / true for remote mode, 0 / false for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. |
PLANNOTATOR_PORT |
Fixed port to use. Default: random locally, 19432 for remote sessions. |
PLANNOTATOR_BROWSER |
Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. |
PLANNOTATOR_SHARE_URL |
Custom share portal URL for self-hosting. Default: https://share.plannotator.ai. |
PLANNOTATOR_PASTE_URL |
Custom paste service URL for self-hosting. Default: https://plannotator-paste.plannotator.workers.dev. |
PLANNOTATOR_PLAN_TIMEOUT_SECONDS |
Timeout for submit_plan review wait. Default: 345600 (96h). Set 0 to disable timeout. |
PLANNOTATOR_BIN |
Override the CLI path used by the OpenCode plugin's CLI runtime fallback. Default: plannotator on PATH. |
Devcontainer / Docker
Works in containerized environments. Set the env vars and forward the port:
{
"containerEnv": {
"PLANNOTATOR_REMOTE": "1",
"PLANNOTATOR_PORT": "9999"
},
"forwardPorts": [9999]
}
If nothing opens automatically, open http://localhost:9999 when submit_plan is called.
See devcontainer.md for full setup details.
Obsidian Integration
Save approved plans directly to your Obsidian vault.
- Open Settings in Plannotator UI
- Enable "Obsidian Integration" and select your vault
- Approved plans save automatically with:
- Human-readable filenames:
Title - Jan 2, 2026 2-30pm.md - YAML frontmatter (
created,source,tags) - Auto-extracted tags from plan title and code languages
- Backlink to
[[Plannotator Plans]]for graph view
- Human-readable filenames:
Links
License
Copyright 2025 backnotprop Licensed under MIT or Apache-2.0.