You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
tui: saved tabs are shared between local and remote servers #47998
The V2 TUI shares saved session tabs between the managed local server and explicit remote servers. Opening another server can restore the previous server's tabs, and changing that layout can affect the tabs restored when returning.
Each server should restore its own tabs in both global and current-directory scopes. Selected terminal panes should also stay separate, even when servers have identical session or terminal IDs.
Steps to reproduce
Open the managed local TUI and leave some session tabs open.
Exit, then launch with --server <remote-url> using the same TUI storage channel. For current-directory scope, use the same launch directory.
Observe that saved tabs are shared with the other server.
Change the remote layout, exit, and return locally. The local layout should remain independent.
Adding reproduction evidence measured on 2.0.19, since this report had no attached data. Same setup as described: managed local service plus an explicit --server remote, both clients sharing one cli.json with tabs.mode: "on" and tabs.scope: "global".
The tab store is one file per machine, and --server is not part of its key
~/.local/state/opencode/latest/tui/tabs.json has two buckets, global and cwd. A client connected to the remote server writes its session IDs into that same local file:
All three are remote sessions. The cwd bucket stayed empty throughout, so this is purely a consequence of scope: "global".
API-level confirmation
Each stored ID resolves on the remote server and is a 404 on the local one — deterministic, not intermittent:
sessionID
local service
remote --server
ses_f1307fc2…
404 SessionNotFoundError
present (location.directory: /projects)
ses_f13c841…
404 SessionNotFoundError
present
ses_f13cb85…
404 SessionNotFoundError
present
$ opencode api get /api/session/ses_f1307fc2…HTTP 404 Not Found{"_tag":"SessionNotFoundError","sessionID":"ses_f1307fc2…","message":"Session not found: ses_f1307fc2…"}
$ opencode api get /api/session/ses_f1307fc2… --server http://<remote-url>{"data":{"id":"ses_f1307fc2…","title":"WebHarness","location":{"directory":"/projects"}, …}}
Points that may matter for the fix
Not a version-skew artifact. Both sides report 2.0.19.
No role=cli log entry. Grepping ~/.local/share/opencode/log/opencode.log for the error returns nothing from the client; it surfaces only in the TUI. Any fix or regression test relying on client logs will not find it as-is.
Session IDs carry no server identity, so the store needs a namespace — the endpoint URL, or something stable derived from it. Nothing else in the entry distinguishes the origin.
The cwd workaround is incidental here, not a guarantee. It works in this setup only because the two path namespaces cannot collide (/Users/… locally vs. /projects/… on the remote). A client sharing a directory string with a foreign server hits the same failure, which matches the scope of this report.
Workarounds tried
Workaround
Result
tabs.scope: "cwd"
Works here, incidental. Fails if directory strings ever collide across servers.
OPENCODE_CLI_CONFIG_CONTENT='{"tabs":{"mode":"off"}}' per invocation
Works, disables the feature for that client only.
Environment: opencode 2.0.19 (client and server), macOS 27.0.1 arm64, Apple_Terminal, zsh, channel latest, no plugins.
Full write-up including the redacted draft: #52080
@reisi007 Your reproduction matches the collision addressed by #47999: saved global and cwd layouts are now scoped by server, including when directory strings or session IDs coincide. Existing legacy storage is preserved for managed local use, so previously mixed remote tabs may still need closing once. The branch is updated against V2; 70 focused tests and the repository lint/typecheck check pass. Could you verify your local/remote workflow against it?
Still reproduces on 2.0.26 (fresh client, single machine) — and the collision covers cwd buckets too; per-client launch directories work as a reliable workaround
Since there's an open question above about which build includes the fix, here's fresh data: this is still present in the latest release we have — CLI 2.0.26 on macOS.
Repro (one machine, three TUI clients, one per server):
Launch each client from the same directory (e.g. a shared home dir): opencode --server http://hostA:49374 / http://hostB:… / http://hostC:…
Open two sessions on client B (server B).
The two tabs immediately show up in the tab bars of clients A and C. Clicking one of the foreign tabs shows Session not found: ses_… and interrupts the current view; after that failed click the foreign tabs get pruned and both clients return to normal on their own.
State evidence from the client machine — ~/.local/state/opencode/latest/tui/tabs.json:
A freshly launched 2.0.26 client still writes this unscoped shape, and pre-existing mixed buckets aren't migrated either — so it doesn't look like the fix(tui): keep saved tabs separate by server #47999 scoping has shipped in 2.0.26. Happy to re-test as soon as a build that includes it is available.
Workaround until then: give each client its own launch directory (we spawn every client with a per-server cwd). With distinct cwds the tab stores are fully isolated and the cross-server leak stops.
Heads-up for anyone watching this issue: the linked fix PR #47999 was closed on Oct 8 by the automated PR cleanup (age/volume criteria) — not because it was rejected, superseded, or merged. As of today there is no open PR carrying the server-scoping fix, and the unscoped tab store is still present on dev (packages/tui/src/context/session-tabs.tsx persists tabs with no server identity) and still reproduces on the latest release.
@abeisleem would you consider reopening #47999 or pushing a fresh branch? Since the cleanup criteria weigh community engagement, a 👍 on this issue and on the PR should also help the fix survive the next sweep.
Thanks for the report. I reproduced this on 2.2.0, and it still happens on the latest dev (d1bd20fa).
I ran a separate opencode serve as the remote server and launched the TUI with the same storage and from the same directory:
The local TUI opened a session LOCAL-ONLY-SESSION.
opencode --server http://127.0.0.1:4199 restored LOCAL-ONLY-SESSION in its tab bar, although that server has no such session.
After opening REMOTE-ONLY-SESSION on the remote, the next local launch showed both tabs.
Both layouts are written to the same tui/tabs.json with no per-server key. The selected terminal pane is saved the same way (session-terminal-selection), but I only confirmed that part from the code. The script below reproduces it. It drives the real TUI in a pty, because our recording tool can't launch the TUI with --server, so there's no video.
Related: #18302 (the same problem in the desktop app), #37667 and #45346.
Reproduction script (repro.sh)
#!/usr/bin/env bash# Reproduces anomalyco/opencode#47998: TUI saved tabs are shared between the# managed local server and a remote --server.# Needs: opencode (v2) on PATH, python3, curl. Set OC="bun ... packages/cli/src/index.ts" to run from source.set -euo pipefail
OC=${OC:-opencode}
W=${W:-/tmp/opencode/repro-47998}
rm -rf "$W"; mkdir -p "$W/local""$W/remote""$W/proj"cd"$W/proj"# Minimal pty runner: runs the TUI for N seconds, then Ctrl-C; saves raw output.
cat >"$W/pty_run.py"<<'PY'import os, pty, sys, time, select, fcntl, termios, struct, signalsecs=float(sys.argv[1]); out=sys.argv[2]; cmd=sys.argv[3:]pid, fd = pty.fork()if pid == 0: os.environ["TERM"]="xterm-256color"; os.execvp("bash", ["bash","-c"," ".join(cmd)])fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH",40,140,0,0))buf=b""def pump(t): global buf end=time.time()+t while time.time()<end: r,_,_=select.select([fd],[],[],0.1) if r: try: d=os.read(fd,65536) except OSError: return if not d: return buf+=dpump(secs)for _ in range(3): try: os.write(fd,b"\x03") except OSError: break pump(1)try: os.kill(pid, signal.SIGTERM)except ProcessLookupError: passpump(1); open(out,"wb").write(buf)PYtabbar() { sed 's/\x1b\[[0-9;?<>=]*[a-zA-Z]//g; s/\x1b[][^\x07]*\x07//g'"$1"| tr -s ''| grep -aoE "(LOCAL|REMOTE)-ONLY-SESSION.{0,40}New session"| head -1 ||true; }
# "Remote" server: separate HOME, so its sessions are separate.
HOME=$W/remote $OC serve --hostname 127.0.0.1 --port 4199 >"$W/remote.log"2>&1&
SERVE=$!;trap'kill $SERVE 2>/dev/null || true; HOME=$W/local $OC service stop >/dev/null 2>&1 || true' EXIT
sleep 4
PW=$(grep -o 'server password .*'"$W/remote.log"| awk '{print $3}')export HOME=$W/local # same TUI storage for both launches$OC service start >/dev/null
LID=$($OC api POST /api/session -d '{"title":"LOCAL-ONLY-SESSION"}'| python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["id"])')
RID=$(curl -s -u opencode:$PW -XPOST -H 'content-type: application/json' 127.0.0.1:4199/api/session -d '{"title":"REMOTE-ONLY-SESSION"}'| python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["id"])')echo"== 1. local TUI opens its session =="
python3 "$W/pty_run.py" 8 "$W/1.raw"$OC -s $LID; tabbar "$W/1.raw"echo"== 2. remote TUI (--server), no session given: tab bar =="
OPENCODE_SERVER_PASSWORD=$PW python3 "$W/pty_run.py" 8 "$W/2.raw"$OC --server http://127.0.0.1:4199; tabbar "$W/2.raw"echo"== 3. remote TUI opens its own session =="
OPENCODE_SERVER_PASSWORD=$PW python3 "$W/pty_run.py" 8 "$W/3.raw"$OC --server http://127.0.0.1:4199 -s $RID; tabbar "$W/3.raw"echo"== 4. back to local TUI: tab bar =="
python3 "$W/pty_run.py" 8 "$W/4.raw"$OC; tabbar "$W/4.raw"echo"== tabs.json (no per-server key) =="
cat "$HOME"/.local/state/opencode/*/tui/tabs.json;echo
Thanks for the detailed report — #60887 is the same bug and I'm marking it as a duplicate there.
I re-read the current dev implementation and the diagnosis holds:
packages/tui/src/context/session-tabs.tsx persists { global, cwd } and selects store.cwd[paths.cwd] / store.global without any server identity, so two servers share the same tui/tabs.json entries.
packages/tui/src/context/storage.tsx watches the state directory and reloads every entry, so each process observes the other server's session IDs and then rewrites the shared entry from its own view. That explains both the cross-server tabs and the duplicate/flipping titles.
There is also prior work here: #47999 ("fix(tui): keep saved tabs separate by server") implemented exactly this — a frozen serverIdentity in context/client.tsx, per-server keys for tabs, and per-server terminal state, with tests. It was closed by the automated age/no-linked-issue cleanup, not because the approach was rejected.
The issue is currently assigned to @simonklee, so I don't want to open a competing PR. If you are not actively working on it, I'm happy to revive #47999 rebased onto current dev with Fixes #47998, keeping the original author's work, or to help review/test a fresh implementation. Just say the word.
Adding a data point from my report (#62063): the TUI already prunes tab entries whose sessions don't exist on the connected server — after both TUIs exited, tabs.json only contained sessions belonging to that TUI's server; the dead cross-server entries were gone. The dead tabs are still visible during a running session though, and clicking one shows "session not found". So validating tabs against the connected server at load time (dropping entries whose sessionID doesn't resolve) would match the cleanup that already happens at exit, and make the strips self-heal.
Workaround meanwhile: launch each server connection from its own dedicated directory so the cwd-keyed state never collides.
Activity
reisi007 commentedon Sep 29, 2026
Adding reproduction evidence measured on 2.0.19, since this report had no attached data. Same setup as described: managed local service plus an explicit
--serverremote, both clients sharing onecli.jsonwithtabs.mode: "on"andtabs.scope: "global".The tab store is one file per machine, and
--serveris not part of its key~/.local/state/opencode/latest/tui/tabs.jsonhas two buckets,globalandcwd. A client connected to the remote server writes its session IDs into that same local file:{ "global": { "tabs": [ { "sessionID": "ses_f1307fc2…", "title": "WebHarness" }, { "sessionID": "ses_f13c841…", "title": "Open Accredidation" }, { "sessionID": "ses_f13cb85…", "title": "LuminaRust" } ], "unread": {} }, "cwd": {} }All three are remote sessions. The
cwdbucket stayed empty throughout, so this is purely a consequence ofscope: "global".API-level confirmation
Each stored ID resolves on the remote server and is a 404 on the local one — deterministic, not intermittent:
--serverses_f1307fc2…SessionNotFoundErrorlocation.directory: /projects)ses_f13c841…SessionNotFoundErrorses_f13cb85…SessionNotFoundErrorPoints that may matter for the fix
2.0.19.role=clilog entry. Grepping~/.local/share/opencode/log/opencode.logfor the error returns nothing from the client; it surfaces only in the TUI. Any fix or regression test relying on client logs will not find it as-is.cwdworkaround is incidental here, not a guarantee. It works in this setup only because the two path namespaces cannot collide (/Users/…locally vs./projects/…on the remote). A client sharing a directory string with a foreign server hits the same failure, which matches the scope of this report.Workarounds tried
tabs.scope: "cwd"OPENCODE_CLI_CONFIG_CONTENT='{"tabs":{"mode":"off"}}'per invocationEnvironment: opencode 2.0.19 (client and server), macOS 27.0.1 arm64, Apple_Terminal, zsh, channel
latest, no plugins.Full write-up including the redacted draft: #52080
abeisleem commentedon Oct 5, 2026
@reisi007 Your reproduction matches the collision addressed by #47999: saved global and cwd layouts are now scoped by server, including when directory strings or session IDs coincide. Existing legacy storage is preserved for managed local use, so previously mixed remote tabs may still need closing once. The branch is updated against V2; 70 focused tests and the repository lint/typecheck check pass. Could you verify your local/remote workflow against it?
reisi007 commentedon Oct 5, 2026
Is there a build where it is included?
fanrj3 commentedon Oct 9, 2026
Still reproduces on 2.0.26 (fresh client, single machine) — and the collision covers
cwdbuckets too; per-client launch directories work as a reliable workaroundSince there's an open question above about which build includes the fix, here's fresh data: this is still present in the latest release we have — CLI 2.0.26 on macOS.
Repro (one machine, three TUI clients, one per server):
opencode --server http://hostA:49374/http://hostB:…/http://hostC:…Session not found: ses_…and interrupts the current view; after that failed click the foreign tabs get pruned and both clients return to normal on their own.State evidence from the client machine —
~/.local/state/opencode/latest/tui/tabs.json:{ "global": { "tabs": [], "unread": {} }, "cwd": { "<shared launch dir>": { "tabs": [ { "sessionID": "ses_…", "title": "…" }, { "sessionID": "ses_…", "title": "…" } ], "unread": {} } } }Notes:
{sessionID, title}with no server identity anywhere, andcwdbuckets are keyed by launch directory only — so clients with different--servertargets that share a launch dir share one bucket. (Same mechanism as tui: global tabs are shared across clients with different --server targets, causing SessionNotFound #52080; we hit it through thecwdbucket.)cwd). With distinct cwds the tab stores are fully isolated and the cross-server leak stops.sghng commentedon Oct 16, 2026
Heads-up for anyone watching this issue: the linked fix PR #47999 was closed on Oct 8 by the automated PR cleanup (age/volume criteria) — not because it was rejected, superseded, or merged. As of today there is no open PR carrying the server-scoping fix, and the unscoped tab store is still present on
dev(packages/tui/src/context/session-tabs.tsxpersists tabs with no server identity) and still reproduces on the latest release.@abeisleem would you consider reopening #47999 or pushing a fresh branch? Since the cleanup criteria weigh community engagement, a 👍 on this issue and on the PR should also help the fix survive the next sweep.
opencodejr commentedon Oct 28, 2026
Thanks for the report. I reproduced this on 2.2.0, and it still happens on the latest
dev(d1bd20fa).I ran a separate
opencode serveas the remote server and launched the TUI with the same storage and from the same directory:LOCAL-ONLY-SESSION.opencode --server http://127.0.0.1:4199restoredLOCAL-ONLY-SESSIONin its tab bar, although that server has no such session.REMOTE-ONLY-SESSIONon the remote, the next local launch showed both tabs.Both layouts are written to the same
tui/tabs.jsonwith no per-server key. The selected terminal pane is saved the same way (session-terminal-selection), but I only confirmed that part from the code. The script below reproduces it. It drives the real TUI in a pty, because our recording tool can't launch the TUI with--server, so there's no video.Related: #18302 (the same problem in the desktop app), #37667 and #45346.
Reproduction script (repro.sh)
Run it:
bash repro.shYidaWeng commentedon Oct 31, 2026
Thanks for the detailed report — #60887 is the same bug and I'm marking it as a duplicate there.
I re-read the current
devimplementation and the diagnosis holds:packages/tui/src/context/session-tabs.tsxpersists{ global, cwd }and selectsstore.cwd[paths.cwd]/store.globalwithout any server identity, so two servers share the sametui/tabs.jsonentries.packages/tui/src/context/storage.tsxwatches the state directory and reloads every entry, so each process observes the other server's session IDs and then rewrites the shared entry from its own view. That explains both the cross-server tabs and the duplicate/flipping titles.There is also prior work here: #47999 ("fix(tui): keep saved tabs separate by server") implemented exactly this — a frozen
serverIdentityincontext/client.tsx, per-server keys for tabs, and per-server terminal state, with tests. It was closed by the automated age/no-linked-issue cleanup, not because the approach was rejected.The issue is currently assigned to @simonklee, so I don't want to open a competing PR. If you are not actively working on it, I'm happy to revive #47999 rebased onto current
devwithFixes #47998, keeping the original author's work, or to help review/test a fresh implementation. Just say the word.andycostintoma commentedon Nov 5, 2026
Adding a data point from my report (#62063): the TUI already prunes tab entries whose sessions don't exist on the connected server — after both TUIs exited,
tabs.jsononly contained sessions belonging to that TUI's server; the dead cross-server entries were gone. The dead tabs are still visible during a running session though, and clicking one shows "session not found". So validating tabs against the connected server at load time (dropping entries whosesessionIDdoesn't resolve) would match the cleanup that already happens at exit, and make the strips self-heal.Workaround meanwhile: launch each server connection from its own dedicated directory so the cwd-keyed state never collides.