Skip to content

TUI statusline shows git branches from other workspaces when multiple TUIs attach to one shared server #41839

Closed

Description

@BB-84C

Summary

When multiple TUIs attach to a single shared server (opencode serve), switching the git branch in any workspace updates the statusline branch shown by every attached TUI to the wrong repository's branch. The statusline of a TUI in repo A shows repo B's branch, and the poison persists until the TUI is reloaded.

The branch switch does not even need to happen inside a TUI: switching the branch externally (e.g. in GitHub Desktop, or with plain git in a shell) triggers the same cross-workspace display contamination.

Environment

  • opencode 1.18.7 / 1.18.13 (identical behavior; the relevant code is unchanged between these versions)
  • Reproduced with multiple TUIs attached to one opencode serve --hostname 127.0.0.1 --port 4096 instance, each TUI launched from a different git repository
  • Also reproduced with the resident server pattern where opencode in any directory attaches to the same long-lived server

Reproduction

  1. Start a shared server: opencode serve --hostname 127.0.0.1 --port 4096.
  2. Open TUI A in /repoA and TUI B in /repoB (both git repos, different branches).
  3. In repo A (or externally, e.g. GitHub Desktop), switch its branch to feature/x.
  4. TUI B's statusline changes to repoB:feature/x — the branch shown is repo A's, not repo B's.
  5. Closing and reopening TUI B clears the wrong branch (bootstrap vcs.get restores the correct value) until the next cross-workspace branch event arrives.

Root cause

The TUI subscribes to the control-plane global event stream at /global/event (packages/sdk/js/src/v2/gen/sdk.gen.ts — global.event hardcodes url: "/global/event"). Unlike the instance-routed /event endpoint, /global/event is served by packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts, which subscribes to the GlobalBus with no directory filtering whatsoever — every event from every instance is broadcast to every subscriber. The event envelope does carry the originating directory (GlobalEventSchema: { directory, project, workspace, payload }), and the TUI's event callback receives it (packages/tui/src/context/event.ts passes { directory, workspace } into every handler).

However, the handlers in packages/tui/src/context/sync.tsx ignore the directory:

  • vcs.branch.updated (line ~433): guards only on workspace === project.workspace.current(). With experimental workspaces disabled both sides are undefined, so the guard is always true and any received branch event — from any directory — overwrites the local store.vcs.branch, which is what the statusline renders (packages/tui/src/context/directory.ts → routes/session/footer.tsx).
  • session.updated (line ~279) / session.created (event-reducer): upsert any received session into the local session list, regardless of which directory it belongs to.
  • packages/app/src/context/global-sync/event-reducer.ts has the same unguarded vcs.branch.updated handler (if (input.store.vcs?.branch === props.branch) break; setStore("vcs", ...)).

Impact

  • Display contamination: the statusline branch, and the session list, reflect other workspaces' state. This is the immediately visible symptom.
  • Workspace routing risk: because foreign sessions are upserted into the TUI's session store, a session belonging to another directory can be picked/continued, and the session-routed requests then execute in that other directory (the agent's tools, files and prompts route by session.directory). This is the severe form of the same defect.

Proposed fix

Guard the event handlers on the envelope's directory against the TUI's own instance directory, which is already available at every call site:

  • packages/tui/src/context/sync.tsx:
    • vcs.branch.updated: only write store.vcs when directory === project.instance.directory() (in addition to the existing workspace check).
    • session.updated / session.created: only upsert sessions whose info.directory equals the TUI's directory.
  • packages/app/src/context/global-sync/event-reducer.ts: same directory guard for vcs.branch.updated.

The directory metadata is already in the event envelope precisely so consumers can isolate streams; the TUI side just does not use it today.

Activity

  1. github-actions commented on Aug 11, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. BB-84C commented on Aug 11, 2026

    @BB-84C
    SponsorContributorAuthor

    Duplicate of #39181 (same root cause: the TUI subscribes to /global/event without directory filtering; same affected handlers in packages/tui/src/context/sync.tsx). Closing in favor of #39181.

    This report adds one extra reproduction detail worth keeping: the branch switch does not need to happen inside a TUI — switching the branch externally (e.g. GitHub Desktop) triggers the same cross-workspace statusline contamination, because the fs watcher publishes through the same global stream.

    The client-side directory guards for the vcs and session handlers are implemented in PR #41842 (Closes #39181). The plugin-driven fan-out symptoms documented in #39181 (tui.session.select / tui.toast.show / tui.command.execute / tui.prompt.append) remain open there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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