From abe77d5b630443c7d71d1256b9d8a4e9155d3abc Mon Sep 17 00:00:00 2001 From: Eotel <49769982+Eotel@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:46:32 +0900 Subject: [PATCH] fix(activity): name a resumed session by its own title in the agents list (#41127) The Agents list titled a thread with the tab's generated title, the first prompt that tab saw. After `claude --resume` in a new tab, that is the message sent after the resume, so the list kept a status note as the thread's name while the tab and the agent row showed the session's own name. Rank a named provider title of the pane's own session above the generated title, as resolveTabNameSource does, and share the session ownership check with the agent row. --- .../activity/activity-thread-display.test.ts | 34 +++++++++++++++++++ .../activity/activity-thread-display.ts | 22 ++++++++++-- .../agent-row-conversation-name.ts | 10 ++---- .../contracts/ai-vault-session-title.ts | 9 +++++ 4 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 src/renderer/src/lib/workspace/activity/activity-thread-display.test.ts diff --git a/src/renderer/src/lib/workspace/activity/activity-thread-display.test.ts b/src/renderer/src/lib/workspace/activity/activity-thread-display.test.ts new file mode 100644 index 00000000000..b5d075b0ea5 --- /dev/null +++ b/src/renderer/src/lib/workspace/activity/activity-thread-display.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { getActivityThreadTaskTitle } from './activity-thread-display' + +const resumedTab = { + customTitle: null, + aiVaultTitle: { + agent: 'claude' as const, + sessionId: 'session-1', + title: 'Triage lead handover', + kind: 'named' as const + }, + generatedTitle: 'Mac restarted at 15 01 so this tab stopped', + title: 'Triage lead handover', + defaultTitle: 'Terminal 2' +} + +const resumedEntry = { + prompt: 'How is the rename going?', + stateHistory: [], + agentType: 'claude' as const, + providerSession: { key: 'session_id' as const, id: 'session-1' } +} + +describe('getActivityThreadTaskTitle', () => { + it("names a resumed session by its own name, not the tab's first prompt", () => { + expect( + getActivityThreadTaskTitle({ + entry: resumedEntry, + tab: resumedTab, + generatedTitlesEnabled: true + }) + ).toBe('Triage lead handover') + }) +}) diff --git a/src/renderer/src/lib/workspace/activity/activity-thread-display.ts b/src/renderer/src/lib/workspace/activity/activity-thread-display.ts index ce63c2de9ed..75634737c15 100644 --- a/src/renderer/src/lib/workspace/activity/activity-thread-display.ts +++ b/src/renderer/src/lib/workspace/activity/activity-thread-display.ts @@ -4,6 +4,7 @@ import type { AgentStatusState } from '../../../../../shared/agent-status/contracts/agent-status-types' import type { TerminalTab } from '../../../../../shared/workspace-layout/terminal-tab-types' +import { aiVaultTitleOfSession } from '../../../../../shared/ai-vault/contracts/ai-vault-session-title' import type { Worktree } from '../../../../../shared/worktree/types' import { getAgentRowPrimaryText, @@ -83,8 +84,14 @@ export function getActivityThreadWorkspaceTitle( } export function getActivityThreadTaskTitle(args: { - entry: Pick - tab: Pick + entry: Pick< + AgentStatusEntry, + 'orchestration' | 'prompt' | 'stateHistory' | 'agentType' | 'providerSession' + > + tab: Pick< + TerminalTab, + 'customTitle' | 'aiVaultTitle' | 'generatedTitle' | 'title' | 'defaultTitle' + > generatedTitlesEnabled: boolean }): string { const customTitle = args.tab.customTitle?.trim() @@ -97,6 +104,17 @@ export function getActivityThreadTaskTitle(args: { return orchestrationLabel } + // Like the tab, a session's own name outranks the first prompt this tab saw, e.g. after a resume. + const sessionTitle = aiVaultTitleOfSession( + args.tab.aiVaultTitle, + args.entry.agentType, + args.entry.providerSession?.id + ) + const sessionName = sessionTitle?.kind === 'named' ? sessionTitle.title.trim() : '' + if (sessionName) { + return sessionName + } + // Respect tabAutoGenerateTitle here too so disabled generated titles cannot resurface. const generatedTitle = args.generatedTitlesEnabled ? args.tab.generatedTitle?.trim() : '' if (generatedTitle) { diff --git a/src/shared/agent-status/presentation/agent-row-conversation-name.ts b/src/shared/agent-status/presentation/agent-row-conversation-name.ts index db8f467cd75..c6f3a542a4a 100644 --- a/src/shared/agent-status/presentation/agent-row-conversation-name.ts +++ b/src/shared/agent-status/presentation/agent-row-conversation-name.ts @@ -7,6 +7,7 @@ import { isJcodeIdentityTerminalTitle } from '../title/providers/jcode-terminal- import { SYNTHETIC_AGENT_TITLE_PROFILES } from '../title/synthetic-agent-title' import type { TerminalTab } from '../../workspace-layout/terminal-tab-types' import { resolveTabNameSource } from '../../tab-title-resolution' +import { aiVaultTitleOfSession } from '../../ai-vault/contracts/ai-vault-session-title' export type ConversationNameTab = Pick< TerminalTab, @@ -111,19 +112,12 @@ export function getAgentRowConversationName( ): string | null { const liveTitle = paneLiveTitle === undefined ? (tab.title?.trim() ?? '') : (paneLiveTitle?.trim() ?? '') - // Provider titles belong to their session, not every pane in the tab. - const aiVaultTitle = tab.aiVaultTitle const named = resolveTabNameSource( { customTitle: tab.customTitle, quickCommandLabel: tab.quickCommandLabel, liveTitle, - sessionTitle: - aiVaultTitle && - aiVaultTitle.agent === agentType && - aiVaultTitle.sessionId === providerSessionId - ? aiVaultTitle - : null, + sessionTitle: aiVaultTitleOfSession(tab.aiVaultTitle, agentType, providerSessionId), generatedTitle: tab.generatedTitle }, generatedTitlesEnabled diff --git a/src/shared/ai-vault/contracts/ai-vault-session-title.ts b/src/shared/ai-vault/contracts/ai-vault-session-title.ts index a59fa9731af..482f66ea833 100644 --- a/src/shared/ai-vault/contracts/ai-vault-session-title.ts +++ b/src/shared/ai-vault/contracts/ai-vault-session-title.ts @@ -11,6 +11,15 @@ export type AiVaultSessionTitle = { kind?: AiVaultSessionTitleKind } +/** A tab's provider title names only the pane running that session, not every pane in the tab. */ +export function aiVaultTitleOfSession( + title: AiVaultSessionTitle | null | undefined, + agent: string | null | undefined, + sessionId: string | undefined +): AiVaultSessionTitle | null { + return title && title.agent === agent && title.sessionId === sessionId ? title : null +} + export type AiVaultSessionTitleRequest = { agent: AiVaultSessionTitle['agent'] sessionId: string