What task are you trying to do?
As a non-technical user, I want to discover, enable / disable, and configure PawWork plugins from inside the app — without opening a config file, knowing what a plugin spec looks like, or running a CLI command.
The finished surface should look and behave roughly like the dedicated plugins page in design/src/plugins_view.jsx (tracked design source at the repo root): a page-level view opened from the left sidebar (not a popover), a grid of plugin cards with category chips and a search box, each card showing name / author / version / short description / enabled state with toggle, already-installed plugins exposing a "settings" entry that opens a form tailored to that plugin.
This issue tracks the UI, the backend layers it depends on, and the protocol decisions that have to be made before the GUI is meaningful.
What do you do today?
PawWork inherits the OpenCode plugin core, but the surface is no longer "wholesale" — there is already a real fork at the SDK boundary that the GUI must account for:
- TUI plugin runtime is deleted on PawWork.
packages/plugin/src/tui.ts is gone, the entire packages/opencode/src/cli/cmd/tui/plugin/ tree is gone, and the ./tui export is removed from packages/plugin/package.json. Any upstream plugin that registers TUI slots, prompt traits, or widgets does not run on PawWork.
- PawWork extends the SDK with a private
WorkspaceAdaptor API and experimental_workspace.register(type, adaptor) entry on PluginInput, with auth-scoping via WorkspaceAdaptor.auth.providers. Upstream moved in the opposite direction: anomalyco/opencode#21348 removes workspaces from the plugin API entirely.
packages/plugin/package.json still claims the @opencode-ai/plugin name but version, license, and exports diverge from upstream npm. Third-party plugins pinned against the upstream npm type shape will not see PawWork's extensions.
- The five built-in plugins (codex, github-copilot, cloudflare, gitlab, poe) are all auth providers. There are no first-party productivity plugins yet.
User-facing surface today: plugins are npm packages enabled by adding an entry to the plugin array in opencode.json / pawwork.json. Installation happens via opencode plugin install <spec> in a terminal, or by hand-editing the config file. "Enabled" is implicit — if it is in the config array it loads, otherwise it does not. Plugins have no settings schema, so per-plugin configuration is whatever free-form object the author accepts in the second tuple slot: ["pkg-name", { ... }].
The Electron app surfaces plugins only in the status popover (packages/app/src/components/status-popover-body.tsx:242 and packages/app/src/components/session/session-status-connections.tsx:146) as a read-only list of names read from sync.data.config.plugin. The empty state literally tells the user to edit pawwork.json. There is no HTTP endpoint on the opencode server for plugin list / install / enable / configure — plugins live entirely inside the core process.
For a non-technical target user, every step above is a blocker.
What would a good result look like?
A plugin surface a non-technical user can drive end-to-end without touching a config file or a terminal, plus the backend and protocol decisions that make it possible. The work breaks into five layers; the first three are new dependencies that did not exist when this issue was originally written.
Layer 0 — Server / IPC API. The opencode HTTP server exposes plugin operations the GUI can call: list installed + enabled state, enable / disable, read / write per-plugin settings, surface plugin errors in structured form. Without this layer the GUI can only mirror sync.data.config.plugin — a string list — which is what ships today.
Layer 1 — Enable / disable protocol. Config schema gains a parallel disabledPlugins: string[] field. Presence in plugin array still means "installed and loaded"; presence in disabledPlugins short-circuits the load. GUI toggle adds / removes the plugin name in disabledPlugins. This is a PawWork-private extension to the plugin config schema and is treated as a permanent local difference alongside the existing TUI removal and workspace adaptor extension.
Layer 2 — Bundled first-party registry. A JSON manifest bundled with the app lists the PawWork-recommended plugins: category, display name i18n key, description i18n key, author, version, beta flag. v1 is first-party only (see trust model below) so the registry is the canonical source of what the GUI can show. Concrete lineup is deferred to implementation time (YAGNI).
Layer 3 — Plugin settings schema. Each first-party plugin publishes a settings schema (JSON Schema or zod-equivalent) the app renders as a form. Covers required fields, enum pickers, and secret fields stored via platform-native secret storage (macOS Keychain / Windows Credential Manager) per AGENTS.md "platform defaults" rule, not a self-built crypto layer.
Layer 4 — GUI page, sidebar entry, and lifecycle. Dedicated Plugins page rendering Layer 2 registry as cards. Toggle backed by Layer 1. Configure uses Layer 3 schema, written back via the existing jsonc-aware patch path (patchPluginConfig in packages/opencode/src/plugin/install.ts). Changing a plugin's enabled state or settings triggers an automatic opencode core restart in the background with a lightweight "applying plugin" indicator (~2-3s latency, session state recovered via existing persistence). No uninstall button for v1 — GUI has toggle only, mirroring Claude Code / AionUI / Cline. Sidebar entry is the long-term home but the short-term implementation can keep the status popover view until the Plugins page lands.
Which audience does this matter to most?
Both
Extra context
Plugin protocol stance — where PawWork forks from upstream.
The original framing of this issue said the GUI "must not fork or rename the plugin protocol". That is no longer accurate. PawWork has already forked at these points, and Layer 1 above adds a fourth:
- TUI plugin runtime removed
experimental_workspace.register added (PawWork-private, upstream is removing it)
@opencode-ai/plugin package shape diverged from upstream npm
disabledPlugins config field (added by this issue)
Realistic stance: the Hook API subset (auth, chat.headers, chat.params, config, event, and similar non-TUI / non-workspace hooks) stays compatible with upstream. TUI surface, workspace adaptor, disabled protocol, and the @opencode-ai/plugin package shape are explicitly PawWork-owned. Document the boundary wherever the UI rewrite carve-out is documented.
Trust model. v1 is first-party whitelist only. GUI only shows plugins listed in the bundled registry. No third-party npm installation path in v1. Third-party opens later once signing / permission prompts / publisher identity are designed.
Runtime. First-party plugins are pre-packaged inside the app bundle. No bun install at runtime, no bundled bun / node runtime for plugin install. Sidesteps npm accessibility issues in Mainland China and avoids the ~50MB DMG cost of bundling a JS runtime purely for plugin install. Cost: adding a first-party plugin requires a PawWork release.
experimental_workspace.register scope. Internal-only. Marked internal in the SDK; third-party plugins are not allowed to call it. Keeps the option open to absorb the upstream direction (remove workspaces from plugin API) without breaking third-party contracts.
Tool-registration hook (Hooks.tool.register). The current Hooks interface has no way for a plugin to register a new AI tool. Issue #131 ("move advanced tools to plugins") is blocked on this. The decision from this discussion: the hook is not a prerequisite issue. Whichever issue lands first (#30 or #131) extends the SDK with tool.register as part of its own scope. In practice #131 is the more likely owner since it is the one that actually needs to register tools.
Skill integration is out of scope. Skill management GUI is tracked separately. This issue is plugin-only.
i18n. Plugin manifest name / description are i18n keys; strings live in packages/app/src/i18n/{zh,en}.ts alongside other product copy. Follows the repo zh + en only policy.
Open questions for implementation.
- Crash isolation if one plugin throws inside
Plugin.trigger before the core restart approach fully works
- macOS notarization behavior for bundled plugin code (should be trivial since everything ships inside the signed app bundle, but verify at packaging time)
- First-wave plugin lineup (YAGNI, picked at implementation)
Reference paths.
- Plugin SDK:
packages/plugin/src/index.ts
- Plugin core (loader, install, meta, shared):
packages/opencode/src/plugin/
- CLI install command (reference for the Layer 0 API contract):
packages/opencode/src/cli/cmd/plug.ts
- Existing read-only app surface:
packages/app/src/components/status-popover-body.tsx:242, packages/app/src/components/session/session-status-connections.tsx:146
- Config reader for enabled plugins:
packages/opencode/src/config/plugin.ts, packages/opencode/src/config/config.ts:351 (PawWork / OpenCode config path resolution)
- Design reference:
design/src/plugins_view.jsx, design/HANDOFF.md
Scheduling and related issues.
- Depends on #26 Phase 2 (sidebar redesign) so the Plugins sidebar entry has a home
- #131 is parallel; they share the
tool.register hook question (see above)
- Skill management GUI is tracked in a separate parallel issue at higher priority (P2 vs P3 for this one)
- Priority stays P3 — not blocking Phase 1
What task are you trying to do?
As a non-technical user, I want to discover, enable / disable, and configure PawWork plugins from inside the app — without opening a config file, knowing what a plugin spec looks like, or running a CLI command.
The finished surface should look and behave roughly like the dedicated plugins page in
design/src/plugins_view.jsx(tracked design source at the repo root): a page-level view opened from the left sidebar (not a popover), a grid of plugin cards with category chips and a search box, each card showing name / author / version / short description / enabled state with toggle, already-installed plugins exposing a "settings" entry that opens a form tailored to that plugin.This issue tracks the UI, the backend layers it depends on, and the protocol decisions that have to be made before the GUI is meaningful.
What do you do today?
PawWork inherits the OpenCode plugin core, but the surface is no longer "wholesale" — there is already a real fork at the SDK boundary that the GUI must account for:
packages/plugin/src/tui.tsis gone, the entirepackages/opencode/src/cli/cmd/tui/plugin/tree is gone, and the./tuiexport is removed frompackages/plugin/package.json. Any upstream plugin that registers TUI slots, prompt traits, or widgets does not run on PawWork.WorkspaceAdaptorAPI andexperimental_workspace.register(type, adaptor)entry onPluginInput, with auth-scoping viaWorkspaceAdaptor.auth.providers. Upstream moved in the opposite direction:anomalyco/opencode#21348removes workspaces from the plugin API entirely.packages/plugin/package.jsonstill claims the@opencode-ai/pluginname but version, license, and exports diverge from upstream npm. Third-party plugins pinned against the upstream npm type shape will not see PawWork's extensions.User-facing surface today: plugins are npm packages enabled by adding an entry to the
pluginarray inopencode.json/pawwork.json. Installation happens viaopencode plugin install <spec>in a terminal, or by hand-editing the config file. "Enabled" is implicit — if it is in the config array it loads, otherwise it does not. Plugins have no settings schema, so per-plugin configuration is whatever free-form object the author accepts in the second tuple slot:["pkg-name", { ... }].The Electron app surfaces plugins only in the status popover (
packages/app/src/components/status-popover-body.tsx:242andpackages/app/src/components/session/session-status-connections.tsx:146) as a read-only list of names read fromsync.data.config.plugin. The empty state literally tells the user to editpawwork.json. There is no HTTP endpoint on the opencode server for plugin list / install / enable / configure — plugins live entirely inside the core process.For a non-technical target user, every step above is a blocker.
What would a good result look like?
A plugin surface a non-technical user can drive end-to-end without touching a config file or a terminal, plus the backend and protocol decisions that make it possible. The work breaks into five layers; the first three are new dependencies that did not exist when this issue was originally written.
Layer 0 — Server / IPC API. The opencode HTTP server exposes plugin operations the GUI can call: list installed + enabled state, enable / disable, read / write per-plugin settings, surface plugin errors in structured form. Without this layer the GUI can only mirror
sync.data.config.plugin— a string list — which is what ships today.Layer 1 — Enable / disable protocol. Config schema gains a parallel
disabledPlugins: string[]field. Presence inpluginarray still means "installed and loaded"; presence indisabledPluginsshort-circuits the load. GUI toggle adds / removes the plugin name indisabledPlugins. This is a PawWork-private extension to the plugin config schema and is treated as a permanent local difference alongside the existing TUI removal and workspace adaptor extension.Layer 2 — Bundled first-party registry. A JSON manifest bundled with the app lists the PawWork-recommended plugins: category, display name i18n key, description i18n key, author, version, beta flag. v1 is first-party only (see trust model below) so the registry is the canonical source of what the GUI can show. Concrete lineup is deferred to implementation time (YAGNI).
Layer 3 — Plugin settings schema. Each first-party plugin publishes a settings schema (JSON Schema or zod-equivalent) the app renders as a form. Covers required fields, enum pickers, and secret fields stored via platform-native secret storage (macOS Keychain / Windows Credential Manager) per AGENTS.md "platform defaults" rule, not a self-built crypto layer.
Layer 4 — GUI page, sidebar entry, and lifecycle. Dedicated Plugins page rendering Layer 2 registry as cards. Toggle backed by Layer 1. Configure uses Layer 3 schema, written back via the existing jsonc-aware patch path (
patchPluginConfiginpackages/opencode/src/plugin/install.ts). Changing a plugin's enabled state or settings triggers an automatic opencode core restart in the background with a lightweight "applying plugin" indicator (~2-3s latency, session state recovered via existing persistence). No uninstall button for v1 — GUI has toggle only, mirroring Claude Code / AionUI / Cline. Sidebar entry is the long-term home but the short-term implementation can keep the status popover view until the Plugins page lands.Which audience does this matter to most?
Both
Extra context
Plugin protocol stance — where PawWork forks from upstream.
The original framing of this issue said the GUI "must not fork or rename the plugin protocol". That is no longer accurate. PawWork has already forked at these points, and Layer 1 above adds a fourth:
experimental_workspace.registeradded (PawWork-private, upstream is removing it)@opencode-ai/pluginpackage shape diverged from upstream npmdisabledPluginsconfig field (added by this issue)Realistic stance: the Hook API subset (
auth,chat.headers,chat.params,config,event, and similar non-TUI / non-workspace hooks) stays compatible with upstream. TUI surface, workspace adaptor, disabled protocol, and the@opencode-ai/pluginpackage shape are explicitly PawWork-owned. Document the boundary wherever the UI rewrite carve-out is documented.Trust model. v1 is first-party whitelist only. GUI only shows plugins listed in the bundled registry. No third-party npm installation path in v1. Third-party opens later once signing / permission prompts / publisher identity are designed.
Runtime. First-party plugins are pre-packaged inside the app bundle. No
bun installat runtime, no bundledbun/noderuntime for plugin install. Sidesteps npm accessibility issues in Mainland China and avoids the ~50MB DMG cost of bundling a JS runtime purely for plugin install. Cost: adding a first-party plugin requires a PawWork release.experimental_workspace.registerscope. Internal-only. Marked internal in the SDK; third-party plugins are not allowed to call it. Keeps the option open to absorb the upstream direction (remove workspaces from plugin API) without breaking third-party contracts.Tool-registration hook (
Hooks.tool.register). The currentHooksinterface has no way for a plugin to register a new AI tool. Issue #131 ("move advanced tools to plugins") is blocked on this. The decision from this discussion: the hook is not a prerequisite issue. Whichever issue lands first (#30 or #131) extends the SDK withtool.registeras part of its own scope. In practice #131 is the more likely owner since it is the one that actually needs to register tools.Skill integration is out of scope. Skill management GUI is tracked separately. This issue is plugin-only.
i18n. Plugin manifest
name/descriptionare i18n keys; strings live inpackages/app/src/i18n/{zh,en}.tsalongside other product copy. Follows the repozh + enonly policy.Open questions for implementation.
Plugin.triggerbefore the core restart approach fully worksReference paths.
packages/plugin/src/index.tspackages/opencode/src/plugin/packages/opencode/src/cli/cmd/plug.tspackages/app/src/components/status-popover-body.tsx:242,packages/app/src/components/session/session-status-connections.tsx:146packages/opencode/src/config/plugin.ts,packages/opencode/src/config/config.ts:351(PawWork / OpenCode config path resolution)design/src/plugins_view.jsx,design/HANDOFF.mdScheduling and related issues.
tool.registerhook question (see above)