Skip to content

fix(sidebar): give a split tab's generated title only to the pane that produced it - #20857

Open
bokeeeey wants to merge 6 commits into
stablyai:mainfrom
bokeeeey:fix/split-pane-generated-title
Open

bokeeeey wants to merge 6 commits into
stablyai:mainfrom
bokeeeey:fix/split-pane-generated-title

Conversation

@bokeeeey

@bokeeeey bokeeeey commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

With "Auto-generate tab titles" on, Orca names a tab after the first agent prompt it sees. When a tab is split and each pane runs its own agent, every agent row for that tab shows that one name, so the second agent is listed under the first agent's task. Orca now remembers which pane the name came from, and only that pane's rows use it.

What Changed

  • TerminalTab.generatedTitlePaneKey (optional) records the pane whose prompt produced generatedTitle.
    • applyGeneratedTabTitleUpdates sets it together with the title.
    • It is persisted with .catch(undefined), so a malformed value can't drop the tab on restore.
    • The workspace layout model (shared/workspace-layout/) loads, saves and publishes it with the title, so a runtime-started pane that rewrites the partition keeps it. A generated title set by renameTab names no pane, so it clears the source.
  • isGeneratedTabTitleOwnedByPane(tab, paneKey) in shared/agent-tab-title.ts is the single rule:
    • If no source is recorded (titles saved before this change), or the caller passes no pane, today's tab-wide behavior is kept.
    • Otherwise only the source pane's rows may use the title.
  • The rule is applied at the same place fix(sidebar): let an agent row show the provider's own session title #19936 scopes provider titles:
    • getAgentRowConversationName takes paneKey. useAgentRowConversationName and rowConversationName pass the row's pane key, which covers worktree-card rows, dashboard rows and the board snapshot.
    • getActivityThreadTaskTitle reads entry.paneKey, which covers the Agents list and the Activity page.
    • orchestrationSenderName passes the sender's pane, so a split sibling's orchestration messages don't carry another pane's title.
  • The paired web client carries its client-local source along with the title when it rebuilds a mirrored tab, but only under the same local tab id. terminalTabEqual compares the new field.
  • A paired server's layout frames (server-layout-frame-ids.ts) translate the source with its tab: the tab id changes between the server's and the desktop's id, and the leaf stays. An unreadable source is dropped, so the title stays tab-wide.
  • The tab strip label and first-write-wins are unchanged. An identical replacement title is still a no-op, so the source doesn't flip between panes.

Why

Known limits (out of scope):

Linked Issue

Fixes #20856

Visual Proof

Headless Electron e2e build, two Codex agents, tabAutoGenerateTitle on.

Scenario Before (main) After
Split tab, both agents start Both rows: "Fix the patient intake flow in the" Left: "Fix the patient intake flow in the"; right: "Design a Redis cache strategy for sessions"
Left pane names the tab, then split, then the right agent starts Both rows: the left pane's title Left keeps its title; right: its own prompt
One agent next to a shell "Fix the patient intake flow in the" Unchanged

The Agents list matches the worktree-card rows in each case.

Worktree card rows (split tab, both agents start)

Before (main) After
before-workspace-card v3b-two-workspace-card

Agents list (same scenario)

Before (main) After
before-agents-list v3b-two-agents-list

Left pane names the tab first, then split, then the right agent starts (after)

v3b-late-workspace-card

Testing

  • I manually tested these changes locally (macOS; headless Electron e2e build, scenarios above)
  • Automated tests added/updated
    • agent-generated-tab-title-pane.test.ts: the writer records the source, a later sibling doesn't take it, and a dispatch replacement moves it.
    • workspace-session-schema.test.ts: the field persists, and a malformed value keeps the tab.
    • agent-row-conversation-name.test.ts, use-agent-row-conversation-name.test.ts, dashboard-card-labels.test.ts: the producer keeps the title, a sibling shows its own name, and an omitted pane keeps today's behavior.
    • activity-thread-display.test.ts: the Agents-list title.
    • mirrored-generated-title.test.ts: the web client keeps the source only under the same tab id.
    • runtime-orchestration-sender-names.test.ts: a sender gets the title only from its own pane.
    • server-layout-frame-ids.test.ts: the source follows its tab's id from server to desktop and back.
    • Reverting the production files makes 8 new assertions fail across 7 test files. The mirrored-tab guard (a source recorded under another tab id is not copied) passes either way and pins the copy condition.
    • Workspace layout: the profile fixture's agent tab now carries a source, so the Loader/Serializer round trip, the round-trip self-check, the disk-field table and the frame/restore parity tests cover it. Dropping it from the Serializer fails 5 of them. workspace-layout-commands.test.ts covers the renameTab clear.
  • Follow-up after an adversarial review (Oct 24): the orchestration sender name and the server frame translation above. Both new tests fail without their fix.
  • Rebased on main (Oct 24). The one conflict was renameTab, where main added the first-title rule (The window shows tab, title, move and divider changes only from main's update, never drawing them itself (dark, layout-owner build only) #31257). Both rules are kept: a generated rename without replaceExisting is still refused, and an accepted one clears the source pane.
  • Rebased on main again (Oct 26). Chat renames are saved as the chat's name, so Session History shows them after the tab closes #31602 moved renameTab into workspace-layout-tab-rename.ts, so the source-pane clear moved with it. It also made rowConversationName take two more arguments, which the dashboard test now passes. The other conflict was the comment cleanup ([comment audit] Complete repository-wide comment cleanup #33249) in activity-thread-display.ts.
  • pnpm typecheck and pnpm lint pass locally.
  • pnpm run build:desktop passed through the CLI, Electron and web bundles on the Oct 16 base; I didn't rerun it after this rebase. Its last step (mobile-web) needs the mobile/ dependencies, which my checkout doesn't have.
  • vitest over src/shared, src/main/runtime, src/main/persistence/terminal-topology, and the renderer's runtime, store, app-shell, lib, components/dashboard and components/activity: 46,608 tests pass. One test fails (structured-claude-thinking-display-refusal.test.ts), and it fails the same way on main on my machine. The run also reports unhandled EnvironmentTeardownErrors from late module loads in store and worktree tests; main reports the same kind when I run those files there.

AI Disclosure

I used Claude Code (Claude Opus 5, and Opus 5.5 for the rebase follow-up) to help investigate the bug, write the change and tests, and run adversarial reviews of the diff. I reviewed every change myself and verified it locally, including the e2e scenarios above.

Review

  • Cross-platform: shared/renderer logic only, no platform APIs.
  • SSH / remote / paired web: the field is persisted like generatedTitle. Old sessions and old clients keep today's behavior. There is no RPC change.
  • Mobile / wire: no RPC or runtime-graph payload changes. The new data is the optional persisted field above, which older builds ignore, and the same optional field on a terminal in the layout.subscribe stream, as feat(layout): publish each pane's live terminal title on the layout stream #28928 did for the pane liveTitle. Frame decoding checks only the top-level shape, so a reader that doesn't know the field ignores it and keeps today's tab-wide behavior. conversationName keeps its shape and meaning. Mobile per-leaf surface titles are not changed here; the headless-host projection (buildHeadlessMobileSessionTerminalTabs) still uses the tab-level title.
  • Performance: one string comparison per row; no new selectors or subscriptions.
  • Security: none.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d551382a-fa8a-424f-8d32-294489ffb8c9
📥 Commits

Reviewing files that changed from the base of the PR and between 8490b06e56df81157d4b333ce02333cbe17ef321 and b2570962017b9093ecdf94f6b84a82fe4c0c10a7.

📒 Files selected for processing: 21
  • src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts
  • src/renderer/src/components/dashboard/use-agent-row-conversation-name.ts
  • src/renderer/src/lib/activity-thread-display.test.ts
  • src/renderer/src/lib/activity-thread-display.ts
  • src/renderer/src/runtime/web-session-tabs-sync/mirrored-generated-title.test.ts
  • src/renderer/src/runtime/web-session-tabs-sync/terminal-build.ts
  • src/shared/agent-row-conversation-name.test.ts
  • src/shared/agent-row-conversation-name.ts
  • src/shared/terminal-tab-types.ts
  • src/shared/workspace-layout/workspace-layout-commands.test.ts
  • src/shared/workspace-layout/workspace-layout-disk-fields.ts
  • src/shared/workspace-layout/workspace-layout-from-published.ts
  • src/shared/workspace-layout/workspace-layout-load-tabs.ts
  • src/shared/workspace-layout/workspace-layout-model.ts
  • src/shared/workspace-layout/workspace-layout-profile.test-fixture.ts
  • src/shared/workspace-layout/workspace-layout-published.test.ts
  • src/shared/workspace-layout/workspace-layout-published.ts
  • src/shared/workspace-layout/workspace-layout-save-tabs.ts
  • src/shared/workspace-layout/workspace-layout-tab-commands.ts
  • src/shared/workspace-session-schema.test.ts
  • src/shared/workspace-session-schema.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Walkthrough

The change adds generatedTitlePaneKey to terminal tabs and persists valid values. Generated-title updates record the source pane, while mirrored-tab rebuilds preserve the key only for matching local tabs. Conversation-name and activity-thread resolution now receives pane context and uses a generated title only for its owning pane. Dashboard and agent-row callers pass that context. Tests cover ownership, persistence, synchronization, and split-pane display behavior.

Priority: Normal

Severity of issue fixed: Medium

Merge Risk: Minimal · up to b2570

No confirmed issue blocks merging; generated titles remain associated with the pane that produced them in the inspected row views.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 27 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed The PR meets the coding requirements in [#20856]. applyGeneratedTabTitleUpdates records generatedTitlePaneKey with the generated title. isGeneratedTabTitleOwnedByPane preserves tab-wide behavior…
Out of Scope Changes check Passed The changes stay within [#20856]. The new field, ownership helper, persistence and layout plumbing, mirrored-tab handling, resolver updates, and regression tests support pane-scoped generated titles o…
Title check Passed The title clearly and concisely describes the primary change: limiting a split tab's generated title to the pane that produced it.
Description check Passed The pull request description follows the required template and provides a clear ELI5 summary, implementation details, rationale, linked issue, visual proof, testing results, AI disclosure, review cons…
Full details: Linked Issues check

Explanation

The PR meets the coding requirements in [#20856]. applyGeneratedTabTitleUpdates records generatedTitlePaneKey with the generated title. isGeneratedTabTitleOwnedByPane preserves tab-wide behavior for legacy titles and calls without a pane key. Row resolvers pass pane keys for worktree-card, dashboard, board, Agents list, and Activity paths. Persistence, workspace layout transfer, and mirrored-tab handling carry the field. The schema drops malformed values without rejecting the tab. Tests cover source ownership, sibling behavior, activity display, persistence, renames, and mirrored tabs.

Full details: Out of Scope Changes check

Explanation

The changes stay within [#20856]. The new field, ownership helper, persistence and layout plumbing, mirrored-tab handling, resolver updates, and regression tests support pane-scoped generated titles or backward compatibility. No unrelated product behavior is demonstrated.

Full details: Description check

Explanation

The pull request description follows the required template and provides a clear ELI5 summary, implementation details, rationale, linked issue, visual proof, testing results, AI disclosure, review considerations, known limits, and checklist status. It also discloses that one test fails on the author’s machine and that the build was not rerun after the latest rebase, so CI should verify those items.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one scope question and a small test nitpick.

Reviewed changes

  • Source pane recorded on the tab — TerminalTab.generatedTitlePaneKey is written alongside generatedTitle in applyGeneratedTabTitleUpdates, persisted through terminalTabSchema with .catch(undefined), and compared by terminalTabEqual.
  • Single ownership rule — isGeneratedTabTitleOwnedByPane in shared/agent-tab-title.ts gates the generated title on the row's pane key, falling back to tab-wide for untagged titles or callers that pass no pane.
  • Applied at both row producers — getAgentRowConversationName (worktree-card rows, dashboard rows, board snapshot) and getActivityThreadTaskTitle (Agents list, Activity page) now pass the row's paneKey.
  • Paired web client — buildMirroredTerminalTabs carries the client-local source pane only when the existing tab id matches the mirrored local id, and terminalTabEqual compares the new field.

I verified the tests actually pin the behavior: all 7 changed test files pass (85 tests) with the change, and reverting the 10 non-test files makes 8 new assertions fail across 7 files. tc:web passes, and the related activity / web-session-tabs-sync / generated-title / dashboard suites pass (256 tests).

ℹ️ Headless mobile projection still stamps the tab's generated title on every leaf

The PR body says "the host transport never carried generatedTitle", but the headless path does carry it. buildHeadlessMobileSessionTerminalTabs emits one surface per leaf and sets each leaf's title to tab.generatedTitle, so a headless-host split tab still shows one pane's generated title on every leaf — the same class of mislabel this PR fixes for rows. The renderer publication path deliberately scopes the tab-wide title to the launch-agent leaf only, so the two paths disagree.

Technical details
# Headless mobile projection applies the tab's generated title to every leaf

## Affected sites
- `src/main/runtime/mobile-session-terminal-projection.ts:26-31` — `title = customTitle || generatedTitle || title || defaultTitle` is computed per leaf, so all leaves of a split tab get the tab's `generatedTitle`.
- Contrast `src/renderer/src/runtime/sync-runtime-graph/mobile-session-terminal-tabs.ts:78-86` — the renderer path gates the tab-wide title on `tabWideFallbackSafe` (`launchAgentLeafId === leafId`), so only the launch-agent leaf receives it.

## Required outcome
- Decide whether a headless-host mobile surface should follow the same pane ownership as the desktop rows. If yes, the projection needs the leaf id (`tab.id::leafId` already carries it) compared against `generatedTitlePaneKey`; if no, the PR description's "host transport never carried generatedTitle" claim should be corrected.

## Open questions for the human
- Is the mobile per-leaf surface intentionally tab-wide (mirroring the tab strip), or should it match the row behavior this PR establishes?

ℹ️ Nitpicks

  • The second assertion in mirrored-generated-title.test.ts ("drops a source pane recorded under another tab id so the title is not hidden") passes without the production change — when the field does not exist, no pane key is ever carried. It still guards against an unconditional copy, but the PR's "every new assertion fails without the production change" claim does not hold for it.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@bokeeeey

Copy link
Copy Markdown
Contributor Author

Thanks for the review.

On the headless projection: buildHeadlessMobileSessionTerminalTabs gives every leaf a tab-level name in general, not just generatedTitle. It also falls back to tab.title, which is the focused pane's title, and it doesn't read tabAutoGenerateTitle. Scoping only the generated title there wouldn't make headless leaves pane-accurate, so I kept this PR to the row resolvers and corrected the description, which now notes the headless projection is unchanged. Happy to follow up separately if per-pane titles are wanted there.

On the nitpick: agreed, that guard passes without the change. I've reworded the testing note.

@bokeeeey
bokeeeey force-pushed the fix/split-pane-generated-title branch from 8490b06 to b257096 Compare October 16, 2026 07:03
@bokeeeey
bokeeeey force-pushed the fix/split-pane-generated-title branch from b257096 to b782476 Compare October 23, 2026 22:10
bokeeeey and others added 6 commits October 26, 2026 22:41
…t produced it

The auto-generated tab title comes from one pane's agent prompt but is stored
tab-wide, so every agent row in a split tab showed it. Record the pane that
produced the title and let only that pane's rows use it: workspace-card and
dashboard rows, the board snapshot, and the Agents list. Titles saved before
the source was recorded keep today's behavior.
…ace layout model

The workspace layout model writes terminal rows from its own fields, so a
runtime-started pane that saved the partition dropped generatedTitlePaneKey
from every tab in it, and the disk-field table no longer type-checked. Load,
save and publish the source pane with the generated title. A generated title
set by command names no pane, so it clears the source.

Also import the surface-id helper from shared/, as the renderer re-export
was removed.
… the rebase

A generated rename now replaces an existing title only with replaceExisting, and
buildMirroredTerminalTabs takes the live PTY ids before the sort offset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… own pane

The chat names a terminal sender as its sidebar row does, but it did not pass the
sender's pane, so a split sibling's messages carried the title another pane's
prompt produced. Pass the pane key the naming source already resolves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A paired server's frames move a terminal tab to the desktop's surface id, but the
title's source pane kept the server's tab id, so no row matched it and the title
left every agent row. Translate it both ways with the tab; the leaf is shared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stablyai#31602 made `rowConversationName` take the chat tab names and unified tabs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bokeeeey
bokeeeey force-pushed the fix/split-pane-generated-title branch from 66ae4eb to 4d8b54d Compare October 26, 2026 13:52

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: In a split tab, one agent pane's auto-generated title names every agent row of that tab

1 participant