Repository navigation
Description
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 4096instance, each TUI launched from a different git repository - Also reproduced with the resident server pattern where
opencodein any directory attaches to the same long-lived server
Reproduction
- Start a shared server:
opencode serve --hostname 127.0.0.1 --port 4096. - Open TUI A in
/repoAand TUI B in/repoB(both git repos, different branches). - In repo A (or externally, e.g. GitHub Desktop), switch its branch to
feature/x. - TUI B's statusline changes to
repoB:feature/x— the branch shown is repo A's, not repo B's. - Closing and reopening TUI B clears the wrong branch (bootstrap
vcs.getrestores 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 onworkspace === project.workspace.current(). With experimental workspaces disabled both sides areundefined, so the guard is always true and any received branch event — from any directory — overwrites the localstore.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.tshas the same unguardedvcs.branch.updatedhandler (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 writestore.vcswhendirectory === project.instance.directory()(in addition to the existing workspace check).session.updated/session.created: only upsert sessions whoseinfo.directoryequals the TUI's directory.
packages/app/src/context/global-sync/event-reducer.ts: same directory guard forvcs.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
github-actions commentedon Aug 11, 2026
This issue might be a duplicate of existing issues. Please check:
/global/eventwithout directory filtering, causingvcs.branch.updatedand session events from other directories to contaminate every attached TUI's state. Identical symptoms (wrong branch in statusline, cross-workspace session leaks) and the same affected files (packages/tui/src/context/sync.tsx,packages/app/src/context/global-sync/event-reducer.ts). TUI applies events from other directories when several TUIs share one server #39181 also notes a pending PR fix.BB-84C commentedon Aug 11, 2026
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.