From a6a07249cfdacce7590bb1bfd7f9ca6c15fcbde7 Mon Sep 17 00:00:00 2001 From: Neil Date: Thu, 25 Jun 2026 16:14:53 -0700 Subject: [PATCH] Reveal terminal logs from sidebar agent rows --- .../WorktreeCardAgents.activation.test.tsx | 49 ++++++++++++++++++ .../lib/activate-tab-and-focus-pane.test.ts | 50 ++++++++++++++++++- .../src/lib/activate-tab-and-focus-pane.ts | 6 ++- .../e2e/activity-agent-pane-isolation.spec.ts | 44 ++++++++++++++++ 4 files changed, 146 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx index 71b74957f16..ce2ba1fbc2e 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx @@ -5,6 +5,7 @@ import { createRoot, type Root } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData' +import type * as ActivateTabAndFocusPaneModule from '@/lib/activate-tab-and-focus-pane' import { makePaneKey } from '../../../../shared/stable-pane-id' const LEAF_A = '11111111-1111-4111-8111-111111111111' @@ -51,9 +52,13 @@ let mockAgentActivityDisplayMode: 'compact' | 'full' | undefined let mockTabsByWorktree: Record = {} let mockAgentStatusByPaneKey: Record = {} let mockActiveTabId: string | null = null +let mockActiveTabType: string = 'editor' const mockSetActiveTab = vi.fn((tabId: string) => { mockActiveTabId = tabId }) +const mockSetActiveTabType = vi.fn((tabType: string) => { + mockActiveTabType = tabType +}) let capturedRowActivations: { paneKey: string onActivate: (tabId: string, paneKey: string) => void @@ -71,7 +76,9 @@ function buildMockStoreState(): Record { agentStatusByPaneKey: mockAgentStatusByPaneKey, agentStatusEpoch: 0, activeTabId: mockActiveTabId, + activeTabType: mockActiveTabType, setActiveTab: mockSetActiveTab, + setActiveTabType: mockSetActiveTabType, tabsByWorktree: mockTabsByWorktree, terminalLayoutsByTabId: {}, ptyIdsByTabId: {}, @@ -143,11 +150,13 @@ describe('WorktreeCardAgents activation', () => { beforeEach(() => { vi.clearAllMocks() activationMocks.activateAndRevealWorktree.mockImplementation(() => undefined) + activationMocks.activateTabAndFocusPane.mockImplementation(() => undefined) mockAgents = [] mockAgentActivityDisplayMode = undefined mockTabsByWorktree = {} mockAgentStatusByPaneKey = {} mockActiveTabId = null + mockActiveTabType = 'editor' capturedRowActivations = [] }) @@ -185,6 +194,46 @@ describe('WorktreeCardAgents activation', () => { expect(staleAgentRowMocks.dismissStaleAgentRowByKey).not.toHaveBeenCalled() }) + it('reveals the terminal surface through the helper path when activating a hydrated row', async () => { + mockAgentActivityDisplayMode = 'full' + const tabId = 'visible-worker-tab' + const paneKey = makePaneKey(tabId, LEAF_A) + mockAgents = [ + mockAgent({ + paneKey, + tabId, + agentType: 'codex', + prompt: 'Show full log', + worktreeId: 'wt-1' + }) + ] + mockTabsByWorktree = { 'wt-1': [{ id: tabId }] } + mockAgentStatusByPaneKey = { [paneKey]: { worktreeId: 'wt-1' } } + const actualActivation = await vi.importActual( + '@/lib/activate-tab-and-focus-pane' + ) + // Why: keep the component import mocked for call assertions, but delegate + // this repro to the real helper so it fails if the terminal-surface fix + // regresses. + activationMocks.activateTabAndFocusPane.mockImplementation( + actualActivation.activateTabAndFocusPane + ) + const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents') + + renderToStaticMarkup() + expect(capturedRowActivations).toHaveLength(1) + capturedRowActivations[0].onActivate(tabId, paneKey) + + expect(activationMocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-1') + expect(activationMocks.activateTabAndFocusPane).toHaveBeenCalledWith(tabId, LEAF_A, { + ackPaneKeyOnSuccess: paneKey, + flashFocusedPane: true, + scrollToBottomIfOutputSinceLastView: true + }) + expect(mockActiveTabType).toBe('terminal') + expect(mockActiveTabId).toBe(tabId) + }) + it('keeps a live worktree-attributed row visible while its tab is hydrating', async () => { mockAgentActivityDisplayMode = 'full' const tabId = 'hydrating-worker-tab' diff --git a/src/renderer/src/lib/activate-tab-and-focus-pane.test.ts b/src/renderer/src/lib/activate-tab-and-focus-pane.test.ts index b1573641fa9..b1ad9376a4b 100644 --- a/src/renderer/src/lib/activate-tab-and-focus-pane.test.ts +++ b/src/renderer/src/lib/activate-tab-and-focus-pane.test.ts @@ -1,17 +1,24 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { activateTabAndFocusPane } from './activate-tab-and-focus-pane' const setActiveTab = vi.hoisted(() => vi.fn()) +const setActiveTabType = vi.hoisted(() => vi.fn()) vi.mock('@/store', () => ({ useAppStore: { getState: () => ({ - setActiveTab + setActiveTab, + setActiveTabType }) } })) describe('activateTabAndFocusPane', () => { + beforeEach(() => { + setActiveTab.mockImplementation(() => undefined) + setActiveTabType.mockImplementation(() => undefined) + }) + afterEach(() => { vi.unstubAllGlobals() vi.clearAllMocks() @@ -35,4 +42,43 @@ describe('activateTabAndFocusPane', () => { expect(setActiveTab).toHaveBeenNthCalledWith(2, 'tab-2') expect(cancelAnimationFrame).toHaveBeenCalledWith(12) }) + + it('reveals the terminal surface before selecting the terminal tab', () => { + const callOrder: string[] = [] + setActiveTabType.mockImplementation((tabType: string) => { + callOrder.push(`type:${tabType}`) + }) + setActiveTab.mockImplementation((tabId: string) => { + callOrder.push(`tab:${tabId}`) + }) + vi.stubGlobal( + 'requestAnimationFrame', + vi.fn(() => 12) + ) + vi.stubGlobal('cancelAnimationFrame', vi.fn()) + vi.stubGlobal('window', { + dispatchEvent: vi.fn() + }) + + activateTabAndFocusPane('tab-1', 'leaf-1') + + expect(callOrder).toEqual(['type:terminal', 'tab:tab-1']) + }) + + it('reveals the terminal surface for tab-only activation without dispatching pane focus', () => { + const requestAnimationFrame = vi.fn() + const dispatchEvent = vi.fn() + vi.stubGlobal('requestAnimationFrame', requestAnimationFrame) + vi.stubGlobal('cancelAnimationFrame', vi.fn()) + vi.stubGlobal('window', { + dispatchEvent + }) + + activateTabAndFocusPane('tab-1', null) + + expect(setActiveTabType).toHaveBeenCalledWith('terminal') + expect(setActiveTab).toHaveBeenCalledWith('tab-1') + expect(requestAnimationFrame).not.toHaveBeenCalled() + expect(dispatchEvent).not.toHaveBeenCalled() + }) }) diff --git a/src/renderer/src/lib/activate-tab-and-focus-pane.ts b/src/renderer/src/lib/activate-tab-and-focus-pane.ts index eca2922eae3..bad72af0d4b 100644 --- a/src/renderer/src/lib/activate-tab-and-focus-pane.ts +++ b/src/renderer/src/lib/activate-tab-and-focus-pane.ts @@ -19,7 +19,11 @@ export function activateTabAndFocusPane( scrollToBottomIfOutputSinceLastView?: boolean } ): void { - useAppStore.getState().setActiveTab(tabId) + const { setActiveTab, setActiveTabType } = useAppStore.getState() + // Why: selecting a terminal tab is independent from the visible surface; + // force Terminal first so tab-only activation reveals the full log. + setActiveTabType('terminal') + setActiveTab(tabId) cancelPendingFocusPaneFrame() if (leafId === null) { return diff --git a/tests/e2e/activity-agent-pane-isolation.spec.ts b/tests/e2e/activity-agent-pane-isolation.spec.ts index b70df125ccc..4bb7d1ea2ae 100644 --- a/tests/e2e/activity-agent-pane-isolation.spec.ts +++ b/tests/e2e/activity-agent-pane-isolation.spec.ts @@ -8,6 +8,7 @@ import { waitForPaneIdentitySnapshot, type PaneIdentitySnapshot } from './helpers/terminal' +import { clickFileInExplorer } from './helpers/file-explorer' import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' type SeededActivityThread = { @@ -26,6 +27,7 @@ type ActivePaneSelection = { activeWorktreeId: string | null activeGroupId: string | null activeTabId: string | null + activeTabType: string | null activeLeafId: string | null activePaneId: number | null } @@ -189,6 +191,7 @@ async function readActivePaneSelection(page: Page): Promise activeWorktreeId: null, activeGroupId: null, activeTabId: null, + activeTabType: null, activeLeafId: null, activePaneId: null } @@ -208,12 +211,17 @@ async function readActivePaneSelection(page: Page): Promise activeWorktreeId, activeGroupId, activeTabId, + activeTabType: state.activeTabType ?? null, activeLeafId: activePane?.leafId ?? null, activePaneId: activePane?.id ?? null } }) } +function terminalPaneForLeaf(page: Page, leafId: string) { + return page.locator(`.pane[data-leaf-id="${leafId}"]`).first() +} + async function createTerminalInNewSplitGroup(page: Page): Promise { return page.evaluate(() => { const store = window.__store @@ -375,6 +383,42 @@ test.describe('Activity Agent Pane Isolation', () => { }) }) + test('workspace card agent rows reveal terminal logs from a non-terminal surface', async ({ + orcaPage + }) => { + await splitActiveTerminalPane(orcaPage, 'vertical') + await waitForPaneCount(orcaPage, 2) + const snapshot = await waitForPaneIdentitySnapshot(orcaPage, 2) + const [first] = await seedActivityThreadsForSplitPanes(orcaPage, snapshot) + + await enableInlineAgentCards(orcaPage) + await expect(terminalPaneForLeaf(orcaPage, first.leafId)).toBeVisible() + // Why: this reproduces the user-visible failure mode: the agent row is + // visible in the sidebar while the main workspace surface is not Terminal. + await expect(await clickFileInExplorer(orcaPage, ['README.md'])).toBe('README.md') + await expect + .poll(() => readActivePaneSelection(orcaPage)) + .toMatchObject({ + activeTabType: 'editor', + activeTabId: snapshot.tabId + }) + await expect(terminalPaneForLeaf(orcaPage, first.leafId)).toBeHidden() + + await clickWorkspaceCardAgentRow(orcaPage, first.prompt) + + await expect + .poll(async () => readActivePaneSelection(orcaPage), { + timeout: 10_000, + message: 'Workspace-card row did not reveal the terminal log surface' + }) + .toMatchObject({ + activeTabType: 'terminal', + activeTabId: snapshot.tabId, + activeLeafId: first.leafId + }) + await expect(terminalPaneForLeaf(orcaPage, first.leafId)).toBeVisible() + }) + test('workspace card agent rows focus the matching split-group terminal pane', async ({ orcaPage }) => {