Skip to content

Feedback: Plugin tool missing in GUI desktop session but present in CLIΒ #42343

Description

@zysam

Description

πŸ› Found a subtle one while testing an opencode plugin: the tool worked in the CLI but
was missing from the GUI desktop session β€” config said registered, runtime said no.
Root cause turned out to be 3 layers deep (extension-less imports β†’ unbundled workspace
dep β†’ session tool-list is a create-time snapshot; only a new session picks it up).
Full walkthrough with repro + fixes πŸ‘‰

β€” drafted by DeepSeek V4 Flash (via opencode), assisting a client's plugin
development. Content de-identified before posting.

Feedback: Plugin tool missing in GUI desktop session but present in CLI

A detailed, agent-driven investigation of a plugin-injection discrepancy between the
opencode CLI and the GUI desktop app. Written both as a bug report (actionable for
maintainers) and as a walkthrough (showing how the issue was isolated layer by layer).


1. Summary

A custom plugin registers a tool (call it todo-sync) by exporting a thin shim under
.opencode/plugins/. The tool was available in CLI sessions (opencode run) but
absent from the GUI desktop session (OpenCode.app) β€” even though opencode debug agent
reported the tool as registered.

Root causes, in three layers:

  1. Extension-less relative imports in the plugin source (./loader instead of
    ./loader.ts): tolerated by bun, rejected by Node (the GUI desktop loads plugin
    .ts files directly via Node's type-stripping).
  2. Workspace dependency not bundled into the plugin artifact: the plugin imported a
    workspace package (@scope/core) at src/... paths; the built artifact kept an
    unresolved import "@scope/...", which the GUI runtime could not resolve reliably.
  3. Session-scoped tool list snapshot (suspected GUI behavior/bug): the GUI session
    tool list is captured when the session is created. After fixing the plugin, the
    pre-existing session still did not show the tool; only a new session did.

Fixes: add explicit .ts extensions, and build a self-contained JS dist with the
workspace dependency inlined (tsdown noExternal). Verified in new CLI and GUI sessions.


2. Environment

Component Version / Path
opencode CLI 1.18.18 (opencode)
opencode GUI 1.18.18 β€” /Applications/OpenCode.app (Electron 42.3.3)
Electron builtin Node 24.15.0 (via ELECTRON_RUN_AS_NODE=1)
System Node 24.14.0
bun 1.3.14
Bundler tsdown 0.22.14
Plugin shim .opencode/plugins/<name>.ts
Plugin package workspace-symlinked into .opencode/node_modules/@scope/plugin
Data store ~/.local/share/opencode/opencode.db (SQLite)

3. Initial symptoms

Context Observation
CLI (opencode run subprocess) βœ… todo-sync callable, ok:true, full payload
GUI desktop main session ❌ tool not in tool list (only approve / ping / guard-ping present)
opencode debug agent <agent> βœ… reports todo-sync: true at the config layer

The gap between "config layer says registered" and "runtime session tool list missing"
was the first strong signal.


4. Investigation walkthrough (layer by layer)

4.1 Baseline (what was verified as healthy)

  • The project CLI helper version within required range.
  • Plugin source link: .opencode/node_modules/@scope/plugin β†’ symlink β†’ plugin source dir. βœ…
  • bun build .opencode/plugins/<name>.ts --bundle succeeded (no stubbed node: modules). βœ…
  • The other plugin tools (ping, approve, guard-ping) were injected in GUI. βœ…

β†’ Conclusion: CLI chain healthy; the problem was isolated to the GUI environment.

4.2 CLI-side scenario tests (S1–S4)

  • Basic sync β†’ βœ… ok
  • State transition (pending β†’ completed) β†’ βœ…
  • Default taskId (latest task discovery, incl. 3-digit ids) β†’ βœ…
  • The project CLI's validate closed loop ("todo synced to board") β†’ βœ…

β†’ CLI plugin chain fully healthy; focus shifted to how the GUI loads plugins.

4.3 GUI loading mechanism

  • ps aux β†’ GUI is Electron 42.3.3.
  • GUI server log (~/Library/Application Support/ai.opencode.desktop/logs/*/server.log)
    showed MODULE_TYPELESS_PACKAGE_JSON warnings only for some shims (guard,
    verify-guard) β€” never for the failing plugin across multiple restarts.
  • Extracted the app bundle (npx @electron/asar extract .../app.asar) and read the server
    chunk: plugin loading is PluginLoader.load β†’ await import(row.entry) β€” i.e. the GUI
    directly imports the .ts shim via Node, with no bundling step.
  • ELECTRON_RUN_AS_NODE=1 "<app>/Contents/MacOS/OpenCode" -e "console.log(process.versions)"
    β†’ Electron builtin Node = 24.15.0.

Key insight: CLI (bun) and GUI (Electron Node) differ in .ts handling:

Host Loads .ts shim via Tolerates extension-less relative imports Resolves src/... workspace deps
CLI / bun native import βœ… βœ… (bundled)
GUI / Electron Node import(row.entry) + type-stripping ❌ ⚠️ unreliable

4.4 Layer 1 β€” extension-less relative imports

Reproduced with Node type-stripping (simulating the GUI loader):

$ node --experimental-strip-types -e "import('./.opencode/plugins/<name>.ts')"
FAIL: Cannot find module '.../src/todo/loader' imported from .../src/todo/opencode.ts

Audit showed the failing module used "./loader" / "./parse" / "./types" (no .ts),
while sibling plugins (guard/, question-tier/) used "./config.ts" (with .ts) and
loaded fine.

Fix 1: add explicit .ts extensions (3 files, 6 lines). Node direct-import passed;
CLI did not regress (tests green, bun build exit 0).

β†’ GUI still missing the tool after restart β†’ moved to the next layer.

4.5 Layer 2 β€” workspace dependency not bundled

The plugin imported @scope/core/src/... (framework-keywords, trace-light). The plugin
package had no build step β€” its exports pointed at src/*.ts, so the runtime
resolved the workspace chain directly.

Fix 2: add a tsdown.config.ts with:

  • 4 entry points (one per plugin submodule);
  • noExternal: [/@scope/] so the workspace dependency is inlined into the artifact;
  • exports repointed to dist/*/index.mjs.

Verification of the self-contained artifact:

  • dist/<name>/index.mjs contains inlined trace-light (3Γ—) + a local chunk for
    framework-keywords β€” no from "@scope/..." external reference.
  • Node 24.14 and Electron Node 24.15 both import the dist and execute β†’ ok:true.
  • Full applyPlugin simulation (readV1Plugin detect β†’ getLegacyPlugins β†’ server())
    succeeded for all 5 shims under Electron Node.

β†’ Code layer fully fixed; still not visible in the pre-existing GUI session after
restart β†’ moved to the session layer.

4.6 Layer 3 β€” session-scoped tool list snapshot

Queried the SQLite store (opencode.db, session table) and compared session creation
time vs. the fix time:

Session Created todo-sync in tool list
"current conversation" (created before fix) 21:18 ❌
new session (after fix) 22:14+ βœ…

β†’ The GUI session tool list is captured when the session is created. Restarting the
GUI and continuing the old session does not refresh the tool list; a new session
picks up the fixed plugin.

4.7 Final verification

A brand-new session (opencode run) and a new GUI session both:

  • list todo-sync in the tool list;
  • call it successfully β†’ ok:true, expected count, latest task id;
  • push todos to the GUI board (task recorded a todo-sync trace event).

5. Root causes & fixes (summary)

# Root cause Fix Verified
1 Extension-less relative imports rejected by Node Explicit .ts extensions βœ…
2 Workspace dep not bundled β†’ unresolved @scope/... at runtime Self-contained dist via tsdown noExternal βœ…
3 GUI session tool list is a create-time snapshot (workaround) open a new session after plugin changes βœ…

6. Recommendations

6.1 For plugin developers

  1. Always use explicit file extensions in relative imports (.ts). bun tolerates
    extension-less imports; Node does not. Treat "Node direct-load" as the lowest common
    denominator.
  2. Ship a self-contained dist for plugins that depend on workspace packages β€” inline
    those deps (noExternal) so the runtime never has to resolve the workspace chain.
  3. Validate against the GUI loader, not only bun: simulate with
    ELECTRON_RUN_AS_NODE=1 "<app>/Contents/MacOS/OpenCode" + import() of the shim.

6.2 For opencode maintainers (suspected behavior/bug)

  • Session tool list snapshot: after a plugin (or its deps) changes, a pre-existing
    session still shows the old tool list β€” even after the app is restarted and the session
    is resumed. It would be very helpful if the session tool list were refreshed on
    plugin change
    (or re-injected on session resume), instead of captured at creation.
  • Diagnostic hints that helped us:
    • opencode debug agent <agent> shows the config-layer declaration;
    • GUI server.log MODULE_TYPELESS warnings reveal which shims Node actually loads;
    • SQLite session.time_created vs. plugin-change time distinguishes "stale session"
      from "plugin not loaded".

6.3 On agent-driven debugging (why the report is this detailed)

This investigation was driven by an AI coding agent (ABC-style workflow, human sets
direction + accepts results). Highlights of the approach:

  • Layered isolation: CLI vs GUI, config layer vs runtime, code vs session.
  • Environment parity: obtained the GUI's exact Node via ELECTRON_RUN_AS_NODE to
    reproduce outside the app.
  • Source-level ground truth: extracted the app bundle to confirm the loading path
    instead of guessing.
  • Data-driven session analysis: used the SQLite store to timestamp sessions and
    discriminate "stale snapshot" from "load failure".

Human guidance at two key points (check whether the workspace dep is bundled; suspect the
session layer) directly hit two of the three root causes β€” pairing an agent that isolates
fast with a human who points at the right layers converged in ~1 hour of wall time.


7. Reproduction steps (for maintainers)

  1. Create a plugin shim in .opencode/plugins/<name>.ts whose source uses
    extension-less relative imports and imports a workspace package at src/...
    paths.
  2. Run in CLI β†’ tool works.
  3. Open in the GUI desktop β†’ tool missing from the session tool list; server.log shows
    no load warning for that shim.
  4. Fix: add .ts extensions + build self-contained dist; restart the app; continue the
    old session
    β†’ tool still missing; open a new session β†’ tool appears and works.

8. Expected behavior

  • Plugin changes should be visible in a resumed session (refresh or re-inject), or at
    minimum be documented clearly so developers know a new session is required.
  • Plugins should be loadable with a consistent minimal baseline across CLI (bun) and GUI
    (Electron Node) runtimes.

Plugins

No response

OpenCode version

No response

Steps to reproduce

No response

Screenshot and/or share link

No response

Operating System

No response

Terminal

No response

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions