diff --git a/src/renderer/src/components/sidebar/worktree-agent-live-index-patch.ts b/src/renderer/src/components/sidebar/worktree-agent-live-index-patch.ts index 70f1f3ede4e..e94824aa1ba 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-live-index-patch.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-live-index-patch.ts @@ -4,6 +4,7 @@ import { parsePaneKey } from '../../../../shared/stable-pane-id' export type LiveEntriesByWorktreeCache = { tabsByWorktree: AppState['tabsByWorktree'] + unifiedTabsByWorktree: AppState['unifiedTabsByWorktree'] | undefined agentStatusByPaneKey: AppState['agentStatusByPaneKey'] entriesByWorktree: Map } diff --git a/src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts b/src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts index 46f75858ee9..b64f2adb6f2 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts @@ -3,7 +3,7 @@ import type { AgentStatusEntry, MigrationUnsupportedPtyEntry } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { Tab, TerminalTab } from '../../../../shared/types' import type { RetainedAgentEntry } from '@/store/slices/agent-status' import { makePaneKey } from '../../../../shared/stable-pane-id' import { getLiveEntriesFullRebuildCountForTests } from './worktree-agent-live-index-patch' @@ -152,6 +152,44 @@ describe('selectLiveAgentStatusEntriesForWorktree', () => { expect(selectLiveAgentStatusEntriesForWorktree(state, 'wt-1')).toEqual([childEntry]) }) + it('keeps an idle structured session visible while its unified tab exists', () => { + const entry = makeEntry(PANE_KEY_1, 1000, { + state: 'done', + sessionBoundary: true, + worktreeId: 'wt-1', + tabId: 'tab-1' + }) + const structuredTab = { + id: 'tab-1', + worktreeId: 'wt-1', + groupId: 'group-1', + contentType: 'agent-session', + entityId: 'session-1', + label: 'Codex Chat', + customLabel: null, + color: null, + sortOrder: 0, + createdAt: 0, + isPinned: false, + agentSessionAgent: 'codex' + } satisfies Tab + const state = { + tabsByWorktree: { 'wt-1': [] }, + unifiedTabsByWorktree: { 'wt-1': [structuredTab] }, + agentStatusByPaneKey: { [PANE_KEY_1]: entry }, + migrationUnsupportedByPtyId: {}, + retainedAgentsByPaneKey: {} + } + + expect(selectLiveAgentStatusEntriesForWorktree(state, 'wt-1')).toEqual([entry]) + expect( + selectLiveAgentStatusEntriesForWorktree( + { ...state, unifiedTabsByWorktree: { 'wt-1': [] } }, + 'wt-1' + ) + ).toEqual([]) + }) + it('patches instead of full-rebuilding across within-state pings, and stays correct on transitions', () => { const wt1Entry = makeEntry(PANE_KEY_1, 1000, { state: 'working', prompt: 'wt1 prompt' }) const wt2Entry = makeEntry(PANE_KEY_2, 1000, { state: 'working', prompt: 'wt2 prompt' }) diff --git a/src/renderer/src/components/sidebar/worktree-agent-row-selectors.ts b/src/renderer/src/components/sidebar/worktree-agent-row-selectors.ts index 75de25923f2..c6b6250b70f 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-row-selectors.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-row-selectors.ts @@ -28,7 +28,9 @@ type WorktreeAgentRowsState = Pick< | 'migrationUnsupportedByPtyId' | 'retainedAgentsByPaneKey' | 'tabsByWorktree' -> +> & { + unifiedTabsByWorktree?: AppState['unifiedTabsByWorktree'] +} type TabWorktreeIndexCache = { tabsByWorktree: WorktreeAgentRowsState['tabsByWorktree'] @@ -82,18 +84,38 @@ function getTabIdToWorktreeId( return tabIdToWorktreeId } +function getLiveTabIdToWorktreeId( + tabsByWorktree: WorktreeAgentRowsState['tabsByWorktree'], + unifiedTabsByWorktree: WorktreeAgentRowsState['unifiedTabsByWorktree'] +): Map { + const tabIdToWorktreeId = new Map(getTabIdToWorktreeId(tabsByWorktree)) + for (const [worktreeId, tabs] of Object.entries(unifiedTabsByWorktree ?? EMPTY_RECORD)) { + for (const tab of tabs) { + if (tab.contentType === 'agent-session') { + tabIdToWorktreeId.set(tab.id, worktreeId) + } + } + } + return tabIdToWorktreeId +} + function getLiveEntriesByWorktree(state: WorktreeAgentRowsState): Map { const agentStatusByPaneKey = state.agentStatusByPaneKey ?? EMPTY_RECORD const tabsByWorktree = state.tabsByWorktree ?? EMPTY_RECORD + const unifiedTabsByWorktree = state.unifiedTabsByWorktree if ( liveEntriesByWorktreeCache?.tabsByWorktree === tabsByWorktree && + liveEntriesByWorktreeCache.unifiedTabsByWorktree === unifiedTabsByWorktree && liveEntriesByWorktreeCache.agentStatusByPaneKey === agentStatusByPaneKey ) { return liveEntriesByWorktreeCache.entriesByWorktree } - const tabIdToWorktreeId = getTabIdToWorktreeId(tabsByWorktree) - if (liveEntriesByWorktreeCache?.tabsByWorktree === tabsByWorktree) { + const tabIdToWorktreeId = getLiveTabIdToWorktreeId(tabsByWorktree, unifiedTabsByWorktree) + if ( + liveEntriesByWorktreeCache?.tabsByWorktree === tabsByWorktree && + liveEntriesByWorktreeCache.unifiedTabsByWorktree === unifiedTabsByWorktree + ) { const patched = patchLiveEntriesByWorktree( liveEntriesByWorktreeCache, agentStatusByPaneKey, @@ -102,6 +124,7 @@ function getLiveEntriesByWorktree(state: WorktreeAgentRowsState): Map { if (tab.contentType === 'agent-session') { return false