anomalyco / anomalyco/opencode

tui: a plugin that fails to load or initialize produces no diagnostic at any log level

Open
#41,574 0 comments 0 reactions 1 assignee View on GitHub

@simonklee is already working on this.

Since Aug 10, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Summary

A TUI plugin that fails produces no diagnostic anywhere: nothing in opencode.log at --log-level DEBUG, nothing on stderr with --print-logs, no toast, and no entry in plugin-meta.json. This holds for both failure kinds the loader distinguishes internally — a module that throws at import, and a module whose tui() throws.

The shipped bundle contains messages for exactly these cases (failed to load tui plugin, failed to initialize tui plugin, and a report.error switch over install / compatibility / entry stages), so the intent is clearly that they be reported. They never fire.

The practical cost is that a broken plugin and an unloaded plugin are indistinguishable from the outside. I spent several hours concluding that external TUI plugins did not load at all in 1.18.16, because my first plugin was subtly broken and the failure was invisible; a working plugin proved otherwise later. Any user hitting a plugin error has no thread to pull.

Environment

  • opencode: 1.18.16 (npm latest), and reproduced on 0.0.0-dev-202608101200 (npm dev)
  • Binary: official installer copy and the npm opencode-ai@1.18.16 copy are byte-identical (sha256 8e4ac80fe535537e6ee03cee1a8e23af3d0da56db1ae0ce3fffad3ea188a1768)
  • OS: Linux x86_64
  • Isolated HOME, XDG_CONFIG_HOME, XDG_STATE_HOME, XDG_DATA_HOME, XDG_CACHE_HOME — no user config involved
  • Launched with --print-logs --log-level DEBUG

Reproduction

Three plugins, declared together so a working one proves the loader ran at all in the same process:

good.tsx — loads normally, and records that it did:

const LOG = process.env.EVIDENCE_LOG!
const mark = async (what: string) =>
  Bun.write(LOG, (await Bun.file(LOG).text().catch(() => "")) + `${what}\n`)

await mark("GOOD-1-IMPORTED")

export default {
  id: "probe.good",
  tui: async (api: { app: { version: string } }) => {
    await mark(`GOOD-2-TUI-CALLED version=${api.app.version}`)
  },
}

bad.tsx — throws at import:

throw new Error("PROBE-BAD-THREW-AT-IMPORT")

ugly.tsx — imports, then throws inside tui():

export default {
  id: "probe.ugly",
  tui: async () => {
    throw new Error("PROBE-UGLY-THREW-IN-TUI")
  },
}

$XDG_CONFIG_HOME/opencode/tui.json:

{
  "plugin": [
    "/abs/path/good.tsx",
    "/abs/path/bad.tsx",
    "/abs/path/ugly.tsx"
  ]
}

Then: EVIDENCE_LOG=/tmp/run.log opencode --print-logs --log-level DEBUG

Expected

Each failing plugin produces a diagnostic naming the plugin and the error — the messages already written for this in the loader.

Actual

good.tsx loads and runs (GOOD-1-IMPORTED, GOOD-2-TUI-CALLED version=1.18.16), and plugin-meta.json records probe.good and probe.ugly.

For bad.tsx and ugly.tsx:

$ grep -c 'tui plugin' $XDG_DATA_HOME/opencode/log/opencode.log
0
$ grep -cE 'PROBE-(BAD|UGLY)' $XDG_DATA_HOME/opencode/log/opencode.log
0

Nothing on stderr either, and no toast.

Control, same run, same log file — the log is wired and flushing, and the TUI config subsystem does write to it:

level=INFO message="loading tui config" path=.../tui.json
level=INFO message="applying tui config" path=.../tui.json order=1

So the absence is the loader's, not the logger's.

One asymmetry worth noting, because it makes the silence harder to diagnose: bad.tsx leaves no plugin-meta.json entry, while ugly.tsx leaves one with load_count: 1. So a plugin that throws during init is recorded as loaded and reports nothing — from the outside it looks like a plugin that ran fine and did nothing.

Possibly related

  • #33884 — npm-spec TUI plugins silently fail to load (open)
  • #36505 — external TUI plugins parsed but not loaded, closed against the v2 branch
  • #32996 — TUI plugin JSX slot output silently dropped

Those are three different root causes with the same shape: the plugin does not work and nothing says why. This report is only about the missing diagnostic, which would have shortened all three.

Separate minor observation

In three fresh directories, declaring plugin entries caused opencode to write package.json, package-lock.json and .gitignore into the config dir, but node_modules was never materialized — while specs/tui-plugins.md documents package.json, bun.lock, node_modules/ and .gitignore. npm install in that directory then completes in ~150 ms. Plugins that import nothing (like the repro above) are unaffected, which is why this is a footnote rather than the report — but a plugin that does import ... from "@opencode-ai/plugin/tui" at runtime would fail there, and by the above it would fail silently.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.