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, external is false, so Server.listen() is never called and there is no socket.
-
PluginInput.serverUrl fabricates 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.url is undefined (no listener), both the client baseUrl and the public serverUrl getter fall back to http://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)
Summary
When the TUI runs with the default in-process transport (no
--port/--hostname/--mdns), OpenCode does not bind any TCP listener, butPluginInput.serverUrlstill reports a fabricatedhttp://localhost:4096. Plugins that consumectx.serverUrlas a reachable server (for example to drive the TUI viaopencode 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):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):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
Real-world impact: the
@mspiegel31/opencode-cmuxsubagent-viewer doesopencode attach ${serverUrl} --session ${id}. With the fabricatedhttp://localhost:4096(and the plugin author's documentedport 0workaround), there is no port to attach to, so the viewer aborts silently.Expected behavior
PluginInput.serverUrlshould reflect reality. When no HTTP listener is bound, it should surface the in-process transport placeholder (http://opencode.internal, already used intui.tsandrun.ts) rather than a fabricatedlocalhost: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 tohttp://opencode.internal. This is behavior-neutral for the plugin client (the in-process transport routes viaServer.Default().app.fetchregardless of thebaseUrlhost) and only makes the reported URL honest.I have a PR ready that implements this.
Environment
dev)