fix(sidebar): name each split-pane agent row from its own pane title (STA-2811) - #14707
Conversation
…(STA-2811) Agent rows are per pane, but their conversation name came from `tab.title`, which carries only the FOCUSED pane's title. In a split tab every row showed one pane's name, and all of them changed when the user clicked a sibling. Rows on a multi-pane tab now resolve their own leaf's runtime pane title via the existing `resolveRuntimePaneTitleForLeaf`, and fall back to no live title rather than a sibling's. Single-pane tabs pass `undefined` and are unchanged. Extracts the subagent grouping out of build-dashboard-snapshot.ts, which was exactly at the 300-line cap.
📝 WalkthroughWalkthroughThe change adds runtime pane-title resolution for split terminal layouts. Dashboard row and hook logic now passes each row’s pane title to conversation-name generation. Tab-level custom, quick-command, generated, and single-pane naming behavior remains supported. Dashboard snapshot construction now supplies runtime pane titles and uses centralized subagent grouping. Tests cover layout resolution, pane-specific names, fallback behavior, precedence rules, and split-tab snapshots. Merge Risk: 🟡 Moderate · up to In split tabs, an OpenCode live title can override a generated tab-owned name for one row, causing inconsistent naming within the same tab. This is a bounded correctness issue that should be fixed before merge. 🚥 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/renderer/src/components/dashboard/agent-row-pane-live-title.ts (1)
4-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShorten the new API comments.
src/renderer/src/components/dashboard/agent-row-pane-live-title.ts#L4-L15: Replace the long behavior and cost description with one sentence that defines theundefinedandnullcontract.src/shared/agent-row-conversation-name.ts#L117-L121: Reduce the parameter comment to one sentence that defines split-panenulland single-paneundefined.As per coding guidelines, “Comments must be concise, non-obvious, and brief—prefer one line.”
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 1f34c9fb-a9fc-43d9-ac2e-45480234dc08
📒 Files selected for processing (10)
src/renderer/src/components/dashboard/agent-row-pane-live-title.test.tssrc/renderer/src/components/dashboard/agent-row-pane-live-title.tssrc/renderer/src/components/dashboard/build-dashboard-snapshot.test.tssrc/renderer/src/components/dashboard/build-dashboard-snapshot.tssrc/renderer/src/components/dashboard/dashboard-card-labels.tssrc/renderer/src/components/dashboard/dashboard-subagent-cards.tssrc/renderer/src/components/dashboard/use-agent-row-conversation-name.test.tssrc/renderer/src/components/dashboard/use-agent-row-conversation-name.tssrc/shared/agent-row-conversation-name.test.tssrc/shared/agent-row-conversation-name.ts
| const liveTitle = | ||
| paneLiveTitle === undefined ? (tab.title?.trim() ?? '') : (paneLiveTitle?.trim() ?? '') |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve generated titles for every split-pane row.
When generatedTitlesEnabled is true and paneLiveTitle is an OpenCode semantic title, Line 134 returns the pane title before the generated-title check. A split tab can then show generatedTitle for one pane and an OpenCode title for another pane. Check generatedTitle before the OpenCode early return when paneLiveTitle !== undefined. This keeps single-pane precedence unchanged. Add a test with generatedTitlesEnabled === true and OC | ... as paneLiveTitle.
Proposed fix
const liveTitle =
paneLiveTitle === undefined ? (tab.title?.trim() ?? '') : (paneLiveTitle?.trim() ?? '')
+ const generatedTitle = generatedTitlesEnabled ? tab.generatedTitle?.trim() : ''
+ if (paneLiveTitle !== undefined && generatedTitle) {
+ return generatedTitle
+ }
if (isMeaningfulOpenCodeTerminalTitle(liveTitle)) {
return liveTitle
}
- const generatedTitle = generatedTitlesEnabled ? tab.generatedTitle?.trim() : ''
if (generatedTitle) {
return generatedTitle
}The PR objective requires generated tab-owned names to apply to all rows.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const liveTitle = | |
| paneLiveTitle === undefined ? (tab.title?.trim() ?? '') : (paneLiveTitle?.trim() ?? '') | |
| const liveTitle = | |
| paneLiveTitle === undefined ? (tab.title?.trim() ?? '') : (paneLiveTitle?.trim() ?? '') | |
| const generatedTitle = generatedTitlesEnabled ? tab.generatedTitle?.trim() : '' | |
| if (paneLiveTitle !== undefined && generatedTitle) { | |
| return generatedTitle | |
| } | |
| if (isMeaningfulOpenCodeTerminalTitle(liveTitle)) { | |
| return liveTitle | |
| } | |
| if (generatedTitle) { | |
| return generatedTitle | |
| } |
$electron validation — split-pane agent rows are named per paneRan on an isolated dev instance of this branch ( Rig: one tab split into two panes, a different agent in each — The before/after below is a true A/B on the same live instance: the fix was reverted in place ( Before — fix reverted, rows take the focused pane's titleBoth rows carry one name, and both flip when the user clicks the sibling pane. The Codex row is labelled with the Claude pane's title — the exact mislabelling this PR fixes. After — fix applied, each row is named from its own paneTwo rows, two different, correct names — and identical across the focus flip, even though Tab-owned names still survive the split (#11070's diagnosis, kept)Setting a tab custom title ( Full window, fix appliedReview gates
Known limits (found in review, not blockers — filing/leaving as-is)
|
…(STA-2811) (stablyai#14707) Agent rows are per pane, but their conversation name came from `tab.title`, which carries only the FOCUSED pane's title. In a split tab every row showed one pane's name, and all of them changed when the user clicked a sibling. Rows on a multi-pane tab now resolve their own leaf's runtime pane title via the existing `resolveRuntimePaneTitleForLeaf`, and fall back to no live title rather than a sibling's. Single-pane tabs pass `undefined` and are unchanged. Extracts the subagent grouping out of build-dashboard-snapshot.ts, which was exactly at the 300-line cap.
…(STA-2811) (stablyai#14707) Agent rows are per pane, but their conversation name came from `tab.title`, which carries only the FOCUSED pane's title. In a split tab every row showed one pane's name, and all of them changed when the user clicked a sibling. Rows on a multi-pane tab now resolve their own leaf's runtime pane title via the existing `resolveRuntimePaneTitleForLeaf`, and fall back to no live title rather than a sibling's. Single-pane tabs pass `undefined` and are unchanged. Extracts the subagent grouping out of build-dashboard-snapshot.ts, which was exactly at the 300-line cap.






ELI5
Split a terminal tab in two, run an agent in each pane, and the sidebar shows two rows — but both rows carried the same name, and that name changed to whichever pane you clicked last. The name came from the tab's title, and a tab only ever holds the focused pane's title.
Each row now reads its own pane's title.
What Changed
getAgentRowConversationNamegains an optionalpaneLiveTitle. Itsundefineddefault is today's behavior, so a single-pane tab is untouched.Both callers — the sidebar/dashboard hook
useAgentRowConversationNameand its pop-out-board mirrorrowConversationName— now resolve it through one shared helper:Three outcomes, and the middle one is the point:
paneLiveTitleundefinednullTab-owned names still apply to every row in the tab.
customTitle,quickCommandLabelandgeneratedTitleshort-circuit aboveliveTitle; the user gave those to the whole tab and none of them flip on focus. Only the live title is pane-specific by nature, and only it is redirected. Pinned by a test.resolveRuntimePaneTitleForLeafis the repo's existing leaf-keyed live-title resolver (runtime-pane-title-leaf-id.ts, since #7043), already used byrunning-agent-targets.ts,notes-send-agent-targets.tsandactive-agent-note-target.ts. This PR adds no resolver; it stops the naming path from being the one place that reads a tab-scoped title for a pane-scoped row.Incidental: one extraction
build-dashboard-snapshot.tssat at exactly the 300-linemax-linescap, so any addition trips lint andmax-linesdisables are forbidden. The self-contained subagent-grouping loop moved verbatim todashboard-subagent-cards.ts(groupSubagentsByParentPaneKey). No behavior change; the file's existing tests cover it.Why
PR #14702 (STA-3264, the jointly-Running sibling of this bug) located this one precisely and deliberately left it open rather than bundling:
That is the whole mechanism, and it is a different root cause from what #14702 fixed. #14702 corrected runtime-pane-title → leaf attribution inside
buildTitleDerivedAgentRows. This is the naming path: everything upstream of it is already pane-scoped (agentStatusByPaneKey,selectLiveAgentStatusEntriesForWorktree, and the rowpaneKeyitself are alltabId:leafId), and then the last step reaches for a tab-scoped field.pty-connection.tskeepstab.titleon the focused pane on purpose — so two agents in a split don't make the tab title flicker — anduse-terminal-pane-lifecycle.tsre-syncs it on every focus change. That is exactly the "flips to whichever pane was clicked last" in the title of #11069.Introduced by #9989, whose design note reads "row identity consistently follows tab identity". Split panes are the case where one tab holds several independent sessions.
Supersedes #11070 (@pythonstrup)
#11070's diagnosis is correct and this PR keeps it, including the call that tab-owned names must survive in a split. It is superseded on the remedy, not the analysis.
#11070 suppresses the live title in a split (
tabHasSplitPanes ? '' : tab.title) and lets every split row fall back to its per-pane label. That stops the flip, but it also states as a known limit:That channel already exists.
resolveRuntimePaneTitleForLeaf(layout, paneTitles, leafId)bridges exactly those two id spaces and has since #7043. So the infrastructure #11070 deferred to a follow-up is a one-line call, and its second known limit — "hook-less split agents now fall back to their agent label... both read as duplicates" — does not have to ship. Under #11070 two Codex panes in a split both readCodex; here they read their own titles.null(no title resolvable for this leaf) reproduces #11070's behavior exactly, so its safe case is this PR's fallback, not its ceiling.Constraints from the surrounding cluster, checked
fix(sidebar): keep owned agent panes visible when their title carries no agent frame (#14464) #14650's split guard is untouched.
resolveTitleDerivedPaneOwnerreturns an owner only for a single-leaf layout — becauselaunchAgentis tab-scoped and a split pane must not brand its sibling. This PR does not touchworktree-title-derived-agent-rows.tsor any owner resolution.STA-3354 (per-render global status-map scans) is not multiplied. The added selector returns a string primitive, so it cannot churn on identity and does not subscribe rows to title frames. Its cost per store update is:
layout?.root?.type) and return. No map scan, no allocation, no tree walk.It never touches the global status map, so it adds nothing to the scan STA-3354 is about.
parsePaneKeyruns once per render (not per store update), matching theparsePaneKey(parentPaneKey)already in the hook. The pop-out builder reuses the worktree-scoped maps already in scope and hoists one selector call that was previously made inline.Linked Issue
Fixes #11069
Supersedes #11070.
Not superseded, all distinct root causes in the same cluster: #14702 (STA-3264, pane-title → leaf attribution), #14650 (#14464, owner fallback for title-less panes), #14615 (STA-2069, hook status → spawn pane).
Visual Proof
Captured live on one Orca dev instance (isolated
ORCA_DEV_USER_DATA_PATH, CDP + Playwright). Before and after are the same running pane: the fix was reverted in place, Vite HMR flipped the live sidebar, then it was restored — so nothing here is a rebuild or a different session.Two Claude panes in one split tab: the left pane's title is
✳ Linear work log, the right pane's is✳ Redis cache strategy.BEFORE — focus on the left pane. Both rows read the left pane's name:
BEFORE — click the right pane. Both rows rename together. This is the reported symptom:
AFTER — same panes. Each row carries its own name:
AFTER — click the right pane. Nothing renames; the rows are unmoved:
No layout, spacing, color, typography, or component change —
docs/STYLEGUIDE.mdis not engaged. The only difference is which text an already-existing row resolves.Testing
Scoped checks only — a full
pnpm typecheckOOMs on this host, so it was run against a project narrowed to the touched modules and their transitive graph (clean; it caught one real error in a new fixture, a missingexpandedLeafId, which is fixed).oxlinton every changed file — clean. The gate is proven live, not silent: it rejected the first version of this change withmax-linesonbuild-dashboard-snapshot.ts, which is what forced the extraction above.oxlint --config config/oxlint-react-doctor.json(pre-commit) — clean, and it does see the newuseAppStoreselector.tsc --noEmitover the touched modules + their consumers (worktree-card-compact-agent-row.tsx,DashboardAgentRow.tsx) — clean.vitest: dashboard + dashboard-popout + sidebar + shared resolver — 297 files / 2661 tests pass.Tests added — and proven to fail without the fix. The fix was reverted in place (
liveTitleback totab.title) and the suite re-run; these four failed and then passed again on restore:src/shared/agent-row-conversation-name.test.tsuse-agent-row-conversation-name.test.tsuse-agent-row-conversation-name.test.tsuse-agent-row-conversation-name.test.tsThe board half is pinned separately: dropping the
paneLiveTitleargument indashboard-card-labels.tsfailsnames each pane of a split tab from its own titleinbuild-dashboard-snapshot.test.ts— so the sidebar and the pop-out board cannot silently drift apart.Tests that must keep passing under that revert, and do — they pin unchanged behavior:
leaves a single-leaf tab on its tab title,still lends a tab-owned name to every pane,keeps tab-owned names above the pane title.agent-row-pane-live-title.test.tsadditionally covers a nested split, where replay creation order[A, B, C]differs from tree order[A, C, B]— the case where any positional shortcut would silently hand a pane its sibling's title.Merge compatibility — verified by materializing and running, not by GitHub's flags (which were stale elsewhere in this sweep):
git merge-treePlatforms: macOS (this host). The change is a string and a tree walk — no shortcuts, accelerators, modifier keys, path construction, shell invocation, or Electron platform APIs — so there is nothing platform-dependent to diverge.
Review
conversationNamefield is unchanged in shape and bound (boundedLabelOrUndefinedstill applies); only which per-pane string fills it changes, and it was already an optional field.mobile/does not import this resolver (grepped).root: null(a snapshot taken mid-teardown) reads as not-split and reverts to the status quo — degraded to today's behavior, never a new wrong name.Checklist
typecheckandbuildleft to CI (OOM on this host)Author