Repository navigation
fix(tabs): keep an open diff focused while agents stream (STA-4697) - #15390
Conversation
Three copies of toVisibleTabType had drifted: the runtime one omits 'simulator'. Move the canonical projection next to the two unions it maps between and replace the two copies that are already identical to it. The runtime copy is left alone on purpose - unifying it would change behavior, so it goes with the follow-up.
resolveWebSessionVisibleTabId answered 'which tab is the user looking at' by inverting a many-to-one projection: it compared tab.contentType against the coarse activeTabType. Diff tabs open with activeTabType 'editor' but carry contentType 'diff', so the match never succeeded and the guard returned null - which is the reconciler's signal to fall through and activate a terminal. Every agent status echo republished the snapshot, so the diff lost focus ~300ms after opening, once per click. Same for conflict-review and check-details. Resolve the visible tab from group state instead, which is what is actually on screen and is the rule deriveActiveSurfaceForWorktree already uses. The coarse address survives only when there are no group records, now projected rather than compared exactly. Also follow the entity within the group when reconcile rematerializes the visible tab under a new id, and teach the browser-create focus guard to observe the group records the resolver now reads.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
💤 Files with no reviewable changes (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe change centralizes visible tab type conversion in Merge Risk: ⚪ Minimal · up to This localized change keeps the visible diff tab focused during agent activity and includes targeted regression coverage and validation; no actionable merge-blocking risk remains beyond normal checks and review. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review status: LANDCompleted the independent same-model review loop plus readiness checks at both the start and end of the session. No P0/P1/P2 findings; counsel was not required.
No code changes were warranted during this review, so there was nothing additional to commit or push; local and remote PR heads remain Residual validation gaps are narrow and recorded: the visual proof used a branch diff rather than an unstaged diff (same Verdict: land as-is. |
…tablyai#15390) * refactor(tabs): put the visible-tab-type projection in one place Three copies of toVisibleTabType had drifted: the runtime one omits 'simulator'. Move the canonical projection next to the two unions it maps between and replace the two copies that are already identical to it. The runtime copy is left alone on purpose - unifying it would change behavior, so it goes with the follow-up. * fix(tabs): keep an open diff focused while agents stream (STA-4697) resolveWebSessionVisibleTabId answered 'which tab is the user looking at' by inverting a many-to-one projection: it compared tab.contentType against the coarse activeTabType. Diff tabs open with activeTabType 'editor' but carry contentType 'diff', so the match never succeeded and the guard returned null - which is the reconciler's signal to fall through and activate a terminal. Every agent status echo republished the snapshot, so the diff lost focus ~300ms after opening, once per click. Same for conflict-review and check-details. Resolve the visible tab from group state instead, which is what is actually on screen and is the rule deriveActiveSurfaceForWorktree already uses. The coarse address survives only when there are no group records, now projected rather than compared exactly. Also follow the entity within the group when reconcile rematerializes the visible tab under a new id, and teach the browser-create focus guard to observe the group records the resolver now reads.
…tablyai#15390) * refactor(tabs): put the visible-tab-type projection in one place Three copies of toVisibleTabType had drifted: the runtime one omits 'simulator'. Move the canonical projection next to the two unions it maps between and replace the two copies that are already identical to it. The runtime copy is left alone on purpose - unifying it would change behavior, so it goes with the follow-up. * fix(tabs): keep an open diff focused while agents stream (STA-4697) resolveWebSessionVisibleTabId answered 'which tab is the user looking at' by inverting a many-to-one projection: it compared tab.contentType against the coarse activeTabType. Diff tabs open with activeTabType 'editor' but carry contentType 'diff', so the match never succeeded and the guard returned null - which is the reconciler's signal to fall through and activate a terminal. Every agent status echo republished the snapshot, so the diff lost focus ~300ms after opening, once per click. Same for conflict-review and check-details. Resolve the visible tab from group state instead, which is what is actually on screen and is the rule deriveActiveSurfaceForWorktree already uses. The coarse address survives only when there are no group records, now projected rather than compared exactly. Also follow the entity within the group when reconcile rematerializes the visible tab under a new id, and teach the browser-create focus guard to observe the group records the resolver now reads.
…tablyai#15390) * refactor(tabs): put the visible-tab-type projection in one place Three copies of toVisibleTabType had drifted: the runtime one omits 'simulator'. Move the canonical projection next to the two unions it maps between and replace the two copies that are already identical to it. The runtime copy is left alone on purpose - unifying it would change behavior, so it goes with the follow-up. * fix(tabs): keep an open diff focused while agents stream (STA-4697) resolveWebSessionVisibleTabId answered 'which tab is the user looking at' by inverting a many-to-one projection: it compared tab.contentType against the coarse activeTabType. Diff tabs open with activeTabType 'editor' but carry contentType 'diff', so the match never succeeded and the guard returned null - which is the reconciler's signal to fall through and activate a terminal. Every agent status echo republished the snapshot, so the diff lost focus ~300ms after opening, once per click. Same for conflict-review and check-details. Resolve the visible tab from group state instead, which is what is actually on screen and is the rule deriveActiveSurfaceForWorktree already uses. The coarse address survives only when there are no group records, now projected rather than compared exactly. Also follow the entity within the group when reconcile rematerializes the visible tab under a new id, and teach the browser-create focus guard to observe the group records the resolver now reads.


ELI5
Open a diff while an agent is running and the diff would flash up, then the app would jump back to the terminal. The diff tab stayed in the tab bar, just not selected — so clicking more files piled up dead tabs.
The code that decides "which tab is the user looking at?" was asking the wrong question. It only knew a coarse answer — "an editor" — and then went hunting for a tab whose type was literally
editor. A diff tab's type isdiff, so the hunt always failed, and "I found nothing" is the same signal the reconciler uses for "nothing to preserve, go ahead and pick a terminal."Now it just asks the tab groups what is on screen, which is the thing that actually knows.
What Changed
resolveWebSessionVisibleTabIdno longer inverts the coarse address. When group records exist:activeGroupIdByWorktree, elsegroups[0]— the existing recovery inderiveActiveSurfaceForWorktree)null— authoritative that nothing is visible, rather than resolving into some other groupactiveTabId, validated withtab.groupId === activeGroup.idtoVisibleTabTypeinstead of comparing exactlyTwo supporting changes:
web-runtime-session.ts— the browser-create focus guard's change filter now observesgroupsByWorktree/activeGroupIdByWorktree, which the resolver newly reads. Without this a group-only focus move returns early and never re-evaluates.toVisibleTabTypebeside the two unions it maps between and drops the two copies already identical to it.Why
toVisibleTabTypeis many-to-one:editor | diff | conflict-review | check-detailsall collapse to'editor', andactiveTabTypeis produced by it. Inverting that by equality is wrong in principle —'diff'is just the instance that bit. Fixing only the comparison would leave the inversion in place, so the resolver now works forward from concrete group state.That also makes it immune to a real drift:
activateTabwrites group state only, whilesetActiveFile/setActiveTabTypewrite the legacy records separately, so the two disagree transiently in ordinary flows.Pre-existing, not a regression. The guard was introduced 2026-06-15 by #5435 (
413fb31ab5) matchingcontentType === 'editor'— blind to diff tabs from day one, which is a little ironic given #5435 is the focus-steal fix this guard's comment cites. #13876 (70ad65fc94, 08-12) hoisted it and preserved the blindness. Present inv1.4.184. No cherry-pick: the 08-15 release shipped as v1.4.183 with v1.4.184 on top, and no release-prep branch or RC is in flight.Reachability. On
mainthe reducer is reached only viarefreshWebRuntimeSessionTabsSnapshot, gated on a boundactiveRuntimeEnvironmentId— so remote / SSH / paired / headless-serve workspaces can hit this on shipped builds. The report came from an adhoc build of a side branch that adds a second, ungated caller, which is why it reproduced on a plain local workspace there.Deliberately not in this PR
nulland a terminal wins). Preserving them needs a tagged focus result, both callers, and group/layout preservation in the reconciler's group rebuild — an unreserved empty client group is discarded during rebuild regardless of what the resolver returns.activeTabTypestill reports'terminal'on a terminal echo, exactly as today, becausecurrentVisibleTabTypeStillValidhas no simulator branch. Fixing that goes with unifying the thirdtoVisibleTabTypecopy.Linked Issue
STA-4697
Visual Proof
N/A— reproducing the symptom live needs a bound runtime environment plus agents actively streaming, which does not survive a screenshot. The behavior is pinned by an end-to-end test instead: against the pre-fix resolver it fails withexpected 'web-terminal-host-tab-1' to be 'local-diff-tab', which is precisely the reported symptom (diff replaced by the terminal, tab retained).Testing
macOS.
vitest run --config config/vitest.config.ts src/renderer/src/runtime src/renderer/src/store/slices— 374 files, 3473 tests passed.pnpm typecheck— exit 0, and proven live: injectingconst x: number = "s"producedTS2322, then reverted. This repo prints nothing on success, so a silent pass is otherwise indistinguishable from a dead gate.oxfmt --check— clean, echoed 8 files so the list actually arrived.pnpm lint— exit 0.The full-suite run earned its keep: it caught a regression that static review missed. When a local editor tab is rematerialized as a mirrored tab under a new id, the group's pre-reconcile
activeTabIdno longer resolves, so focus was dropped and the snapshot took over — breakingdoes not let stale browser intent override a newer editor selection. Step 4 above fixes it, pinned by two tests (follows the entity; does not follow it across groups).Review
Agent skill upstream boundary
Notes
worktreeIdthroughout.findinstead of scanning for a type/entity match.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)Author