Skip to content

[Feature] Plugin management GUI in the desktop app #30

@Astro-Han

Description

@Astro-Han

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:

  1. TUI plugin runtime removed
  2. experimental_workspace.register added (PawWork-private, upstream is removing it)
  3. @opencode-ai/plugin package shape diverged from upstream npm
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Low priorityenhancementNew feature or requestuiDesign system and user interface

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions