Skip to content

TUI default in-process transport makes PluginInput.serverUrl report a fabricated localhost:4096 (plugins can't attach) #39561

Description

@rgillinlz

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:

  1. 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.

  2. 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)

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