test: remove low-value dashboard and preview tests (#38362)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-04 20:37:28 -08:00
committed by GitHub
co-authored by OrcaWin
parent c9bf506097
commit b6d760d4f9
13 changed files with 7 additions and 2659 deletions
-1
View File
@@ -289,7 +289,6 @@
1 test-no-real-time-wait:src/main/wsl/orcad/wsl-orcad-stdio-bridge.test.ts
1 test-no-real-time-wait:src/renderer/src/components/UpdateCard.error-card.test.tsx
1 test-no-real-time-wait:src/renderer/src/components/activity/activity-portal-readiness-loop.react185.test.tsx
1 test-no-real-time-wait:src/renderer/src/components/dashboard-popout/useDashboardSnapshot.test.tsx
1 test-no-real-time-wait:src/renderer/src/components/editor/DiagramViewerModal.test.tsx
1 test-no-real-time-wait:src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-stale-analysis-work-budget.test.tsx
1 test-no-real-time-wait:src/renderer/src/components/native-chat/NativeChatModelCombobox.test.tsx
-2
View File
@@ -6898,7 +6898,6 @@
"src/renderer/src/components/crash-report/crash-report-submit-notice.test.ts": 208,
"src/renderer/src/components/crash-report/use-crash-report-copy.test.tsx": 425,
"src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx": 1004,
"src/renderer/src/components/dashboard-popout/AgentKanbanCard.test.tsx": 841,
"src/renderer/src/components/dashboard-popout/AgentTerminalDialog.test.tsx": 935,
"src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx": 2983,
"src/renderer/src/components/dashboard-popout/AgentTerminalPreview.frame-lifecycle.test.tsx": 2678,
@@ -6917,7 +6916,6 @@
"src/renderer/src/components/dashboard-popout/useDashboardSnapshot.test.tsx": 432,
"src/renderer/src/components/dashboard/AgentDashboardDrawer.focus.test.tsx": 2567,
"src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx": 1981,
"src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx": 2350,
"src/renderer/src/components/dashboard/agent-dashboard-open-mode.test.ts": 48,
"src/renderer/src/components/dashboard/agent-destination-navigation.integration.test.tsx": 3232,
"src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts": 107,
@@ -1,366 +0,0 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
import type { RepoIcon } from '../../../../shared/repo-icon'
import { i18n } from '@/i18n/i18n'
import { TooltipProvider } from '@/components/ui/tooltip'
import { AgentKanbanCard } from './AgentKanbanCard'
const agentIconRender = vi.fn()
const agentStateDotRender = vi.fn()
vi.mock('@/lib/agent-catalog', () => ({
AgentIcon: () => {
agentIconRender()
return <span data-testid="agent-icon" />
}
}))
vi.mock('@/components/AgentStateDot', () => ({
AgentStateDot: ({ state }: { state: string }) => {
agentStateDotRender(state)
return <span data-testid="state-dot" />
}
}))
function card(overrides: Partial<DashboardCard> = {}): DashboardCard {
return {
paneKey: 'tab:leaf',
ptyId: 'pty-1',
agentType: 'claude',
bucket: 'working',
dotState: 'working',
task: 'Review the change',
repoId: 'repo-1',
worktreeId: 'worktree-1',
tabId: 'tab',
leafId: 'leaf',
repoName: 'Orca',
worktreeName: 'dashboard-review',
startedAt: 1_000,
finishedAt: null,
stateChangedAt: 1_000,
unseen: false,
...overrides
}
}
function renderCard(props: {
card: DashboardCard
now: number
repoIcon?: RepoIcon | null
onOpenTerminal?: () => void
}): ReturnType<typeof render> {
return render(
<TooltipProvider>
<AgentKanbanCard
card={props.card}
repoIcon={props.repoIcon}
now={props.now}
onOpenTerminal={props.onOpenTerminal ?? vi.fn()}
/>
</TooltipProvider>
)
}
describe('AgentKanbanCard', () => {
beforeEach(async () => {
await i18n.changeLanguage('en')
})
afterEach(() => {
cleanup()
vi.clearAllMocks()
})
it('rerenders on a verdict-only change without a clean completion tint', () => {
const initial = card({ dotState: 'done', bucket: 'idle' })
const onOpenTerminal = vi.fn()
const { container, rerender } = renderCard({ card: initial, now: 2000, onOpenTerminal })
for (const verdict of ['interrupted', 'failed', 'unconfirmed'] as const) {
rerender(
<TooltipProvider>
<AgentKanbanCard
card={{ ...initial, verdict }}
now={2000}
onOpenTerminal={onOpenTerminal}
/>
</TooltipProvider>
)
expect(agentStateDotRender).toHaveBeenLastCalledWith(verdict)
expect(container.firstElementChild?.className).not.toContain('emerald')
}
})
it('does not render an invented age when the start time is unknown', () => {
renderCard({ card: card({ startedAt: 0 }), now: 2_000_000_000 })
expect(screen.queryByText(/\d+d/)).not.toBeInTheDocument()
})
it('shows the question glyph once when a summary is available', () => {
const attentionCard = card({
bucket: 'attention',
dotState: 'waiting',
askSummary: 'Approve deploy?',
verdict: 'failed'
})
const { container, rerender } = renderCard({ card: attentionCard, now: 2_000 })
expect(screen.queryByTestId('state-dot')).not.toBeInTheDocument()
expect(container.querySelectorAll('.lucide-message-circle-question-mark')).toHaveLength(1)
rerender(
<TooltipProvider>
<AgentKanbanCard
card={{ ...attentionCard, askSummary: undefined }}
now={2_000}
onOpenTerminal={vi.fn()}
/>
</TooltipProvider>
)
expect(screen.getByTestId('state-dot')).toBeInTheDocument()
expect(container.querySelector('.lucide-message-circle-question-mark')).toBeNull()
})
it('shows the saved SSH host beside the repository metadata', () => {
const { container } = renderCard({
card: card({
hostKind: 'ssh',
executionHostId: 'ssh:opaque-target',
hostLabel: 'openclaw'
}),
now: 2_000
})
expect(screen.getByLabelText('SSH host · openclaw')).toHaveAttribute(
'data-dashboard-host-badge',
'ssh'
)
expect(container.querySelector('.lucide-server')).toBeInTheDocument()
})
it('shows review metadata and expands grouped subagents without opening the terminal', () => {
const onOpenTerminal = vi.fn()
renderCard({
card: card({
review: { number: 11012, state: 'open' },
subagents: [
{ id: 'child-1', name: 'Review loop', dotState: 'working' },
{ id: 'child-2', name: 'Smoke tests', dotState: 'done' }
]
}),
now: 2_000,
onOpenTerminal
})
expect(screen.getByText('#11012')).toBeInTheDocument()
expect(screen.getByRole('img', { name: 'Open review #11012' })).toBeInTheDocument()
expect(screen.queryByText('Review loop')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '2 subagents' }))
expect(screen.getByText('Review loop')).toBeInTheDocument()
expect(screen.getByText('Smoke tests')).toBeInTheDocument()
expect(onOpenTerminal).not.toHaveBeenCalled()
})
it('opens the terminal from the footer while keeping subagent disclosure isolated', () => {
const onOpenTerminal = vi.fn()
renderCard({
card: card({
conversationName: 'Dashboard review',
review: { number: 11042, state: 'open' },
subagents: [{ id: 'child-1', name: 'Review loop', dotState: 'working' }]
}),
now: 61_000,
onOpenTerminal
})
fireEvent.click(screen.getByText('#11042'))
expect(onOpenTerminal).toHaveBeenCalledTimes(1)
fireEvent.click(screen.getByRole('button', { name: '1 subagent' }))
expect(onOpenTerminal).toHaveBeenCalledTimes(1)
})
it('labels one subagent accessibly and never renders a workspace-status dot', () => {
renderCard({
card: card({
workspaceStatusId: 'in-review',
workspaceStatusLabel: 'In review',
workspaceStatusColor: 'emerald',
subagents: [{ id: 'child-1', name: 'Review loop', dotState: 'working' }]
}),
now: 2_000
})
expect(screen.getByRole('button', { name: '1 subagent' })).toBeInTheDocument()
expect(screen.queryByRole('img', { name: 'In review' })).not.toBeInTheDocument()
})
it('tints unseen Done green and keeps acknowledged Done neutral as Idle', () => {
const { container: attention } = renderCard({
card: card({ bucket: 'attention', dotState: 'waiting' }),
now: 2_000
})
expect(attention.firstElementChild?.className).toContain('border-agent-question/40')
cleanup()
const { container: done } = renderCard({
card: card({ bucket: 'done', dotState: 'done', unseen: true }),
now: 2_000
})
expect(done.firstElementChild?.className).toContain('border-emerald-500/40')
cleanup()
const { container: idle } = renderCard({
card: card({ bucket: 'idle', dotState: 'done', unseen: false }),
now: 2_000
})
const idleClassName = idle.firstElementChild?.className ?? ''
expect(idleClassName).toContain('border-border/60')
expect(idleClassName).not.toContain('emerald')
expect(idleClassName).not.toContain('agent-question')
})
it('heads the card with the conversation name and drops the worktree to the footer', () => {
const { container } = renderCard({
card: card({ lastUserMessage: 'ship it', conversationName: 'Sparse-checkout parser' }),
now: 2_000
})
const cardElement = container.firstElementChild!
const header = cardElement.querySelector('button')!.firstElementChild!
const footer = cardElement.lastElementChild!
expect(header).toHaveTextContent('Sparse-checkout parser')
expect(header).not.toHaveTextContent('dashboard-review')
expect(footer).toHaveTextContent('dashboard-review')
expect(screen.getByText('You')).toBeInTheDocument()
})
it('heads the card with the worktree when no name resolves, without repeating it', () => {
const { container } = renderCard({ card: card({ lastUserMessage: 'ship it' }), now: 2_000 })
expect(screen.getAllByText('dashboard-review')).toHaveLength(1)
expect(container.querySelector('button')!.firstElementChild).toHaveTextContent(
'dashboard-review'
)
})
it('shows the repo as an icon labelled with its name instead of inline text', () => {
renderCard({
card: card(),
now: 2_000,
repoIcon: { type: 'emoji', emoji: '🐳' }
})
expect(screen.getByLabelText('Orca')).toBeInTheDocument()
expect(screen.getByText('🐳')).toBeInTheDocument()
})
it('skips structured-clone rerenders until visible card data or its age changes', () => {
const onOpenTerminal = vi.fn()
const initial = card({
startedAt: 1_000,
subagents: [{ id: 'child-1', name: 'Review loop', dotState: 'working' }]
})
const repoIcon: RepoIcon = { type: 'lucide', name: 'Rocket' }
const { rerender } = render(
<TooltipProvider>
<AgentKanbanCard
card={initial}
repoIcon={repoIcon}
now={61_500}
onOpenTerminal={onOpenTerminal}
/>
</TooltipProvider>
)
expect(agentIconRender).toHaveBeenCalledTimes(1)
expect(screen.getByText('1m')).toBeInTheDocument()
// A fresh structured clone of identical data — including the repo icon.
rerender(
<TooltipProvider>
<AgentKanbanCard
card={{ ...initial, subagents: initial.subagents?.map((subagent) => ({ ...subagent })) }}
repoIcon={{ ...repoIcon }}
now={62_000}
onOpenTerminal={onOpenTerminal}
/>
</TooltipProvider>
)
expect(agentIconRender).toHaveBeenCalledTimes(1)
rerender(
<TooltipProvider>
<AgentKanbanCard
card={{ ...initial, subagents: initial.subagents?.map((subagent) => ({ ...subagent })) }}
repoIcon={{ ...repoIcon }}
now={121_500}
onOpenTerminal={onOpenTerminal}
/>
</TooltipProvider>
)
expect(agentIconRender).toHaveBeenCalledTimes(2)
expect(screen.getByText('2m')).toBeInTheDocument()
})
it('rerenders when a working card enters monitoring', () => {
const initial = card()
const { rerender } = renderCard({ card: initial, now: 2_000 })
expect(agentStateDotRender).toHaveBeenLastCalledWith('working')
rerender(
<TooltipProvider>
<AgentKanbanCard
card={{ ...initial, workingMode: 'monitoring' }}
now={2_000}
onOpenTerminal={vi.fn()}
/>
</TooltipProvider>
)
expect(agentStateDotRender).toHaveBeenLastCalledWith('monitoring')
})
it('rerenders when the repo icon changes', () => {
const onOpenTerminal = vi.fn()
const initial = card({ startedAt: 1_000 })
const { rerender } = render(
<TooltipProvider>
<AgentKanbanCard
card={initial}
repoIcon={{ type: 'lucide', name: 'Rocket' }}
now={61_500}
onOpenTerminal={onOpenTerminal}
/>
</TooltipProvider>
)
expect(agentIconRender).toHaveBeenCalledTimes(1)
rerender(
<TooltipProvider>
<AgentKanbanCard
card={{ ...initial }}
repoIcon={{ type: 'lucide', name: 'Database' }}
now={61_500}
onOpenTerminal={onOpenTerminal}
/>
</TooltipProvider>
)
expect(agentIconRender).toHaveBeenCalledTimes(2)
})
it('updates the relative age when the UI language changes', async () => {
renderCard({ card: card({ startedAt: 1_000 }), now: 121_500 })
expect(screen.getByText('2m')).toBeInTheDocument()
await act(async () => {
await i18n.changeLanguage('ja')
})
expect(screen.getByText('2分')).toBeInTheDocument()
})
})
@@ -37,15 +37,6 @@ vi.mock('./AgentTerminalPreview', () => ({
)
}))
const TERMINAL_INPUT: DashboardCardTerminalInput = {
hostPlatform: 'win32',
localWindowsConpty: true,
osRelease: '10.0.22631',
windowsShiftEnterEncoding: 'csi-u',
ctrlEnterCsiU: false,
kittyKeyboardAdvertised: false
}
function card(overrides: Partial<DashboardCard> = {}): DashboardCard {
return {
paneKey: 'tab1:leaf1',
@@ -74,121 +65,6 @@ afterEach(() => {
// This prop is the only path carrying relayed host-input facts into the preview; omission silently falls back to client-OS routing.
describe('AgentTerminalDialog', () => {
it("hands the card's relayed host-input profile to the preview terminal", () => {
render(
<AgentTerminalDialog
card={card({ terminalInput: TERMINAL_INPUT })}
onOpenChange={() => {}}
onReveal={() => {}}
/>
)
expect(screen.getByTestId('preview')).toHaveAttribute(
'data-terminal-input',
JSON.stringify(TERMINAL_INPUT)
)
})
it.each([
['interrupted', 'Interrupted'],
['failed', 'Failed'],
['unconfirmed', 'Couldn’t confirm']
] as const)('keeps an acknowledged %s verdict in the terminal header', (verdict, label) => {
const statusCard = { ...card({ dotState: 'done', bucket: 'idle', unseen: false }), verdict }
render(<AgentTerminalPanel card={statusCard} onOpenChange={() => {}} onReveal={() => {}} />)
expect(screen.getByText(new RegExp(label))).toBeInTheDocument()
})
it('passes null when the card carries no profile, so the preview routes by client OS', () => {
render(<AgentTerminalDialog card={card()} onOpenChange={() => {}} onReveal={() => {}} />)
expect(screen.getByTestId('preview')).toHaveAttribute('data-terminal-input', 'null')
})
it('does not claim a remote pane closed when the card carries no live pty', () => {
render(
<AgentTerminalDialog
card={card({ ptyId: null, hostKind: 'ssh' })}
onOpenChange={() => {}}
onReveal={() => {}}
/>
)
// Loss of contact with an SSH host is `unverifiable`, never `exited`.
expect(screen.getByText(/remote session/)).toBeInTheDocument()
expect(screen.queryByText(/pane has closed/)).not.toBeInTheDocument()
})
it('keeps a local agent openable without claiming its missing preview means closure', () => {
render(
<AgentTerminalDialog
card={card({ ptyId: null, hostKind: 'local' })}
onOpenChange={() => {}}
onReveal={() => {}}
/>
)
expect(screen.getByText(/No preview available/)).toBeInTheDocument()
expect(screen.queryByText(/pane has closed/)).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Open worktree' })).toBeEnabled()
})
it('labels acknowledged completions idle without review or pin controls', () => {
render(
<AgentTerminalDialog
card={card({ bucket: 'idle', dotState: 'done', finishedAt: 100, unseen: false })}
onOpenChange={() => {}}
onReveal={() => {}}
/>
)
expect(screen.getByText(/Claude · Idle/)).toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'Keep visible' })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'Mark reviewed' })).not.toBeInTheDocument()
expect(screen.getByTestId('preview')).toHaveAttribute('data-pty-id', 'pty-1')
})
it('labels unseen completions done', () => {
render(
<AgentTerminalDialog
card={card({ bucket: 'done', dotState: 'done', finishedAt: 100, unseen: true })}
onOpenChange={() => {}}
onReveal={() => {}}
/>
)
expect(screen.getByText(/Claude · Done/)).toBeInTheDocument()
})
it('preserves the execution host when revealing a colliding worktree ID', () => {
const onReveal = vi.fn()
render(
<AgentTerminalDialog
card={card({ executionHostId: 'runtime:env-1' })}
onOpenChange={() => {}}
onReveal={onReveal}
/>
)
fireEvent.click(screen.getByRole('button', { name: 'Open worktree' }))
expect(onReveal).toHaveBeenCalledWith({
repoId: 'r1',
worktreeId: 'w1',
executionHostId: 'runtime:env-1',
tabId: 'tab1',
leafId: 'leaf1'
})
})
it('reuses the terminal surface as a non-modal adjacent panel', () => {
render(<AgentTerminalPanel card={card()} onOpenChange={() => {}} onReveal={() => {}} />)
expect(screen.getByRole('dialog', { name: 'wt' })).toHaveAttribute('data-state', 'open')
expect(screen.getByTestId('preview')).toHaveClass('min-h-0', 'flex-1')
expect(document.querySelector('[data-slot="dialog-overlay"]')).not.toBeInTheDocument()
expect(screen.getByRole('heading', { name: 'wt' })).toBeInTheDocument()
})
it('lets a nested Radix layer consume Escape before closing the panel', () => {
const onOpenChange = vi.fn()
render(
@@ -14,7 +14,6 @@ import {
BRACKETED_PASTE_START
} from '../../../../shared/terminal/input/terminal-bracketed-paste-text'
import { dispatchAppMenuPasteEvent } from '@/lib/app-menu-paste'
import { dispatchAppMenuSelectionAction } from '@/lib/app-menu-selection-actions'
const terminalHarness = vi.hoisted(() => ({
instances: [] as {
@@ -216,245 +215,6 @@ describe('AgentTerminalPreview clipboard routes', () => {
vi.clearAllMocks()
})
it('claims the app-menu paste event and pastes while the preview owns focus', async () => {
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
focusInsidePreview(view.container)
let claimed = false
act(() => {
claimed = dispatchAppMenuPasteEvent()
})
// The claim is what keeps App-level paste out of xterm's hidden textarea.
expect(claimed).toBe(true)
await waitFor(() => expect(terminal.paste).toHaveBeenCalledWith('clip-text'))
expect(input).toHaveBeenCalledWith('pty-1', 'clip-text')
})
it('encodes a leading newline for a remote Windows Codex preview without submitting', async () => {
readClipboardText.mockResolvedValueOnce('\nsecond line')
const view = render(
<AgentTerminalPreview
ptyId="remote:windows-box@@pty-1"
terminalInput={{
hostPlatform: 'win32',
localWindowsConpty: false,
windowsShiftEnterEncoding: 'alt-enter',
windowsInputRecordPasteNewline: 'alt-enter',
ctrlEnterCsiU: false,
kittyKeyboardAdvertised: false
}}
/>
)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
focusInsidePreview(view.container)
act(() => {
dispatchAppMenuPasteEvent()
})
await waitFor(() =>
expect(input).toHaveBeenCalledWith('remote:windows-box@@pty-1', '\x1b\rsecond line')
)
expect(terminal.input).toHaveBeenCalledWith('\x1b\rsecond line')
expect(terminal.paste).not.toHaveBeenCalled()
})
it('claims app-menu selection actions while the preview owns focus', async () => {
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
terminal.selectionText = 'selected text'
const focusTarget = focusInsidePreview(view.container, 'textarea')
focusTarget.className = 'xterm-helper-textarea'
let selectAllClaimed = false
let copyClaimed = false
act(() => {
selectAllClaimed = dispatchAppMenuSelectionAction('select-all')
})
act(() => {
copyClaimed = dispatchAppMenuSelectionAction('copy')
})
expect(selectAllClaimed).toBe(true)
expect(copyClaimed).toBe(true)
expect(terminal.selectAll).toHaveBeenCalledOnce()
await waitFor(() => expect(writeTerminalClipboardText).toHaveBeenCalledWith('selected text'))
})
it('leaves app-menu selection actions unclaimed for text controls inside the preview', async () => {
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
focusInsidePreview(view.container)
let claimed = true
act(() => {
claimed = dispatchAppMenuSelectionAction('select-all')
})
// Unclaimed so the App-level handler performs the native selection action.
expect(claimed).toBe(false)
expect(terminal.selectAll).not.toHaveBeenCalled()
})
it('leaves a copy action with no terminal selection unclaimed', async () => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
let claimed = true
act(() => {
claimed = dispatchAppMenuSelectionAction('copy')
})
expect(claimed).toBe(false)
expect(writeTerminalClipboardText).not.toHaveBeenCalled()
})
it('leaves the app-menu paste event unclaimed when focus is outside the preview', async () => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
let claimed = true
await act(async () => {
claimed = dispatchAppMenuPasteEvent()
})
expect(claimed).toBe(false)
expect(readClipboardText).not.toHaveBeenCalled()
expect(terminalHarness.instances[0]!.paste).not.toHaveBeenCalled()
})
it('stops claiming app-menu clipboard events once unmounted', async () => {
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
focusInsidePreview(view.container)
view.unmount()
expect(dispatchAppMenuPasteEvent()).toBe(false)
expect(dispatchAppMenuSelectionAction('select-all')).toBe(false)
})
it('pastes on plain Ctrl+V on Windows, where no Edit-menu accelerator ever fires', async () => {
platformState.value = 'win32'
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
focusInsidePreview(view.container)
const plain = new KeyboardEvent('keydown', {
key: 'v',
code: 'KeyV',
ctrlKey: true,
cancelable: true
})
expect(terminal.customKeyHandler!(plain)).toBe(false)
expect(plain.defaultPrevented).toBe(true)
await waitFor(() => expect(terminal.paste).toHaveBeenCalledWith('clip-text'))
// The repeat is swallowed without a second clipboard read, and so is the keyup.
expect(
terminal.customKeyHandler!(
new KeyboardEvent('keydown', { key: 'v', code: 'KeyV', ctrlKey: true, repeat: true })
)
).toBe(false)
expect(terminal.customKeyHandler!(new KeyboardEvent('keyup', { key: 'v', code: 'KeyV' }))).toBe(
false
)
expect(readClipboardText).toHaveBeenCalledTimes(1)
})
it('handles the shifted paste chord on Linux', async () => {
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
focusInsidePreview(view.container)
const shifted = new KeyboardEvent('keydown', {
key: 'V',
code: 'KeyV',
ctrlKey: true,
shiftKey: true,
cancelable: true
})
expect(terminal.customKeyHandler!(shifted)).toBe(false)
expect(shifted.defaultPrevented).toBe(true)
await waitFor(() => expect(terminal.paste).toHaveBeenCalledWith('clip-text'))
expect(readClipboardText).toHaveBeenCalledTimes(1)
})
it('leaves plain Cmd+V to the macOS Edit-menu accelerator', async () => {
platformState.value = 'darwin'
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
focusInsidePreview(view.container)
// macOS Cmd+V uses its native accelerator; handling it here too would paste twice.
const plain = terminal.customKeyHandler!(
new KeyboardEvent('keydown', { key: 'v', code: 'KeyV', metaKey: true })
)
expect(plain).toBe(true)
expect(readClipboardText).not.toHaveBeenCalled()
})
it('pastes on terminal-style right-click when the setting is on', async () => {
storeState.settings = { terminalRightClickToPaste: true }
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
focusInsidePreview(view.container)
const host = view.container.querySelector<HTMLElement>('.origin-bottom-left')!
const menuEvent = new MouseEvent('contextmenu', { bubbles: true, cancelable: true })
act(() => {
host.dispatchEvent(menuEvent)
})
expect(menuEvent.defaultPrevented).toBe(true)
await waitFor(() => expect(terminal.paste).toHaveBeenCalledWith('clip-text'))
})
it('copies the selection on terminal-style right-click instead of pasting', async () => {
storeState.settings = { terminalRightClickToPaste: true }
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
terminal.selectionText = 'selected text'
focusInsidePreview(view.container)
const host = view.container.querySelector<HTMLElement>('.origin-bottom-left')!
act(() => {
host.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))
})
await waitFor(() => expect(writeTerminalClipboardText).toHaveBeenCalledWith('selected text'))
expect(readClipboardText).not.toHaveBeenCalled()
})
it('leaves right-click to the native menu when the setting is off', async () => {
storeState.settings = { terminalRightClickToPaste: false }
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
focusInsidePreview(view.container)
const host = view.container.querySelector<HTMLElement>('.origin-bottom-left')!
const menuEvent = new MouseEvent('contextmenu', { bubbles: true, cancelable: true })
act(() => {
host.dispatchEvent(menuEvent)
})
expect(menuEvent.defaultPrevented).toBe(false)
expect(readClipboardText).not.toHaveBeenCalled()
})
it('cancels an async paste when the preview loses focus', async () => {
let resolveClipboard!: (text: string) => void
readClipboardText.mockReturnValueOnce(
@@ -158,74 +158,6 @@ async function settleGeometry(): Promise<void> {
})
}
it.each([
{
updates: { terminalFontSize: 18 },
cols: 50,
rows: 15,
scale: 'scale(0.625)',
anchor: 'top left'
},
{
updates: { terminalFontFamily: 'Fira Code' },
cols: 50,
rows: 15,
scale: 'scale(0.625)',
anchor: 'top left'
},
{
updates: { terminalFontWeight: 900 },
cols: 50,
rows: 15,
scale: 'scale(0.625)',
anchor: 'top left'
},
{
updates: { terminalFontWeightBold: 900 },
cols: 50,
rows: 15,
scale: 'scale(0.625)',
anchor: 'top left'
},
{
updates: { terminalLineHeight: 1.5 },
cols: 60,
rows: 10,
scale: 'scale(0.75)',
anchor: 'bottom left'
},
{
updates: { terminalLigatures: 'on' },
cols: 60,
rows: 15,
scale: 'scale(0.75)',
anchor: 'top left'
}
] satisfies {
updates: Partial<GlobalSettings>
cols: number
rows: number
scale: string
anchor: string
}[])(
'recreates the owner and fits the new geometry for $updates',
async ({ updates, cols, rows, scale, anchor }) => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await settleGeometry()
expect(fit).toHaveBeenLastCalledWith('pty-1', 60, 15)
await act(async () => useAppStore.getState().updateSettings(updates))
await settleGeometry()
expect(fit).toHaveBeenCalledTimes(2)
expect(fit).toHaveBeenLastCalledWith('pty-1', cols, rows)
expect(harness.instances).toHaveLength(2)
expect(connect).toHaveBeenCalledTimes(2)
expect(unsubscribe).toHaveBeenCalledExactlyOnceWith('pty-1')
expect(harness.instances[1]?.container?.style.transform).toBe(scale)
expect(harness.instances[1]?.container?.style.transformOrigin).toBe(anchor)
}
)
it('ignores a pending connection retired by a metric change', async () => {
const gate = Promise.withResolvers<TerminalPreviewConnectResult>()
connect.mockReturnValueOnce(gate.promise)
@@ -6,7 +6,7 @@ import type { ITerminalOptions } from '@xterm/xterm'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { TerminalPreviewConnectResult } from '../../../../shared/terminal/replay/terminal-preview'
import { createGlobalSettingsFixture } from '../../../../shared/global-settings-test-fixture'
import { makeCustomTerminalThemeSelection } from '../../../../shared/terminal-themes/terminal-custom-themes'
import { useAppStore } from '@/store'
import { AgentTerminalPreview } from './AgentTerminalPreview'
@@ -79,17 +79,13 @@ const initial = useAppStore.getInitialState()
const connect = vi.fn<Window['api']['terminalPreview']['connect']>()
const unsubscribe = vi.fn(async () => {})
const offData = vi.fn()
const onData = vi.fn((listener: Parameters<Window['api']['terminalPreview']['onData']>[0]) => {
emitData = listener
return offData
})
const onData = vi.fn(() => offData)
const connection: TerminalPreviewConnectResult = {
snapshot: { data: 'agent prompt', cols: 80, rows: 24, seq: 1 },
replay: []
}
let settings: GlobalSettings
let originalApi: PropertyDescriptor | undefined
let emitData: Parameters<Window['api']['terminalPreview']['onData']>[0] | undefined
async function updateSettings(updates: Partial<GlobalSettings>): Promise<void> {
await act(async () => useAppStore.getState().updateSettings(updates))
@@ -107,7 +103,6 @@ beforeEach(() => {
vi.clearAllMocks()
harness.instances.length = 0
harness.systemPrefersDark = false
emitData = undefined
settings = createGlobalSettingsFixture({ theme: 'dark', activeRuntimeEnvironmentId: null })
connect.mockReset().mockResolvedValue(connection)
originalApi = Object.getOwnPropertyDescriptor(window, 'api')
@@ -158,80 +153,6 @@ describe('preview terminal settings lifetime', () => {
expect(unsubscribe).not.toHaveBeenCalled()
})
it('updates cursor options on the existing terminal', async () => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await act(async () => {})
await updateSettings({
terminalCursorStyle: 'bar',
terminalCursorBlink: false
})
expect(connect).toHaveBeenCalledOnce()
expect(terminal().options).toMatchObject({
cursorStyle: 'bar',
cursorBlink: false
})
})
it.each([
{ terminalThemeDark: 'Builtin Tango Light' },
{ terminalColorOverrides: { background: '#123456' } },
{ terminalBackgroundOpacity: 0.5 },
{ terminalCursorOpacity: 0.5 },
{ terminalMinimumContrastRatio: 7 }
] satisfies Partial<GlobalSettings>[])(
'replaces the terminal when theme or contrast changes: %o',
async (updates) => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await act(async () => {})
await updateSettings(updates)
expect(connect).toHaveBeenCalledTimes(2)
expect(unsubscribe).toHaveBeenCalledExactlyOnceWith('pty-1')
expect(terminal().dispose).toHaveBeenCalledOnce()
expect(harness.instances).toHaveLength(2)
if ('terminalMinimumContrastRatio' in updates) {
expect(terminal(1).options.minimumContrastRatio).toBe(7)
} else {
expect(terminal(1).options.theme).not.toEqual(terminal().options.theme)
}
}
)
it('keeps a cloned imported theme but reconnects when its selected colors change', async () => {
const custom = {
id: 'manual:preview',
name: 'Preview',
source: 'manual' as const,
mode: 'dark' as const,
terminal: { background: '#123456', foreground: '#eeeeee', red: '#ff0000' },
importedAt: '2026-09-25'
}
await updateSettings({
terminalCustomThemes: [custom],
terminalThemeDark: makeCustomTerminalThemeSelection(custom.id)
})
render(<AgentTerminalPreview ptyId="pty-1" />)
await act(async () => {})
expect(terminal().options.theme?.background).toBe('#123456')
await updateSettings({ editorAutoSaveDelayMs: 1234 })
expect(connect).toHaveBeenCalledOnce()
await updateSettings({
terminalCustomThemes: [{ ...custom, terminal: { ...custom.terminal, background: '#654321' } }]
})
expect(connect).toHaveBeenCalledTimes(2)
expect(terminal(1).options.theme?.background).toBe('#654321')
})
it('reconnects when the effective OS theme changes', async () => {
await updateSettings({ theme: 'system' })
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await act(async () => {})
harness.systemPrefersDark = true
view.rerender(<AgentTerminalPreview ptyId="pty-1" />)
await act(async () => {})
expect(connect).toHaveBeenCalledTimes(2)
expect(terminal(1).options.theme).not.toEqual(terminal().options.theme)
})
it('uses current live options when an unrelated update arrives during connect', async () => {
const gate = Promise.withResolvers<TerminalPreviewConnectResult>()
connect.mockReturnValueOnce(gate.promise)
@@ -243,18 +164,6 @@ describe('preview terminal settings lifetime', () => {
expect(terminal().options.cursorStyle).toBe('bar')
})
it('ignores an old pending connection after a relevant theme change', async () => {
const gate = Promise.withResolvers<TerminalPreviewConnectResult>()
connect.mockReturnValueOnce(gate.promise)
render(<AgentTerminalPreview ptyId="pty-1" />)
await updateSettings({ terminalMinimumContrastRatio: 7 })
expect(connect).toHaveBeenCalledTimes(2)
expect(harness.instances).toHaveLength(1)
await act(async () => gate.resolve(connection))
expect(harness.instances).toHaveLength(1)
expect(terminal().options.minimumContrastRatio).toBe(7)
})
it('does not install a terminal after unmounting a pending connection', async () => {
const gate = Promise.withResolvers<TerminalPreviewConnectResult>()
connect.mockReturnValueOnce(gate.promise)
@@ -266,27 +175,4 @@ describe('preview terminal settings lifetime', () => {
expect(connect).toHaveBeenCalledOnce()
expect(unsubscribe).toHaveBeenCalledExactlyOnceWith('ssh:host@@pty-1')
})
it('keeps a hidden mounted preview connected through unrelated settings updates', async () => {
render(
<div hidden>
<AgentTerminalPreview ptyId="pty-1" />
</div>
)
await act(async () => {})
await updateSettings({ editorAutoSaveDelayMs: 1234 })
expect(connect).toHaveBeenCalledOnce()
expect(unsubscribe).not.toHaveBeenCalled()
})
it('still resyncs the same terminal after unrelated settings updates', async () => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await act(async () => {})
await updateSettings({ editorAutoSaveDelayMs: 1234 })
await act(async () => emitData?.({ type: 'resync', ptyId: 'pty-1' }))
expect(connect).toHaveBeenCalledTimes(2)
expect(harness.instances).toHaveLength(1)
expect(terminal().reset).toHaveBeenCalledOnce()
expect(unsubscribe).not.toHaveBeenCalled()
})
})
@@ -253,277 +253,6 @@ describe('AgentTerminalPreview', () => {
expect(ack).toHaveBeenCalledWith('pty-1', 4)
})
it('installs the macOS IME native-text forwarder and lets its claims bypass chord handling', async () => {
platformState.value = 'darwin'
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
expect(imeHarness.forwarders).toHaveLength(1)
expect(imeHarness.trackers).toHaveLength(1)
imeHarness.forwarders[0]!.sendInput('。')
expect(terminal.input).toHaveBeenCalledOnce()
expect(input).toHaveBeenCalledOnce()
expect(input).toHaveBeenCalledWith('pty-1', '。')
// A claimed native-text key bypasses xterm AND the clipboard chords.
imeHarness.claimResult = true
terminal.selectionText = 'selected text'
const handled = terminal.customKeyHandler!(
new KeyboardEvent('keydown', { key: 'C', code: 'KeyC', ctrlKey: true, shiftKey: true })
)
expect(handled).toBe(false)
expect(writeClipboardText).not.toHaveBeenCalled()
expect(writeTerminalClipboardText).not.toHaveBeenCalled()
// Unclaimed events still reach the chord handling.
imeHarness.claimResult = false
const copied = terminal.customKeyHandler!(
new KeyboardEvent('keydown', { key: 'c', code: 'KeyC', metaKey: true })
)
expect(copied).toBe(false)
expect(writeTerminalClipboardText).toHaveBeenCalledWith('selected text')
expect(writeClipboardText).not.toHaveBeenCalled()
})
it('does not install the IME native-text forwarder on Windows', async () => {
platformState.value = 'win32'
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
await waitFor(() => expect(terminalHarness.instances[0]!.customKeyHandler).not.toBeNull())
expect(imeHarness.forwarders).toHaveLength(0)
expect(imeHarness.trackers).toHaveLength(0)
})
// fcitx4 delivers a single-character pick as the Space/digit keypress's text (#28213).
it('installs the IME native-text forwarder on desktop Linux', async () => {
platformState.value = 'linux'
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(imeHarness.forwarders).toHaveLength(1))
})
it('hands the forwarder the live mirror seeded from the snapshot flags', async () => {
platformState.value = 'darwin'
connect.mockResolvedValue({
snapshot: { data: '', cols: 80, rows: 24, seq: 1, kittyKeyboardFlags: 8 },
replay: []
})
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(imeHarness.forwarders).toHaveLength(1))
await waitFor(() => expect(imeHarness.forwarders[0]!.getKittyKeyboardFlags()).toBe(8))
// The popout xterm gets the same flags, so its encoder agrees with the mirror.
expect(terminalHarness.instances[0]!.write).toHaveBeenCalledWith(
'\x1b[<99u\x1b[=8u',
expect.any(Function)
)
// Live output keeps advancing the same mirror the forwarder reads.
act(() => {
emitData?.({ type: 'data', ptyId: 'pty-1', data: '\x1b[<u', bytes: 4 })
})
expect(imeHarness.forwarders[0]!.getKittyKeyboardFlags()).toBe(0)
})
it('disposes the IME bridge on unmount', async () => {
platformState.value = 'darwin'
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(imeHarness.forwarders).toHaveLength(1))
view.unmount()
expect(imeHarness.forwarders[0]!.dispose).toHaveBeenCalledTimes(1)
expect(imeHarness.trackers[0]!.dispose).toHaveBeenCalledTimes(1)
})
it('disposes the IME bridge once when the PTY disappears', async () => {
platformState.value = 'darwin'
connect.mockResolvedValueOnce({
snapshot: { data: '', cols: 80, rows: 24, seq: 1 },
replay: []
})
connect.mockResolvedValueOnce({ snapshot: null, replay: [] })
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(imeHarness.forwarders).toHaveLength(1))
act(() => emitData?.({ type: 'resync', ptyId: 'pty-1' }))
await waitFor(() => expect(imeHarness.forwarders[0]!.dispose).toHaveBeenCalledOnce())
expect(imeHarness.trackers[0]!.dispose).toHaveBeenCalledOnce()
view.unmount()
expect(imeHarness.forwarders[0]!.dispose).toHaveBeenCalledOnce()
expect(imeHarness.trackers[0]!.dispose).toHaveBeenCalledOnce()
})
it('copies the terminal selection on the copy chord and blocks xterm handling', async () => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
terminal.selectionText = 'selected text'
const keydown = new KeyboardEvent('keydown', {
key: 'C',
code: 'KeyC',
ctrlKey: true,
shiftKey: true,
cancelable: true
})
const handled = terminal.customKeyHandler!(keydown)
const keyupHandled = terminal.customKeyHandler!(
new KeyboardEvent('keyup', { key: 'C', code: 'KeyC', ctrlKey: true, shiftKey: true })
)
expect(handled).toBe(false)
expect(keyupHandled).toBe(false)
expect(keydown.defaultPrevented).toBe(true)
expect(writeTerminalClipboardText).toHaveBeenCalledWith('selected text')
expect(writeClipboardText).not.toHaveBeenCalled()
})
it('keeps an empty copy chord from leaking terminal input', async () => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
const handled = terminal.customKeyHandler!(
new KeyboardEvent('keydown', { key: 'C', code: 'KeyC', ctrlKey: true, shiftKey: true })
)
expect(handled).toBe(false)
expect(writeTerminalClipboardText).not.toHaveBeenCalled()
})
it('leaves bare Ctrl+C available to the terminal without a selection', async () => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
const handled = terminal.customKeyHandler!(
new KeyboardEvent('keydown', { key: 'c', code: 'KeyC', ctrlKey: true })
)
expect(handled).toBe(true)
expect(writeTerminalClipboardText).not.toHaveBeenCalled()
})
it('hands unselected Cmd+C to a kitty app and copies a selection instead', async () => {
platformState.value = 'darwin'
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
const cmdC = (type: string): KeyboardEvent =>
new KeyboardEvent(type, { key: 'c', code: 'KeyC', metaKey: true, cancelable: true })
// A plain shell's unselected Cmd+C sends nothing.
expect(terminal.customKeyHandler!(cmdC('keydown'))).toBe(false)
expect(terminal.customKeyHandler!(cmdC('keyup'))).toBe(false)
act(() => {
emitData?.({ type: 'data', ptyId: 'pty-1', data: '\x1b[>1u', bytes: 5 })
})
expect(terminal.customKeyHandler!(cmdC('keydown'))).toBe(true)
expect(terminal.customKeyHandler!(cmdC('keyup'))).toBe(true)
// A highlight of blank cells copies no text but is still Orca's selection, as in the pane.
Object.assign(terminal, { hasSelection: () => true })
expect(terminal.customKeyHandler!(cmdC('keydown'))).toBe(false)
expect(terminal.customKeyHandler!(cmdC('keyup'))).toBe(false)
expect(writeTerminalClipboardText).not.toHaveBeenCalled()
terminal.selectionText = 'selected text'
expect(terminal.customKeyHandler!(cmdC('keydown'))).toBe(false)
expect(terminal.customKeyHandler!(cmdC('keyup'))).toBe(false)
expect(writeTerminalClipboardText).toHaveBeenCalledWith('selected text')
})
it('selects all terminal text on Cmd+A and blocks xterm handling', async () => {
platformState.value = 'darwin'
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
const keydown = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
metaKey: true,
cancelable: true
})
expect(terminal.customKeyHandler!(keydown)).toBe(false)
expect(keydown.defaultPrevented).toBe(true)
const repeat = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
metaKey: true,
repeat: true,
cancelable: true
})
expect(terminal.customKeyHandler!(repeat)).toBe(false)
expect(repeat.defaultPrevented).toBe(true)
expect(terminal.selectAll).toHaveBeenCalledOnce()
})
it('sends the word-kill byte on Ctrl+Backspace and blocks xterm handling', async () => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
const keydown = new KeyboardEvent('keydown', {
key: 'Backspace',
code: 'Backspace',
ctrlKey: true,
cancelable: true
})
const handled = terminal.customKeyHandler!(keydown)
expect(handled).toBe(false)
expect(keydown.defaultPrevented).toBe(true)
expect(terminal.input).toHaveBeenCalledWith('\x17')
await waitFor(() => expect(input).toHaveBeenCalledWith('pty-1', '\x17'))
})
it('swallows a pane-scoped chord instead of leaking its control byte to the agent', async () => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
// Ctrl+Shift+D splits a pane on Linux; xterm would otherwise send Ctrl+D.
const keydown = new KeyboardEvent('keydown', {
key: 'D',
code: 'KeyD',
ctrlKey: true,
shiftKey: true,
cancelable: true
})
const handled = terminal.customKeyHandler!(keydown)
expect(handled).toBe(false)
expect(keydown.defaultPrevented).toBe(true)
expect(terminal.input).not.toHaveBeenCalled()
expect(input).not.toHaveBeenCalled()
})
it('dismisses the hosting dialog on the pane-close chord', async () => {
const onRequestClose = vi.fn()
render(<AgentTerminalPreview ptyId="pty-1" onRequestClose={onRequestClose} />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
const keydown = new KeyboardEvent('keydown', {
key: 'w',
code: 'KeyW',
ctrlKey: true,
cancelable: true
})
expect(terminal.customKeyHandler!(keydown)).toBe(false)
expect(keydown.defaultPrevented).toBe(true)
expect(onRequestClose).toHaveBeenCalledOnce()
expect(input).not.toHaveBeenCalled()
})
it('keeps a native input-source chord from inserting text into the preview', async () => {
storeState.keybindings = { 'terminal.switchInputSource': ['Shift+Space'] }
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
@@ -573,27 +302,6 @@ describe('AgentTerminalPreview', () => {
view.unmount()
})
it('defers Option chords to xterm once the TUI negotiates kitty keyboard mode', async () => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
const altBackspace = (): KeyboardEvent =>
new KeyboardEvent('keydown', { key: 'Backspace', code: 'Backspace', altKey: true })
expect(terminal.customKeyHandler!(altBackspace())).toBe(false)
expect(terminal.input).toHaveBeenCalledWith('\x1b\x7f')
// The agent's TUI pushes kitty flags (CSI > 1 u) on the live stream.
act(() => {
emitData?.({ type: 'data', ptyId: 'pty-1', data: '\x1b[>1u', bytes: 5 })
})
terminal.input.mockClear()
expect(terminal.customKeyHandler!(altBackspace())).toBe(true)
expect(terminal.input).not.toHaveBeenCalled()
})
// Snapshot replay can redeliver a kitty push; it must not leave stale flags after the TUI’s single pop.
it('does not let a redelivered kitty push outlive the TUI pop', async () => {
connect.mockResolvedValueOnce({
@@ -618,36 +326,6 @@ describe('AgentTerminalPreview', () => {
expect(terminal.input).toHaveBeenCalledWith('\x1b\x7f')
})
it('scrolls the viewport on the macOS scrollback chord', async () => {
platformState.value = 'darwin'
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
const handled = terminal.customKeyHandler!(
new KeyboardEvent('keydown', { key: 'ArrowUp', code: 'ArrowUp', metaKey: true })
)
expect(handled).toBe(false)
expect(terminal.scrollToTop).toHaveBeenCalled()
expect(terminal.input).not.toHaveBeenCalled()
})
it('leaves an unmodified Backspace to xterm', async () => {
render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
await waitFor(() => expect(terminal.customKeyHandler).not.toBeNull())
const handled = terminal.customKeyHandler!(
new KeyboardEvent('keydown', { key: 'Backspace', code: 'Backspace', cancelable: true })
)
expect(handled).toBe(true)
expect(terminal.input).not.toHaveBeenCalled()
})
it('keeps the existing terminal visible while a resync snapshot is captured', async () => {
let resolveRefresh!: (value: TerminalPreviewConnectResult) => void
connect
@@ -704,56 +382,6 @@ describe('AgentTerminalPreview', () => {
expect(unsubscribe).toHaveBeenCalledWith('pty-1')
})
it('does not claim a remote pane closed when no snapshot can exist for it', async () => {
connect.mockResolvedValueOnce({ snapshot: null, replay: [] })
const view = render(<AgentTerminalPreview ptyId="ssh:devbox@@pty-3" />)
await waitFor(() => expect(view.getByText(/remote session/)).toBeInTheDocument())
expect(view.queryByText(/pane has closed/)).not.toBeInTheDocument()
})
it('connects a replacement pty after the previous pty was gone', async () => {
connect.mockResolvedValueOnce({ snapshot: null, replay: [] }).mockResolvedValueOnce({
snapshot: { data: 'replacement', cols: 80, rows: 24, seq: 1 },
replay: []
})
const view = render(<AgentTerminalPreview ptyId="pty-gone" />)
await waitFor(() => expect(view.getByText(/No preview available/)).toBeInTheDocument())
view.rerender(<AgentTerminalPreview ptyId="pty-live" />)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
expect(connect).toHaveBeenLastCalledWith('pty-live', { scrollbackRows: 24 })
expect(view.queryByText(/No preview available/)).not.toBeInTheDocument()
})
it('claims a grid sized to the dialog box and never re-requests an unchanged target', async () => {
vi.useFakeTimers()
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await vi.waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const host = view.container.querySelector<HTMLElement>('.origin-bottom-left')!
const box = host.parentElement!
Object.defineProperty(box, 'clientWidth', { configurable: true, value: 900 })
Object.defineProperty(box, 'clientHeight', { configurable: true, value: 480 })
// 80×24 grid rendered at 800×384 → 10×16 cells → the box holds 90×30.
const screen = document.createElement('div')
screen.className = 'xterm-screen'
Object.defineProperty(screen, 'offsetWidth', { configurable: true, value: 800 })
Object.defineProperty(screen, 'offsetHeight', { configurable: true, value: 384 })
host.appendChild(screen)
await vi.advanceTimersByTimeAsync(200)
expect(fit).toHaveBeenCalledTimes(1)
expect(fit).toHaveBeenCalledWith('pty-1', 90, 30)
// Unchanged target geometry must not reclaim the grid on reconnect and fight the host.
act(() => emitData?.({ type: 'resync', ptyId: 'pty-1' }))
await vi.waitFor(() => expect(connect).toHaveBeenCalledTimes(2))
await vi.advanceTimersByTimeAsync(400)
expect(fit).toHaveBeenCalledTimes(1)
})
it('delays repeated capture after an overflow and cancels the retry on unmount', async () => {
vi.useFakeTimers()
connect.mockResolvedValue({
@@ -777,40 +405,4 @@ describe('AgentTerminalPreview', () => {
await vi.advanceTimersByTimeAsync(150)
expect(connect).toHaveBeenCalledTimes(2)
})
it("previews a paired host's pane from that host, not this app's main process", async () => {
render(<AgentTerminalPreview ptyId="remote:env-1@@term-7" />)
await waitFor(() => expect(multiplexerHarness.subscriptions).toHaveLength(1))
expect(multiplexerHarness.environments).toEqual(['env-1'])
expect(multiplexerHarness.subscriptions[0]!.terminal).toBe('term-7')
expect(connect).not.toHaveBeenCalled()
act(() =>
multiplexerHarness.subscriptions[0]!.callbacks.onSnapshot('agent screen', {
cols: 100,
rows: 30,
seq: 5
})
)
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
const terminal = terminalHarness.instances[0]!
expect(terminal.write).toHaveBeenCalledWith('agent screen', expect.any(Function))
act(() => multiplexerHarness.subscriptions[0]!.callbacks.onData('live', { seq: 9 }))
expect(terminal.write).toHaveBeenCalledWith('live', expect.any(Function))
act(() => {
terminalHarness.userInputListener?.()
terminal.onDataListener?.('y')
})
expect(multiplexerHarness.sendInput).toHaveBeenCalledWith('y')
})
it('shows the unavailable message instead of a blank dialog when the bridge is missing', async () => {
// The web client's fallback proxy resolves every missing bridge call to undefined.
Object.assign(window.api.terminalPreview, { connect: async () => undefined })
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
await waitFor(() => expect(view.getByText(/No preview available/)).toBeInTheDocument())
})
})
@@ -16,12 +16,11 @@ import {
mountPreviewLinkInteraction
} from './dashboard-preview-dom-test-fixture'
import { verifyPreviewLinkPopulation } from './dashboard-preview-link-population-test-fixture'
import { verifyPopoutDefaultLinkPath } from './dashboard-preview-popout-default-test-fixture'
import { Terminal } from '@xterm/xterm'
import { toast } from 'sonner'
import { runtimeStatuses } from '@/store/slices/browser-slice-test-harness'
import { openStandaloneTerminal } from '../standalone-terminal/open-standalone-terminal'
import { TerminalKittyKeyboardModeTracker } from '../../../../shared/terminal/protocol/input-modes/terminal-kitty-keyboard-mode-tracker'
import type { openStandaloneTerminal } from '../standalone-terminal/open-standalone-terminal'
import { getDefaultSettings } from '../../../../shared/constants'
import { createStoreCascadesMockApi } from '@/store/slices/store-cascades-test-harness'
import {
@@ -37,8 +36,7 @@ import { openWorkspaceBrowserTabForLink } from '@/lib/workspace-browser-tab-open
import { AgentTerminalPreview } from './AgentTerminalPreview'
import { terminalPreviewApiFor } from './terminal-preview-api'
import { AgentKanbanBoard } from './AgentKanbanBoard'
import { AgentTerminalPanel } from './AgentTerminalDialog'
import { handleTerminalHttpLink } from '@/components/terminal-pane/terminal-url-link-hit-testing'
import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { ExecutionHostId } from '../../../../shared/execution-host'
@@ -214,42 +212,6 @@ async function output(data: string, ptyId = 'pty-1') {
}
const osc = `\x1b]8;;${URL_TO_OPEN}\x07hidden label\x1b]8;;\x07`
it('uses the real Dashboard constructor, OSC primer/provider and genuine claim; context Copy and live destinations share the same URI', async () => {
const foreground = vi.fn()
const view = render(
<AgentTerminalPreview ptyId="pty-1" linkSource={source} onBrowserForeground={foreground} />
)
await waitFor(() => screenFor(view.container))
await output(`\x1b[?1000h\x1b[?1006h${osc}`)
const screen = screenFor(view.container)
// No hover is delivered: actual capture priming must discover the hidden OSC label.
pointer(screen, 'mousedown')
pointer(screen, 'mouseup')
await waitFor(() => expect(document.querySelector('[data-link-action-popover]')).not.toBeNull())
expect(document.querySelector('[data-terminal-link-destination]')?.textContent).toBe(URL_TO_OPEN)
expect(
input.mock.calls.flatMap(([, data]) => data).filter((data) => data.startsWith('\x1b[<'))
).toEqual([])
fireEvent.click(view.getByRole('button', { name: 'Copy link' }))
await waitFor(() => expect(window.api.ui.writeClipboardText).toHaveBeenCalledWith(URL_TO_OPEN))
fireEvent.click(view.getByRole('button', { name: /System Browser/ }))
await waitFor(() => expect(openUrl).toHaveBeenCalledWith(URL_TO_OPEN))
expect(foreground).not.toHaveBeenCalled()
store.setState((state) => ({ settings: { ...state.settings!, openLinksInApp: true } }))
view.rerender(
<AgentTerminalPreview ptyId="pty-1" linkSource={source} onBrowserForeground={foreground} />
)
pointer(screen, 'mousemove')
pointer(screen, 'contextmenu', { button: 2, buttons: 0 })
await waitFor(() => expect(view.getByRole('button', { name: /Orca Browser/ })).toBeTruthy())
fireEvent.click(view.getByRole('button', { name: /Orca Browser/ }))
await waitFor(() => expect(foreground).toHaveBeenCalledOnce())
expect(store.getState().browserPagesByWorkspace).not.toEqual({})
view.unmount()
expect(document.querySelector('[data-link-action-popover]')).toBeNull()
})
it.each(['actions', 'open'] as const)(
'retains the provider-owned hidden OSC target across unrelated live row output for %s and context',
async (behavior) => {
@@ -319,56 +281,6 @@ function seedRemoteWorkspace(
}))
}
it.each([false, true])(
'keeps paired SSH OSC actions system-only with exact Copy despite the in-app preference (streaming=%s)',
async (streaming) => {
const hostId = 'ssh:private-target'
const ptyId = 'remote:hub-a@@handle-1'
const linkSource = {
...source,
executionHostId: hostId,
ptyId
} satisfies DashboardPreviewLinkSource
const foreground = vi.fn()
seedRemoteWorkspace(hostId, 'hub-a', streaming)
const props = { ptyId, linkSource, onBrowserForeground: foreground }
const connect = vi.spyOn(terminalPreviewApiFor(ptyId), 'connect')
const view = render(<AgentTerminalPreview {...props} />)
await waitFor(() => screenFor(view.container))
await output(osc, ptyId)
const screen = screenFor(view.container)
pointer(screen, 'mousedown')
pointer(screen, 'mouseup')
const system = await view.findByRole('button', { name: /System Browser/ })
expect(view.queryByRole('button', { name: /Orca Browser|Open in new tab/ })).toBeNull()
fireEvent.click(view.getByRole('button', { name: 'Copy link' }))
await waitFor(() => expect(window.api.ui.writeClipboardText).toHaveBeenCalledWith(URL_TO_OPEN))
fireEvent.click(system)
await waitFor(() => expect(openUrl).toHaveBeenCalledExactlyOnceWith(URL_TO_OPEN))
pointer(screen, 'mousedown', { ctrlKey: true })
pointer(screen, 'mouseup', { ctrlKey: true })
await waitFor(() => expect(openUrl).toHaveBeenCalledTimes(2))
expect(foreground).not.toHaveBeenCalled()
expect(toast.error).not.toHaveBeenCalled()
const wrongPtyId = 'remote:hub-b@@handle-1'
view.rerender(
<AgentTerminalPreview
{...props}
ptyId={wrongPtyId}
linkSource={{ ...linkSource, ptyId: wrongPtyId }}
/>
)
await waitFor(() => expect(connect).toHaveBeenCalledTimes(2))
await output(osc, wrongPtyId)
const wrongScreen = screenFor(view.container)
pointer(wrongScreen, 'mousedown', { ctrlKey: true })
pointer(wrongScreen, 'mouseup', { ctrlKey: true })
pointer(wrongScreen, 'contextmenu', { button: 2 })
expect(document.querySelector('[data-link-action-popover]')).toBeNull()
expect(openUrl).toHaveBeenCalledTimes(2)
}
)
it('gives one useful refusal when a real saved chooser action loses runtime browser eligibility', async () => {
const hostId = 'runtime:e'
const ptyId = 'remote:e@@handle-1'
@@ -398,126 +310,6 @@ it('gives one useful refusal when a real saved chooser action loses runtime brow
expect(input.mock.calls.map(([, data]) => data)).toEqual([])
})
it('keeps None/middle/drag/nonlink reports and ordinary input live, refuses parser queries, and leaves native/paste context alone', async () => {
store.setState((state) => ({
settings: { ...state.settings!, terminalLinkClickBehavior: 'none' }
}))
const view = render(<AgentTerminalPreview ptyId="pty-1" linkSource={source} />)
await waitFor(() => screenFor(view.container))
await output(`\x1b[?1000h\x1b[?1006h${URL_TO_OPEN}`)
const screen = screenFor(view.container)
pointer(screen, 'mousedown')
pointer(screen, 'mouseup')
pointer(screen, 'mousedown', { button: 1, buttons: 4 })
pointer(screen, 'mouseup', { button: 1 })
await waitFor(() =>
expect(input.mock.calls.some(([, data]) => data.startsWith('\x1b[<0;'))).toBe(true)
)
expect(input.mock.calls.map(([, data]) => data)).toEqual([
'\x1b[<0;1;1M',
'\x1b[<0;1;1m',
'\x1b[<1;1;1M',
'\x1b[<1;1;1m'
])
expect(document.querySelector('[data-link-action-popover]')).toBeNull()
expect(openUrl).not.toHaveBeenCalled()
const prior = input.mock.calls.length
await output('\x1b[6n')
expect(input).toHaveBeenCalledTimes(prior)
const textarea = view.container.querySelector('textarea')!
fireEvent.keyDown(textarea, { key: 'x', code: 'KeyX', keyCode: 88 })
fireEvent.input(textarea, { data: 'x', inputType: 'insertText' })
await waitFor(() => expect(input).toHaveBeenCalledWith('pty-1', 'x'))
const native = pointer(screen, 'contextmenu', { button: 2, ctrlKey: true })
expect(native.defaultPrevented).toBe(false)
store.setState((state) => ({ settings: { ...state.settings!, terminalRightClickToPaste: true } }))
view.rerender(<AgentTerminalPreview ptyId="pty-1" linkSource={source} />)
textarea.focus()
pointer(screen, 'contextmenu', { button: 2 })
await waitFor(() => expect(window.api.ui.readClipboardText).toHaveBeenCalled())
expect(document.querySelector('[data-link-action-popover]')).toBeNull()
})
it.each([false, true])(
'preserves standalone normal/SSH defaults and sole-emulator query policy (answersQueries=%s)',
async (answersQueries) => {
const container = document.createElement('div')
document.body.appendChild(container)
const send = vi.fn(() => true)
const instance = openStandaloneTerminal({
container,
options: { cols: 80, rows: 8, allowProposedApi: true },
kittyKeyboardModes: new TerminalKittyKeyboardModeTracker({ kittyKeyboard: true }),
sessionKey: 'plain-ssh:test',
sendInput: send,
answersQueries,
getSettings: () => store.getState().settings,
getMacOptionAsAlt: () => 'false',
getTerminalInput: () => null
})
instances.push(instance)
if (!instance) {
throw new Error('Real constructor refused test layout')
}
const terminal = instance.terminal
await new Promise<void>((resolve) =>
instance.write(`${osc}\r\n${URL_TO_OPEN}\x1b[6n`, { live: true, onDone: resolve })
)
expect(send.mock.calls.length > 0).toBe(answersQueries)
terminal.input('ordinary')
expect(send).toHaveBeenLastCalledWith('ordinary')
const screen = screenFor(container)
pointer(screen, 'mousemove', { buttons: 0 })
await waitFor(() => expect(terminal.element?.getAttribute('title')).toBe(URL_TO_OPEN))
pointer(screen, 'mousedown', { ctrlKey: true })
pointer(screen, 'mouseup', { ctrlKey: true })
await waitFor(() => expect(openUrl).toHaveBeenCalledWith(URL_TO_OPEN))
expect(terminal.hasSelection()).toBe(false)
expect(terminal.element?.getAttribute('title')).toBe(URL_TO_OPEN)
pointer(screen, 'mouseleave')
expect(terminal.element?.getAttribute('title')).toBeNull()
// The same default callback is captured by the detected-URL provider, not only OSC.
pointer(screen, 'mousemove', { clientY: 27 })
pointer(screen, 'mousedown', { ctrlKey: true, clientY: 27 })
pointer(screen, 'mouseup', { ctrlKey: true, clientY: 27 })
await waitFor(() => expect(openUrl).toHaveBeenCalledTimes(2))
expect(openUrl.mock.calls.map(([url]) => url)).toEqual([URL_TO_OPEN, URL_TO_OPEN])
pointer(screen, 'mousedown', { clientY: 27 })
pointer(screen, 'mouseup', { clientY: 27 })
expect(openUrl).toHaveBeenCalledTimes(2)
const injected = mountedInteraction()
const injectedInteraction = injected.install()
const raw = 'https:example.com'
for (const current of [instance, injected.instance]) {
const term = current.terminal
const target = screenFor(term.element!.parentElement!)
pointer(target, 'mouseleave')
await new Promise<void>((resolve) =>
current.write(`\x1b[2J\x1b[H\x1b]8;;${raw}\x07Link\x1b]8;;\x07`, {
live: true,
onDone: resolve
})
)
pointer(target, 'mousemove', { buttons: 0 })
await waitFor(() => expect(term.element?.getAttribute('title')).toBe(raw))
term.select(0, 0, 4)
const clear = vi.spyOn(term, 'clearSelection')
pointer(target, 'mousedown', { ctrlKey: true })
pointer(target, 'mouseup', { ctrlKey: true })
await waitFor(() => expect(openUrl).toHaveBeenLastCalledWith(raw))
expect(clear).toHaveBeenCalled()
expect(term.hasSelection()).toBe(false)
clear.mockRestore()
}
expect(openUrl.mock.calls.map(([url]) => url)).toEqual([URL_TO_OPEN, URL_TO_OPEN, raw, raw])
expect(injected.request).not.toHaveBeenCalled()
expect(document.querySelector('[data-link-action-popover]')).toBeNull()
injectedInteraction.dispose()
instance.dispose()
container.remove()
}
)
function mountedInteraction() {
return mountPreviewLinkInteraction(
source,
@@ -592,88 +384,6 @@ function cards(count: number): DashboardCard[] {
}))
}
it('adds no provider or preview connection when the real board grows from 10 to 100 cards without an open preview', async () => {
const provider = vi.spyOn(Terminal.prototype, 'registerLinkProvider')
const view = render(<AgentKanbanBoard snapshot={{ generatedAt: 1, cards: cards(10) }} />)
view.rerender(<AgentKanbanBoard snapshot={{ generatedAt: 2, cards: cards(100) }} />)
expect(provider).not.toHaveBeenCalled()
expect(window.api.terminalPreview.connect).not.toHaveBeenCalled()
view.unmount()
await verifyPopoutDefaultLinkPath({
source,
card: { ...cards(1)[0]!, ...source },
provider,
osc,
readSettings: () => store.getState().settings,
trackInstance: (instance) => instances.push(instance),
pointer,
screenFor,
output,
input,
openUrl,
subscribeToRouter: mocks.subscribe
})
})
it('honors primary/alternate and Open on real literal URLs, keeps background context, and refuses unqualified/mismatched hosts before feedback', async () => {
store.setState((state) => ({
settings: { ...state.settings!, openLinksInApp: true, terminalLinkClickBehavior: 'open' }
}))
const foreground = vi.fn()
const view = render(
<AgentTerminalPreview ptyId="pty-1" linkSource={source} onBrowserForeground={foreground} />
)
await waitFor(() => screenFor(view.container))
await output(URL_TO_OPEN)
const screen = screenFor(view.container)
// First literal click exercises the fallback when the provider has not finished hover discovery.
pointer(screen, 'mousedown')
pointer(screen, 'mouseup')
await waitFor(() => expect(foreground).toHaveBeenCalledOnce())
expect(document.querySelector('[data-link-action-popover]')).toBeNull()
expect(openUrl).not.toHaveBeenCalled()
pointer(screen, 'mousedown', { ctrlKey: true, shiftKey: true })
pointer(screen, 'mouseup', { ctrlKey: true, shiftKey: true })
await waitFor(() => expect(openUrl).toHaveBeenCalledExactlyOnceWith(URL_TO_OPEN))
expect(foreground).toHaveBeenCalledOnce()
store.setState((state) => ({
settings: { ...state.settings!, openLinksInAppInBackground: true }
}))
view.rerender(
<AgentTerminalPreview ptyId="pty-1" linkSource={source} onBrowserForeground={foreground} />
)
const observed = mocks.subscribe.mock.calls.length
pointer(screen, 'mousedown', { ctrlKey: true })
pointer(screen, 'mouseup', { ctrlKey: true })
await waitFor(() => expect(mocks.subscribe.mock.results[observed]?.value).toHaveBeenCalledOnce())
expect(foreground).toHaveBeenCalledOnce()
view.rerender(
<AgentTerminalPreview
ptyId="pty-1"
linkSource={{ ...source, executionHostId: undefined }}
onBrowserForeground={foreground}
/>
)
pointer(screen, 'mousedown')
pointer(screen, 'mouseup')
expect(document.querySelector('[data-link-action-popover]')).toBeNull()
expect(openUrl).toHaveBeenCalledTimes(1)
const remotePty = 'ssh:other@@pty-1'
view.rerender(
<AgentTerminalPreview
ptyId={remotePty}
linkSource={{ ...source, ptyId: remotePty, executionHostId: 'ssh:other' }}
onBrowserForeground={foreground}
/>
)
await waitFor(() => expect(window.api.terminalPreview.connect).toHaveBeenCalledTimes(2))
await output(URL_TO_OPEN, remotePty)
pointer(screenFor(view.container), 'contextmenu', { button: 2 })
// This real-shaped remote card has no eligible managed-browser route in the local-only workspace.
await waitFor(() => expect(view.getByRole('button', { name: /System Browser/ })).toBeTruthy())
expect(view.queryByRole('button', { name: /Orca Browser/ })).toBeNull()
})
it('refuses real selected/drag/nonlink reports and retains the real in-window Board chooser and Settings handoff', async () => {
const preview = mountedInteraction()
const interaction = preview.install()
@@ -728,65 +438,3 @@ it('refuses real selected/drag/nonlink reports and retains the real in-window Bo
expect(store.getState().activeView).toBe('settings')
await waitFor(() => expect(document.querySelector('.xterm')).toBeNull())
})
it('hands Settings through the actual in-window frame and shared popover without leaving the covering panel open', async () => {
const close = vi.fn()
const handoff = vi.fn()
const card = {
...cards(1)[0]!,
...source,
paneKey: 'pane-1',
leafId: 'leaf-1',
worktreeName: 'Workspace'
}
const view = render(
<AgentTerminalPanel
card={card}
onOpenChange={close}
onReveal={vi.fn()}
onBrowserForeground={handoff}
/>
)
await waitFor(() => screenFor(view.container))
await output(osc)
const screen = screenFor(view.container)
pointer(screen, 'mousedown')
pointer(screen, 'mouseup')
await waitFor(() =>
expect(view.getByRole('button', { name: 'Terminal link settings' })).toBeTruthy()
)
fireEvent.click(view.getByRole('button', { name: 'Terminal link settings' }))
expect(close).toHaveBeenCalledExactlyOnceWith(false)
expect(handoff).toHaveBeenCalledOnce()
expect(store.getState().activeView).toBe('settings')
})
it.each(['local', 'unknown'] as const)(
'keeps the common pane route and real Dashboard route destination/refusal equivalent for source=%s',
async (owner) => {
store.setState((state) => ({ settings: { ...state.settings!, openLinksInApp: false } }))
handleTerminalHttpLink(URL_TO_OPEN, new MouseEvent('mouseup', { button: 0, ctrlKey: true }), {
worktreeId: WORKSPACE,
sourceTabId: 'source',
sourceOwner: owner === 'local' ? { kind: 'local' } : { kind: 'unknown' },
actionDestinations: { primary: 'system' }
})
const expected = owner === 'local' ? [URL_TO_OPEN] : []
expect(openUrl.mock.calls.map(([url]) => url)).toEqual(expected)
openUrl.mockClear()
const view = render(
<AgentTerminalPreview
ptyId="pty-1"
linkSource={{ ...source, executionHostId: owner === 'local' ? 'local' : undefined }}
onBrowserForeground={vi.fn()}
/>
)
await waitFor(() => screenFor(view.container))
await output(osc)
const screen = screenFor(view.container)
pointer(screen, 'mousedown', { ctrlKey: true })
pointer(screen, 'mouseup', { ctrlKey: true })
expect(openUrl.mock.calls.map(([url]) => url)).toEqual(expected)
expect(document.querySelector('[data-link-action-popover]')).toBeNull()
}
)
@@ -1,135 +0,0 @@
import { act, fireEvent, render as renderWithoutApp, waitFor } from '@testing-library/react'
import { expect, vi, type Mock, type MockInstance } from 'vitest'
import type { Terminal } from '@xterm/xterm'
import { toast } from 'sonner'
import { URL_TO_OPEN } from '@/lib/browser-link-open.test-fixture'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { DashboardCard, DashboardSnapshot } from '../../../../shared/dashboard-snapshot'
import * as standalone from '../standalone-terminal/open-standalone-terminal'
import type { DashboardPreviewLinkSource } from './dashboard-preview-http-actions'
import {
mountPreviewLinkInteraction,
observePreviewListeners
} from './dashboard-preview-dom-test-fixture'
import { DashboardPopoutRoot } from './DashboardPopoutRoot'
export async function verifyPopoutDefaultLinkPath({
source,
card,
provider,
osc,
readSettings,
trackInstance,
pointer,
screenFor,
output,
input,
openUrl,
subscribeToRouter
}: {
source: DashboardPreviewLinkSource
card: DashboardCard
provider: MockInstance<Terminal['registerLinkProvider']>
osc: string
readSettings: () => GlobalSettings | null
trackInstance: (instance: standalone.StandaloneTerminal | null) => void
pointer: (screen: HTMLElement, type: string, options?: MouseEventInit) => MouseEvent
screenFor: (container: HTMLElement) => HTMLElement
output: (data: string, ptyId?: string) => Promise<void>
input: Mock<(pty: string, data: string) => Promise<boolean>>
openUrl: Mock<(url: string) => Promise<void>>
subscribeToRouter: Mock
}): Promise<void> {
const bridges = new Set<object>()
const subscribe = () => {
const owner = {}
bridges.add(owner)
return () => bridges.delete(owner)
}
let snapshot: ((value: DashboardSnapshot) => void) | undefined
Object.assign(window.api, {
dashboard: {
...window.api.dashboard,
onSnapshot: (listener: typeof snapshot) => {
snapshot = listener
return subscribe()
},
requestSnapshot: vi.fn(async () => {})
},
agentStatus: { onSet: subscribe, onClear: subscribe },
ui: {
...window.api.ui,
onAppMenuPaste: subscribe,
onEditableContextPaste: subscribe,
onAppMenuSelectionAction: subscribe
},
telemetryGetConsentState: async () => ({ effective: 'disabled', reason: 'ci' })
})
const constructor = vi.spyOn(standalone, 'openStandaloneTerminal')
const baseline = mountPreviewLinkInteraction(source, readSettings, trackInstance, false)
expect(constructor.mock.calls[0]![0].linkHandlers).toBeUndefined()
expect(provider).toHaveBeenCalledTimes(1)
baseline.instance.dispose()
baseline.container.remove()
const census = observePreviewListeners([])
const popout = renderWithoutApp(<DashboardPopoutRoot />)
act(() => snapshot?.({ generatedAt: 3, cards: [card] }))
expect(bridges.size).toBe(6)
const rootOwners = Array.from(census.active)
fireEvent.click(popout.getByRole('button', { name: /Workspace 0/ }))
expect(window.api.dashboard.ackAgent).toHaveBeenCalledExactlyOnceWith('pane-0', 'explicit')
await waitFor(() => screenFor(document.body))
const terminal = constructor.mock.results[1]!.value!.terminal
expect(constructor.mock.calls[1]![0].linkHandlers).toBeUndefined()
expect(provider).toHaveBeenCalledTimes(2)
const raw = 'https:example.com'
await output(
`\x1b[?1000h\x1b[?1006h${osc.replace(URL_TO_OPEN, raw)}\r\n${URL_TO_OPEN}\r\n/tmp/plain.txt`
)
const screen = screenFor(document.body)
pointer(screen, 'mousemove', { buttons: 0 })
await waitFor(() => expect(terminal.element?.getAttribute('title')).toBe(raw))
pointer(screen, 'mousedown')
pointer(screen, 'mouseup')
pointer(screen, 'mousedown', { button: 1, buttons: 4 })
pointer(screen, 'mouseup', { button: 1 })
expect(pointer(screen, 'contextmenu', { button: 2, ctrlKey: true }).defaultPrevented).toBe(false)
await waitFor(() =>
expect(input.mock.calls.map(([, data]) => data)).toEqual([
'\x1b[<0;1;1M',
'\x1b[<0;1;1m',
'\x1b[<1;1;1M',
'\x1b[<1;1;1m'
])
)
expect(openUrl).not.toHaveBeenCalled()
openUrl.mockRejectedValueOnce(new Error('System unavailable'))
for (const [clientY, uri] of [
[7, raw],
[27, URL_TO_OPEN]
] as const) {
pointer(screen, 'mousemove', { buttons: 0, clientY })
await waitFor(() => expect(terminal.element?.getAttribute('title')).toBe(uri))
pointer(screen, 'mousedown', { ctrlKey: true, shiftKey: true, clientY })
pointer(screen, 'mouseup', { ctrlKey: true, shiftKey: true, clientY })
}
await waitFor(() => expect(openUrl.mock.calls.map(([uri]) => uri)).toEqual([raw, URL_TO_OPEN]))
expect(terminal.hasSelection()).toBe(false)
pointer(screen, 'mouseleave')
expect(terminal.element?.getAttribute('title')).toBeNull()
expect(document.querySelector('[data-link-action-popover], [data-sonner-toaster]')).toBeNull()
expect(
popout.queryByRole('button', { name: /Copy link|System Browser|Terminal link settings/ })
).toBeNull()
expect(window.api.ui.writeClipboardText).not.toHaveBeenCalled()
expect(subscribeToRouter).not.toHaveBeenCalled()
expect(toast.error).not.toHaveBeenCalled()
fireEvent.click(popout.getByRole('button', { name: 'Close' }))
await waitFor(() => expect(document.querySelector('.xterm')).toBeNull())
expect(Array.from(census.active)).toEqual(rootOwners)
popout.unmount()
expect(census.active.size).toBe(0)
census.restore()
expect(bridges.size).toBe(0)
expect(window.api.terminalPreview.unsubscribe).toHaveBeenCalledExactlyOnceWith('pty-1')
}
@@ -1,5 +1,4 @@
// @vitest-environment happy-dom
// Exercise the real forwarder’s exact commit/release bytes; wiring mocks cannot verify negotiated CSI-u or macOS event ordering.
import { Terminal } from '@xterm/xterm'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { installPreviewImeBridge } from './preview-terminal-ime-bridge'
@@ -19,7 +18,6 @@ type Session = {
commit: (text: string) => void
/** An input event the text system produced without committing text. */
swallow: () => void
xtermKittyFlags: () => number
dispose: () => void
}
@@ -78,12 +76,6 @@ function open(initialFlags: number): Session {
keyup: (init = {}) => key('keyup', init),
commit: (text) => input(text),
swallow: () => input(null),
xtermKittyFlags: () =>
(
terminal as unknown as {
_core?: { coreService?: { kittyKeyboard?: { flags?: number } } }
}
)._core?.coreService?.kittyKeyboard?.flags ?? 0,
dispose: () => {
bridge.dispose()
terminal.dispose()
@@ -102,15 +94,6 @@ describe('preview IME bridge outbound bytes', () => {
document.body.replaceChildren()
})
it('encodes the commit from the flags proven before any live output', () => {
const session = open(8)
session.keydown()
session.commit(',')
session.keyup()
expect(session.emitted.join('')).toBe('\x1b[44u')
session.dispose()
})
describe.each([
['keydown -> insertText -> keyup', false],
// Some macOS input sources commit after keyup; preserve press ownership until then.
@@ -140,15 +123,6 @@ describe('preview IME bridge outbound bytes', () => {
session.dispose()
})
it('owns the release even though xterm holds no kitty state', () => {
const session = open(10)
// Keep this harness’s xterm flags at zero to prove the forwarder emits releases without xterm’s help.
expect(session.xtermKittyFlags()).toBe(0)
type(session)
expect(session.emitted.join('')).toBe('\x1b[44u\x1b[44;1:3u')
session.dispose()
})
it('reads the mutable getter exactly once, at commit time', () => {
const session = open(0)
session.keydown()
@@ -4,7 +4,7 @@ import { act } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { renderHook } from '@testing-library/react'
import type { DashboardCard, DashboardSnapshot } from '../../../../shared/dashboard-snapshot'
import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status/contracts/agent-status-freshness'
import type {
AgentStatusClearIpcPayload,
AgentStatusIpcPayload
@@ -58,37 +58,12 @@ const startViewTransition = vi.fn((cb: () => void) => {
})
const originalVisibilityState = Object.getOwnPropertyDescriptor(document, 'visibilityState')
const UNHANDLED_REJECTION_SETTLE_MS = 20
async function collectUnhandledRejections(run: () => void): Promise<unknown[]> {
const reasons: unknown[] = []
const onUnhandledRejection = (reason: unknown): void => {
reasons.push(reason)
}
process.on('unhandledRejection', onUnhandledRejection)
try {
run()
await new Promise((resolve) => setTimeout(resolve, UNHANDLED_REJECTION_SETTLE_MS))
} finally {
process.off('unhandledRejection', onUnhandledRejection)
}
return reasons
}
function hideDocument(): void {
Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true })
}
/** Mimic Radix’s open-dialog attributes so the top-layer conflict guard recognizes the terminal. */
function openTerminalDialog(): HTMLElement {
const el = document.createElement('div')
el.setAttribute('role', 'dialog')
el.setAttribute('data-state', 'open')
document.body.appendChild(el)
return el
}
describe('useDashboardSnapshot', () => {
beforeEach(() => {
@@ -127,156 +102,6 @@ describe('useDashboardSnapshot', () => {
vi.clearAllMocks()
})
it('runs a view transition when a card changes column', () => {
const { result } = renderHook(() => useDashboardSnapshot())
act(() => apply(snapshot([card({ bucket: 'idle' })])))
startViewTransition.mockClear() // ignore the initial populate
act(() => apply(snapshot([card({ bucket: 'working' })])))
expect(startViewTransition).toHaveBeenCalledTimes(1)
expect(result.current.cards[0].bucket).toBe('working')
})
it('skips the view transition — but still applies the update — while the terminal dialog is open', () => {
const { result } = renderHook(() => useDashboardSnapshot())
act(() => apply(snapshot([card({ bucket: 'idle' })])))
startViewTransition.mockClear() // ignore the initial populate
openTerminalDialog()
act(() => apply(snapshot([card({ bucket: 'working' })])))
// View-transition snapshots paint above any z-index, including the open terminal dialog.
expect(startViewTransition).not.toHaveBeenCalled()
expect(result.current.cards[0].bucket).toBe('working')
})
it('skips the view transition — but still applies the update — while the window is hidden', () => {
const { result } = renderHook(() => useDashboardSnapshot())
act(() => apply(snapshot([card({ bucket: 'idle' })])))
startViewTransition.mockClear()
hideDocument()
act(() => apply(snapshot([card({ bucket: 'working' })])))
expect(startViewTransition).not.toHaveBeenCalled()
expect(result.current.cards[0].bucket).toBe('working')
})
it('swallows the ready rejection when the window is occluded mid-transition', async () => {
const { result } = renderHook(() => useDashboardSnapshot())
act(() => apply(snapshot([card({ bucket: 'idle' })])))
readyRejection = new DOMException(
'Transition was aborted because of invalid state',
'InvalidStateError'
)
hideDocumentAfterTransitionStart = true
const reasons = await collectUnhandledRejections(() => {
act(() => apply(snapshot([card({ bucket: 'working' })])))
})
expect(startViewTransition).toHaveBeenCalled()
expect(reasons).toEqual([])
expect(result.current.cards[0].bucket).toBe('working')
})
it('reports an unrelated ready error when the window becomes hidden', async () => {
const { result } = renderHook(() => useDashboardSnapshot())
act(() => apply(snapshot([card({ bucket: 'idle' })])))
const readyError = new TypeError('Transition callback failed')
readyRejection = readyError
hideDocumentAfterTransitionStart = true
const reasons = await collectUnhandledRejections(() => {
act(() => apply(snapshot([card({ bucket: 'working' })])))
})
expect(startViewTransition).toHaveBeenCalled()
expect(reasons).toEqual([readyError])
expect(result.current.cards[0].bucket).toBe('working')
})
it('reports an InvalidStateError when the visible transition fails', async () => {
const { result } = renderHook(() => useDashboardSnapshot())
act(() => apply(snapshot([card({ bucket: 'idle' })])))
const readyError = new DOMException(
'Duplicate transition names are not allowed',
'InvalidStateError'
)
readyRejection = readyError
const reasons = await collectUnhandledRejections(() => {
act(() => apply(snapshot([card({ bucket: 'working' })])))
})
expect(startViewTransition).toHaveBeenCalled()
expect(reasons).toEqual([readyError])
expect(result.current.cards[0].bucket).toBe('working')
})
// Throttled snapshots omit unchanged icons; retain the previous map rather than losing icons on the next tick.
it('retains the last repo icons when a republish omits them', () => {
const icons = {
r1: { type: 'image' as const, src: 'data:image/png;base64,AAAA', source: 'upload' as const }
}
const { result } = renderHook(() => useDashboardSnapshot())
act(() => apply({ ...snapshot([card({})]), repoIconsByRepoId: icons }))
expect(result.current.repoIconsByRepoId).toEqual(icons)
act(() => apply(snapshot([card({ task: 'moved on' })])))
expect(result.current.repoIconsByRepoId).toEqual(icons)
expect(result.current.cards[0].task).toBe('moved on')
})
it('replaces retained icons when the bridge sends a new map', () => {
const first = {
r1: { type: 'image' as const, src: 'data:image/png;base64,AAAA', source: 'upload' as const }
}
const second = { r1: { type: 'emoji' as const, emoji: '🦑' } }
const { result } = renderHook(() => useDashboardSnapshot())
act(() => apply({ ...snapshot([card({})]), repoIconsByRepoId: first }))
act(() => apply({ ...snapshot([card({})]), repoIconsByRepoId: second }))
expect(result.current.repoIconsByRepoId).toEqual(second)
// An omitted map must retain the NEW icons, not resurrect the old ones.
act(() => apply(snapshot([card({})])))
expect(result.current.repoIconsByRepoId).toEqual(second)
})
// An explicit empty map clears icons; only omission means retention.
it('honours an explicitly empty icon map', () => {
const icons = { r1: { type: 'emoji' as const, emoji: '🦑' } }
const { result } = renderHook(() => useDashboardSnapshot())
act(() => apply({ ...snapshot([card({})]), repoIconsByRepoId: icons }))
act(() => apply({ ...snapshot([card({})]), repoIconsByRepoId: {} }))
expect(result.current.repoIconsByRepoId).toEqual({})
})
it('applies known hook status without requesting a rebuilt snapshot', () => {
const { result } = renderHook(() => useDashboardSnapshot())
act(() => apply(snapshot([card({ statusUpdatedAt: 100 })])))
requestSnapshot.mockClear()
act(() =>
applyStatus({
paneKey: 'pk',
state: 'blocked',
prompt: 'Choose',
connectionId: null,
receivedAt: 300,
stateStartedAt: 250
})
)
expect(requestSnapshot).not.toHaveBeenCalled()
expect(result.current.cards[0]).toMatchObject({
bucket: 'attention',
task: 'Choose',
statusUpdatedAt: 300
})
})
it('trailing-debounces an unknown-pane burst into one topology refresh', () => {
vi.useFakeTimers()
renderHook(() => useDashboardSnapshot())
@@ -333,31 +158,4 @@ describe('useDashboardSnapshot', () => {
expect(requestSnapshot).toHaveBeenCalledTimes(1)
vi.useRealTimers()
})
it('refreshes when a monitoring card crosses the stale boundary', () => {
vi.useFakeTimers()
const statusUpdatedAt = 1_000
vi.setSystemTime(statusUpdatedAt)
renderHook(() => useDashboardSnapshot())
act(() =>
apply(
snapshot([
card({
dotState: 'working',
workingMode: 'monitoring',
statusUpdatedAt
})
])
)
)
requestSnapshot.mockClear()
act(() => vi.advanceTimersByTime(AGENT_STATUS_STALE_AFTER_MS))
expect(requestSnapshot).not.toHaveBeenCalled()
act(() => vi.advanceTimersByTime(250))
expect(requestSnapshot).not.toHaveBeenCalled()
act(() => vi.advanceTimersByTime(1))
expect(requestSnapshot).toHaveBeenCalledTimes(1)
vi.useRealTimers()
})
})
@@ -1,614 +0,0 @@
import { renderToStaticMarkup } from 'react-dom/server'
import type { ComponentProps } from 'react'
import { describe, expect, it, vi } from 'vitest'
import type { AgentStatusEntry } from '../../../../shared/agent-status/contracts/agent-status-types'
import type { TerminalTab } from '../../../../shared/workspace-layout/terminal-tab-types'
import { TooltipProvider } from '../ui/tooltip'
import DashboardAgentRow from './DashboardAgentRow'
import type { DashboardAgentRow as DashboardAgentRowData } from './useDashboardData'
const NOW = 120_000
function makeAgent(
overrides: Partial<DashboardAgentRowData> = {},
entryOverrides: Partial<AgentStatusEntry> = {}
): DashboardAgentRowData {
const paneKey = overrides.paneKey ?? 'tab-1:leaf-1'
const tab: TerminalTab = {
id: 'tab-1',
ptyId: null,
worktreeId: 'wt-1',
title: 'Terminal 1',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
const entry: AgentStatusEntry = {
state: 'working',
prompt: 'Fix hover scope',
updatedAt: 60_000,
stateStartedAt: 60_000,
agentType: 'codex',
paneKey,
stateHistory: [],
...entryOverrides
}
return {
paneKey,
entry,
tab,
agentType: entry.agentType ?? 'codex',
state: entry.state,
startedAt: entry.stateStartedAt,
...overrides
}
}
function renderRow(agent: DashboardAgentRowData): string {
return renderToStaticMarkup(
<TooltipProvider>
<DashboardAgentRow
agent={agent}
onDismiss={vi.fn()}
onActivate={vi.fn()}
now={NOW}
hideIdentityIcon
hideExpand
/>
</TooltipProvider>
)
}
function renderSendTargetRow(
props: Pick<
ComponentProps<typeof DashboardAgentRow>,
'sendTargetStatus' | 'sendTargetDisabledReason'
>
): string {
return renderToStaticMarkup(
<TooltipProvider>
<DashboardAgentRow
agent={makeAgent()}
onDismiss={vi.fn()}
onActivate={vi.fn()}
now={NOW}
hideIdentityIcon
hideExpand
{...props}
/>
</TooltipProvider>
)
}
function classAttributes(markup: string): string[] {
return Array.from(markup.matchAll(/class="([^"]*)"/g), (match) => match[1])
}
function classTokens(markup: string): string[] {
return classAttributes(markup).flatMap((className) => className.split(/\s+/).filter(Boolean))
}
function hoverSwapClasses(markup: string): string[] {
return classAttributes(markup).filter(
(className) =>
className.includes('group-hover') || className.includes('focus-visible:opacity-100')
)
}
function dismissButtonClass(markup: string): string {
const match = markup.match(/<button\b(?=[^>]*aria-label="Dismiss agent")[^>]*class="([^"]*)"/)
if (!match) {
throw new Error('Expected dismiss agent button in rendered markup')
}
return match[1]
}
function dismissButtonClassTokens(markup: string): string[] {
return dismissButtonClass(markup).split(/\s+/).filter(Boolean)
}
function tokenCount(markup: string, token: string): number {
return classTokens(markup).filter((classToken) => classToken === token).length
}
function classTokensForTaggedElement(markup: string, dataAttribute: string): string[] {
const tagMatch = markup.match(new RegExp(`<[^>]+${dataAttribute}[^>]*>`))
if (!tagMatch) {
throw new Error(`Expected tagged element for ${dataAttribute}`)
}
const classMatch = tagMatch[0].match(/class="([^"]*)"/)
if (!classMatch) {
throw new Error(`Expected class attribute for ${dataAttribute}`)
}
return classMatch[1].split(/\s+/).filter(Boolean)
}
describe('DashboardAgentRow', () => {
it('renders orchestration task preview instead of the raw dispatch preamble prompt', () => {
const markup = renderRow(
makeAgent(
{},
{
prompt: 'You are working inside Orca, a multi-agent IDE.',
orchestration: {
taskId: 'task-1',
dispatchId: 'ctx-1',
taskTitle: 'Checkout race',
displayName: 'Fix checkout race'
}
}
)
)
expect(markup).toContain('Fix checkout race')
expect(markup).not.toContain('You are working inside Orca')
})
it('shows the active model beside the agent label', () => {
const markup = renderRow(makeAgent({}, { model: 'gpt-5.4-mini' }))
expect(markup).toContain('title="gpt-5.4-mini"')
expect(markup).toContain('>gpt-5.4-mini</span>')
expect(classTokens(markup)).toContain('font-mono')
})
it('uses the hover background as the focused-pane row highlight', () => {
const markup = renderToStaticMarkup(
<TooltipProvider>
<DashboardAgentRow
agent={makeAgent()}
onDismiss={vi.fn()}
onActivate={vi.fn()}
now={NOW}
hideIdentityIcon
hideExpand
isFocusedPane
/>
</TooltipProvider>
)
expect(markup).toContain('data-focused-agent-pane="true"')
expect(classTokens(markup)).toContain('worktree-agent-row-hover')
})
it('marks eligible send-target rows with an inline send target button', () => {
const markup = renderSendTargetRow({ sendTargetStatus: 'eligible' })
const tokens = classTokens(markup)
expect(markup).toContain('data-agent-send-target="eligible"')
expect(tokens).not.toContain('worktree-agent-send-target')
expect(tokens).not.toContain('ring-offset-sidebar')
expect(tokens).toContain('worktree-agent-row-hover')
expect(markup).toContain('aria-label="Send to this agent"')
expect(tokens).toContain('worktree-agent-send-target-button')
expect(tokens).toContain('h-5')
expect(markup).toContain('lucide-send')
expect(markup).not.toContain('aria-label="Dismiss agent"')
})
it('marks disabled send-target rows as muted without an eligibility ring', () => {
const markup = renderSendTargetRow({
sendTargetStatus: 'disabled',
sendTargetDisabledReason: 'Terminal is no longer available'
})
const tokens = classTokens(markup)
expect(markup).toContain('data-agent-send-target="disabled"')
expect(markup).toContain('title="Terminal is no longer available • started 1m ago"')
expect(tokens).toContain('cursor-default')
expect(tokens).toContain('opacity-60')
expect(tokens).not.toContain('worktree-agent-send-target')
expect(tokens).not.toContain('ring-offset-sidebar')
expect(markup).not.toContain('aria-label="Send to this agent"')
})
it('marks sending rows with a non-clickable progress treatment', () => {
const markup = renderSendTargetRow({
sendTargetStatus: 'sending',
sendTargetDisabledReason: 'Sending...'
})
const tokens = classTokens(markup)
expect(markup).toContain('data-agent-send-target="sending"')
expect(markup).toContain('title="Sending... • started 1m ago"')
expect(tokens).not.toContain('worktree-agent-send-target')
expect(tokens).not.toContain('ring-offset-sidebar')
expect(tokens).toContain('worktree-agent-send-target-button')
expect(tokens).toContain('cursor-progress')
expect(tokens).toContain('opacity-75')
expect(markup).toContain('aria-label="Send to this agent"')
expect(markup).toContain('disabled=""')
})
it('scopes the timestamp and dismiss hover swap to the row-owned group', () => {
const markup = renderRow(makeAgent())
const classes = hoverSwapClasses(markup)
const tokens = classTokens(markup)
expect(tokens).toContain('group/agent-row')
expect(tokens).toContain('group-hover/agent-row:opacity-0')
expect(dismissButtonClassTokens(markup)).toContain('group-hover/agent-row:opacity-100')
expect(dismissButtonClassTokens(markup)).toContain('focus-visible:opacity-100')
expect(classes.every((className) => !/\bgroup-hover:/.test(className))).toBe(true)
})
it('uses the row-owned group for the standalone dismiss control without timestamps', () => {
const markup = renderRow(
makeAgent({ startedAt: 0 }, { updatedAt: 0, stateStartedAt: 0, stateHistory: [] })
)
const classes = hoverSwapClasses(markup)
expect(dismissButtonClassTokens(markup)).toContain('group-hover/agent-row:opacity-100')
expect(dismissButtonClassTokens(markup)).toContain('focus-visible:opacity-100')
expect(classes.every((className) => !/\bgroup-hover:/.test(className))).toBe(true)
})
it('renders waiting rows with the shared question glyph', () => {
const markup = renderRow(makeAgent({}, { state: 'waiting' }))
const tokens = classTokens(markup)
expect(markup).toContain('aria-label="Waiting for input"')
expect(markup).toContain('lucide-message-circle-question-mark')
expect(tokens).toContain('text-agent-question')
expect(tokens).not.toContain('text-amber-500')
expect(tokens).not.toContain('bg-red-500')
})
it('keeps blocked rows red', () => {
const markup = renderRow(makeAgent({}, { state: 'blocked' }))
const tokens = classTokens(markup)
expect(markup).toContain('aria-label="Blocked"')
expect(tokens).toContain('bg-red-500')
expect(tokens).not.toContain('bg-amber-500')
})
it('keeps each row hover boundary inside an anonymous ancestor group', () => {
const markup = renderToStaticMarkup(
<TooltipProvider>
<div className="group">
<DashboardAgentRow
agent={makeAgent({ paneKey: 'tab-1:leaf-1' })}
onDismiss={vi.fn()}
onActivate={vi.fn()}
now={NOW}
hideIdentityIcon
hideExpand
/>
<DashboardAgentRow
agent={makeAgent({ paneKey: 'tab-1:leaf-2' })}
onDismiss={vi.fn()}
onActivate={vi.fn()}
now={NOW}
hideIdentityIcon
hideExpand
/>
</div>
</TooltipProvider>
)
const classes = hoverSwapClasses(markup)
expect(markup).toContain('class="group"')
expect(tokenCount(markup, 'group/agent-row')).toBe(2)
expect(tokenCount(markup, 'group-hover/agent-row:opacity-100')).toBe(2)
expect(tokenCount(markup, 'group-hover/agent-row:opacity-0')).toBe(2)
expect(classes.every((className) => !/\bgroup-hover:/.test(className))).toBe(true)
})
function renderEndedRow(ending: Partial<AgentStatusEntry>): string {
return renderRow(
makeAgent(
{ state: 'done', startedAt: 1_000 },
{
state: 'done',
prompt: 'Give me a quick update',
updatedAt: 2_000,
stateStartedAt: 2_000,
stateHistory: [{ state: 'working', prompt: 'Give me a quick update', startedAt: 1_000 }],
...ending
}
)
)
}
it('renders a crash-cut row as failed, with the red dot and no interrupted tag', () => {
const markup = renderEndedRow({
mainAgent: { state: 'done', outcome: 'interruption', stateStartedAt: 2_000 }
})
expect(markup).toContain('aria-label="Failed"')
expect(markup).toContain('bg-red-500')
expect(markup).not.toContain('>interrupted<')
expect(markup).not.toContain('lucide-circle-check')
})
it.each([
['recorded', { mainAgent: { state: 'done', outcome: 'cancellation', stateStartedAt: 2_000 } }],
["an old host's flag", { interrupted: true }]
] as const)(
"renders a user's Stop (%s) as interrupted, with plain text on the secondary line",
(_, ending) => {
const markup = renderEndedRow(ending)
const promptIndex = markup.indexOf('Give me a quick update')
const interruptedIndex = markup.indexOf('>interrupted<')
expect(markup).toContain('data-slot="tooltip-trigger"')
expect(markup).toContain('aria-label="Interrupted by user"')
expect(markup).not.toContain('data-slot="badge"')
expect(interruptedIndex).toBeGreaterThan(promptIndex)
expect(markup).toContain('bg-muted-foreground')
expect(markup).not.toContain('bg-red-500')
expect(markup).not.toContain('lucide-circle-check')
}
)
it('renders a turn a newer request replaced as interrupted, naming no one', () => {
const markup = renderEndedRow({
mainAgent: { state: 'done', outcome: 'superseded', stateStartedAt: 2_000 }
})
expect(markup).toContain('aria-label="Interrupted"')
expect(markup).not.toContain('Interrupted by user')
expect(markup).toContain('>interrupted<')
expect(markup).toContain('bg-muted-foreground')
})
it('reserves a real working tool line before tool metadata arrives', () => {
const emptyToolMarkup = renderRow(makeAgent())
const activeToolMarkup = renderRow(
makeAgent({}, { toolName: 'ListDir', toolInput: '/Users/nwparker/orca' })
)
// Antigravity working hooks can omit tool metadata; preserve the second-line slot to prevent height jumps.
expect(emptyToolMarkup).toContain('data-agent-row-tool-slot=""')
expect(activeToolMarkup).toContain('data-agent-row-tool-slot=""')
expect(
classTokensForTaggedElement(emptyToolMarkup, 'data-agent-row-tool-placeholder="true"')
).toContain('h-[1lh]')
expect(
classTokensForTaggedElement(activeToolMarkup, 'data-agent-row-tool-header="true"')
).toContain('h-[1lh]')
expect(emptyToolMarkup).not.toContain('lucide-wrench')
expect(activeToolMarkup).toContain('lucide-wrench')
expect(activeToolMarkup).toContain('ListDir')
})
it("says Stopping in place of the tool line while a person's Stop ends the turn", () => {
const markup = renderRow(
makeAgent(
{},
{
toolName: 'Bash',
toolInput: 'pnpm test',
mainAgent: { state: 'working', stopping: true, stateStartedAt: 60_000 }
}
)
)
expect(markup).toContain('Stopping…')
expect(markup).not.toContain('lucide-wrench')
expect(markup).toContain('data-agent-spinner')
})
it('renders monitoring without a spinner or active tool line', () => {
const markup = renderRow(
makeAgent(
{},
{
workingMode: 'monitoring',
prompt: '',
toolName: 'Bash',
toolInput: 'pnpm dev'
}
)
)
expect(markup).toContain('Monitoring background tasks')
expect(markup).toContain('lucide-activity')
expect(classTokens(markup)).toContain('text-agent-working')
expect(markup).not.toContain('data-agent-spinner')
expect(markup).not.toContain('data-agent-row-tool-slot')
})
it('names what a blocked approval is waiting on', () => {
const markup = renderRow(
makeAgent(
{ state: 'waiting' },
{ state: 'waiting', toolName: 'bash', toolInput: 'rm -rf build/' }
)
)
// Waiting approvals need the tool line to explain what the amber state asks the reader to approve.
expect(markup).toContain('lucide-wrench')
expect(markup).toContain('bash')
expect(markup).toContain('rm -rf build/')
})
it('leaves a wait without tool metadata unchanged', () => {
const markup = renderRow(makeAgent({ state: 'waiting' }, { state: 'waiting' }))
// Question waits expect no tool metadata; reserve height only for imminent working details.
expect(markup).not.toContain('data-agent-row-tool-slot=""')
expect(markup).not.toContain('lucide-wrench')
})
it('keeps a resolved tool off a finished row', () => {
for (const state of ['done', 'blocked', 'idle'] as const) {
const markup = renderRow(
makeAgent(
{ state },
// Why: 'idle' is a row-only state; the entry it is derived from still reports 'done'.
{ state: state === 'idle' ? 'done' : state, toolName: 'bash', toolInput: 'rm -rf build/' }
)
)
expect(markup).not.toContain('lucide-wrench')
expect(markup).not.toContain('rm -rf build/')
}
})
it('renders orchestration child rows with a connector and tree level', () => {
const markup = renderRow(
makeAgent({
lineage: {
depth: 1,
isFirstSibling: true,
isLastSibling: true,
childCount: 0
}
})
)
expect(markup).toContain('data-agent-lineage-connector="last"')
expect(markup).toContain('role="treeitem"')
expect(markup).toContain('aria-level="2"')
expect(classTokens(markup)).toContain('pl-5')
expect(classTokens(markup)).toContain('left-[13px]')
expect(classTokens(markup)).toContain('top-[0.7rem]')
expect(classTokens(markup)).toContain('w-1.5')
expect(classTokens(markup)).toContain('border-l-[1.5px]')
expect(classTokens(markup)).toContain('border-t-[1.5px]')
expect(classTokens(markup)).toContain('border-muted-foreground/45')
})
it('annotates parent identity icon when it dispatched children', () => {
const markup = renderToStaticMarkup(
<TooltipProvider>
<DashboardAgentRow
agent={makeAgent({
lineage: {
depth: 0,
isFirstSibling: true,
isLastSibling: true,
childCount: 2
}
})}
onDismiss={vi.fn()}
onActivate={vi.fn()}
now={NOW}
hideExpand
/>
</TooltipProvider>
)
expect(markup).toContain('title="Codex - dispatched 2 agents"')
expect(markup).toContain('data-agent-lineage-parent-connector="true"')
expect(classTokens(markup)).toContain('left-[13px]')
expect(markup).toContain('aria-level="1"')
})
it('marks child-disclosure rows as lineage manager rows', () => {
const markup = renderToStaticMarkup(
<TooltipProvider>
<DashboardAgentRow
agent={makeAgent()}
onDismiss={vi.fn()}
onActivate={vi.fn()}
now={NOW}
hideIdentityIcon
hideExpand
childAgentCount={2}
childAgentsExpanded={false}
onToggleChildAgents={vi.fn()}
/>
</TooltipProvider>
)
expect(classTokens(markup)).toContain('worktree-agent-lineage-parent-row')
expect(markup).toContain('aria-label="Show 2 child agents"')
})
describe('with an OSC 7501 program status', () => {
it('says a block the program reports in place of the tool step', () => {
const markup = renderRow(
makeAgent(
{},
{
toolName: 'Bash',
toolInput: 'pnpm test',
programStatus: {
state: 'blocked',
kind: 'auth',
message: 'Session expired',
observedAt: 1
}
}
)
)
expect(markup).toContain('Sign-in required: Session expired')
expect(markup).not.toContain('lucide-wrench')
})
it('says nothing for an approval the program reports after the turn finished', () => {
const markup = renderRow(
makeAgent(
{},
{
state: 'done',
programStatus: {
state: 'blocked',
kind: 'permission',
message: 'Approve plan',
observedAt: 1
}
}
)
)
expect(markup).not.toContain('Approve plan')
expect(markup).not.toContain('Needs approval')
})
it("falls back to the program's message when the row names no tool", () => {
const markup = renderRow(
makeAgent(
{},
{ programStatus: { state: 'working', message: 'Running tests', observedAt: 1 } }
)
)
expect(markup).toContain('data-agent-row-tool-input="true"')
expect(markup).toContain('Running tests')
})
it("shows a blocked program row's message, which has no tool step to ride", () => {
const markup = renderRow(
makeAgent(
{},
{
state: 'blocked',
agentType: 'gh',
programStatus: {
state: 'blocked',
kind: 'auth',
message: 'Run gh auth login',
observedAt: 1
}
}
)
)
expect(markup).toContain('Sign-in required: Run gh auth login')
})
it("keeps a real agent's input without a tool name off the row", () => {
const markup = renderRow(makeAgent({}, { toolInput: 'src/app.ts' }))
expect(markup).not.toContain('src/app.ts')
expect(markup).toContain('data-agent-row-tool-placeholder="true"')
})
it('labels the progress ring with the same percentage the tooltip shows', () => {
const markup = renderRow(
makeAgent({}, { programStatus: { state: 'working', progress: 42, observedAt: 1 } })
)
expect(markup).toContain('role="progressbar"')
expect(markup).toContain('aria-label="Working · 42%"')
expect(markup).not.toContain('>42%<')
})
})
})