diff --git a/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx index 4cf81f263fe..422a7089432 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx @@ -51,6 +51,8 @@ function mockAgent({ let mockAgents: DashboardAgentRowData[] = [] let mockAgentActivityDisplayMode: 'compact' | 'full' | undefined let mockTabsByWorktree: Record = {} +let mockTerminalLayoutsByTabId: Record = {} +let mockPtyIdsByTabId: Record = {} let mockStructuredTabIds = new Set() let mockAgentStatusByPaneKey: Record = {} let mockActiveTabId: string | null = null @@ -83,8 +85,8 @@ function buildMockStoreState(): Record { setActiveTab: mockSetActiveTab, setActiveTabType: mockSetActiveTabType, tabsByWorktree: mockTabsByWorktree, - terminalLayoutsByTabId: {}, - ptyIdsByTabId: {}, + terminalLayoutsByTabId: mockTerminalLayoutsByTabId, + ptyIdsByTabId: mockPtyIdsByTabId, runtimePaneTitlesByTabId: {}, sendPromptToSidebarAgentTarget: vi.fn(), settings: { @@ -173,6 +175,8 @@ describe('WorktreeCardAgents activation', () => { mockAgents = [] mockAgentActivityDisplayMode = undefined mockTabsByWorktree = {} + mockTerminalLayoutsByTabId = {} + mockPtyIdsByTabId = {} mockStructuredTabIds = new Set() mockAgentStatusByPaneKey = {} mockActiveTabId = null @@ -183,6 +187,54 @@ describe('WorktreeCardAgents activation', () => { ) }) + it.each([ + ['opens a retained row whose exact pane still has a live PTY', ['pty-a'], true], + ['keeps a retained row inert when its pane PTY is no longer live', [], false] + ])('%s (#20803)', async (_name, livePtyIds, shouldActivate) => { + mockAgentActivityDisplayMode = 'full' + const tabId = 'retained-tab' + const paneKey = makePaneKey(tabId, LEAF_A) + mockAgents = [ + { + ...mockAgent({ paneKey, tabId, agentType: 'codex', prompt: 'Idle', worktreeId: 'wt-1' }), + rowSource: 'retained', + state: 'done' + } + ] + mockTabsByWorktree = { 'wt-1': [{ id: tabId }] } + mockTerminalLayoutsByTabId = { + [tabId]: { + root: { + type: 'split', + direction: 'vertical', + first: { type: 'leaf', leafId: LEAF_A }, + second: { type: 'leaf', leafId: LEAF_B } + }, + activeLeafId: LEAF_B, + expandedLeafId: null, + ptyIdsByLeafId: { [LEAF_A]: 'pty-a', [LEAF_B]: 'pty-b' } + } + } + mockPtyIdsByTabId = { [tabId]: [...livePtyIds, 'pty-b'] } + const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents') + + renderToStaticMarkup() + capturedRowActivations[0].onActivate(tabId, paneKey) + + if (shouldActivate) { + expect(activationMocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-1') + expect(activationMocks.activateTabAndFocusPane).toHaveBeenCalledWith( + tabId, + LEAF_A, + expect.anything() + ) + } else { + expect(activationMocks.activateAndRevealWorktree).not.toHaveBeenCalled() + expect(activationMocks.activateTabAndFocusPane).not.toHaveBeenCalled() + } + expect(staleAgentRowMocks.dismissStaleAgentRowByKey).not.toHaveBeenCalled() + }) + it('activates a projected structured session row through the unified tab path', async () => { mockAgentActivityDisplayMode = 'full' const tabId = 'structured-tab' diff --git a/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx index 058ecf07d15..255317e16bd 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx @@ -396,19 +396,6 @@ describe('WorktreeCardAgents', () => { expect(markup).toContain('data-pane-key="tab-1:2"') }) - it('keeps retained completion rows passive when activated', async () => { - mockAgentActivityDisplayMode = 'full' - mockAgents = [mockAgent({ rowSource: 'retained', state: 'done' })] - const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents') - - renderToStaticMarkup() - expect(capturedRowActivations).toHaveLength(1) - capturedRowActivations[0].onActivate('tab-1', 'tab-1:1') - - expect(activationMocks.activateAndRevealWorktree).not.toHaveBeenCalled() - expect(activationMocks.activateTabAndFocusPane).not.toHaveBeenCalled() - }) - it('shows orchestration child agent rows under their parent by default', async () => { mockAgentActivityDisplayMode = 'full' mockAgents = [ diff --git a/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx b/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx index 696ad0928ee..d10162348cc 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardAgents.tsx @@ -29,6 +29,7 @@ import { useWorktreeAgentExpansionState } from './worktree-card-agents-expansion import { translate } from '@/i18n/i18n' import { activateStructuredAgentSessionTab } from '@/lib/agent-session/structured-agent-session-tab-activation' import { selectAgentRowsUnread } from './worktree-card-agent-ack-inputs' +import { stablePaneHasLivePty } from '@/lib/sleeping-agent-pane-ownership' export const SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT = 'orca-suppress-worktree-list-scroll-adjustment' @@ -185,9 +186,26 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({ }, [worktreeId] ) - const handleActivateRetainedAgent = useCallback(() => { - // Retained rows are completion evidence; activation would resume sleeping sessions, so keep them inert. - }, []) + const handleActivateRetainedAgent = useCallback( + (tabId: string, paneKey: string) => { + const parsed = parsePaneKey(paneKey) + const state = useAppStore.getState() + // Retained rows are completion evidence; only open a still-live pane so a sleeping target pane is never resumed. + if ( + parsed?.tabId === tabId && + state.tabsByWorktree[worktreeId]?.some((tab) => tab.id === tabId) && + stablePaneHasLivePty( + tabId, + parsed.leafId, + state.ptyIdsByTabId, + state.terminalLayoutsByTabId[tabId] + ) + ) { + handleActivateAgentTab(tabId, paneKey) + } + }, + [handleActivateAgentTab, worktreeId] + ) const now = useNow(30_000) const { rootRows: rootAgents, childrenByParentPaneKey } = useMemo(