Repository navigation
Description
Summary
When the TUI runs with the default in-process transport (no --port / --hostname / --mdns), OpenCode does not bind any TCP listener, but PluginInput.serverUrl still reports a fabricated http://localhost:4096. Plugins that consume ctx.serverUrl as a reachable server (for example to drive the TUI via opencode attach <serverUrl> --session <id>) are handed a dead URL and fail silently.
Root cause
Two connected spots:
-
Default TUI never binds.
packages/opencode/src/cli/cmd/tui.ts(≈ L233–249):const external = hasArg("--port") || hasArg("--hostname") || network.mdns === true const transport = external ? { url: (await client.call("server", network)).url, ... } // real TCP listener : { url: "http://opencode.internal", fetch: createWorkerFetch(client), ... } // in-process RPC, no bind
With no network flags,
externalis false, soServer.listen()is never called and there is no socket. -
PluginInput.serverUrlfabricates a live-looking URL anyway.packages/opencode/src/plugin/index.ts(≈ L141–161):const serverUrl = Server.url // undefined when not bound baseUrl: serverUrl?.toString() ?? "http://localhost:4096", ... get serverUrl(): URL { return Server.url ?? new URL("http://localhost:4096") // nothing listening here }
When
Server.urlisundefined(no listener), both the clientbaseUrland the publicserverUrlgetter fall back tohttp://localhost:4096, which implies a reachable TCP server. There isn't one.
Reproduction
# 1. Start the TUI with no network flags (default).
# 2. Confirm it binds nothing:
lsof -nP -i TCP -sTCP:LISTEN -p <tui-pid> # → empty
# Compare:
opencode --port 4096 # binds 127.0.0.1:4096 (reachable)Real-world impact: the @mspiegel31/opencode-cmux subagent-viewer does opencode attach ${serverUrl} --session ${id}. With the fabricated http://localhost:4096 (and the plugin author's documented port 0 workaround), there is no port to attach to, so the viewer aborts silently.
Expected behavior
PluginInput.serverUrl should reflect reality. When no HTTP listener is bound, it should surface the in-process transport placeholder (http://opencode.internal, already used in tui.ts and run.ts) rather than a fabricated localhost:4096, so plugins can detect there is no external TCP server.
Proposed fix
In plugin/index.ts, change both ?? "http://localhost:4096" / ?? new URL("http://localhost:4096") fallbacks to http://opencode.internal. This is behavior-neutral for the plugin client (the in-process transport routes via Server.Default().app.fetch regardless of the baseUrl host) and only makes the reported URL honest.
I have a PR ready that implements this.
Environment
- opencode 1.18.9 (also present on
dev) - macOS (darwin/arm64)
Activity
github-actions commentedon Jul 29, 2026
This issue might be a duplicate of existing issues. Please check:
ctx.serverUrlreturns a fabricatedhttp://localhost:4096when no HTTP server is bound in default TUI mode — and requests anensureServer()API as a fix.localhost:4096when no listener exists) and proposes exporting the actual server URL as an environment variable.Both issues stem from the same code path you've identified in
plugin/index.ts. It may be worth coordinating with those threads, especially since your proposed fix (changing the fallback tohttp://opencode.internal) is complementary to the solutions discussed there.rgillinlz commentedon Jul 29, 2026
Attaching a ready-to-apply patch implementing the proposed fix, in case it's useful before a PR is up. Apply with:
git am < plugin-serverurl-inprocess.patchSummary of the change (in
packages/opencode/src/plugin/index.ts):baseUrlfallback:"http://localhost:4096"→"http://opencode.internal"serverUrlgetter fallback:new URL("http://localhost:4096")→new URL("http://opencode.internal")Behavior-neutral for the plugin client (the in-process transport routes via
Server.Default().app.fetchregardless ofbaseUrlhost); it only makes the reportedserverUrlhonest so plugins can detect there's no external TCP server.plugin-serverurl-inprocess.patch
github-actions commentedon Sep 29, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.