Reveal terminal logs from sidebar agent rows

This commit is contained in:
Neil
2026-06-25 16:14:53 -07:00
parent 667519d9ea
commit a6a07249cf
4 changed files with 146 additions and 3 deletions
@@ -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<string, { id: string }[]> = {}
let mockAgentStatusByPaneKey: Record<string, { worktreeId?: string }> = {}
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<string, unknown> {
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<typeof ActivateTabAndFocusPaneModule>(
'@/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(<WorktreeCardAgents worktreeId="wt-1" />)
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'
@@ -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()
})
})
@@ -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
@@ -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<ActivePaneSelection>
activeWorktreeId: null,
activeGroupId: null,
activeTabId: null,
activeTabType: null,
activeLeafId: null,
activePaneId: null
}
@@ -208,12 +211,17 @@ async function readActivePaneSelection(page: Page): Promise<ActivePaneSelection>
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<SplitGroupTerminal> {
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
}) => {