diff --git a/src/renderer/src/components/sidebar/WorktreeCard.test.ts b/src/renderer/src/components/sidebar/WorktreeCard.test.ts index abc03a3c4fd..9b16d03b19f 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.test.ts +++ b/src/renderer/src/components/sidebar/WorktreeCard.test.ts @@ -52,8 +52,9 @@ describe('getWorktreeStatus', () => { livePtyIds ) ).toBe('permission') + // Why: status must be agent-attributable (identity or spinner+launch), not a bare keyword. expect( - getWorktreeStatus([makeTerminalTab('mimo working')], [{ id: 'browser-1' }], livePtyIds) + getWorktreeStatus([makeTerminalTab('✦ Codex')], [{ id: 'browser-1' }], livePtyIds) ).toBe('working') }) }) diff --git a/src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts b/src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts index 913e197691a..de3e2cfefd4 100644 --- a/src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts +++ b/src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts @@ -140,7 +140,8 @@ describe('buildWorktreeAgentRows', () => { expect(rows[0].agentType).toBe('codex') }) - it('prefers an unrelated live title over the launched tab agent for unknown rows', () => { + // Why (#13341): unknown hook rows still honor launch ownership over a nested child title. + it('keeps launch ownership over an unrelated live title for unknown rows', () => { const rows = buildWorktreeAgentRows({ tabs: [makeTab('tab-1', { launchAgent: 'omp', title: '\u280b Codex' })], entries: [ @@ -153,7 +154,7 @@ describe('buildWorktreeAgentRows', () => { now: 2000 }) - expect(rows[0].agentType).toBe('codex') + expect(rows[0].agentType).toBe('omp') }) it('normalizes live Pi-compatible rows from the launched OMP tab agent', () => { diff --git a/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts b/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts index 3610a388587..c1ba2cf81eb 100644 --- a/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts +++ b/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts @@ -203,22 +203,17 @@ export function resolveTitleDerivedAgentType( const agentType = TITLE_AGENT_LABEL_TO_TYPE[label] ?? 'unknown' const owner = ownerAgentType && ownerAgentType !== 'unknown' ? ownerAgentType : null if (agentType === 'claude') { - // Why: Claude's task-title spinner heuristic has no provider identity. In - // split panes it can match arbitrary terminal spinners, so sidebar rows only - // accept Claude when the title itself names Claude. + // Why: spinner heuristic is provider-neutral — require a Claude token. if (!CLAUDE_AGENT_TOKEN_RE.test(title)) { return null } - // Why (#8940 + #13341): Claude mentions never steal ownership, and a nested - // Claude identity frame under a durable non-Claude owner keeps the parent. - // Pane reuse is modeled by clearing launchAgent first (tab-agent path). + // Why (#8940/#13341): never steal a non-Claude owner; reclaim after launch clears. if (owner && owner !== 'claude') { return null } return agentType } - // Why (#13341): nested Codex/Gemini/... titles must not rebrand a pane whose - // launch owner is still a different agent — fall through to ownerAgentType. + // Why (#13341): nested child titles keep the durable owner until launch clears. if (owner && agentType !== 'unknown' && agentType !== owner) { return null } diff --git a/src/renderer/src/lib/use-tab-agent-nested-identity.test.ts b/src/renderer/src/lib/use-tab-agent-nested-identity.test.ts index 55b8c94f0eb..e6da6c2a667 100644 --- a/src/renderer/src/lib/use-tab-agent-nested-identity.test.ts +++ b/src/renderer/src/lib/use-tab-agent-nested-identity.test.ts @@ -102,4 +102,57 @@ describe('resolveTabAgentFromSignals — nested child identity (#13341)', () => }) ).toBe('codex') }) + + it('treats shell-foreground as exit so a later process can own the icon', () => { + expect( + resolveTabAgentFromSignals({ + hasObservedAgentSignal: true, + isRemote: false, + title: '✳ Claude Code', + hookAgent: null, + processAgent: null, + processShellForeground: true, + launchAgent: 'claude' + }) + ).toBeNull() + expect( + resolveTabAgentFromSignals({ + hasObservedAgentSignal: true, + isRemote: false, + title: '✦ Codex', + hookAgent: null, + processAgent: 'codex', + processShellForeground: false, + launchAgent: undefined + }) + ).toBe('codex') + }) + + it('keeps remote parent ownership when only a nested child title is visible', () => { + // Remote panes lack local process evidence; nested title alone must not rebrand. + expect( + resolveTabAgentFromSignals({ + hasObservedAgentSignal: true, + isRemote: true, + title: '✦ Codex', + hookAgent: null, + processAgent: null, + launchAgent: 'claude' + }) + ).toBe('claude') + }) + + it('lets a live different-group hook outrank launch (explicit takeover)', () => { + // Why: hooks are ground truth; fenced nested hooks keep parent agentType upstream. + expect( + resolveTabAgentFromSignals({ + hasObservedAgentSignal: true, + isRemote: false, + title: '✦ Codex', + hookAgent: 'codex', + processAgent: 'codex', + launchAgent: 'claude' + }) + ).toBe('codex') + }) }) diff --git a/src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts b/src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts index abf724a67c1..483d2585c1b 100644 --- a/src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts +++ b/src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts @@ -241,6 +241,7 @@ describe('OpenCode native title tab identity', () => { launchAgent: 'claude' }) ).toBe('claude') + // Why (#13341): nested process under active launch is not stronger than parent ownership. expect( resolveTabAgentFromSignals({ hasObservedAgentSignal: false, @@ -251,7 +252,7 @@ describe('OpenCode native title tab identity', () => { sleepingSessionAgent: 'claude', launchAgent: 'claude' }) - ).toBe('codex') + ).toBe('claude') for (const title of [ 'OpenCode ready', diff --git a/src/renderer/src/lib/use-tab-agent-retained-identity.test.ts b/src/renderer/src/lib/use-tab-agent-retained-identity.test.ts index cccc3e662fe..8ede55ffbf3 100644 --- a/src/renderer/src/lib/use-tab-agent-retained-identity.test.ts +++ b/src/renderer/src/lib/use-tab-agent-retained-identity.test.ts @@ -126,7 +126,8 @@ describe('useTabAgent retained completion identity', () => { expect(latestAgent).toBe('gemini') }) - it('lets an explicit cross-agent title reclaim a retained idle pane', async () => { + // Why (#13341): active launch ownership is durable; nested Claude title does not reclaim. + it('keeps launch ownership over an explicit cross-agent title on a retained idle pane', async () => { const paneKey = makePaneKey(TAB_ID, FOCUSED_LEAF_ID) useAppStore.setState({ retainedAgentsByPaneKey: { [paneKey]: retainedEntry(paneKey, 'codex') } @@ -134,7 +135,7 @@ describe('useTabAgent retained completion identity', () => { await renderProbe({ ...baseTab, launchAgent: 'codex', title: '✳ Claude Code' }) - expect(latestAgent).toBe('claude') + expect(latestAgent).toBe('codex') }) it('keeps focused launch metadata ahead of sibling retained identity', async () => {