diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 103ed90f4fe..2a3a9f4b4b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,9 @@ importers: .: configDependencies: {} packageManagerDependencies: + '@pnpm/exe': + specifier: 12.0.0 + version: 12.0.0 pnpm: specifier: 12.0.0 version: 12.0.0 @@ -56,6 +59,11 @@ packages: cpu: [x64] os: [win32] + '@pnpm/exe@12.0.0': + resolution: {integrity: sha512-405vw2qYPPghNxoPRt2cvkGtGNU5gnInDcIe5TztQxe73yUZLqFJN/jCJFGs1xbVaIWDXEVuQ71P8npFZeatZQ==} + engines: {node: '>=18.*'} + hasBin: true + pnpm@12.0.0: resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==} engines: {node: '>=18.*'} @@ -87,6 +95,17 @@ snapshots: '@pnpm/exe.win32-x64@12.0.0': optional: true + '@pnpm/exe@12.0.0': + optionalDependencies: + '@pnpm/exe.darwin-arm64': 12.0.0 + '@pnpm/exe.darwin-x64': 12.0.0 + '@pnpm/exe.linux-arm64': 12.0.0 + '@pnpm/exe.linux-arm64-musl': 12.0.0 + '@pnpm/exe.linux-x64': 12.0.0 + '@pnpm/exe.linux-x64-musl': 12.0.0 + '@pnpm/exe.win32-arm64': 12.0.0 + '@pnpm/exe.win32-x64': 12.0.0 + pnpm@12.0.0: optionalDependencies: '@pnpm/exe.darwin-arm64': 12.0.0 diff --git a/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts b/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts index 5b2c160f2c6..945d452302d 100644 --- a/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts +++ b/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts @@ -1,4 +1,5 @@ // @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. +import { defaultAgentChatLabel } from '../../shared/agent-session-chat-label' import { OrcaRuntimeWithResolveRecoveredStructuredTuiTranscript } from './orca-runtime-resolve-recovered-structured-tui-transcript' import { getStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' import { collectSavedStructuredAgentSessionIds } from './saved-structured-agent-session-restoration' @@ -105,7 +106,7 @@ export class OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce extends OrcaRu const tab: RuntimeMobileSessionAgentTab = { type: 'agent-session', id, - title: input.agent === 'claude' ? 'Claude Chat' : 'Codex Chat', + title: defaultAgentChatLabel(input.agent), sessionId: input.sessionId, agent: input.agent, isActive: input.activate diff --git a/src/renderer/src/runtime/web-session-tabs-sync/mirrored-agent-tab-label.test.ts b/src/renderer/src/runtime/web-session-tabs-sync/mirrored-agent-tab-label.test.ts new file mode 100644 index 00000000000..91e58f3cedf --- /dev/null +++ b/src/renderer/src/runtime/web-session-tabs-sync/mirrored-agent-tab-label.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest' +import type { RuntimeMobileSessionTabsResult } from '../../../../shared/runtime-types' +import type { Tab } from '../../../../shared/tab-types' +import { buildMirroredAgentTabs } from './terminal-surfaces' + +const WORKTREE = 'repo-1::worktree-1' +const GROUP = 'group-1' + +function snapshotWith(agent: 'claude' | 'codex', title: string): RuntimeMobileSessionTabsResult { + return { + worktree: WORKTREE, + publicationEpoch: 'epoch-1', + snapshotVersion: 1, + activeGroupId: GROUP, + activeTabId: null, + activeTabType: null, + tabs: [ + { + type: 'agent-session', + id: 'host-tab-1', + title, + sessionId: `${agent}-1`, + agent, + isActive: false + } + ] + } as RuntimeMobileSessionTabsResult +} + +function build( + snapshot: RuntimeMobileSessionTabsResult, + currentUnifiedTabs: readonly Tab[] = [] +): Tab { + const [mirrored] = buildMirroredAgentTabs( + snapshot, + new Map(), + GROUP, + 0, + currentUnifiedTabs, + 1_000 + ) + return mirrored.unifiedTab +} + +describe('buildMirroredAgentTabs', () => { + it('falls back to the agent-specific placeholder when the host publishes no title', () => { + expect(build(snapshotWith('claude', '')).label).toBe('Claude Chat') + expect(build(snapshotWith('codex', ' ')).label).toBe('Codex Chat') + }) + + it('prefers the host title over the placeholder', () => { + expect(build(snapshotWith('claude', 'Flaky retry test')).label).toBe('Flaky retry test') + }) + + it('keeps a manual rename across host snapshots', () => { + const snapshot = snapshotWith('codex', 'Codex Chat') + const renamed = build(snapshot) + const existing: Tab = { ...renamed, customLabel: 'My rename' } + expect(build(snapshot, [existing]).customLabel).toBe('My rename') + }) + + it('leaves customLabel null when the tab was never renamed', () => { + expect(build(snapshotWith('codex', 'Codex Chat')).customLabel).toBeNull() + }) +}) diff --git a/src/renderer/src/runtime/web-session-tabs-sync/terminal-surfaces.ts b/src/renderer/src/runtime/web-session-tabs-sync/terminal-surfaces.ts index 7480306ed5b..0534b4f6ec0 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync/terminal-surfaces.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync/terminal-surfaces.ts @@ -3,6 +3,7 @@ import type { RuntimeMobileSessionAgentTab } from '../../../../shared/runtime-types' import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import { defaultAgentChatLabel } from '../../../../shared/agent-session-chat-label' import { sanitizeTerminalLayoutPaneTitlesForLabels } from '@/lib/terminal-pane-title-sanitization' import { resolveTerminalLayoutRoot } from '../remote-terminal-layout-resolution' import { getRemoteRuntimePtyEnvironmentId } from '../runtime-terminal-stream' @@ -73,8 +74,10 @@ export function buildMirroredAgentTabs( worktreeId: snapshot.worktree, contentType: 'agent-session', agentSessionAgent: tab.agent, - label: tab.title.trim() || 'Codex Chat', - customLabel: null, + label: tab.title.trim() || defaultAgentChatLabel(tab.agent), + // Why: a manual rename lives only on the client; re-nulling it here made + // every host snapshot silently discard the user's title. + customLabel: existing?.customLabel ?? null, color: tab.color !== undefined ? tab.color : (existing?.color ?? null), sortOrder: sortOffset + index, createdAt: existing?.createdAt ?? now + sortOffset + index, diff --git a/src/renderer/src/store/terminals/terminal-tab-attention.ts b/src/renderer/src/store/terminals/terminal-tab-attention.ts index 062043b546e..0d07319a926 100644 --- a/src/renderer/src/store/terminals/terminal-tab-attention.ts +++ b/src/renderer/src/store/terminals/terminal-tab-attention.ts @@ -87,9 +87,12 @@ export function createTerminalTabAttentionActions( scheduleRuntimeGraphSync() return { tabsByWorktree: next } }) - const item = Object.values(get().unifiedTabsByWorktree) - .flat() - .find((entry) => entry.contentType === 'terminal' && entry.entityId === tabId) + const unified = Object.values(get().unifiedTabsByWorktree).flat() + // Why: a structured chat tab has no TerminalTab record, and its rename + // arrives keyed by the unified tab id rather than a terminal entityId. + const item = + unified.find((entry) => entry.contentType === 'terminal' && entry.entityId === tabId) ?? + unified.find((entry) => entry.contentType === 'agent-session' && entry.id === tabId) if (item) { get().setTabCustomLabel(item.id, title, opts) } diff --git a/src/shared/agent-session-chat-label.ts b/src/shared/agent-session-chat-label.ts new file mode 100644 index 00000000000..aec5bc70cb3 --- /dev/null +++ b/src/shared/agent-session-chat-label.ts @@ -0,0 +1,4 @@ +/** Placeholder tab label for a structured chat that has no conversation name yet. */ +export function defaultAgentChatLabel(agent: 'claude' | 'codex' | null | undefined): string { + return agent === 'claude' ? 'Claude Chat' : 'Codex Chat' +}