From 860ad56cfd1b7414354e52d056c57d040ae455fb Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Sun, 6 Sep 2026 18:39:23 -0700 Subject: [PATCH] fix(chat): retain clear pane identity with reopened history --- ...tured-conversation-tab-replacement.test.ts | 42 ++++++++++++++++--- .../terminal-surfaces.ts | 37 +++++++++++----- 2 files changed, 63 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/runtime/structured-conversation-tab-replacement.test.ts b/src/renderer/src/runtime/structured-conversation-tab-replacement.test.ts index f3230c30409..635d4848bd5 100644 --- a/src/renderer/src/runtime/structured-conversation-tab-replacement.test.ts +++ b/src/renderer/src/runtime/structured-conversation-tab-replacement.test.ts @@ -14,9 +14,13 @@ import { beforeEach(resetWebSessionTabsSyncTestState) describe('clear pane identity', () => { - it.each(['agent-session', 'terminal'] as const)( - 'replaces a %s pane in its existing local position', - (contentType) => { + it.each( + (['agent-session', 'terminal'] as const).flatMap((contentType) => + (['absent', 'before', 'after'] as const).map((history) => ({ contentType, history })) + ) + )( + 'replaces a $contentType pane with reopened history $history the replacement', + ({ contentType, history }) => { const state = makeState({ unifiedTabsByWorktree: { [WT]: [ @@ -83,13 +87,30 @@ describe('clear pane identity', () => { ], { activeTabId: 'agent-session:new-session', activeTabType: 'agent-session' } ) + if (history !== 'absent') { + const oldTab = { + type: 'agent-session' as const, + id: 'agent-session:old-session', + sessionId: 'old-session', + agent: 'codex' as const, + title: 'History', + isActive: false + } + if (history === 'before') { + snapshot.tabs.unshift(oldTab) + } else { + snapshot.tabs.push(oldTab) + } + } const next = applyWebSessionTabsSnapshot(state, snapshot, ENV, NOW, { contentScope: 'agent-session', preserveLocalLayout: true, terminalPtyMode: 'local' }) - expect(next.unifiedTabsByWorktree?.[WT]).toHaveLength(1) - expect(next.unifiedTabsByWorktree?.[WT]?.[0]).toMatchObject({ + expect(next.unifiedTabsByWorktree?.[WT]).toHaveLength(history === 'absent' ? 1 : 2) + expect( + next.unifiedTabsByWorktree?.[WT]?.find((tab) => tab.entityId === 'new-session') + ).toMatchObject({ id: 'local-pane', entityId: 'new-session', contentType: 'agent-session', @@ -97,10 +118,19 @@ describe('clear pane identity', () => { isPinned: true }) expect(next.groupsByWorktree?.[WT]?.[0]).toMatchObject({ - tabOrder: ['local-pane'], activeTabId: 'local-pane' }) + expect(next.groupsByWorktree?.[WT]?.[0]?.tabOrder[0]).toBe('local-pane') + expect(next.activeTabIdByWorktree?.[WT] ?? state.activeTabIdByWorktree[WT]).toBe('local-pane') expect(next.tabsByWorktree?.[WT] ?? []).toEqual([]) + const repeated = applyWebSessionTabsSnapshot({ ...state, ...next }, snapshot, ENV, NOW + 1, { + contentScope: 'agent-session', + preserveLocalLayout: true, + terminalPtyMode: 'local' + }) + expect(repeated.unifiedTabsByWorktree?.[WT] ?? next.unifiedTabsByWorktree?.[WT]).toEqual( + next.unifiedTabsByWorktree?.[WT] + ) } ) it('gives reopened history its own tab when clear retained its former local ID', () => { 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 ee2487d61e3..3c43eec8d5c 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 @@ -60,23 +60,40 @@ export function buildMirroredAgentTabs( now: number ): MirroredAgentTab[] { const agentTabs = snapshot.tabs.filter(isAgentSessionTab) - const publishedSessions = new Set(agentTabs.map((tab) => tab.sessionId)) const occupiedIds = new Set(currentUnifiedTabs.map((tab) => tab.id)) const assignedIds = new Set() - return agentTabs.map((tab, index) => { + const replacementTabs = new Map() + const replacementIds = new Set() + for (const tab of agentTabs) { + if (!tab.replacesSessionId) { + continue + } const existing = currentUnifiedTabs.find( (candidate) => candidate.contentType === 'agent-session' && candidate.entityId === tab.sessionId ) ?? - (tab.replacesSessionId && !publishedSessions.has(tab.replacesSessionId) - ? currentUnifiedTabs.find( - (candidate) => - candidate.structuredSessionId === tab.replacesSessionId || - (candidate.contentType === 'agent-session' && - candidate.entityId === tab.replacesSessionId) - ) - : undefined) + currentUnifiedTabs.find( + (candidate) => + !replacementIds.has(candidate.id) && + (candidate.structuredSessionId === tab.replacesSessionId || + (candidate.contentType === 'agent-session' && + candidate.entityId === tab.replacesSessionId)) + ) + if (existing) { + replacementTabs.set(tab.sessionId, existing) + replacementIds.add(existing.id) + } + } + return agentTabs.map((tab, index) => { + const existing = + replacementTabs.get(tab.sessionId) ?? + currentUnifiedTabs.find( + (candidate) => + !replacementIds.has(candidate.id) && + candidate.contentType === 'agent-session' && + candidate.entityId === tab.sessionId + ) const baseId = structuredAgentSessionTabId(tab.sessionId) let localId = existing?.id ?? baseId if (!existing || assignedIds.has(localId)) {