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:
- 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).
- 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.
- 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
- 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.
- 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.
- 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)
- Create a plugin shim in
.opencode/plugins/<name>.ts whose source uses
extension-less relative imports and imports a workspace package at src/...
paths.
- Run in CLI β tool works.
- Open in the GUI desktop β tool missing from the session tool list;
server.log shows
no load warning for that shim.
- 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
Description
Feedback: Plugin tool missing in GUI desktop session but present in CLI
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) butabsent from the GUI desktop session (OpenCode.app) β even though
opencode debug agentreported the tool as registered.
Root causes, in three layers:
./loaderinstead of./loader.ts): tolerated by bun, rejected by Node (the GUI desktop loads plugin.tsfiles directly via Node's type-stripping).workspace package (
@scope/core) atsrc/...paths; the built artifact kept anunresolved
import "@scope/...", which the GUI runtime could not resolve reliably.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
.tsextensions, and build a self-contained JS dist with theworkspace dependency inlined (
tsdownnoExternal). Verified in new CLI and GUI sessions.2. Environment
opencode)/Applications/OpenCode.app(Electron 42.3.3)ELECTRON_RUN_AS_NODE=1).opencode/plugins/<name>.ts.opencode/node_modules/@scope/plugin~/.local/share/opencode/opencode.db(SQLite)3. Initial symptoms
opencode runsubprocess)todo-synccallable,ok:true, full payloadapprove/ping/guard-pingpresent)opencode debug agent <agent>todo-sync: trueat the config layerThe 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)
.opencode/node_modules/@scope/pluginβ symlink β plugin source dir. βbun build .opencode/plugins/<name>.ts --bundlesucceeded (no stubbednode:modules). β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)
validateclosed 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.~/Library/Application Support/ai.opencode.desktop/logs/*/server.log)showed
MODULE_TYPELESS_PACKAGE_JSONwarnings only for some shims (guard,verify-guard) β never for the failing plugin across multiple restarts.npx @electron/asar extract .../app.asar) and read the serverchunk: plugin loading is
PluginLoader.loadβawait import(row.entry)β i.e. the GUIdirectly imports the
.tsshim 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
.tshandling:.tsshim viasrc/...workspace depsimport(row.entry)+ type-stripping4.4 Layer 1 β extension-less relative imports
Reproduced with Node type-stripping (simulating the GUI loader):
Audit showed the failing module used
"./loader"/"./parse"/"./types"(no.ts),while sibling plugins (
guard/,question-tier/) used"./config.ts"(with.ts) andloaded fine.
Fix 1: add explicit
.tsextensions (3 files, 6 lines). Node direct-import passed;CLI did not regress (tests green,
bun buildexit 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 pluginpackage had no build step β its
exportspointed atsrc/*.ts, so the runtimeresolved the workspace chain directly.
Fix 2: add a
tsdown.config.tswith:noExternal: [/@scope/]so the workspace dependency is inlined into the artifact;exportsrepointed todist/*/index.mjs.Verification of the self-contained artifact:
dist/<name>/index.mjscontains inlined trace-light (3Γ) + a local chunk forframework-keywords β no
from "@scope/..."external reference.ok:true.applyPluginsimulation (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,sessiontable) and compared session creationtime vs. the fix time:
todo-syncin tool listβ 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:todo-syncin the tool list;ok:true, expected count, latest task id;todo-synctrace event).5. Root causes & fixes (summary)
.tsextensions@scope/...at runtimetsdownnoExternal6. Recommendations
6.1 For plugin developers
.ts). bun toleratesextension-less imports; Node does not. Treat "Node direct-load" as the lowest common
denominator.
those deps (
noExternal) so the runtime never has to resolve the workspace chain.ELECTRON_RUN_AS_NODE=1 "<app>/Contents/MacOS/OpenCode"+import()of the shim.6.2 For opencode maintainers (suspected behavior/bug)
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.
opencode debug agent <agent>shows the config-layer declaration;server.logMODULE_TYPELESSwarnings reveal which shims Node actually loads;session.time_createdvs. 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:
ELECTRON_RUN_AS_NODEtoreproduce outside the app.
instead of guessing.
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)
.opencode/plugins/<name>.tswhose source usesextension-less relative imports and imports a workspace package at
src/...paths.
server.logshowsno load warning for that shim.
.tsextensions + build self-contained dist; restart the app; continue theold session β tool still missing; open a new session β tool appears and works.
8. Expected behavior
minimum be documented clearly so developers know a new session is required.
(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