diff --git a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-05.spec.ts b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-05.spec.ts index 307fc1331a5..d610bc4ff24 100644 --- a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-05.spec.ts +++ b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-05.spec.ts @@ -89,6 +89,62 @@ describe('OrcaRuntimeService', () => { expect(suppressed?.type === 'terminal' && suppressed.agentStatus?.terminalTitle).toBeUndefined() }) + it('publishes a renamed tab over the live agent title, and the live title once cleared', async () => { + const runtime = new OrcaRuntimeService(store) + runtime.setPtyController({ + spawn: vi.fn().mockResolvedValue({ id: 'renamed-pty' }), + write: () => true, + kill: () => true, + getForegroundProcess: async () => 'claude' + }) + const leafId = HEADLESS_LEAF_ID + await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { tabId: 'renamed-tab', leafId }) + runtime.attachWindow(1) + const publish = (snapshotVersion: number, customTitle: string | null) => + runtime.syncWindowGraph(1, { + tabs: [ + { + tabId: 'renamed-tab', + worktreeId: TEST_WORKTREE_ID, + title: '✳ Claude Code', + activeLeafId: leafId, + layout: null + } + ], + leaves: [], + mobileSessionTabs: [ + { + worktree: TEST_WORKTREE_ID, + publicationEpoch: 'renderer-renamed', + snapshotVersion, + activeGroupId: null, + activeTabId: `renamed-tab::${leafId}`, + activeTabType: 'terminal', + tabs: [ + { + type: 'terminal', + id: `renamed-tab::${leafId}`, + parentTabId: 'renamed-tab', + leafId, + title: '✳ Claude Code', + customTitle, + isActive: true + } + ] + } + ] + }) + publish(1, 'Build fixer') + runtime.onPtyData('renamed-pty', '\x1b]0;⠂ Claude Code\x07', 124) + + const renamed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) + expect(renamed.tabs[0]).toEqual(expect.objectContaining({ title: 'Build fixer' })) + + publish(2, null) + const cleared = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) + expect(cleared.tabs[0]).toEqual(expect.objectContaining({ title: '⠂ Claude Code' })) + }) + it('uses fresh neutral PTY titles over stale mobile snapshot and OSC titles', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' }) const runtime = new OrcaRuntimeService(store) diff --git a/src/main/runtime/runtime-mobile-session-projection.ts b/src/main/runtime/runtime-mobile-session-projection.ts index 82d2c8162d1..f31994b57b2 100644 --- a/src/main/runtime/runtime-mobile-session-projection.ts +++ b/src/main/runtime/runtime-mobile-session-projection.ts @@ -179,11 +179,11 @@ export function projectRuntimeMobileSessionTabs( const ownerAgent = ownerRecord?.agent ?? liveLeafPty?.foregroundAgent ?? pty?.foregroundAgent ?? null const ownerOptions = { ownerIsLaunch: ownerRecord?.ownerIsLaunch === true } - const title = normalizeCompatibleAgentTitleForOwner( - trackerOnlyTitle ?? leafTitle ?? ptyTitle ?? syncedTab?.title ?? tab.title, - ownerAgent, - ownerOptions - ) + const liveTitle = trackerOnlyTitle ?? leafTitle ?? ptyTitle ?? syncedTab?.title ?? tab.title + // Why: agents repaint their title constantly, so a user's rename must outrank it. + const title = + tab.customTitle?.trim() || + normalizeCompatibleAgentTitleForOwner(liveTitle, ownerAgent, ownerOptions) const liveTitleEvidence = leafTitle ?? ptyTitle // Why: renderer status can precede hook session identity, leaving native chat with no transcript address. const rendererStatusAgent =