mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 08:03:09 +00:00
Reveal terminal logs from sidebar agent rows
This commit is contained in:
@@ -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
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user