diff --git a/config/direct-link-open-baseline.txt b/config/direct-link-open-baseline.txt
index 2676e9c9328..02f0bc88674 100644
--- a/config/direct-link-open-baseline.txt
+++ b/config/direct-link-open-baseline.txt
@@ -15,7 +15,7 @@
1 src/renderer/src/components/browser-pane/client-hosted-browser-load-failure.tsx
2 src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-context-menu.tsx
1 src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-viewport.tsx
-1 src/renderer/src/components/dashboard-popout/preview-terminal-links.ts
+1 src/renderer/src/components/standalone-terminal/rendering/preview-terminal-links.ts
1 src/renderer/src/components/jira-connect-credential-guide.tsx
2 src/renderer/src/components/linear-api-key-dialog.tsx
1 src/renderer/src/components/native-chat/media/images/NativeChatMarkdownImage.tsx
diff --git a/config/react-compiler-bailout-baseline.txt b/config/react-compiler-bailout-baseline.txt
index 923bd4d2bde..8e4782ddcb6 100644
--- a/config/react-compiler-bailout-baseline.txt
+++ b/config/react-compiler-bailout-baseline.txt
@@ -76,9 +76,9 @@
1 src/renderer/src/components/crash-report/CrashReportDialog.tsx
1 src/renderer/src/components/crash-report/CrashReportDialogSurface.tsx
3 src/renderer/src/components/crash-report/use-crash-report-copy.ts
-1 src/renderer/src/components/dashboard-popout/use-agent-kanban-lane-virtualizer.ts
-1 src/renderer/src/components/dashboard/useAgentBucketCounts.ts
-1 src/renderer/src/components/dashboard/useLiveDashboardSnapshot.ts
+1 src/renderer/src/components/dashboard/board/use-agent-kanban-lane-virtualizer.ts
+1 src/renderer/src/components/dashboard/bucket-counts/useAgentBucketCounts.ts
+1 src/renderer/src/components/dashboard/snapshot/useLiveDashboardSnapshot.ts
2 src/renderer/src/components/diff-comments/DiffCommentCard.tsx
5 src/renderer/src/components/diff-comments/DiffCommentPopover.tsx
2 src/renderer/src/components/diff-comments/diff-comment-draft-zone.ts
diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc
index 17cf1049340..371aedfe5ae 100644
--- a/config/reliability-gates.jsonc
+++ b/config/reliability-gates.jsonc
@@ -9775,7 +9775,7 @@
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/input/ime/terminal-ime-native-text-forwarder.test.ts src/renderer/src/components/terminal-pane/input/ime/terminal-ime-macos-keybinding-dict-trace.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/input/ime/terminal-ime-linux-fcitx4-trace-replay.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/input/ime/terminal-ime-xterm-resumed-preedit-visibility.test.ts src/renderer/src/components/terminal-pane/input/ime/terminal-ime-composer-placeholder-mask.test.ts src/shared/terminal/input/terminal-composer-draft.test.ts src/main/daemon/emulation/testing/terminal-cursor-line-context.test.ts",
- "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts src/shared/terminal/protocol/input-modes/terminal-kitty-keyboard-mode-tracker.test.ts src/main/ipc/terminal-preview-output-stream.test.ts src/main/daemon/providers/adapter/testing/daemon-pty-adapter.test.ts",
+ "pnpm exec vitest run --config config/vitest.config.ts src/shared/terminal/protocol/input-modes/terminal-kitty-keyboard-mode-tracker.test.ts src/main/ipc/terminal-preview-output-stream.test.ts src/main/daemon/providers/adapter/testing/daemon-pty-adapter.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/input/keyboard/terminal-option-kitty-release.test.ts src/main/ipc/macos-keyboard-layout-change-notifications.test.ts src/renderer/src/web/web-preload-api-ui.test.ts",
"pnpm run test:e2e -- tests/e2e/chinese-ime-chat-input-repro.spec.ts",
"pnpm run test:e2e -- tests/e2e/terminal-korean-endofrow-preedit-cell-span.spec.ts tests/e2e/terminal-korean-midline-preedit-occlusion.spec.ts",
@@ -9793,7 +9793,6 @@
"src/renderer/src/components/terminal-pane/input/keyboard/terminal-option-kitty-release.test.ts",
"src/main/ipc/macos-keyboard-layout-change-notifications.test.ts",
"src/renderer/src/web/web-preload-api-ui.test.ts",
- "src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts",
"src/shared/terminal/protocol/input-modes/terminal-kitty-keyboard-mode-tracker.test.ts",
"src/main/ipc/terminal-preview-output-stream.test.ts",
"src/main/daemon/providers/adapter/testing/daemon-pty-adapter.test.ts",
@@ -9835,17 +9834,6 @@
"rapid input-source changes coalesce to one latest-generation refresh signal"
]
},
- {
- "file": "src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts",
- "assertions": [
- "the real forwarder behind the Preview bridge encodes the commit from the snapshot-proven flags before any live output",
- "flags 0/1 commit raw text, flags 2 add one CSI-u release, flags 8 emit one CSI-u press, and flags 10 emit a CSI-u press followed by exactly one release",
- "both keydown->insertText->keyup and keydown->keyup->insertText orderings produce the same bytes",
- "the release is emitted by the forwarder while xterm's internal kitty flags stay 0",
- "the mutable flags getter is read exactly once, at commit time, never on keydown or keyup",
- "auto-repeat, two-key rollover, a swallowed commit, a retired claim, blur, and disposal each emit at most one release and never synthesize one"
- ]
- },
{
"file": "src/shared/terminal/protocol/input-modes/terminal-kitty-keyboard-mode-tracker.test.ts",
"assertions": [
diff --git a/config/renderer-layout-writer-baseline.txt b/config/renderer-layout-writer-baseline.txt
index ae7525844d7..a37b85ca88d 100644
--- a/config/renderer-layout-writer-baseline.txt
+++ b/config/renderer-layout-writer-baseline.txt
@@ -5,7 +5,7 @@
3 src/renderer/src/attention/notification-subject-owner.ts
1 src/renderer/src/components/activity/use-agent-pane-threads.ts
3 src/renderer/src/components/cmd-j/palette-live-status.tsx
-2 src/renderer/src/components/dashboard/worktree-agent-rows-cache.ts
+2 src/renderer/src/components/dashboard/rows/worktree-agent-rows-cache.ts
1 src/renderer/src/components/sidebar/workspace-card/activity/use-worktree-activity-status.ts
1 src/renderer/src/components/sidebar/workspace-card/activity/use-worktree-activity-statuses.ts
4 src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-send-target-inputs.ts
diff --git a/config/workspace-record-read-baseline.txt b/config/workspace-record-read-baseline.txt
index ff68d13d62d..7d01f621795 100644
--- a/config/workspace-record-read-baseline.txt
+++ b/config/workspace-record-read-baseline.txt
@@ -5,11 +5,11 @@
# fields leave the store they read undefined and silently fall back, so P5b-2 must move them.
# Prune after a fix: pnpm check:workspace-record-read-ratchet --prune
3 src/renderer/src/attention/notification-subject-owner.ts # P5b-2 must clear: loosely typed state param
-4 src/renderer/src/components/dashboard/build-dashboard-bucket-counts.ts # P5b-2 must clear: optional state fields; memo compare goes stale when absent
-3 src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts # P5b-2 must clear: optional state fields default to empty
-1 src/renderer/src/components/dashboard/dashboard-snapshot-workspaces.ts # P5b-2 must clear: optional state field defaults to empty
-4 src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts # P5b-2 must clear: change detection on optional fields never fires when absent
-2 src/renderer/src/components/dashboard/useRetainedAgents.ts # reads a passed-in copy
+4 src/renderer/src/components/dashboard/bucket-counts/build-dashboard-bucket-counts.ts # P5b-2 must clear: optional state fields; memo compare goes stale when absent
+3 src/renderer/src/components/dashboard/snapshot/dashboard-card-terminal-input.ts # P5b-2 must clear: optional state fields default to empty
+1 src/renderer/src/components/dashboard/snapshot/dashboard-snapshot-workspaces.ts # P5b-2 must clear: optional state field defaults to empty
+4 src/renderer/src/components/dashboard/popout-bridge/useDashboardPopoutBridge.ts # P5b-2 must clear: change detection on optional fields never fires when absent
+2 src/renderer/src/components/dashboard/retention/useRetainedAgents.ts # reads a passed-in copy
1 src/renderer/src/components/editor/markdown/documents/markdown-document-worktree-path-selector.ts # P5b-2 must clear: optional state field
2 src/renderer/src/components/right-sidebar/file-explorer/watching/useFileExplorerWatch.ts # P5b-2 must clear: optional state fields default to empty
1 src/renderer/src/components/sidebar/project-admission/completion/add-repo-skip-finalization.ts # P5b-2 must clear: loosely typed state param
diff --git a/src/renderer/src/app-shell/AppBackgroundServices.tsx b/src/renderer/src/app-shell/AppBackgroundServices.tsx
index 6bb876961c5..6a8294aafe7 100644
--- a/src/renderer/src/app-shell/AppBackgroundServices.tsx
+++ b/src/renderer/src/app-shell/AppBackgroundServices.tsx
@@ -2,7 +2,7 @@ import { Suspense } from 'react'
import { lazyWithRetry as lazy } from '@/app-shell/startup/lazy-with-retry'
import { AgentHibernationGate } from '../components/AgentHibernationGate'
import { AiVaultTabTitleSyncGate } from '../components/AiVaultTabTitleSyncGate'
-import RetainedAgentsSyncGate from '../components/dashboard/RetainedAgentsSyncGate'
+import RetainedAgentsSyncGate from '../components/dashboard/retention/RetainedAgentsSyncGate'
import { WorkspacePortScanner } from '../components/ports/WorkspacePortScanner'
import { SavedHostScopeWidenGate } from '../components/sidebar/worktree-list/listing/SavedHostScopeWidenGate'
import { MacosTccPromptNoticeHost } from '../hooks/MacosTccPromptNoticeHost'
@@ -14,7 +14,9 @@ import { StructuredAgentSessionStatusBridge } from '../components/native-chat/se
import { NativeChatVisualGuestThemePublisher } from '../components/native-chat/media/visuals/NativeChatVisualGuestThemePublisher'
import { usePairedMobileDevices } from '../components/mobile/paired-mobile-devices'
-const DashboardPopoutBridge = lazy(() => import('../components/dashboard/DashboardPopoutBridge'))
+const DashboardPopoutBridge = lazy(
+ () => import('../components/dashboard/popout-bridge/DashboardPopoutBridge')
+)
const AgentListVerdictPublisher = lazy(
() => import('../components/activity/agent-list-verdict-publisher')
)
diff --git a/src/renderer/src/app-shell/use-global-keybindings.ts b/src/renderer/src/app-shell/use-global-keybindings.ts
index 1155f49dd98..0a23cd094c8 100644
--- a/src/renderer/src/app-shell/use-global-keybindings.ts
+++ b/src/renderer/src/app-shell/use-global-keybindings.ts
@@ -40,7 +40,7 @@ import type { AppChromeLayout } from './use-app-chrome-layout'
import type { FloatingWorkspacePanelState } from './use-floating-workspace-panel'
import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router'
import { textInputOwnsComposition } from '../../../shared/text-input-shortcut-policy'
-import { isAgentTerminalPreviewTarget } from '../components/dashboard-popout/agent-terminal-preview-target'
+import { isAgentTerminalPreviewTarget } from '../components/dashboard/terminal-preview/agent-terminal-preview-target'
/** Register window listeners once; each key event reads the latest committed shortcut state through a ref. */
export function useGlobalKeybindings(args: {
diff --git a/src/renderer/src/components/activity/activity-thread-child-agent.ts b/src/renderer/src/components/activity/activity-thread-child-agent.ts
index b04bdbf5e04..352c1041671 100644
--- a/src/renderer/src/components/activity/activity-thread-child-agent.ts
+++ b/src/renderer/src/components/activity/activity-thread-child-agent.ts
@@ -3,7 +3,7 @@ import {
buildAgentRowLineageTree,
resolveAgentRowParentPaneKey,
type AgentLineageSourceRow
-} from '../dashboard/agent-row-lineage-model'
+} from '../dashboard/rows/agent-row-lineage-model'
import { isRunningActivityState } from './activity-event-state'
import type { ActivityLiveAgentState } from './activity-thread-types'
diff --git a/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx b/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx
deleted file mode 100644
index 942d101997c..00000000000
--- a/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-// @vitest-environment happy-dom
-
-import '@testing-library/jest-dom/vitest'
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { cleanup, fireEvent, render, screen } from '@testing-library/react'
-import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
-import type { RepoIcon } from '../../../../shared/repo-icon'
-import { i18n } from '@/i18n/i18n'
-import { AgentKanbanBoard } from './AgentKanbanBoard'
-
-// Stub card/dialog internals while retaining the board’s dialog wiring.
-vi.mock('./AgentKanbanCard', () => ({
- AgentKanbanCard: ({
- card,
- repoIcon,
- onOpenTerminal
- }: {
- card: DashboardCard
- repoIcon?: RepoIcon | null
- onOpenTerminal: (card: DashboardCard) => void
- }) => (
-
onOpenTerminal(card)}
- >
- {card.worktreeName}
-
- )
-}))
-vi.mock('./AgentTerminalDialog', () => ({
- AgentTerminalDialog: ({
- card,
- onOpenChange
- }: {
- card: DashboardCard | null
- onOpenChange: (open: boolean) => void
- }) => (
-
-
- )
-}))
-
-function card(overrides: Partial): DashboardCard {
- return {
- paneKey: Math.random().toString(36),
- ptyId: 'p1',
- agentType: 'claude',
- bucket: 'working',
- dotState: 'working',
- task: 't',
- repoId: 'r1',
- worktreeId: 'w1',
- tabId: 'tab1',
- leafId: 'l1',
- repoName: 'Repo',
- worktreeName: 'wt',
- startedAt: 0,
- finishedAt: null,
- stateChangedAt: 0,
- unseen: false,
- ...overrides
- }
-}
-
-const ackAgent = vi.fn(async () => {})
-
-describe('AgentKanbanBoard', () => {
- beforeEach(async () => {
- await i18n.changeLanguage('en')
- localStorage.clear()
- vi.stubGlobal(
- 'matchMedia',
- vi.fn(() => ({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() }))
- )
- ;(window as unknown as { api: unknown }).api = { dashboard: { ackAgent } }
- // Give virtual lanes a viewport; happy-dom has no layout.
- vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(600)
- })
- afterEach(() => {
- cleanup()
- vi.clearAllMocks()
- vi.restoreAllMocks()
- vi.unstubAllGlobals()
- })
-
- it('keeps the terminal dialog open across bucket moves and card removal', () => {
- const agent = card({ paneKey: 'pk-1', bucket: 'done', worktreeName: 'wt1' })
- const { rerender } = render(
-
- )
- expect(screen.getByTestId('terminal-dialog').dataset.open).toBe('false')
-
- fireEvent.click(screen.getByTestId('card'))
- expect(screen.getByTestId('terminal-dialog').dataset.open).toBe('true')
-
- // Replying moves done agents to working; the dialog must survive the bucket change.
- const moved = { ...agent, bucket: 'working' as const, dotState: 'working' as const }
- rerender()
- expect(screen.getByTestId('terminal-dialog').dataset.open).toBe('true')
- expect(screen.getByTestId('terminal-dialog').dataset.bucket).toBe('working')
-
- // Keep vanished-card dialogs open until dismissed, but clear stale live routing.
- rerender()
- expect(screen.getByTestId('terminal-dialog').dataset.open).toBe('true')
- expect(screen.getByTestId('terminal-dialog').dataset.ptyId).toBeUndefined()
- })
-})
diff --git a/src/renderer/src/components/dashboard-popout/AgentKanbanLaneCardList.test.tsx b/src/renderer/src/components/dashboard-popout/AgentKanbanLaneCardList.test.tsx
deleted file mode 100644
index 5e30d2f446c..00000000000
--- a/src/renderer/src/components/dashboard-popout/AgentKanbanLaneCardList.test.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-// @vitest-environment happy-dom
-
-import '@testing-library/jest-dom/vitest'
-import { useState } from 'react'
-import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react'
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
-import { TooltipProvider } from '@/components/ui/tooltip'
-import { i18n } from '@/i18n/i18n'
-import { AgentKanbanLaneCardList } from './AgentKanbanLaneCardList'
-
-function laneCards(count: number): DashboardCard[] {
- return Array.from({ length: count }, (_, index) => ({
- paneKey: `pane-${index}`,
- ptyId: `pty-${index}`,
- agentType: 'claude',
- bucket: 'working',
- dotState: 'working',
- task: 'task',
- repoId: 'r1',
- worktreeId: `w${index}`,
- tabId: 'tab1',
- leafId: 'l1',
- repoName: 'Repo',
- worktreeName: `card-${String(index).padStart(2, '0')}`,
- startedAt: 0,
- finishedAt: null,
- stateChangedAt: 0,
- unseen: false,
- subagents: index === 1 ? [{ id: 's1', name: 'helper', dotState: 'working' }] : undefined
- }))
-}
-
-function Lane({ cards }: { cards: DashboardCard[] }): React.JSX.Element {
- const [scrollElement, setScrollElement] = useState(null)
- return (
-
-
-
- )
-}
-
-function card(position: number): HTMLElement {
- const item = screen
- .getAllByRole('listitem')
- .find((row) => row.getAttribute('aria-posinset') === String(position))
- if (!item) {
- throw new Error(`Expected card ${position} to be mounted`)
- }
- return item
-}
-
-function scrollTo(top: number): void {
- const scroller = screen.getByTestId('scroller')
- act(() => {
- scroller.scrollTop = top
- fireEvent.scroll(scroller)
- })
-}
-
-describe('AgentKanbanLaneCardList', () => {
- beforeEach(async () => {
- await i18n.changeLanguage('en')
- // Layout stub: a 300px lane of 100px cards.
- vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function (
- this: HTMLElement
- ) {
- return this.dataset.testid === 'scroller' ? 300 : this.hasAttribute('data-index') ? 100 : 0
- })
- })
- afterEach(() => {
- cleanup()
- vi.restoreAllMocks()
- })
-
- it('tabs to the logical neighbour card instead of the next mounted card', () => {
- render()
- expect(screen.queryByText('card-20')).toBeNull()
-
- // Card 9 is offscreen while the last card is mounted next in DOM order.
- const lastControl = within(card(9)).getAllByRole('button').at(-1)!
- lastControl.focus()
- fireEvent.keyDown(lastControl, { key: 'Tab' })
- expect(document.activeElement).toHaveTextContent('card-09')
-
- const lastCardFirstControl = within(card(40)).getAllByRole('button')[0]
- lastCardFirstControl.focus()
- fireEvent.keyDown(lastCardFirstControl, { key: 'Tab', shiftKey: true })
- expect(document.activeElement).toBe(within(card(39)).getAllByRole('button').at(-1))
-
- // Mounted logical neighbours keep native traversal.
- const firstCardLastControl = within(card(1)).getAllByRole('button').at(-1)!
- expect(fireEvent.keyDown(firstCardLastControl, { key: 'Tab' })).toBe(true)
- })
-
- it('keeps subagent disclosure while scrolled away and resets it when the card leaves the lane', () => {
- const cards = laneCards(40)
- const { rerender } = render()
- fireEvent.click(screen.getByRole('button', { name: '1 subagent' }))
- expect(screen.getByText('helper')).toBeInTheDocument()
-
- scrollTo(2000)
- expect(screen.queryByText('card-01')).toBeNull()
- scrollTo(0)
- expect(screen.getByRole('button', { name: '1 subagent' })).toHaveAttribute(
- 'aria-expanded',
- 'true'
- )
-
- rerender( laneCard.paneKey !== 'pane-1')} />)
- rerender()
- expect(screen.getByRole('button', { name: '1 subagent' })).toHaveAttribute(
- 'aria-expanded',
- 'false'
- )
- })
-})
diff --git a/src/renderer/src/components/dashboard-popout/AgentTerminalDialog.test.tsx b/src/renderer/src/components/dashboard-popout/AgentTerminalDialog.test.tsx
deleted file mode 100644
index bffc3de2dd0..00000000000
--- a/src/renderer/src/components/dashboard-popout/AgentTerminalDialog.test.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-// @vitest-environment happy-dom
-
-import '@testing-library/jest-dom/vitest'
-import { useState } from 'react'
-import { afterEach, describe, expect, it, vi } from 'vitest'
-import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
-import type {
- DashboardCard,
- DashboardCardTerminalInput
-} from '../../../../shared/dashboard-snapshot'
-import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
-import { AgentTerminalDialog, AgentTerminalPanel } from './AgentTerminalDialog'
-
-// Stub xterm/IPC to inspect the routing props passed to the preview.
-vi.mock('./AgentTerminalPreview', () => ({
- AgentTerminalPreview: ({
- ptyId,
- terminalInput,
- className,
- onBrowserForeground
- }: {
- ptyId: string
- terminalInput?: DashboardCardTerminalInput | null
- className?: string
- onBrowserForeground?: () => void
- }) => (
-
- {onBrowserForeground ? (
-
- ) : null}
-
- )
-}))
-
-function card(overrides: Partial = {}): DashboardCard {
- return {
- paneKey: 'tab1:leaf1',
- ptyId: 'pty-1',
- agentType: 'claude',
- bucket: 'working',
- dotState: 'working',
- task: 'task',
- repoId: 'r1',
- worktreeId: 'w1',
- tabId: 'tab1',
- leafId: 'leaf1',
- repoName: 'Repo',
- worktreeName: 'wt',
- startedAt: 0,
- finishedAt: null,
- stateChangedAt: 0,
- unseen: false,
- ...overrides
- }
-}
-
-afterEach(() => {
- cleanup()
-})
-
-// 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('lets a nested Radix layer consume Escape before closing the panel', () => {
- const onOpenChange = vi.fn()
- render(
- <>
- {}} />
-
- Details
- Worktree details
-
- >
- )
-
- fireEvent.keyDown(screen.getByText('Worktree details'), { key: 'Escape' })
-
- expect(screen.queryByText('Worktree details')).not.toBeInTheDocument()
- expect(onOpenChange).not.toHaveBeenCalled()
-
- fireEvent.keyDown(document.body, { key: 'Escape' })
- expect(onOpenChange).toHaveBeenCalledWith(false)
- })
-})
-
-it.each([false, true])(
- 'restores dialog focus only without a confirmed host handoff (%s)',
- async (handoff) => {
- function Host() {
- const [open, setOpen] = useState(false)
- return (
- <>
-
-
- {}}
- onBrowserForeground={
- handoff ? () => document.getElementById('browser-destination')?.focus() : undefined
- }
- />
- >
- )
- }
- render()
- const launch = screen.getByRole('button', { name: 'Launch preview' })
- launch.focus()
- fireEvent.click(launch)
- await waitFor(() => expect(screen.getByTestId('preview')).toBeInTheDocument())
- fireEvent.click(
- screen.getByRole('button', { name: handoff ? 'Completed browser handoff' : 'Close' })
- )
- await waitFor(() => expect(screen.queryByTestId('preview')).not.toBeInTheDocument())
- await waitFor(() =>
- expect(document.activeElement).toBe(
- handoff ? document.getElementById('browser-destination') : launch
- )
- )
- }
-)
diff --git a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx b/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx
deleted file mode 100644
index 78f8cafbaf4..00000000000
--- a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx
+++ /dev/null
@@ -1,234 +0,0 @@
-// @vitest-environment happy-dom
-
-import '@testing-library/jest-dom/vitest'
-import type { ITerminalOptions } from '@xterm/xterm'
-import { act, cleanup, render, waitFor } from '@testing-library/react'
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-
-import '@/components/terminal-pane/input/paste/terminal-bracketed-paste'
-
-import { dispatchAppMenuPasteEvent } from '@/lib/clipboard/menu/app-menu-paste'
-
-const terminalHarness = vi.hoisted(() => ({
- instances: [] as {
- write: ReturnType
- writeCallbacks: (() => void)[]
- onDataListener: ((data: string) => void) | null
- dispose: ReturnType
- resize: ReturnType
- reset: ReturnType
- paste: ReturnType
- input: ReturnType
- scrollToTop: ReturnType
- scrollToBottom: ReturnType
- selectAll: ReturnType
- modes: { bracketedPasteMode: boolean }
- selectionText: string
- customKeyHandler: ((event: KeyboardEvent) => boolean) | null
- }[],
- userInputListener: null as (() => void) | null,
- userInputDispose: vi.fn()
-}))
-
-const platformState = vi.hoisted(() => ({ value: 'linux' }))
-const storeState = vi.hoisted(() => ({
- settings: null as { terminalRightClickToPaste?: boolean } | null,
- keybindings: {} as Record
-}))
-
-const imeHarness = vi.hoisted(() => ({
- forwarders: [] as {
- claimKeyEvent: ReturnType
- dispose: ReturnType
- sendInput: (data: string) => void
- getKittyKeyboardFlags: () => number
- }[],
- trackers: [] as { dispose: ReturnType }[],
- claimResult: false
-}))
-
-vi.mock('@xterm/xterm', () => ({
- Terminal: class {
- cols = 80
- rows = 24
- buffer = { active: { cursorY: 0 } }
- writeCallbacks: (() => void)[] = []
- onDataListener: ((data: string) => void) | null = null
- customKeyHandler: ((event: KeyboardEvent) => boolean) | null = null
- selectionText = ''
- write = vi.fn((_data: string, callback?: () => void) => {
- if (callback) {
- this.writeCallbacks.push(callback)
- }
- })
- open = vi.fn()
- focus = vi.fn()
- dispose = vi.fn()
- resize = vi.fn()
- reset = vi.fn()
- modes = { bracketedPasteMode: false }
- paste = vi.fn((data: string) => {
- terminalHarness.userInputListener?.()
- this.onDataListener?.(data)
- })
- input = vi.fn((data: string) => {
- terminalHarness.userInputListener?.()
- this.onDataListener?.(data)
- })
- element = document.createElement('div')
- unicode = { activeVersion: '6', versions: ['6', '11'], register: vi.fn() }
- loadAddon = vi.fn()
- options: ITerminalOptions = {}
- attachCustomWheelEventHandler = vi.fn()
- scrollToTop = vi.fn()
- scrollToBottom = vi.fn()
- selectAll = vi.fn()
- getSelection = vi.fn(() => this.selectionText)
- hasSelection = vi.fn(() => this.selectionText !== '')
- attachCustomKeyEventHandler = vi.fn((handler: (event: KeyboardEvent) => boolean) => {
- this.customKeyHandler = handler
- })
- onData = vi.fn((listener: (data: string) => void) => {
- this.onDataListener = listener
- return { dispose: vi.fn() }
- })
-
- constructor() {
- terminalHarness.instances.push(this)
- }
- }
-}))
-vi.mock(import('@/lib/pane-manager/pane-terminal-options'), async (importOriginal) => ({
- ...(await importOriginal()),
- buildDefaultTerminalOptions: () => ({})
-}))
-vi.mock('@/components/terminal-pane/input/terminal-user-input-signal', () => ({
- subscribeToTerminalUserInput: (_terminal: unknown, listener: () => void) => {
- terminalHarness.userInputListener = listener
- return { dispose: terminalHarness.userInputDispose }
- }
-}))
-vi.mock('@/hooks/theme/use-system-prefers-dark', () => ({
- useSystemPrefersDark: () => false
-}))
-vi.mock('@/lib/keyboard/shortcut-platform', () => ({
- getShortcutPlatform: () => platformState.value
-}))
-vi.mock('@/components/terminal-pane/input/ime/terminal-ime-native-text-forwarder', () => ({
- installTerminalImeNativeTextForwarder: (args: {
- sendInput: (data: string) => void
- getKittyKeyboardFlags?: () => number
- }) => {
- const forwarder = {
- claimKeyEvent: vi.fn(() => imeHarness.claimResult),
- dispose: vi.fn(),
- sendInput: args.sendInput,
- // Read the same live keyboard-mode mirror the real commit forwarder receives.
- getKittyKeyboardFlags: args.getKittyKeyboardFlags ?? ((): number => 0)
- }
- imeHarness.forwarders.push(forwarder)
- return forwarder
- }
-}))
-vi.mock('@/components/terminal-pane/input/ime/terminal-ime-composition-tracker', () => ({
- installTerminalImeCompositionTracker: () => {
- const tracker = { isActive: () => false, dispose: vi.fn() }
- imeHarness.trackers.push(tracker)
- return tracker
- }
-}))
-vi.mock('@/store', () => {
- const useAppStore = (selector: (s: typeof storeState) => unknown): unknown => selector(storeState)
- useAppStore.getState = (): typeof storeState => storeState
- return { useAppStore }
-})
-
-vi.mock('./terminal-preview-api', () => ({
- terminalPreviewApiFor: () => window.api.terminalPreview
-}))
-
-import { AgentTerminalPreview } from './AgentTerminalPreview'
-
-describe('AgentTerminalPreview clipboard routes', () => {
- const input = vi.fn(async (_ptyId: string, _data: string) => true)
- const fit = vi.fn(async (_ptyId: string, cols: number, rows: number) => ({ cols, rows }))
- const ack = vi.fn(async () => {})
- const unsubscribe = vi.fn(async () => {})
- const connect = vi.fn()
- const readClipboardText = vi.fn(async () => 'clip-text')
- const writeClipboardText = vi.fn(async () => {})
- const writeTerminalClipboardText = vi.fn(async () => {})
- const performNativeSelectionAction = vi.fn()
-
- /** Focuses a stand-in for xterm's helper textarea inside the preview container. */
- const focusInsidePreview = (container: HTMLElement, tagName = 'input'): HTMLElement => {
- const host = container.querySelector('.origin-bottom-left')!
- const focusTarget = document.createElement(tagName)
- host.appendChild(focusTarget)
- focusTarget.focus()
- return focusTarget
- }
-
- beforeEach(() => {
- terminalHarness.instances.length = 0
- terminalHarness.userInputListener = null
- platformState.value = 'linux'
- storeState.keybindings = {}
- imeHarness.forwarders.length = 0
- imeHarness.trackers.length = 0
- imeHarness.claimResult = false
- storeState.settings = null
- connect.mockResolvedValue({
- snapshot: { data: '', cols: 80, rows: 24, seq: 1 },
- replay: []
- })
- readClipboardText.mockResolvedValue('clip-text')
- Object.assign(window, {
- api: {
- terminalPreview: {
- connect,
- input,
- fit,
- ack,
- unsubscribe,
- onData: () => vi.fn()
- },
- ui: {
- readClipboardText,
- writeClipboardText,
- writeTerminalClipboardText,
- performNativeSelectionAction
- }
- }
- })
- })
-
- afterEach(() => {
- cleanup()
- vi.clearAllMocks()
- })
-
- it('cancels an async paste when the preview loses focus', async () => {
- let resolveClipboard!: (text: string) => void
- readClipboardText.mockReturnValueOnce(
- new Promise((resolve) => {
- resolveClipboard = resolve
- })
- )
- const view = render()
- await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
- const terminal = terminalHarness.instances[0]!
- focusInsidePreview(view.container)
- const outsideInput = document.createElement('input')
- view.container.appendChild(outsideInput)
-
- act(() => {
- dispatchAppMenuPasteEvent()
- })
- outsideInput.focus()
- await act(async () => resolveClipboard('stale text'))
-
- expect(terminal.paste).not.toHaveBeenCalled()
- expect(input).not.toHaveBeenCalled()
- })
-})
diff --git a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.frame-lifecycle.test.tsx b/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.frame-lifecycle.test.tsx
deleted file mode 100644
index 0b1c68a49b8..00000000000
--- a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.frame-lifecycle.test.tsx
+++ /dev/null
@@ -1,229 +0,0 @@
-// @vitest-environment happy-dom
-
-import { act, cleanup, render } from '@testing-library/react'
-import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import type { ITerminalOptions } from '@xterm/xterm'
-import type { GlobalSettings } from '../../../../shared/global-settings-types'
-import { createGlobalSettingsFixture } from '../../../../shared/global-settings-test-fixture'
-import { useAppStore } from '@/store'
-import { AgentTerminalPreview } from './AgentTerminalPreview'
-
-type PreviewInstance = {
- options: ITerminalOptions
- container: HTMLElement | null
- dispose: ReturnType
- writeCallbacks: (() => void)[]
-}
-const harness = vi.hoisted(() => ({
- instances: new Array(),
- deferWrites: false
-}))
-
-vi.mock('@xterm/xterm', () => ({
- Terminal: class {
- cols = 80
- rows = 24
- buffer = { active: { cursorY: 14 } }
- container: HTMLElement | null = null
- screen = document.createElement('div')
- writeCallbacks: (() => void)[] = []
- write = vi.fn((_data: string, callback?: () => void) => {
- if (!callback) {
- return
- }
- if (harness.deferWrites) {
- this.writeCallbacks.push(callback)
- } else {
- callback()
- }
- })
- focus = vi.fn()
- resize = vi.fn()
- reset = vi.fn()
- onData = vi.fn(() => ({ dispose: vi.fn() }))
- dispose = vi.fn(() => this.screen.remove())
- constructor(public options: ITerminalOptions) {
- harness.instances.push(this)
- }
- open(container: HTMLElement): void {
- this.container = container
- this.screen.className = 'xterm-screen'
- Object.defineProperties(this.screen, {
- offsetWidth: {
- get: () =>
- this.cols *
- (this.options.fontSize === 18 ||
- this.options.fontFamily?.includes('Fira Code') ||
- this.options.fontWeight === 900 ||
- this.options.fontWeightBold === 900
- ? 12
- : 10)
- },
- offsetHeight: { get: () => this.rows * 16 * (this.options.lineHeight ?? 1) }
- })
- const box = container.parentElement
- if (!box) {
- throw new Error('Missing preview box')
- }
- Object.defineProperties(box, {
- clientWidth: { configurable: true, value: 600 },
- clientHeight: { configurable: true, value: 240 }
- })
- container.append(this.screen)
- }
- }
-}))
-vi.mock('@/components/terminal-pane/input/terminal-user-input-signal', () => ({
- subscribeToTerminalUserInput: () => ({ dispose: vi.fn() })
-}))
-vi.mock('@/hooks/theme/use-system-prefers-dark', () => ({
- useSystemPrefersDark: () => false
-}))
-vi.mock('@/lib/keyboard-layout/use-effective-mac-option-as-alt', () => ({
- useEffectiveMacOptionAsAlt: (value: string) => value
-}))
-vi.mock('./preview-terminal-ligatures', () => ({ syncPreviewTerminalLigatures: vi.fn() }))
-vi.mock('./preview-terminal-compatibility', () => ({
- installPreviewTerminalCompatibility: () => vi.fn()
-}))
-vi.mock('./preview-terminal-ime-bridge', () => ({
- installPreviewImeBridge: () => ({ claimKeyEvent: () => false, dispose: vi.fn() })
-}))
-vi.mock('./preview-terminal-key-handler', () => ({
- installPreviewTerminalKeyHandler: () => vi.fn()
-}))
-vi.mock('@/components/terminal-pane/clipboard/terminal-native-copy-gutter', () => ({
- installTerminalNativeCopyGutterTrim: () => ({ dispose: vi.fn() })
-}))
-vi.mock('./preview-terminal-app-menu-clipboard', () => ({
- installPreviewTerminalAppMenuClipboard: () => vi.fn()
-}))
-vi.mock('./preview-terminal-right-click-paste', () => ({
- installPreviewTerminalRightClickPaste: () => vi.fn()
-}))
-
-const initial = useAppStore.getInitialState()
-const connect = vi.fn()
-const fit = vi.fn(async (_ptyId: string, cols: number, rows: number) => ({ cols, rows }))
-const unsubscribe = vi.fn(async () => {})
-let settings: GlobalSettings
-let originalApi: PropertyDescriptor | undefined
-const frames = new Map()
-let nextFrame = 0
-const cancelFrame = vi.fn((id: number) => frames.delete(id))
-function flushFrames(): void {
- for (const [id, callback] of frames) {
- frames.delete(id)
- callback(16)
- }
-}
-
-beforeEach(() => {
- vi.useFakeTimers()
- vi.clearAllMocks()
- harness.instances.length = 0
- harness.deferWrites = false
- frames.clear()
- nextFrame = 0
- vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
- const id = nextFrame++
- frames.set(id, callback)
- return id
- })
- vi.stubGlobal('cancelAnimationFrame', cancelFrame)
- // No resize notification or later output: replay must perform its own fit and grid claim.
- vi.stubGlobal(
- 'ResizeObserver',
- class {
- observe = vi.fn()
- disconnect = vi.fn()
- }
- )
- settings = createGlobalSettingsFixture({
- theme: 'dark',
- activeRuntimeEnvironmentId: null,
- terminalFontSize: 14,
- terminalFontFamily: 'JetBrains Mono',
- terminalFontWeight: 500,
- terminalFontWeightBold: 700,
- terminalLineHeight: 1,
- terminalLigatures: 'off'
- })
- connect
- .mockReset()
- .mockResolvedValue({ snapshot: { data: '', cols: 80, rows: 24, seq: 1 }, replay: [] })
- originalApi = Object.getOwnPropertyDescriptor(window, 'api')
- Object.defineProperty(window, 'api', {
- configurable: true,
- value: {
- settings: {
- set: async (updates: Partial) => {
- settings = structuredClone({ ...settings, ...updates })
- return settings
- }
- },
- terminalPreview: { connect, fit, unsubscribe, onData: () => vi.fn() }
- }
- })
- useAppStore.setState({ ...initial, settings }, true)
-})
-
-afterEach(() => {
- cleanup()
- for (const instance of harness.instances) {
- expect(instance.dispose).toHaveBeenCalledOnce()
- }
- useAppStore.setState(initial, true)
- if (originalApi) {
- Object.defineProperty(window, 'api', originalApi)
- } else {
- Reflect.deleteProperty(window, 'api')
- }
- frames.clear()
- vi.unstubAllGlobals()
- vi.useRealTimers()
-})
-
-it('does not schedule an old parsed write after actual unmount', async () => {
- harness.deferWrites = true
- const view = render()
- await act(async () => {})
- const terminal = harness.instances[0]
- expect(terminal).toBeDefined()
- flushFrames()
- view.unmount()
- for (const callback of terminal?.writeCallbacks.splice(0) ?? []) {
- callback()
- }
- expect(connect).toHaveBeenCalledOnce()
- expect(unsubscribe).toHaveBeenCalledOnce()
- expect(terminal?.dispose).toHaveBeenCalledOnce()
- expect(frames.size).toBe(0)
-})
-
-it('retains the replacement frame on the same DOM and ignores retired parsed writes', async () => {
- harness.deferWrites = true
- render()
- await act(async () => {})
- const old = harness.instances[0]
- await act(async () => useAppStore.getState().updateSettings({ terminalFontSize: 18 }))
- const replacement = harness.instances[1]
- const pending = frames.size
- for (const callback of old?.writeCallbacks.splice(0) ?? []) {
- callback()
- }
- const afterOldWrites = frames.size
- flushFrames()
- await act(async () => {
- await vi.advanceTimersByTimeAsync(200)
- })
- expect(connect).toHaveBeenCalledTimes(2)
- expect(unsubscribe).toHaveBeenCalledExactlyOnceWith('pty-1')
- expect(old?.dispose).toHaveBeenCalledOnce()
- expect(replacement?.container).toBe(old?.container)
- expect(replacement?.container?.style.transform).toBe('scale(0.625)')
- expect(replacement?.container?.style.transformOrigin).toBe('top left')
- expect(fit).toHaveBeenCalledExactlyOnceWith('pty-1', 50, 15)
- expect(frames.size).toBe(0)
- expect([pending, afterOldWrites]).toEqual([1, 1])
-})
diff --git a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-geometry.test.tsx b/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-geometry.test.tsx
deleted file mode 100644
index 3f188c2aefb..00000000000
--- a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-geometry.test.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-// @vitest-environment happy-dom
-
-import { act, cleanup, render } from '@testing-library/react'
-import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-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 { useAppStore } from '@/store'
-import { AgentTerminalPreview } from './AgentTerminalPreview'
-
-type PreviewInstance = {
- options: ITerminalOptions
- container: HTMLElement | null
- dispose: ReturnType
-}
-const harness = vi.hoisted(() => ({ instances: new Array() }))
-
-vi.mock('@xterm/xterm', () => ({
- Terminal: class {
- cols = 80
- rows = 24
- buffer = { active: { cursorY: 14 } }
- container: HTMLElement | null = null
- screen = document.createElement('div')
- write = vi.fn((_data: string, callback?: () => void) => callback?.())
- focus = vi.fn()
- resize = vi.fn()
- reset = vi.fn()
- onData = vi.fn(() => ({ dispose: vi.fn() }))
- dispose = vi.fn(() => this.screen.remove())
- constructor(public options: ITerminalOptions) {
- harness.instances.push(this)
- }
- open(container: HTMLElement): void {
- this.container = container
- this.screen.className = 'xterm-screen'
- Object.defineProperties(this.screen, {
- offsetWidth: {
- get: () => this.cols * (this.options.fontSize === 18 ? 12 : 10)
- },
- offsetHeight: { get: () => this.rows * 16 * (this.options.lineHeight ?? 1) }
- })
- const box = container.parentElement
- if (!box) {
- throw new Error('Missing preview box')
- }
- Object.defineProperties(box, {
- clientWidth: { configurable: true, value: 600 },
- clientHeight: { configurable: true, value: 240 }
- })
- container.append(this.screen)
- }
- }
-}))
-vi.mock('@/components/terminal-pane/input/terminal-user-input-signal', () => ({
- subscribeToTerminalUserInput: () => ({ dispose: vi.fn() })
-}))
-vi.mock('@/hooks/theme/use-system-prefers-dark', () => ({
- useSystemPrefersDark: () => false
-}))
-vi.mock('@/lib/keyboard-layout/use-effective-mac-option-as-alt', () => ({
- useEffectiveMacOptionAsAlt: (value: string) => value
-}))
-vi.mock('./preview-terminal-ligatures', () => ({ syncPreviewTerminalLigatures: vi.fn() }))
-vi.mock('./preview-terminal-compatibility', () => ({
- installPreviewTerminalCompatibility: () => vi.fn()
-}))
-vi.mock('./preview-terminal-ime-bridge', () => ({
- installPreviewImeBridge: () => ({ claimKeyEvent: () => false, dispose: vi.fn() })
-}))
-vi.mock('./preview-terminal-key-handler', () => ({
- installPreviewTerminalKeyHandler: () => vi.fn()
-}))
-vi.mock('@/components/terminal-pane/clipboard/terminal-native-copy-gutter', () => ({
- installTerminalNativeCopyGutterTrim: () => ({ dispose: vi.fn() })
-}))
-vi.mock('./preview-terminal-app-menu-clipboard', () => ({
- installPreviewTerminalAppMenuClipboard: () => vi.fn()
-}))
-vi.mock('./preview-terminal-right-click-paste', () => ({
- installPreviewTerminalRightClickPaste: () => vi.fn()
-}))
-
-const initial = useAppStore.getInitialState()
-const connect = vi.fn()
-const fit = vi.fn(async (_ptyId: string, cols: number, rows: number) => ({ cols, rows }))
-const unsubscribe = vi.fn(async () => {})
-let settings: GlobalSettings
-let originalApi: PropertyDescriptor | undefined
-
-beforeEach(() => {
- vi.useFakeTimers()
- vi.clearAllMocks()
- harness.instances.length = 0
- // No resize notification or later output: replay must perform its own fit and grid claim.
- vi.stubGlobal(
- 'ResizeObserver',
- class {
- observe = vi.fn()
- disconnect = vi.fn()
- }
- )
- settings = createGlobalSettingsFixture({
- theme: 'dark',
- activeRuntimeEnvironmentId: null,
- terminalFontSize: 14,
- terminalFontFamily: 'JetBrains Mono',
- terminalFontWeight: 500,
- terminalFontWeightBold: 700,
- terminalLineHeight: 1,
- terminalLigatures: 'off'
- })
- connect
- .mockReset()
- .mockResolvedValue({ snapshot: { data: '', cols: 80, rows: 24, seq: 1 }, replay: [] })
- originalApi = Object.getOwnPropertyDescriptor(window, 'api')
- Object.defineProperty(window, 'api', {
- configurable: true,
- value: {
- settings: {
- set: async (updates: Partial) => {
- settings = structuredClone({ ...settings, ...updates })
- return settings
- }
- },
- terminalPreview: { connect, fit, unsubscribe, onData: () => vi.fn() }
- }
- })
- useAppStore.setState({ ...initial, settings }, true)
-})
-
-afterEach(() => {
- cleanup()
- for (const instance of harness.instances) {
- expect(instance.dispose).toHaveBeenCalledOnce()
- }
- useAppStore.setState(initial, true)
- if (originalApi) {
- Object.defineProperty(window, 'api', originalApi)
- } else {
- Reflect.deleteProperty(window, 'api')
- }
- vi.unstubAllGlobals()
- vi.useRealTimers()
-})
-
-async function settleGeometry(): Promise {
- await act(async () => {
- await vi.advanceTimersByTimeAsync(250)
- })
-}
-
-it('ignores a pending connection retired by a metric change', async () => {
- const gate = Promise.withResolvers()
- connect.mockReturnValueOnce(gate.promise)
- render()
- await act(async () => useAppStore.getState().updateSettings({ terminalFontSize: 18 }))
- await settleGeometry()
- await act(async () =>
- gate.resolve({ snapshot: { data: '', cols: 80, rows: 24, seq: 1 }, replay: [] })
- )
- await settleGeometry()
- expect(connect).toHaveBeenCalledTimes(2)
- expect(harness.instances).toHaveLength(1)
- expect(harness.instances[0]?.options.fontSize).toBe(18)
-})
diff --git a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-lifetime.test.tsx b/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-lifetime.test.tsx
deleted file mode 100644
index d692dfec4b9..00000000000
--- a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-lifetime.test.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-// @vitest-environment happy-dom
-
-import { act, cleanup, render } from '@testing-library/react'
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-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 { useAppStore } from '@/store'
-import { AgentTerminalPreview } from './AgentTerminalPreview'
-
-type PreviewInstance = {
- options: ITerminalOptions
- dispose: ReturnType
- reset: ReturnType
-}
-
-const harness = vi.hoisted(() => ({
- instances: new Array(),
- systemPrefersDark: false
-}))
-
-vi.mock('@xterm/xterm', () => ({
- Terminal: class {
- cols = 80
- rows = 24
- buffer = { active: { cursorY: 0 } }
- options: ITerminalOptions
- write = vi.fn((_data: string, callback?: () => void) => callback?.())
- open = vi.fn()
- focus = vi.fn()
- dispose = vi.fn()
- resize = vi.fn()
- reset = vi.fn()
- onData = vi.fn(() => ({ dispose: vi.fn() }))
- constructor(options: ITerminalOptions) {
- this.options = options
- harness.instances.push(this)
- }
- }
-}))
-vi.mock('@/components/terminal-pane/input/terminal-user-input-signal', () => ({
- subscribeToTerminalUserInput: () => ({ dispose: vi.fn() })
-}))
-vi.mock('@/hooks/theme/use-system-prefers-dark', () => ({
- useSystemPrefersDark: () => harness.systemPrefersDark
-}))
-vi.mock('@/lib/keyboard-layout/use-effective-mac-option-as-alt', () => ({
- useEffectiveMacOptionAsAlt: (value: string) => value
-}))
-vi.mock('./preview-grid-claim', () => ({
- createPreviewGridClaim: () => ({ schedule: vi.fn(), dispose: vi.fn() })
-}))
-vi.mock('./preview-terminal-box-fit', () => ({
- createPreviewBoxFit: () => ({ schedule: vi.fn(), dispose: vi.fn() })
-}))
-vi.mock('./preview-terminal-ligatures', () => ({ syncPreviewTerminalLigatures: vi.fn() }))
-vi.mock('./preview-terminal-compatibility', () => ({
- installPreviewTerminalCompatibility: () => vi.fn()
-}))
-vi.mock('./preview-terminal-ime-bridge', () => ({
- installPreviewImeBridge: () => ({ claimKeyEvent: () => false, dispose: vi.fn() })
-}))
-vi.mock('./preview-terminal-key-handler', () => ({
- installPreviewTerminalKeyHandler: () => vi.fn()
-}))
-vi.mock('@/components/terminal-pane/clipboard/terminal-native-copy-gutter', () => ({
- installTerminalNativeCopyGutterTrim: () => ({ dispose: vi.fn() })
-}))
-vi.mock('./preview-terminal-app-menu-clipboard', () => ({
- installPreviewTerminalAppMenuClipboard: () => vi.fn()
-}))
-vi.mock('./preview-terminal-right-click-paste', () => ({
- installPreviewTerminalRightClickPaste: () => vi.fn()
-}))
-
-const initial = useAppStore.getInitialState()
-const connect = vi.fn()
-const unsubscribe = vi.fn(async () => {})
-const offData = vi.fn()
-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
-
-async function updateSettings(updates: Partial): Promise {
- await act(async () => useAppStore.getState().updateSettings(updates))
-}
-
-function terminal(index = 0): PreviewInstance {
- const instance = harness.instances[index]
- if (!instance) {
- throw new Error(`Missing terminal ${index}`)
- }
- return instance
-}
-
-beforeEach(() => {
- vi.clearAllMocks()
- harness.instances.length = 0
- harness.systemPrefersDark = false
- settings = createGlobalSettingsFixture({ theme: 'dark', activeRuntimeEnvironmentId: null })
- connect.mockReset().mockResolvedValue(connection)
- originalApi = Object.getOwnPropertyDescriptor(window, 'api')
- Object.defineProperty(window, 'api', {
- configurable: true,
- value: {
- settings: {
- set: async (updates: Partial) => {
- settings = structuredClone({ ...settings, ...updates })
- return settings
- }
- },
- terminalPreview: {
- connect,
- unsubscribe,
- onData
- }
- }
- })
- useAppStore.setState({ ...initial, settings }, true)
-})
-
-afterEach(() => {
- cleanup()
- for (const instance of harness.instances) {
- expect(instance.dispose).toHaveBeenCalledOnce()
- }
- expect(offData).toHaveBeenCalledTimes(onData.mock.calls.length)
- useAppStore.setState(initial, true)
- if (originalApi) {
- Object.defineProperty(window, 'api', originalApi)
- } else {
- Reflect.deleteProperty(window, 'api')
- }
-})
-
-describe('preview terminal settings lifetime', () => {
- it('keeps one preview connection across ten unrelated settings snapshots', async () => {
- render()
- await act(async () => {})
- const theme = terminal().options.theme
- for (let index = 1; index <= 10; index += 1) {
- await updateSettings({ editorAutoSaveDelayMs: 1000 + index })
- }
- expect(connect).toHaveBeenCalledExactlyOnceWith('pty-1', { scrollbackRows: 24 })
- expect(harness.instances).toHaveLength(1)
- expect(terminal().options.theme).toBe(theme)
- expect(unsubscribe).not.toHaveBeenCalled()
- })
-
- it('uses current live options when an unrelated update arrives during connect', async () => {
- const gate = Promise.withResolvers()
- connect.mockReturnValueOnce(gate.promise)
- render()
- await updateSettings({ terminalCursorStyle: 'bar' })
- expect(connect).toHaveBeenCalledOnce()
- await act(async () => gate.resolve(connection))
- expect(harness.instances).toHaveLength(1)
- expect(terminal().options.cursorStyle).toBe('bar')
- })
-
- it('does not install a terminal after unmounting a pending connection', async () => {
- const gate = Promise.withResolvers()
- connect.mockReturnValueOnce(gate.promise)
- const view = render()
- await updateSettings({ editorAutoSaveDelayMs: 1234 })
- view.unmount()
- await act(async () => gate.resolve(connection))
- expect(harness.instances).toHaveLength(0)
- expect(connect).toHaveBeenCalledOnce()
- expect(unsubscribe).toHaveBeenCalledExactlyOnceWith('ssh:host@@pty-1')
- })
-})
diff --git a/src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.tsx b/src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.tsx
index 21826639e31..9a51da4b151 100644
--- a/src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.tsx
+++ b/src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.tsx
@@ -1,8 +1,8 @@
import { useAppMenuPaste } from '@/hooks/clipboard/menu/useAppMenuPaste'
import { useAppMenuSelectionActions } from '@/hooks/app-commands/useAppMenuSelectionActions'
-import { AgentKanbanBoard } from './AgentKanbanBoard'
-import { useDashboardSnapshot } from './useDashboardSnapshot'
-import { useAgentDashboardTelemetry } from './use-agent-dashboard-telemetry'
+import { AgentKanbanBoard } from '../dashboard/board/AgentKanbanBoard'
+import { useDashboardSnapshot } from './snapshot/useDashboardSnapshot'
+import { useAgentDashboardTelemetry } from '../dashboard/board/use-agent-dashboard-telemetry'
export function DashboardPopoutRoot(): React.JSX.Element {
// The pop-out has no App shell to translate Edit-menu IPC into terminal ownership events.
diff --git a/src/renderer/src/components/dashboard-popout/dashboard-preview-dom-test-fixture.ts b/src/renderer/src/components/dashboard-popout/dashboard-preview-dom-test-fixture.ts
deleted file mode 100644
index 563b9c0589a..00000000000
--- a/src/renderer/src/components/dashboard-popout/dashboard-preview-dom-test-fixture.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import type { PreviewTerminalLinkHandler } from './preview-terminal-links'
-import { vi, type Mock } from 'vitest'
-import type { GlobalSettings } from '../../../../shared/global-settings-types'
-import { TerminalKittyKeyboardModeTracker } from '../../../../shared/terminal/protocol/input-modes/terminal-kitty-keyboard-mode-tracker'
-import {
- openStandaloneTerminal,
- type StandaloneTerminal
-} from '../standalone-terminal/open-standalone-terminal'
-import { installDashboardPreviewLinkInteraction } from './dashboard-preview-link-interaction'
-import type { DashboardPreviewLinkSource } from './dashboard-preview-http-actions'
-
-/** Model the browser layout boundary; xterm still measures, renders and hit-tests its own cells. */
-export function installPreviewDomFixture(): () => void {
- const style = document.createElement('style')
- style.textContent = '.xterm, .xterm-screen { padding: 0px; }'
- document.head.appendChild(style)
- vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(() => {
- const context: CanvasRenderingContext2D = Object.create(null)
- context.font = '13px monospace'
- context.measureText = (text) => Object.assign(Object.create(null), { width: text.length * 10 })
- context.clearRect = vi.fn()
- context.fillRect = vi.fn()
- return context
- })
- vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (
- this: HTMLElement
- ) {
- return new DOMRect(0, 0, this.offsetWidth, this.offsetHeight)
- })
- vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(function (
- this: HTMLElement
- ) {
- return this.classList.contains('xterm-char-measure-element')
- ? 320
- : Number.parseFloat(this.style.width) || 800
- })
- vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function (
- this: HTMLElement
- ) {
- return this.classList.contains('xterm-char-measure-element')
- ? 20
- : Number.parseFloat(this.style.height) || 160
- })
- vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(800)
- vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(160)
- vi.stubGlobal(
- 'ResizeObserver',
- class {
- observe() {}
- unobserve() {}
- disconnect() {}
- }
- )
- return () => style.remove()
-}
-
-export function mountPreviewLinkInteraction(
- source: DashboardPreviewLinkSource,
- readSettings: () => GlobalSettings | null,
- trackInstance: (instance: ReturnType) => void,
- withInteraction = true
-): {
- instance: StandaloneTerminal
- install: () => ReturnType
- sink: Mock<(data: string) => boolean | Promise>
- getSource: Mock<() => DashboardPreviewLinkSource | undefined>
- getSettings: Mock<() => GlobalSettings | null>
- request: Mock[1]['request']>
- container: HTMLDivElement
-} {
- const container = document.createElement('div')
- document.body.appendChild(container)
- let interaction: ReturnType | undefined
- let defaultHandlers: PreviewTerminalLinkHandler | undefined
- const sink = vi.fn<(data: string) => boolean | Promise>(() => true)
- const instance = openStandaloneTerminal({
- container,
- options: { cols: 80, rows: 8, allowProposedApi: true },
- kittyKeyboardModes: new TerminalKittyKeyboardModeTracker({ kittyKeyboard: true }),
- sessionKey: source.ptyId,
- sendInput: (data) => (interaction ? interaction.sendInput(data) : sink(data)),
- answersQueries: false,
- getSettings: readSettings,
- getMacOptionAsAlt: () => 'false',
- getTerminalInput: () => null,
- ...(withInteraction
- ? {
- linkHandlers: (defaults: PreviewTerminalLinkHandler): PreviewTerminalLinkHandler => {
- defaultHandlers = defaults
- return {
- activate: (e, uri, range) => interaction?.handlers.activate(e, uri, range),
- hover: (e, uri, range) => interaction?.handlers.hover?.(e, uri, range),
- leave: (e, uri, range) => interaction?.handlers.leave?.(e, uri, range)
- }
- }
- }
- : {})
- })
- if (!instance) {
- throw new Error('Actual standalone constructor failed')
- }
- const request = vi.fn[1]['request']>()
- const getSource = vi.fn<() => DashboardPreviewLinkSource | undefined>(() => source)
- const getSettings = vi.fn(readSettings)
- const install = () => {
- interaction = installDashboardPreviewLinkInteraction(instance.terminal, {
- getSource,
- getSettings,
- defaultHandlers,
- onForegroundOpen: () => undefined,
- request,
- sendInput: sink
- })
- return interaction
- }
- const dispose = () => {
- interaction?.dispose()
- instance.dispose()
- container.remove()
- }
- trackInstance({ ...instance, dispose })
- return { instance, install, sink, getSource, getSettings, request, container }
-}
diff --git a/src/renderer/src/components/dashboard-popout/dashboard-preview-link-composition.test.tsx b/src/renderer/src/components/dashboard-popout/dashboard-preview-link-composition.test.tsx
deleted file mode 100644
index bf688a8de9d..00000000000
--- a/src/renderer/src/components/dashboard-popout/dashboard-preview-link-composition.test.tsx
+++ /dev/null
@@ -1,322 +0,0 @@
-// @vitest-environment happy-dom
-import {
- act,
- cleanup,
- fireEvent,
- render as renderWithoutApp,
- waitFor
-} from '@testing-library/react'
-import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import type { RemoteRuntimeMultiplexedTerminalCallbacks } from '@/runtime/hosted-terminals/streams/remote-terminal-stream-contracts'
-
-import { parseRemoteRuntimePtyId } from '../../../../shared/remote-runtime/remote-runtime-pty-id'
-import { resetRemoteRuntimeTerminalPreviewSessionsForTests } from './remote-runtime-terminal-preview-api'
-import {
- installPreviewDomFixture,
- mountPreviewLinkInteraction
-} from './dashboard-preview-dom-test-fixture'
-
-import type { openStandaloneTerminal } from '../standalone-terminal/open-standalone-terminal'
-
-import { getDefaultSettings } from '../../../../shared/constants'
-import { createStoreCascadesMockApi } from '@/store/testing/app-store-cascade-api.test-fixture'
-import {
- createBrowserLinkTestStore,
- WORKSPACE,
- URL_TO_OPEN
-} from '@/lib/browser/navigation/browser-link-open.test-fixture'
-import {
- registerHttpLinkStoreAccessor,
- registerWorkspaceHttpLinkBrowserOpener
-} from '@/lib/browser/navigation/http-link-routing'
-import { openWorkspaceBrowserTabForLink } from '@/lib/browser/workspace/workspace-browser-tab-open'
-
-import { terminalPreviewApiFor } from './terminal-preview-api'
-import { AgentKanbanBoard } from './AgentKanbanBoard'
-
-import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
-import type { GlobalSettings } from '../../../../shared/global-settings-types'
-
-const mocks = vi.hoisted(() => ({
- getState: vi.fn(),
- subscribe: vi.fn(),
- remote: new Map()
-}))
-vi.mock(
- '@/runtime/hosted-terminals/streams/registry/remote-runtime-terminal-multiplexer-registry',
- () => ({
- getRemoteRuntimeTerminalMultiplexer: (environmentId: string) => ({
- subscribeTerminal: async ({
- terminal,
- callbacks
- }: {
- terminal: string
- callbacks: RemoteRuntimeMultiplexedTerminalCallbacks
- }) => {
- const ptyId = `remote:${environmentId}@@${terminal}`
- mocks.remote.set(ptyId, callbacks)
- queueMicrotask(() => callbacks.onSnapshot('', { cols: 80, rows: 8, seq: 1 }))
- return {
- sendInput: (data: string) => input(ptyId, data),
- claimViewport: () => true,
- serializeBufferOutcome: async () => ({
- availability: { kind: 'snapshot' },
- snapshot: { data: '', cols: 80, rows: 8, seq: 1 }
- }),
- close: () => {
- mocks.remote.delete(ptyId)
- }
- }
- }
- })
- })
-)
-
-vi.mock('@/store', () => ({
- useAppStore: Object.assign(
- (selector: (state: ReturnType) => unknown) => selector(mocks.getState()),
- { getState: mocks.getState }
- )
-}))
-vi.mock('@/hooks/theme/use-system-prefers-dark', () => ({
- useSystemPrefersDark: () => false
-}))
-vi.mock('@/lib/keyboard/shortcut-platform', () => ({ getShortcutPlatform: () => 'linux' }))
-vi.mock('sonner', () => ({
- toast: { error: vi.fn(), success: vi.fn(), info: vi.fn(), warning: vi.fn() }
-}))
-const domWindow = window
-const api = createStoreCascadesMockApi()
-Object.assign(globalThis, { window: domWindow })
-Object.assign(window, { api })
-let store: ReturnType
-const instances: ReturnType[] = []
-const input = vi.fn(async (_pty: string, _data: string) => true)
-const openUrl = vi.fn(async (_url: string) => {})
-let onData: ((payload: unknown) => void) | undefined
-let disposeDom: () => void
-const source = {
- worktreeId: WORKSPACE,
- executionHostId: 'local',
- tabId: 'source',
- ptyId: 'pty-1'
-} as const
-
-beforeEach(() => {
- store = createBrowserLinkTestStore()
- store.setState((state) => ({
- settings: {
- ...state.settings,
- ...getDefaultSettings('/home/test'),
- terminalLigatures: 'auto',
- terminalLinkClickBehavior: 'actions',
- terminalRightClickToPaste: false,
- openLinksInApp: false
- } satisfies GlobalSettings
- }))
- mocks.getState.mockImplementation(() => store.getState())
- mocks.subscribe.mockImplementation((listener) => vi.fn(store.subscribe(listener)))
- registerHttpLinkStoreAccessor(() => store.getState(), mocks.subscribe)
- registerWorkspaceHttpLinkBrowserOpener(async (request) => {
- if (!request.linkContext) {
- throw new Error('Expected a genuine link request')
- }
- const outcome = await openWorkspaceBrowserTabForLink({
- ...request,
- linkContext: request.linkContext
- })
- return outcome.status === 'opened' ? outcome.result.browserPageId : null
- })
- disposeDom = installPreviewDomFixture()
- Object.assign(window.api, {
- shell: { ...window.api.shell, openUrl },
- dashboard: {
- ...window.api.dashboard,
- ackAgent: vi.fn(async (_pane: string, _intent: 'view' | 'explicit') => {})
- },
- terminalPreview: {
- connect: vi.fn(async () => ({
- snapshot: { data: '', cols: 80, rows: 8, seq: 1 },
- replay: []
- })),
- onData: (listener: typeof onData) => {
- onData = listener
- return () => {
- onData = undefined
- }
- },
- input,
- ack: vi.fn(async () => {}),
- fit: vi.fn(async () => ({ cols: 80, rows: 8 })),
- unsubscribe: vi.fn(async () => {})
- },
- ui: {
- ...window.api.ui,
- readClipboardText: vi.fn(async () => 'clipboard'),
- writeClipboardText: vi.fn(async () => {}),
- writeTerminalClipboardText: vi.fn(async () => {})
- }
- })
-})
-afterEach(() => {
- cleanup()
- for (const instance of instances.splice(0)) {
- instance?.dispose()
- }
- registerWorkspaceHttpLinkBrowserOpener(null)
- resetRemoteRuntimeTerminalPreviewSessionsForTests()
- disposeDom()
- vi.restoreAllMocks()
- vi.clearAllMocks()
- vi.unstubAllGlobals()
-})
-
-function screenFor(container: HTMLElement): HTMLElement {
- const screen = container.querySelector('.xterm-screen')
- if (!screen) {
- throw new Error('Actual xterm screen did not mount')
- }
- return screen
-}
-function pointer(screen: HTMLElement, type: string, options: MouseEventInit = {}): MouseEvent {
- const event = new MouseEvent(type, {
- bubbles: true,
- cancelable: true,
- button: 0,
- buttons: type === 'mouseup' ? 0 : 1,
- clientX: 7,
- clientY: 7,
- ...options
- })
- screen.dispatchEvent(event)
- return event
-}
-async function output(data: string, ptyId = 'pty-1') {
- const ack = vi.spyOn(terminalPreviewApiFor(ptyId), 'ack').mockClear()
- act(() => {
- if (parseRemoteRuntimePtyId(ptyId)) {
- const callbacks = mocks.remote.get(ptyId)
- if (!callbacks) {
- throw new Error('Missing selected remote preview subscription')
- }
- callbacks.onData(data)
- } else {
- onData?.({ type: 'data', ptyId, data, bytes: data.length })
- }
- })
- await waitFor(() => expect(ack).toHaveBeenCalledExactlyOnceWith(ptyId, data.length))
-}
-const osc = `\x1b]8;;${URL_TO_OPEN}\x07hidden label\x1b]8;;\x07`
-
-function mountedInteraction() {
- return mountPreviewLinkInteraction(
- source,
- () => store.getState().settings,
- (instance) => instances.push(instance)
- )
-}
-
-it('retains actual boolean/Promise forwarding results and flushes unclaimed reports in order, with finite overflow and cleanup', async () => {
- const preview = mountedInteraction()
- const interaction = preview.install()
- preview.sink.mockReturnValueOnce(false)
- expect(interaction.sendInput('refused-key')).toBe(false)
- const promise = Promise.resolve(true)
- preview.sink.mockReturnValueOnce(promise)
- expect(interaction.sendInput('async-key')).toBe(promise)
- await new Promise((resolve) => preview.instance.write(osc, { live: true, onDone: resolve }))
- const screen = screenFor(preview.container)
- const reports = Array.from({ length: 65 }, (_, i) => `\x1b[<0;${i + 1};1M`)
- // Protocol fixture feeds qualified input during a real event dispatch; native timing remains QA's separate obligation.
- const emitReports = () => {
- preview.instance.terminal.input(reports[0]!)
- expect(preview.sink).toHaveBeenCalledTimes(2)
- for (const report of reports.slice(1)) {
- preview.instance.terminal.input(report)
- }
- }
- screen.addEventListener('mousedown', emitReports)
- pointer(screen, 'mousedown')
- screen.removeEventListener('mousedown', emitReports)
- pointer(screen, 'mouseup')
- expect(preview.sink.mock.calls.slice(2).map(([data]) => data)).toEqual(reports)
- expect(preview.request).not.toHaveBeenCalled()
- interaction.dispose()
- expect(preview.sink.mock.calls.slice(2).map(([data]) => data)).toEqual(reports)
-})
-
-function cards(count: number): DashboardCard[] {
- return Array.from({ length: count }, (_, i) => ({
- paneKey: `pane-${i}`,
- ptyId: `pty-${i}`,
- agentType: 'claude',
- bucket: 'working',
- dotState: 'working',
- task: 'task',
- repoId: 'repo1',
- worktreeId: WORKSPACE,
- tabId: `tab-${i}`,
- leafId: `leaf-${i}`,
- repoName: 'Repo',
- worktreeName: `Workspace ${i}`,
- startedAt: 0,
- finishedAt: null,
- stateChangedAt: 0,
- unseen: false
- }))
-}
-
-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()
- await new Promise((resolve) =>
- preview.instance.write(`\x1b[?1000h\x1b[?1006h${URL_TO_OPEN}`, { live: true, onDone: resolve })
- )
- const terminal = preview.instance.terminal
- const screen = screenFor(preview.container)
- terminal.select(0, 0, 5)
- pointer(screen, 'mousedown')
- pointer(screen, 'mouseup')
- expect(preview.request).not.toHaveBeenCalled()
- terminal.clearSelection()
- pointer(screen, 'mousedown')
- pointer(screen, 'mousemove', { clientX: 30 })
- pointer(screen, 'mouseup', { clientX: 30 })
- expect(preview.request).not.toHaveBeenCalled()
- pointer(screen, 'mousedown', { clientX: 700 })
- pointer(screen, 'mouseup', { clientX: 700 })
- expect(preview.request).not.toHaveBeenCalled()
- const reports = expect.poll(() => preview.sink.mock.calls.map(([data]) => data))
- await reports.toEqual([
- '\x1b[<0;1;1M',
- '\x1b[<0;1;1m',
- '\x1b[<0;1;1M',
- '\x1b[<0;4;1m',
- '\x1b[<0;71;1M',
- '\x1b[<0;71;1m'
- ])
- interaction.dispose()
- preview.instance.dispose()
- preview.container.remove()
-
- const foreground = vi.fn()
- const view = renderWithoutApp(
-
- )
- fireEvent.click(view.getByRole('button', { name: /Workspace 0/ }))
- await waitFor(() => screenFor(document.body))
- await output(osc)
- const realScreen = screenFor(document.body)
- pointer(realScreen, 'mousedown')
- pointer(realScreen, 'mouseup')
- const settings = await view.findByRole('button', { name: 'Terminal link settings' })
- expect(document.querySelector('[data-terminal-link-destination]')?.textContent).toBe(URL_TO_OPEN)
- expect(view.getByRole('button', { name: /System Browser/ })).toBeTruthy()
- fireEvent.click(settings)
- expect(foreground).toHaveBeenCalledOnce()
- expect(store.getState().activeView).toBe('settings')
- await waitFor(() => expect(document.querySelector('.xterm')).toBeNull())
-})
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-box-fit-lifecycle.test.ts b/src/renderer/src/components/dashboard-popout/preview-terminal-box-fit-lifecycle.test.ts
deleted file mode 100644
index bbfd1bf39e0..00000000000
--- a/src/renderer/src/components/dashboard-popout/preview-terminal-box-fit-lifecycle.test.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-// @vitest-environment happy-dom
-import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import { Terminal } from '@xterm/xterm'
-import { createPreviewBoxFit } from './preview-terminal-box-fit'
-
-const frames = new Map()
-const cancelFrame = vi.fn((id: number) => frames.delete(id))
-let nextFrame = 0
-const terminals: Terminal[] = []
-
-beforeEach(() => {
- nextFrame = 0
- frames.clear()
- vi.clearAllMocks()
- vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
- const id = nextFrame++
- frames.set(id, callback)
- return id
- })
- vi.stubGlobal('cancelAnimationFrame', cancelFrame)
-})
-
-afterEach(() => {
- for (const terminal of terminals.splice(0)) {
- terminal.dispose()
- }
- vi.unstubAllGlobals()
-})
-
-function harness(getTerminal?: () => null) {
- const box = document.createElement('div')
- const container = document.createElement('div')
- const screen = document.createElement('div')
- screen.className = 'xterm-screen'
- box.append(container)
- container.append(screen)
- Object.defineProperties(box, {
- clientWidth: { value: 600 },
- clientHeight: { value: 240 }
- })
- Object.defineProperties(screen, {
- offsetWidth: { value: 800 },
- offsetHeight: { value: 384 }
- })
- const terminal = new Terminal({ rows: 24, cols: 80, allowProposedApi: true })
- terminals.push(terminal)
- const read = vi.fn(getTerminal ?? (() => terminal))
- return { ...createPreviewBoxFit({ container, getTerminal: read }), box, container, read }
-}
-
-function flush(id = 0): void {
- const callback = frames.get(id)
- frames.delete(id)
- callback?.(16)
-}
-
-function thrown(operation: () => void): unknown {
- try {
- operation()
- } catch (error) {
- return error
- }
- throw new Error('Expected an error')
-}
-
-it('cancels frame zero once and suppresses captured or later callbacks after disposal', () => {
- const owner = harness()
- owner.schedule()
- const captured = frames.get(0)
- owner.dispose()
- owner.dispose()
- captured?.(16)
- owner.schedule()
- expect(owner.read).not.toHaveBeenCalled()
- expect(owner.container.style.transform).toBe('')
- expect(frames.size).toBe(0)
- expect(cancelFrame).toHaveBeenCalledExactlyOnceWith(0)
-})
-
-it('preserves a fit error and allows the next live request after completion', () => {
- const error = new Error('terminal read failed')
- const owner = harness()
- owner.read.mockImplementationOnce(() => {
- throw error
- })
- owner.schedule()
- expect(thrown(() => flush())).toBe(error)
- owner.schedule()
- flush(1)
- expect(owner.read).toHaveBeenCalledTimes(2)
- owner.dispose()
- expect(cancelFrame).not.toHaveBeenCalled()
-})
-
-it('does not cancel another owner when both use the same container', () => {
- const old = harness()
- const current = createPreviewBoxFit({ container: old.container, getTerminal: () => null })
- old.schedule()
- current.schedule()
- old.dispose()
- expect([...frames.keys()]).toEqual([1])
- current.dispose()
- expect(cancelFrame.mock.calls).toEqual([[0], [1]])
-})
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts b/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts
deleted file mode 100644
index 99642873d34..00000000000
--- a/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-// @vitest-environment happy-dom
-import { Terminal } from '@xterm/xterm'
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { installPreviewImeBridge } from './preview-terminal-ime-bridge'
-
-vi.mock('@/lib/keyboard/shortcut-platform', () => ({
- getShortcutPlatform: () => 'darwin'
-}))
-
-const COMMA = { key: ',', code: 'Comma', keyCode: 188 }
-
-type Session = {
- emitted: string[]
- setFlags: (flags: number) => void
- keydown: (init?: { shiftKey?: boolean; repeat?: boolean; key?: string; code?: string }) => void
- keyup: (init?: { shiftKey?: boolean; key?: string; code?: string }) => void
- commit: (text: string) => void
- dispose: () => void
-}
-
-function open(initialFlags: number): Session {
- const container = document.createElement('div')
- document.body.appendChild(container)
- const terminal = new Terminal()
- terminal.open(container)
- const textarea = terminal.textarea!
- let flags = initialFlags
- const bridge = installPreviewImeBridge(terminal, {
- getKittyKeyboardFlags: () => flags
- })!
- terminal.attachCustomKeyEventHandler((event) => !bridge.claimKeyEvent(event))
- const emitted: string[] = []
- terminal.onData((data) => emitted.push(data))
-
- const key = (
- type: string,
- init: { key?: string; code?: string; shiftKey?: boolean; repeat?: boolean }
- ): void => {
- const event = new KeyboardEvent(type, {
- key: init.key ?? COMMA.key,
- code: init.code ?? COMMA.code,
- shiftKey: init.shiftKey === true,
- repeat: init.repeat === true,
- bubbles: true,
- cancelable: true
- })
- Object.defineProperty(event, 'keyCode', { value: COMMA.keyCode })
- textarea.dispatchEvent(event)
- }
-
- const input = (data: string | null): void => {
- for (const type of ['beforeinput', 'input']) {
- const event = new InputEvent(type, { bubbles: true })
- Object.defineProperty(event, 'inputType', { value: 'insertText' })
- Object.defineProperty(event, 'data', { value: data })
- textarea.dispatchEvent(event)
- }
- }
-
- return {
- emitted,
- setFlags: (next) => {
- flags = next
- },
- keydown: (init = {}) => key('keydown', init),
- keyup: (init = {}) => key('keyup', init),
- commit: (text) => input(text),
- dispose: () => {
- bridge.dispose()
- terminal.dispose()
- }
- }
-}
-
-describe('preview IME bridge outbound bytes', () => {
- beforeEach(() => {
- vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
- measureText: () => ({ width: 10 })
- } as unknown as CanvasRenderingContext2D)
- })
- afterEach(() => {
- vi.restoreAllMocks()
- document.body.replaceChildren()
- })
-
- it('encodes the release from the keyup state when Shift is released first', () => {
- const session = open(10)
- // `!` on Digit1: the press reports the base key with the Shift modifier.
- session.keydown({ key: '!', code: 'Digit1', shiftKey: true })
- session.commit('!')
- session.keyup({ key: '1', code: 'Digit1', shiftKey: false })
- // 49 is `1`; the release carries no modifier because none was held at release.
- expect(session.emitted.join('')).toBe('\x1b[49;2u\x1b[49;1:3u')
- session.dispose()
- })
-
- it('emits one release for a held key across auto-repeat', () => {
- const session = open(10)
- session.keydown()
- session.commit(',')
- session.keydown({ repeat: true })
- session.commit(',')
- session.keyup()
- // Press, repeat, then exactly one release for the single physical press.
- expect(session.emitted.join('')).toBe('\x1b[44u\x1b[44;1:2u\x1b[44;1:3u')
- session.dispose()
- })
-
- it('keeps an owed release when a later repeat commits under flags that owe none', () => {
- const session = open(10)
- session.keydown()
- session.commit(',')
- session.setFlags(8)
- session.keydown({ repeat: true })
- session.commit(',')
- session.setFlags(10)
- session.keyup()
- // A repeat’s new flags cannot erase the release owed by the originally delivered press.
- expect(session.emitted.join('')).toBe('\x1b[44u\x1b[44u\x1b[44;1:3u')
- session.dispose()
- })
-
- it('suppresses the owed release when event types are no longer negotiated at keyup', () => {
- // Match xterm: once event reporting is popped, do not send CSI-u releases to an app or successor shell that no longer negotiates them.
- const session = open(10)
- session.keydown()
- session.commit(',')
- session.setFlags(0)
- session.keyup()
- expect(session.emitted.join('')).toBe('\x1b[44u')
- session.dispose()
- })
-
- it('releases each key of a two-key rollover exactly once', () => {
- const session = open(10)
- session.keydown()
- session.commit(',')
- session.keydown({ key: '.', code: 'Period' })
- session.commit('。')
- session.keyup()
- session.keyup({ key: '.', code: 'Period' })
- expect(session.emitted.join('')).toBe('\x1b[44u\x1b[46u\x1b[44;1:3u\x1b[46;1:3u')
- session.dispose()
- })
-
- it('leaves no release behind when a claim is retired without committing', () => {
- const session = open(10)
- session.keydown()
- // The input source ate the first press; the next keydown retires it.
- session.keydown({ key: '.', code: 'Period' })
- session.commit('。')
- session.keyup()
- session.keyup({ key: '.', code: 'Period' })
- expect(session.emitted.join('')).toBe('\x1b[46u\x1b[46;1:3u')
- session.dispose()
- })
-
- it('synthesizes nothing on blur or disposal', () => {
- const session = open(10)
- session.keydown()
- session.commit(',')
- session.emitted.length = 0
- document.querySelector('.xterm')?.dispatchEvent(new FocusEvent('blur', { bubbles: true }))
- session.keyup()
- session.dispose()
- expect(session.emitted.join('')).toBe('')
- })
-})
diff --git a/src/renderer/src/components/dashboard-popout/dashboard-agent-status-patch.ts b/src/renderer/src/components/dashboard-popout/snapshot/dashboard-agent-status-patch.ts
similarity index 83%
rename from src/renderer/src/components/dashboard-popout/dashboard-agent-status-patch.ts
rename to src/renderer/src/components/dashboard-popout/snapshot/dashboard-agent-status-patch.ts
index e97e46f3995..fb39706d783 100644
--- a/src/renderer/src/components/dashboard-popout/dashboard-agent-status-patch.ts
+++ b/src/renderer/src/components/dashboard-popout/snapshot/dashboard-agent-status-patch.ts
@@ -1,15 +1,15 @@
-import { agentVerdictDisplayMark } from '../../../../shared/agent-status/lifecycle/agent-main-agent-verdict'
-import { interactivePromptLabel } from '../../../../shared/native-chat/input/native-chat-interactive-prompt'
-import { sayNativeChatApprovalTranslated } from '../native-chat/prompts/native-chat-interactive-prompt'
-import type { AgentStatusIpcPayload } from '../../../../shared/agent-status/contracts/agent-status-ipc-payload'
+import { agentVerdictDisplayMark } from '../../../../../shared/agent-status/lifecycle/agent-main-agent-verdict'
+import { interactivePromptLabel } from '../../../../../shared/native-chat/input/native-chat-interactive-prompt'
+import { sayNativeChatApprovalTranslated } from '../../native-chat/prompts/native-chat-interactive-prompt'
+import type { AgentStatusIpcPayload } from '../../../../../shared/agent-status/contracts/agent-status-ipc-payload'
import {
dashboardCardBaseDisplayState,
type DashboardCard,
type DashboardCardSubagent,
type DashboardSnapshot
-} from '../../../../shared/dashboard-snapshot'
-import { dashboardBucketForDotState } from '../dashboard/dashboard-card-bucket'
-import { dashboardCardDotState } from '../dashboard/dashboard-row-bucket'
+} from '../../../../../shared/dashboard-snapshot'
+import { dashboardBucketForDotState } from '../../dashboard/snapshot/dashboard-card-bucket'
+import { dashboardCardDotState } from '../../dashboard/rows/dashboard-row-bucket'
import { isAgentTurnUnacknowledged } from '@/attention/agent-attention-acknowledgement'
export type DashboardAgentStatusPatchResult = {
diff --git a/src/renderer/src/components/dashboard-popout/useDashboardSnapshot.ts b/src/renderer/src/components/dashboard-popout/snapshot/useDashboardSnapshot.ts
similarity index 97%
rename from src/renderer/src/components/dashboard-popout/useDashboardSnapshot.ts
rename to src/renderer/src/components/dashboard-popout/snapshot/useDashboardSnapshot.ts
index d5332c4d99b..9902fe66fb7 100644
--- a/src/renderer/src/components/dashboard-popout/useDashboardSnapshot.ts
+++ b/src/renderer/src/components/dashboard-popout/snapshot/useDashboardSnapshot.ts
@@ -1,10 +1,10 @@
import { useEffect, useRef, useState } from 'react'
import { flushSync } from 'react-dom'
-import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status/contracts/agent-status-freshness'
+import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../../shared/agent-status/contracts/agent-status-freshness'
import {
EMPTY_DASHBOARD_SNAPSHOT,
type DashboardSnapshot
-} from '../../../../shared/dashboard-snapshot'
+} from '../../../../../shared/dashboard-snapshot'
import { patchDashboardSnapshotFromAgentStatus } from './dashboard-agent-status-patch'
const TOPOLOGY_REFRESH_DEBOUNCE_MS = 250
diff --git a/src/renderer/src/components/dashboard/AgentDashboardDrawer.focus.test.tsx b/src/renderer/src/components/dashboard/AgentDashboardDrawer.focus.test.tsx
deleted file mode 100644
index 5ed42ac97d4..00000000000
--- a/src/renderer/src/components/dashboard/AgentDashboardDrawer.focus.test.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-// @vitest-environment happy-dom
-
-import '@testing-library/jest-dom/vitest'
-import type { ComponentProps } from 'react'
-import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { useAppStore } from '@/store'
-import type { AgentKanbanBoard } from '../dashboard-popout/AgentKanbanBoard'
-
-const mocks = vi.hoisted(() => ({
- activateAndRevealWorkspace: vi.fn(() => true),
- activateTabAndFocusPane: vi.fn(),
- openPopout: vi.fn().mockResolvedValue(undefined)
-}))
-
-vi.mock('@/lib/workspace/activation/worktree-activation', () => ({
- activateAndRevealWorkspace: mocks.activateAndRevealWorkspace
-}))
-vi.mock('@/lib/workspace/tabs/activate-tab-and-focus-pane', () => ({
- activateTabAndFocusPane: mocks.activateTabAndFocusPane
-}))
-vi.mock('./useLiveDashboardSnapshot', () => ({
- useLiveDashboardSnapshot: () => ({ generatedAt: 1, cards: [] })
-}))
-vi.mock('../dashboard-popout/AgentKanbanBoard', () => ({
- AgentKanbanBoard: ({
- onClose,
- onRevealAgent,
- headerActions
- }: ComponentProps) => (
- <>
-
-
-
- {headerActions}
- >
- )
-}))
-vi.mock('./AgentDashboardSettingsMenu', () => ({
- AgentDashboardSettingsMenu: ({ onSwitchToPopout }: { onSwitchToPopout: () => void }) => (
-
- )
-}))
-
-import { AgentDashboardDrawer } from './AgentDashboardDrawer'
-
-const initialState = useAppStore.getInitialState()
-
-beforeEach(() => {
- vi.clearAllMocks()
- mocks.activateAndRevealWorkspace.mockReturnValue(true)
- useAppStore.setState({ agentDashboardDrawerOpen: false, sidebarOpen: true, sidebarWidth: 320 })
- Object.defineProperty(window, 'api', {
- configurable: true,
- value: { dashboard: { openPopout: mocks.openPopout } }
- })
-})
-
-afterEach(async () => {
- cleanup()
- // Radix schedules close autofocus after unmount, including cleanup.
- await act(async () => {
- await new Promise((resolve) => setTimeout(resolve, 0))
- })
- useAppStore.setState(initialState, true)
- vi.restoreAllMocks()
-})
-
-function fixture(): ReturnType {
- const open = (): void => useAppStore.getState().setAgentDashboardDrawerOpen(true)
- return render(
- <>
-
-
-
-
- >
- )
-}
-
-async function openFrom(name = 'Opener A'): Promise {
- const opener = screen.getByRole('button', { name })
- opener.focus()
- await userEvent.keyboard('{Enter}')
- expect(document.activeElement).toBe(opener)
- screen.getByRole('textbox', { name: 'Search agents' }).focus()
- return opener
-}
-
-async function closed(): Promise {
- await waitFor(() => expect(screen.queryByRole('textbox', { name: 'Search agents' })).toBeNull())
- await act(async () => {
- await new Promise((resolve) => setTimeout(resolve, 0))
- })
-}
-
-describe('AgentDashboardDrawer focus return', () => {
- it.each(['Escape', 'Close dashboard'])('returns to the opener after %s', async (dismiss) => {
- fixture()
- const opener = await openFrom()
- await (dismiss === 'Escape'
- ? userEvent.keyboard('{Escape}')
- : userEvent.click(screen.getByRole('button', { name: dismiss })))
- await closed()
- expect(document.activeElement).toBe(opener)
- })
-
- it('keeps outside focus when the store closes the drawer', async () => {
- fixture()
- await openFrom()
- const outside = screen.getByRole('textbox', { name: 'Outside input' })
- outside.focus()
- act(() => useAppStore.getState().setAgentDashboardDrawerOpen(false))
- await closed()
- expect(document.activeElement).toBe(outside)
- })
-
- it('lets the real outside pointerdown dismissal hand focus to its target', async () => {
- fixture()
- const opener = await openFrom()
- const sheet = screen.getByRole('dialog', { name: 'Agents' })
- vi.spyOn(sheet, 'getBoundingClientRect').mockReturnValue(new DOMRect(320, 40, 600, 600))
- const outside = screen.getByRole('textbox', { name: 'Outside input' })
- const returned: Event[] = []
- opener.addEventListener('focus', (event) => returned.push(event))
- await userEvent.pointer({
- target: outside,
- keys: '[MouseLeft]',
- coords: { clientX: 1000, clientY: 100 }
- })
- await closed()
- expect(document.activeElement).toBe(outside)
- expect(returned).toHaveLength(0)
- await userEvent.keyboard('focus-test')
- expect(outside).toHaveValue('focus-test')
- })
-
- it('restores after store dismissal and captures the next opener', async () => {
- fixture()
- const first = await openFrom()
- act(() => useAppStore.getState().setAgentDashboardDrawerOpen(false))
- await closed()
- expect(document.activeElement).toBe(first)
- const second = await openFrom('Opener B')
- await userEvent.keyboard('{Escape}')
- await closed()
- expect(document.activeElement).toBe(second)
- })
-
- it('does not let an old close callback take focus from a reopened drawer', async () => {
- fixture()
- await openFrom()
- act(() => useAppStore.getState().setAgentDashboardDrawerOpen(false))
- const second = screen.getByRole('button', { name: 'Opener B' })
- second.focus()
- act(() => useAppStore.getState().setAgentDashboardDrawerOpen(true))
- const search = screen.getByRole('textbox', { name: 'Search agents' })
- search.focus()
- await act(async () => {
- await new Promise((resolve) => setTimeout(resolve, 0))
- })
- expect(document.activeElement).toBe(search)
- await userEvent.keyboard('{Escape}')
- await closed()
- expect(document.activeElement).toBe(second)
- })
-
- it.each([true, false])('restores only when agent reveal fails (success: %s)', async (success) => {
- fixture()
- const opener = await openFrom()
- mocks.activateAndRevealWorkspace.mockReturnValue(success)
- await userEvent.click(screen.getByRole('button', { name: 'Reveal agent' }))
- await closed()
- expect(document.activeElement === opener).toBe(!success)
- })
-
- it('leaves focus handoff to the popout', async () => {
- fixture()
- const opener = await openFrom()
- await userEvent.click(screen.getByRole('button', { name: 'Pop out' }))
- await closed()
- expect(mocks.openPopout).toHaveBeenCalledOnce()
- expect(document.activeElement).not.toBe(opener)
- })
-})
diff --git a/src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx b/src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx
deleted file mode 100644
index 1094ff92a0f..00000000000
--- a/src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-// @vitest-environment happy-dom
-
-import { act, cleanup, fireEvent, render } from '@testing-library/react'
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { useAppStore } from '@/store'
-
-const mocks = vi.hoisted(() => ({
- useLiveDashboardSnapshot: vi.fn(() => ({ generatedAt: 1, cards: [] })),
- useAgentDashboardTelemetry: vi.fn(),
- blockingOverlay: false,
- boardProps: null as Record | null,
- activateTabAndFocusPane: vi.fn(),
- activateAndRevealWorkspace: vi.fn(() => ({ primaryTabId: null }) as unknown)
-}))
-
-vi.mock('@/lib/workspace/tabs/activate-tab-and-focus-pane', () => ({
- activateTabAndFocusPane: mocks.activateTabAndFocusPane
-}))
-
-vi.mock('@/lib/workspace/activation/worktree-activation', () => ({
- activateAndRevealWorkspace: mocks.activateAndRevealWorkspace
-}))
-
-vi.mock('./useLiveDashboardSnapshot', () => ({
- useLiveDashboardSnapshot: mocks.useLiveDashboardSnapshot
-}))
-
-vi.mock('../dashboard-popout/use-agent-dashboard-telemetry', () => ({
- useAgentDashboardTelemetry: mocks.useAgentDashboardTelemetry
-}))
-
-vi.mock('../dashboard-popout/AgentKanbanBoard', () => ({
- AgentKanbanBoard: (props: Record) => {
- mocks.boardProps = props
- return mocks.blockingOverlay ? : null
- }
-}))
-
-vi.mock('./AgentDashboardSettingsMenu', () => ({
- AgentDashboardSettingsMenu: () => null
-}))
-
-vi.mock('../sidebar/workspace-board/surface/use-workspace-kanban-outside-dismiss', () => ({
- isWorkspaceBoardKeepOpenTarget: () => false,
- useWorkspaceKanbanOutsideDismiss: () => undefined
-}))
-
-import { AgentDashboardDrawer } from './AgentDashboardDrawer'
-
-const initialState = useAppStore.getInitialState()
-
-beforeEach(() => {
- useAppStore.setState(
- {
- agentDashboardDrawerOpen: false,
- sidebarOpen: true,
- sidebarWidth: 320
- },
- false
- )
- mocks.useLiveDashboardSnapshot.mockClear()
- mocks.useAgentDashboardTelemetry.mockClear()
- mocks.activateTabAndFocusPane.mockClear()
- mocks.activateAndRevealWorkspace.mockClear()
- mocks.activateAndRevealWorkspace.mockReturnValue({ primaryTabId: null })
- mocks.blockingOverlay = false
- mocks.boardProps = null
- ;(window as unknown as { api: unknown }).api = {
- dashboard: { openPopout: vi.fn().mockResolvedValue(undefined) }
- }
-})
-
-afterEach(() => {
- cleanup()
- document.body.replaceChildren()
- useAppStore.setState(initialState, true)
-})
-
-describe('AgentDashboardDrawer', () => {
- it('keeps the dismissal Escape from a focused terminal but not from other fields', () => {
- const terminalInput = document.createElement('textarea')
- terminalInput.className = 'xterm-helper-textarea'
- const field = document.createElement('input')
- document.body.append(terminalInput, field)
- const terminalKeyDown = vi.fn()
- const fieldKeyDown = vi.fn()
- terminalInput.addEventListener('keydown', terminalKeyDown)
- field.addEventListener('keydown', fieldKeyDown)
- render()
-
- act(() => useAppStore.setState({ agentDashboardDrawerOpen: true }))
- fireEvent.keyDown(terminalInput, { key: 'Escape' })
- expect(useAppStore.getState().agentDashboardDrawerOpen).toBe(false)
- expect(terminalKeyDown).not.toHaveBeenCalled()
-
- act(() => useAppStore.setState({ agentDashboardDrawerOpen: true }))
- fireEvent.keyDown(field, { key: 'Escape' })
- expect(useAppStore.getState().agentDashboardDrawerOpen).toBe(false)
- expect(fieldKeyDown).toHaveBeenCalledTimes(1)
- })
-})
diff --git a/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx b/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx
index 157d9b99e8a..5f6502ba077 100644
--- a/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx
+++ b/src/renderer/src/components/dashboard/AgentDashboardDrawer.tsx
@@ -2,10 +2,10 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
import { useAppStore } from '@/store'
import type { AgentSubjectReadIntent } from '@/attention/agent-subject-read-actions'
import { Sheet, SheetContent, SheetTitle } from '@/components/ui/sheet'
-import { revealDashboardAgent } from './reveal-dashboard-agent'
-import { AgentKanbanBoard } from '../dashboard-popout/AgentKanbanBoard'
+import { revealDashboardAgent } from './navigation/reveal-dashboard-agent'
+import { AgentKanbanBoard } from './board/AgentKanbanBoard'
import { isXtermHelperTextarea } from '../terminal-pane/input/focus/regular-terminal-focus-ownership'
-import type { AgentRevealArgs } from '../dashboard-popout/AgentTerminalDialog'
+import type { DashboardRevealAgentArgs as AgentRevealArgs } from '../../../../shared/dashboard-snapshot'
import {
isWorkspaceBoardKeepOpenTarget,
useWorkspaceKanbanOutsideDismiss
@@ -14,9 +14,9 @@ import {
STATUS_BAR_RESERVE_HEIGHT,
WORKSPACE_TOP_CHROME_HEIGHT
} from '../sidebar/workspace-chrome-metrics'
-import { AgentDashboardSettingsMenu } from './AgentDashboardSettingsMenu'
-import { useLiveDashboardSnapshot } from './useLiveDashboardSnapshot'
-import { useAgentDashboardTelemetry } from '../dashboard-popout/use-agent-dashboard-telemetry'
+import { AgentDashboardSettingsMenu } from './settings/AgentDashboardSettingsMenu'
+import { useLiveDashboardSnapshot } from './snapshot/useLiveDashboardSnapshot'
+import { useAgentDashboardTelemetry } from './board/use-agent-dashboard-telemetry'
import { useTranslate } from '@/i18n/use-translate'
import { trackSidebarLiveWidth } from '@/components/sidebar/sidebar-live-width'
diff --git a/src/renderer/src/components/dashboard/DashboardAgentRow.tsx b/src/renderer/src/components/dashboard/DashboardAgentRow.tsx
index 77578e0d987..a86997c77f4 100644
--- a/src/renderer/src/components/dashboard/DashboardAgentRow.tsx
+++ b/src/renderer/src/components/dashboard/DashboardAgentRow.tsx
@@ -4,7 +4,7 @@ import {
opensClaudeParentConversation,
viewClaudeAgentFromRow
} from '@/lib/agent-session/navigation/claude-agent-row-view'
-import { useClaudeAgentRowView } from './use-claude-agent-row-view'
+import { useClaudeAgentRowView } from './rows/use-claude-agent-row-view'
import {
AgentStateDot,
agentStateLabel,
@@ -14,9 +14,9 @@ import {
import { AgentTypeIcon } from '@/components/agent/icons/agent-type-icon'
import { formatAgentTypeLabel } from '../../../../shared/agent-status/presentation/agent-type-label'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
-import { DashboardAgentRowMessage } from './DashboardAgentRowMessage'
-import { DashboardAgentRowTrailingControls } from './DashboardAgentRowTrailingControls'
-import { DashboardAgentRowToolStep } from './DashboardAgentRowToolStep'
+import { DashboardAgentRowMessage } from './rows/DashboardAgentRowMessage'
+import { DashboardAgentRowTrailingControls } from './rows/DashboardAgentRowTrailingControls'
+import { DashboardAgentRowToolStep } from './rows/DashboardAgentRowToolStep'
import { programStatusAttentionLine } from '../../../../shared/program-status/presentation'
import {
agentRowToolInput,
@@ -37,10 +37,10 @@ import {
agentVerdictDisplayMark
} from '../../../../shared/agent-status/lifecycle/agent-main-agent-verdict'
import { agentVerdictStatusLine } from '@/lib/agent-status/presentation/agent-verdict-status-line'
-import type { DashboardAgentRow as DashboardAgentRowData } from './useDashboardData'
+import type { DashboardAgentRow as DashboardAgentRowData } from './rows/dashboard-agent-row'
import { getAgentRowPrimaryText } from '../../../../shared/agent-status/presentation/agent-row-primary-text'
-import { useAgentRowConversationName } from './use-agent-row-conversation-name'
-import { lastEnteredDoneAt } from './agent-finished-timestamp'
+import { useAgentRowConversationName } from './rows/use-agent-row-conversation-name'
+import { lastEnteredDoneAt } from './rows/agent-finished-timestamp'
import {
agentChildRowMessageLine,
agentChildRowNoUpdateLabel
diff --git a/src/renderer/src/components/dashboard/agent-destination-navigation.integration.test.tsx b/src/renderer/src/components/dashboard/agent-destination-navigation.integration.test.tsx
deleted file mode 100644
index f75b08695cb..00000000000
--- a/src/renderer/src/components/dashboard/agent-destination-navigation.integration.test.tsx
+++ /dev/null
@@ -1,396 +0,0 @@
-import type * as RuntimeRpcCallModule from '@/runtime/rpc/calls/runtime-rpc-call'
-// @vitest-environment happy-dom
-
-import { cleanup, fireEvent, render, screen } from '@testing-library/react'
-import { afterEach, describe, expect, it, vi } from 'vitest'
-import { useAppStore } from '@/store'
-import { getExecutionHostIdForWorktree } from '@/lib/workspace/ownership/worktree-runtime-owner'
-import {
- makeCreatedAgentWorktree,
- seedAlreadyActiveWorktree
-} from '@/lib/workspace/activation/worktree-activation-created-agent.test-fixture'
-import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
-import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
-import type { Tab } from '../../../../shared/tab-types'
-
-import type * as WebRuntimeSessionEnvironment from '@/runtime/web-session-tabs-sync/identity/web-runtime-session-environment'
-import { AgentKanbanBoard } from '../dashboard-popout/AgentKanbanBoard'
-import WorktreeCardAgents from '../sidebar/workspace-card/activity/WorktreeCardAgents'
-import { TooltipProvider } from '@/components/ui/tooltip'
-import { buildDashboardSnapshot } from './build-dashboard-snapshot'
-import { revealDashboardAgent } from './reveal-dashboard-agent'
-import { activateRetainedAgentRow } from '../sidebar/workspace-card/activity/activate-retained-agent-row'
-import { activateWorktreeAgentPane } from '../sidebar/workspace-card/activity/use-worktree-agent-activation'
-import { structuredAgentSessionPaneKey } from '../../../../shared/agent-session/view/structured-agent-session-projection'
-
-const effects = vi.hoisted(() => ({
- rpc: vi.fn(async () => ({ ok: true })),
- composerFocus: vi.fn(),
- environmentCall: vi.fn(async (_environmentId: string, _args: unknown) => ({
- id: 'navigation-test',
- ok: false as const,
- error: { code: 'unavailable', message: 'Unit-test transport boundary' }
- }))
-}))
-vi.mock('@/runtime/rpc/calls/runtime-rpc-call', async (importOriginal) => ({
- ...(await importOriginal()),
- callRuntimeRpc: effects.rpc
-}))
-vi.mock('@/components/native-chat/composer/focus/native-chat-composer-focus-intent', () => ({
- requestNativeChatComposerFocus: effects.composerFocus
-}))
-vi.mock('../dashboard-popout/AgentTerminalPreview', () => ({ AgentTerminalPreview: () => null }))
-vi.mock(
- '@/runtime/web-session-tabs-sync/identity/web-runtime-session-environment',
- async (importOriginal) => ({
- ...(await importOriginal()),
- captureRuntimeEnvironmentCall: (environmentId: string) => (args: unknown) =>
- effects.environmentCall(environmentId, args)
- })
-)
-
-const initialState = useAppStore.getState()
-const LEAF = '11111111-1111-4111-8111-111111111111'
-const WORKSPACE_HOST = 'runtime:host-A' as const
-const TAB_HOST = 'runtime:host-B' as const
-const NOW = 1_000_000_000
-
-afterEach(() => {
- cleanup()
- useAppStore.setState(initialState, true)
- vi.clearAllMocks()
- vi.restoreAllMocks()
-})
-
-describe('Dashboard and retained sidebar destination contract', () => {
- it.each([
- ['worktree', 'native'],
- ['folder', 'native'],
- ['worktree', 'terminal'],
- ['folder', 'terminal']
- ] as const)(
- 'opens the exact %s %s destination and preserves reveal failure',
- async (scope, surface) => {
- const native = surface === 'native'
- const owner = native ? TAB_HOST : WORKSPACE_HOST
- const worktree = {
- ...makeCreatedAgentWorktree(),
- hostId: WORKSPACE_HOST,
- ...(scope === 'folder' ? { id: folderWorkspaceKey('folder-1') } : {})
- }
- seedAlreadyActiveWorktree(worktree)
- const seeded = useAppStore.getState()
- const terminal = { ...seeded.tabsByWorktree[worktree.id][0], executionHostId: owner }
- const tabId = native ? 'native-B' : terminal.id
- const paneKey = makePaneKey(tabId, LEAF)
- const group = seeded.groupsByWorktree[worktree.id][0]
- const nativeTab: Tab = {
- id: tabId,
- worktreeId: worktree.id,
- executionHostId: TAB_HOST,
- contentType: 'agent-session',
- entityId: 'session-B',
- groupId: group.id,
- label: 'Codex',
- customLabel: null,
- color: null,
- sortOrder: 1,
- createdAt: NOW
- }
- const folder = {
- id: 'folder-1',
- projectGroupId: 'project-1',
- name: 'Notes',
- folderPath: '/workspace/notes',
- executionHostId: WORKSPACE_HOST,
- linkedTask: null,
- comment: '',
- isArchived: false,
- isUnread: false,
- isPinned: false,
- sortOrder: 0,
- lastActivityAt: NOW,
- createdAt: NOW,
- updatedAt: NOW
- }
- useAppStore.setState({
- activeWorktreeId: null,
- activeWorkspaceExecutionHostId: null,
- activeTabType: 'editor',
- agentActivityDisplayMode: 'compact',
- repos: scope === 'folder' ? [] : seeded.repos,
- worktreesByRepo:
- scope === 'folder'
- ? {}
- : {
- [worktree.repoId]: [
- worktree,
- ...(native ? [{ ...worktree, hostId: TAB_HOST }] : [])
- ]
- },
- folderWorkspaces:
- scope === 'folder'
- ? [folder, ...(native ? [{ ...folder, executionHostId: TAB_HOST }] : [])]
- : [],
- getFreshFolderWorkspacePathStatus: () => ({ path: folder.folderPath, exists: true }),
- tabsByWorktree: { [worktree.id]: [terminal] },
- unifiedTabsByWorktree: {
- [worktree.id]: [
- ...seeded.unifiedTabsByWorktree[worktree.id],
- ...(native ? [nativeTab] : [])
- ]
- },
- groupsByWorktree: {
- [worktree.id]: [{ ...group, tabOrder: [...group.tabOrder, ...(native ? [tabId] : [])] }]
- },
- terminalLayoutsByTabId: native
- ? {}
- : {
- [tabId]: {
- root: { type: 'leaf', leafId: LEAF },
- activeLeafId: LEAF,
- expandedLeafId: null
- }
- },
- retainedAgentsByPaneKey: {
- [paneKey]: {
- entry: {
- paneKey,
- tabId,
- worktreeId: worktree.id,
- state: 'done',
- agentType: 'codex',
- prompt: 'Open this conversation',
- updatedAt: NOW,
- stateStartedAt: NOW,
- stateHistory: []
- },
- worktreeId: worktree.id,
- tab: { ...terminal, id: tabId, ptyId: native ? null : terminal.ptyId },
- agentType: 'codex',
- startedAt: NOW
- }
- }
- })
-
- const snapshot = buildDashboardSnapshot(
- { ...useAppStore.getState(), chatNames: new Map() },
- NOW
- )
- const card = snapshot.cards.find((candidate) => candidate.executionHostId === WORKSPACE_HOST)
- expect(card).toMatchObject({ worktreeId: worktree.id, tabId, ptyId: null })
- if (!card) {
- throw new Error('Expected a card from the production snapshot builder')
- }
- // Give the virtual lane a viewport; happy-dom has no layout.
- vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(600)
- const view = render(
-
- {}}
- onRevealAgent={revealDashboardAgent}
- />
-
- )
- fireEvent.click(screen.getByRole('button', { name: /Open this conversation/ }))
- fireEvent.click(screen.getByRole('button', { name: 'Open worktree' }))
-
- const assertDestination = async (): Promise => {
- const state = useAppStore.getState()
- expect(state.activeWorktreeId).toBe(worktree.id)
- expect(getExecutionHostIdForWorktree(state, worktree.id)).toBe(owner)
- expect(state.activeTabType).toBe(native ? 'agent-session' : 'terminal')
- if (!native) {
- expect(state.activeTabId).toBe(tabId)
- }
- expect(state.getActiveTab(worktree.id)?.id).toBe(tabId)
- expect(state.groupsByWorktree[worktree.id][0].activeTabId).toBe(tabId)
- if (native) {
- expect(effects.composerFocus).toHaveBeenLastCalledWith(tabId)
- expect(effects.rpc).toHaveBeenCalledWith(
- { kind: 'environment', environmentId: 'host-B' },
- 'session.tabs.activate',
- expect.objectContaining({ tabId: 'agent-session:session-B' })
- )
- } else {
- await vi.waitFor(() =>
- expect(effects.environmentCall).toHaveBeenCalledWith(
- 'host-A',
- expect.objectContaining({
- method: 'session.tabs.activate',
- params: expect.objectContaining({ leafId: LEAF })
- })
- )
- )
- }
- }
- await assertDestination()
- view.unmount()
-
- useAppStore.setState({
- activeWorktreeId: null,
- activeWorkspaceExecutionHostId: null,
- activeTabType: 'editor',
- activeTabId: null
- })
- const sidebar = render(
-
-
-
- )
- const row = sidebar.container.querySelector('.compact-agent-row')
- if (!row) {
- throw new Error('Expected the retained sidebar row')
- }
- effects.environmentCall.mockClear()
- fireEvent.click(row)
- await assertDestination()
-
- // Keep the exact tab alive while its workspace disappears: neither entry may focus or notify it.
- cleanup()
- useAppStore.setState({
- worktreesByRepo: {},
- folderWorkspaces: [],
- activeWorktreeId: null,
- activeWorkspaceExecutionHostId: null,
- activeTabType: 'editor',
- activeTabId: null
- })
- effects.rpc.mockClear()
- effects.composerFocus.mockClear()
- effects.environmentCall.mockClear()
- expect(revealDashboardAgent(card)).toBe(false)
- const missing = render(
-
-
-
- )
- const missingRow = missing.container.querySelector('.compact-agent-row')
- if (!missingRow) {
- throw new Error('Expected the retained row after workspace removal')
- }
- fireEvent.click(missingRow)
- expect(useAppStore.getState()).toMatchObject({
- activeWorktreeId: null,
- activeTabId: null,
- activeTabType: 'editor'
- })
- expect(effects.composerFocus).not.toHaveBeenCalled()
- expect(effects.rpc).not.toHaveBeenCalled()
- expect(effects.environmentCall).not.toHaveBeenCalled()
- }
- )
-
- it('notifies the exact live retained terminal owner instead of the workspace default', async () => {
- const worktree = { ...makeCreatedAgentWorktree(), hostId: WORKSPACE_HOST }
- seedAlreadyActiveWorktree(worktree, {
- worktreesByRepo: { [worktree.repoId]: [worktree, { ...worktree, hostId: TAB_HOST }] },
- activeWorkspaceExecutionHostId: WORKSPACE_HOST,
- ptyIdsByTabId: { 'tab-1': ['remote:host-B@@pty-B'] },
- terminalLayoutsByTabId: {
- 'tab-1': {
- root: { type: 'leaf', leafId: LEAF },
- activeLeafId: LEAF,
- expandedLeafId: null,
- ptyIdsByLeafId: { [LEAF]: 'remote:host-B@@pty-B' }
- }
- }
- })
-
- activateRetainedAgentRow(worktree.id, 'tab-1', makePaneKey('tab-1', LEAF))
- expect(useAppStore.getState().activeWorkspaceExecutionHostId).toBe(TAB_HOST)
- await vi.waitFor(() =>
- expect(effects.environmentCall).toHaveBeenCalledWith(
- 'host-B',
- expect.objectContaining({
- method: 'session.tabs.activate',
- params: expect.objectContaining({ leafId: LEAF })
- })
- )
- )
- expect(
- effects.environmentCall.mock.calls.every(([environmentId]) => environmentId === 'host-B')
- ).toBe(true)
- expect(useAppStore.getState().getActiveTab(worktree.id)?.id).toBe('tab-1')
- })
-})
-
-it('rejects a live terminal row on a different host before workspace activation', () => {
- const worktree = { ...makeCreatedAgentWorktree(), hostId: WORKSPACE_HOST }
- seedAlreadyActiveWorktree(worktree, {
- activeWorktreeId: null,
- activeWorkspaceExecutionHostId: null,
- activeTabId: null,
- ptyIdsByTabId: { 'tab-1': ['remote:host-B@@pty-B'] },
- terminalLayoutsByTabId: {
- 'tab-1': {
- root: { type: 'leaf', leafId: LEAF },
- activeLeafId: LEAF,
- expandedLeafId: null,
- ptyIdsByLeafId: { [LEAF]: 'remote:host-B@@pty-B' }
- }
- }
- })
-
- activateWorktreeAgentPane(worktree.id, 'tab-1', makePaneKey('tab-1', LEAF), WORKSPACE_HOST)
-
- expect(useAppStore.getState()).toMatchObject({
- activeWorktreeId: null,
- activeWorkspaceExecutionHostId: null,
- activeTabId: null
- })
-})
-
-it.each(['live', 'retained'] as const)(
- 'opens a %s paired chat from its physical SSH workspace',
- (source) => {
- const worktree = {
- ...makeCreatedAgentWorktree(),
- hostId: 'ssh:server' as const,
- runtimeOwnerEnvironmentId: 'host-B'
- }
- seedAlreadyActiveWorktree(worktree, {
- activeWorktreeId: null,
- activeWorkspaceExecutionHostId: null,
- activeTabType: 'editor',
- unifiedTabsByWorktree: {
- [worktree.id]: [
- {
- id: 'native-B',
- worktreeId: worktree.id,
- executionHostId: TAB_HOST,
- contentType: 'agent-session',
- agentSessionAgent: 'claude',
- entityId: 'session-B',
- groupId: 'group-1',
- label: 'Codex',
- customLabel: null,
- color: null,
- sortOrder: 1,
- createdAt: NOW
- }
- ]
- }
- })
- const activate = source === 'live' ? activateWorktreeAgentPane : activateRetainedAgentRow
- activate(
- worktree.id,
- 'native-B',
- structuredAgentSessionPaneKey('native-B', 'session-B'),
- 'ssh:server'
- )
-
- expect(useAppStore.getState()).toMatchObject({
- activeWorktreeId: worktree.id,
- activeWorkspaceExecutionHostId: 'ssh:server',
- activeTabType: 'agent-session'
- })
- expect(effects.composerFocus).toHaveBeenCalledWith('native-B')
- expect(effects.rpc).toHaveBeenCalledWith(
- { kind: 'environment', environmentId: 'host-B' },
- 'session.tabs.activate',
- expect.objectContaining({ tabId: 'agent-session:session-B' })
- )
- }
-)
diff --git a/src/renderer/src/components/dashboard-popout/AgentCardLastMessages.tsx b/src/renderer/src/components/dashboard/board/AgentCardLastMessages.tsx
similarity index 88%
rename from src/renderer/src/components/dashboard-popout/AgentCardLastMessages.tsx
rename to src/renderer/src/components/dashboard/board/AgentCardLastMessages.tsx
index 61680753967..8199948ea4f 100644
--- a/src/renderer/src/components/dashboard-popout/AgentCardLastMessages.tsx
+++ b/src/renderer/src/components/dashboard/board/AgentCardLastMessages.tsx
@@ -1,5 +1,5 @@
-import { formatAgentTypeLabel } from '../../../../shared/agent-status/presentation/agent-type-label'
-import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
+import { formatAgentTypeLabel } from '../../../../../shared/agent-status/presentation/agent-type-label'
+import type { DashboardCard } from '../../../../../shared/dashboard-snapshot'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/class-names'
diff --git a/src/renderer/src/components/dashboard-popout/AgentDashboardFilterChips.tsx b/src/renderer/src/components/dashboard/board/AgentDashboardFilterChips.tsx
similarity index 100%
rename from src/renderer/src/components/dashboard-popout/AgentDashboardFilterChips.tsx
rename to src/renderer/src/components/dashboard/board/AgentDashboardFilterChips.tsx
diff --git a/src/renderer/src/components/dashboard-popout/AgentDashboardToolbar.tsx b/src/renderer/src/components/dashboard/board/AgentDashboardToolbar.tsx
similarity index 97%
rename from src/renderer/src/components/dashboard-popout/AgentDashboardToolbar.tsx
rename to src/renderer/src/components/dashboard/board/AgentDashboardToolbar.tsx
index f225ad2fd67..0b20e6ad6e8 100644
--- a/src/renderer/src/components/dashboard-popout/AgentDashboardToolbar.tsx
+++ b/src/renderer/src/components/dashboard/board/AgentDashboardToolbar.tsx
@@ -12,8 +12,11 @@ import {
import { Input } from '@/components/ui/input'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/class-names'
-import { getWorkspaceStatusVisualMeta } from '../sidebar/workspace-status/presentation/workspace-status'
-import type { DashboardCard, DashboardFilterOptions } from '../../../../shared/dashboard-snapshot'
+import { getWorkspaceStatusVisualMeta } from '../../sidebar/workspace-status/presentation/workspace-status'
+import type {
+ DashboardCard,
+ DashboardFilterOptions
+} from '../../../../../shared/dashboard-snapshot'
import {
activeDashboardFilterCount,
type DashboardFilters,
diff --git a/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.tsx b/src/renderer/src/components/dashboard/board/AgentKanbanBoard.tsx
similarity index 97%
rename from src/renderer/src/components/dashboard-popout/AgentKanbanBoard.tsx
rename to src/renderer/src/components/dashboard/board/AgentKanbanBoard.tsx
index 38bfa9609d6..cd96cc478c3 100644
--- a/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.tsx
+++ b/src/renderer/src/components/dashboard/board/AgentKanbanBoard.tsx
@@ -4,15 +4,16 @@ import {
DASHBOARD_BUCKET_ORDER,
type DashboardBucket,
type DashboardCard,
- type DashboardSnapshot
-} from '../../../../shared/dashboard-snapshot'
-import type { RepoIcon } from '../../../../shared/repo-icon'
+ type DashboardSnapshot,
+ type DashboardRevealAgentArgs as AgentRevealArgs
+} from '../../../../../shared/dashboard-snapshot'
+import type { RepoIcon } from '../../../../../shared/repo-icon'
import { cn } from '@/lib/class-names'
import { TooltipProvider } from '@/components/ui/tooltip'
import { installWindowVisibilityInterval } from '@/lib/window-activity/window-visibility-interval'
import { AgentKanbanLaneCardList } from './AgentKanbanLaneCardList'
import { AgentDashboardToolbar } from './AgentDashboardToolbar'
-import { AgentTerminalDialog, type AgentRevealArgs } from './AgentTerminalDialog'
+import { AgentTerminalDialog } from '../terminal-preview/AgentTerminalDialog'
import {
EMPTY_DASHBOARD_FILTERS,
filterDashboardCards,
diff --git a/src/renderer/src/components/dashboard-popout/AgentKanbanCard.tsx b/src/renderer/src/components/dashboard/board/AgentKanbanCard.tsx
similarity index 99%
rename from src/renderer/src/components/dashboard-popout/AgentKanbanCard.tsx
rename to src/renderer/src/components/dashboard/board/AgentKanbanCard.tsx
index 7bdc989994f..fff4d97dcdb 100644
--- a/src/renderer/src/components/dashboard-popout/AgentKanbanCard.tsx
+++ b/src/renderer/src/components/dashboard/board/AgentKanbanCard.tsx
@@ -16,8 +16,8 @@ import { cn } from '@/lib/class-names'
import {
dashboardCardDisplayState,
type DashboardCard
-} from '../../../../shared/dashboard-snapshot'
-import type { RepoIcon } from '../../../../shared/repo-icon'
+} from '../../../../../shared/dashboard-snapshot'
+import type { RepoIcon } from '../../../../../shared/repo-icon'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { DashboardHostBadge } from './DashboardHostBadge'
diff --git a/src/renderer/src/components/dashboard-popout/AgentKanbanLaneCardList.tsx b/src/renderer/src/components/dashboard/board/AgentKanbanLaneCardList.tsx
similarity index 97%
rename from src/renderer/src/components/dashboard-popout/AgentKanbanLaneCardList.tsx
rename to src/renderer/src/components/dashboard/board/AgentKanbanLaneCardList.tsx
index b967d32caf4..a890956a334 100644
--- a/src/renderer/src/components/dashboard-popout/AgentKanbanLaneCardList.tsx
+++ b/src/renderer/src/components/dashboard/board/AgentKanbanLaneCardList.tsx
@@ -1,6 +1,6 @@
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
-import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
-import type { RepoIcon } from '../../../../shared/repo-icon'
+import type { DashboardCard } from '../../../../../shared/dashboard-snapshot'
+import type { RepoIcon } from '../../../../../shared/repo-icon'
import { AgentKanbanCard } from './AgentKanbanCard'
import {
focusAgentKanbanCardEdge,
diff --git a/src/renderer/src/components/dashboard-popout/DashboardHostBadge.tsx b/src/renderer/src/components/dashboard/board/DashboardHostBadge.tsx
similarity index 95%
rename from src/renderer/src/components/dashboard-popout/DashboardHostBadge.tsx
rename to src/renderer/src/components/dashboard/board/DashboardHostBadge.tsx
index 0e95bb2ee15..e5f5fb25f45 100644
--- a/src/renderer/src/components/dashboard-popout/DashboardHostBadge.tsx
+++ b/src/renderer/src/components/dashboard/board/DashboardHostBadge.tsx
@@ -3,8 +3,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { cn } from '@/lib/class-names'
-import type { DashboardCardHostKind } from '../../../../shared/dashboard-snapshot'
-import { parseExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
+import type { DashboardCardHostKind } from '../../../../../shared/dashboard-snapshot'
+import { parseExecutionHostId, type ExecutionHostId } from '../../../../../shared/execution-host'
type DashboardHostBadgeProps = {
hostKind?: DashboardCardHostKind
diff --git a/src/renderer/src/components/dashboard-popout/FilterOptionCount.tsx b/src/renderer/src/components/dashboard/board/FilterOptionCount.tsx
similarity index 100%
rename from src/renderer/src/components/dashboard-popout/FilterOptionCount.tsx
rename to src/renderer/src/components/dashboard/board/FilterOptionCount.tsx
diff --git a/src/renderer/src/components/dashboard-popout/agent-board-filtering.ts b/src/renderer/src/components/dashboard/board/agent-board-filtering.ts
similarity index 98%
rename from src/renderer/src/components/dashboard-popout/agent-board-filtering.ts
rename to src/renderer/src/components/dashboard/board/agent-board-filtering.ts
index b7e245f766d..0062c35cc54 100644
--- a/src/renderer/src/components/dashboard-popout/agent-board-filtering.ts
+++ b/src/renderer/src/components/dashboard/board/agent-board-filtering.ts
@@ -1,4 +1,4 @@
-import type { DashboardCard, DashboardCardReview } from '../../../../shared/dashboard-snapshot'
+import type { DashboardCard, DashboardCardReview } from '../../../../../shared/dashboard-snapshot'
export type DashboardReviewFilter = DashboardCardReview['state'] | 'none'
diff --git a/src/renderer/src/components/dashboard-popout/agent-board-transitions.css b/src/renderer/src/components/dashboard/board/agent-board-transitions.css
similarity index 100%
rename from src/renderer/src/components/dashboard-popout/agent-board-transitions.css
rename to src/renderer/src/components/dashboard/board/agent-board-transitions.css
diff --git a/src/renderer/src/components/dashboard-popout/agent-dashboard-filter-options.ts b/src/renderer/src/components/dashboard/board/agent-dashboard-filter-options.ts
similarity index 98%
rename from src/renderer/src/components/dashboard-popout/agent-dashboard-filter-options.ts
rename to src/renderer/src/components/dashboard/board/agent-dashboard-filter-options.ts
index 1ec983eb14e..ff0f9ad90b2 100644
--- a/src/renderer/src/components/dashboard-popout/agent-dashboard-filter-options.ts
+++ b/src/renderer/src/components/dashboard/board/agent-dashboard-filter-options.ts
@@ -1,7 +1,7 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { translateWorkspaceBoardStatusLabel } from '@/components/sidebar/workspace-board-status-label'
-import type { DashboardCard, DashboardFilterOption } from '../../../../shared/dashboard-snapshot'
+import type { DashboardCard, DashboardFilterOption } from '../../../../../shared/dashboard-snapshot'
import type { DashboardReviewFilter } from './agent-board-filtering'
export type FilterOption = { id: string; label: string; count: number; color?: string }
diff --git a/src/renderer/src/components/dashboard-popout/agent-kanban-lane-tab-bridge.ts b/src/renderer/src/components/dashboard/board/agent-kanban-lane-tab-bridge.ts
similarity index 100%
rename from src/renderer/src/components/dashboard-popout/agent-kanban-lane-tab-bridge.ts
rename to src/renderer/src/components/dashboard/board/agent-kanban-lane-tab-bridge.ts
diff --git a/src/renderer/src/components/dashboard-popout/use-agent-dashboard-telemetry.ts b/src/renderer/src/components/dashboard/board/use-agent-dashboard-telemetry.ts
similarity index 92%
rename from src/renderer/src/components/dashboard-popout/use-agent-dashboard-telemetry.ts
rename to src/renderer/src/components/dashboard/board/use-agent-dashboard-telemetry.ts
index 9fd788ea8cc..61d6f293dcf 100644
--- a/src/renderer/src/components/dashboard-popout/use-agent-dashboard-telemetry.ts
+++ b/src/renderer/src/components/dashboard/board/use-agent-dashboard-telemetry.ts
@@ -1,5 +1,5 @@
import { useEffect } from 'react'
-import type { AgentDashboardSurface } from '../../../../shared/telemetry-agent-dashboard-event-schemas'
+import type { AgentDashboardSurface } from '../../../../../shared/telemetry-agent-dashboard-event-schemas'
import { getConsentState, track, trackWithResult } from '@/diagnostics/telemetry/telemetry'
import { installWindowVisibilityInterval } from '@/lib/window-activity/window-visibility-interval'
import { useAppStore } from '@/store/index'
diff --git a/src/renderer/src/components/dashboard-popout/use-agent-kanban-lane-virtualizer.ts b/src/renderer/src/components/dashboard/board/use-agent-kanban-lane-virtualizer.ts
similarity index 97%
rename from src/renderer/src/components/dashboard-popout/use-agent-kanban-lane-virtualizer.ts
rename to src/renderer/src/components/dashboard/board/use-agent-kanban-lane-virtualizer.ts
index 80ce45a05d1..15af8b6d43a 100644
--- a/src/renderer/src/components/dashboard-popout/use-agent-kanban-lane-virtualizer.ts
+++ b/src/renderer/src/components/dashboard/board/use-agent-kanban-lane-virtualizer.ts
@@ -1,6 +1,6 @@
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'
import { useVirtualizer, type Range } from '@tanstack/react-virtual'
-import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
+import type { DashboardCard } from '../../../../../shared/dashboard-snapshot'
import { extractPinnedVirtualRange } from '@/lib/list-interaction/pinned-virtual-range'
// Estimates follow the card's fixed rows; measurement replaces them for message/question variants.
diff --git a/src/renderer/src/components/dashboard/bucket-counts/build-dashboard-bucket-counts.equivalence.test.ts b/src/renderer/src/components/dashboard/bucket-counts/build-dashboard-bucket-counts.equivalence.test.ts
new file mode 100644
index 00000000000..0881db2e2e4
--- /dev/null
+++ b/src/renderer/src/components/dashboard/bucket-counts/build-dashboard-bucket-counts.equivalence.test.ts
@@ -0,0 +1,187 @@
+import { describe, expect, it } from 'vitest'
+import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../../shared/agent-status/contracts/agent-status-freshness'
+import type { AgentStatusEntry } from '../../../../../shared/agent-status/contracts/agent-status-types'
+
+import type { FolderWorkspace } from '../../../../../shared/folder-workspaces/folder-workspace-types'
+import type { ProjectGroup } from '../../../../../shared/project-group-types'
+import { makePaneKey } from '../../../../../shared/workspace-layout/stable-pane-id'
+import type { TerminalTab } from '../../../../../shared/workspace-layout/terminal-tab-types'
+import type { Worktree } from '../../../../../shared/worktree/types'
+import { folderWorkspaceKey } from '../../../../../shared/workspace-scope'
+import '../../sidebar/workspace-card/activity/worktree-agent-orchestration-batch'
+
+import type { DashboardSnapshotState } from '../snapshot/build-dashboard-snapshot'
+import {
+ buildDashboardBucketCounts,
+ createDashboardBucketCountsCache
+} from './build-dashboard-bucket-counts'
+
+const BASE = 1_700_000_000_000
+const STALE = AGENT_STATUS_STALE_AFTER_MS
+const LEAF_1 = '11111111-1111-4111-8111-111111111111'
+const LEAF_2 = '22222222-2222-4222-8222-222222222222'
+const LEAF_3 = '33333333-3333-4333-8333-333333333333'
+const PANE_1 = makePaneKey('tab1', LEAF_1)
+const PANE_2 = makePaneKey('tab2', LEAF_2)
+const FOLDER_WORKSPACE_ID = folderWorkspaceKey('folder-1')
+const PANE_3 = makePaneKey('tab3', LEAF_3)
+
+function worktree(id: string): Worktree {
+ return {
+ id,
+ repoId: 'r1',
+ path: `/r1/${id}`,
+ head: 'abc123',
+ branch: 'main',
+ isBare: false,
+ isMainWorktree: false,
+ displayName: id,
+ comment: '',
+ linkedIssue: null,
+ linkedPR: null,
+ linkedLinearIssue: null,
+ isArchived: false,
+ isUnread: false,
+ isPinned: false,
+ sortOrder: 0,
+ lastActivityAt: BASE
+ }
+}
+
+function tab(id: string, worktreeId: string): TerminalTab {
+ return {
+ id,
+ ptyId: `pty-${id}`,
+ worktreeId,
+ title: 'shell',
+ customTitle: null,
+ color: null,
+ sortOrder: 0,
+ createdAt: BASE
+ }
+}
+
+function entry(
+ paneKey: string,
+ tabId: string,
+ worktreeId: string,
+ overrides: Partial = {}
+): AgentStatusEntry {
+ return {
+ paneKey,
+ state: 'working',
+ prompt: 'do the thing',
+ updatedAt: BASE,
+ stateStartedAt: BASE - 5_000,
+ stateHistory: [],
+ agentType: 'claude',
+ tabId,
+ worktreeId,
+ ...overrides
+ }
+}
+
+function leafLayout(tabId: string, leafId: string) {
+ return {
+ root: { type: 'leaf', leafId } as const,
+ activeLeafId: leafId,
+ expandedLeafId: null,
+ ptyIdsByLeafId: { [leafId]: `pty-${tabId}` }
+ }
+}
+
+function folderWorkspace(): FolderWorkspace {
+ return {
+ id: 'folder-1',
+ projectGroupId: 'group-1',
+ name: 'Docs workspace',
+ folderPath: '/workspace/docs',
+ connectionId: null,
+ linkedTask: null,
+ comment: '',
+ isArchived: false,
+ isUnread: false,
+ isPinned: false,
+ sortOrder: 0,
+ lastActivityAt: BASE,
+ createdAt: BASE,
+ updatedAt: BASE
+ }
+}
+
+function projectGroup(): ProjectGroup {
+ return {
+ id: 'group-1',
+ name: 'Documentation',
+ parentPath: '/workspace',
+ connectionId: null,
+ parentGroupId: null,
+ createdFrom: 'folder-scan',
+ tabOrder: 0,
+ isCollapsed: false,
+ color: null,
+ createdAt: BASE,
+ updatedAt: BASE
+ }
+}
+
+function baseState(): DashboardSnapshotState {
+ return {
+ repos: [
+ {
+ id: 'r1',
+ path: '/r1',
+ displayName: 'Repo One',
+ badgeColor: '#000',
+ addedAt: 1
+ }
+ ],
+ worktreesByRepo: { r1: [worktree('w1'), worktree('w2')] },
+ folderWorkspaces: [folderWorkspace()],
+ projectGroups: [projectGroup()],
+ tabsByWorktree: {
+ w1: [tab('tab1', 'w1')],
+ w2: [tab('tab2', 'w2')],
+ [FOLDER_WORKSPACE_ID]: [tab('tab3', FOLDER_WORKSPACE_ID)]
+ },
+ agentStatusByPaneKey: {
+ [PANE_1]: entry(PANE_1, 'tab1', 'w1'),
+ [PANE_2]: entry(PANE_2, 'tab2', 'w2', { state: 'done' }),
+ [PANE_3]: entry(PANE_3, 'tab3', FOLDER_WORKSPACE_ID, {
+ state: 'waiting'
+ })
+ },
+ retainedAgentsByPaneKey: {},
+ migrationUnsupportedByPtyId: {},
+ runtimeAgentOrchestrationByPaneKey: {},
+ terminalLayoutsByTabId: {
+ tab1: leafLayout('tab1', LEAF_1),
+ tab2: leafLayout('tab2', LEAF_2),
+ tab3: leafLayout('tab3', LEAF_3)
+ },
+ ptyIdsByTabId: {
+ tab1: ['pty-tab1'],
+ tab2: ['pty-tab2'],
+ tab3: ['pty-tab3']
+ },
+ runtimePaneTitlesByTabId: { tab1: { 0: 'shell' } },
+ acknowledgedAgentsByPaneKey: {},
+ settings: null
+ } as unknown as DashboardSnapshotState
+}
+
+describe('buildDashboardBucketCounts equivalence with the unmemoized walk', () => {
+ it('keeps the last-fresh instant working and decays one ms later', () => {
+ const cache = createDashboardBucketCountsCache()
+ const state = baseState()
+
+ expect(buildDashboardBucketCounts(state, BASE, cache, 1).working).toBe(1)
+
+ // At now-observedAt === STALE the cache still hits and the entry remains fresh.
+ const atBoundary = buildDashboardBucketCounts(state, BASE + STALE, cache, 1)
+ expect(atBoundary.working).toBe(1)
+
+ const pastBoundary = buildDashboardBucketCounts(state, BASE + STALE + 1, cache, 2)
+ expect(pastBoundary.working).toBe(0)
+ })
+})
diff --git a/src/renderer/src/components/dashboard/build-dashboard-bucket-counts.ts b/src/renderer/src/components/dashboard/bucket-counts/build-dashboard-bucket-counts.ts
similarity index 89%
rename from src/renderer/src/components/dashboard/build-dashboard-bucket-counts.ts
rename to src/renderer/src/components/dashboard/bucket-counts/build-dashboard-bucket-counts.ts
index ae817a14b5f..7868b30733a 100644
--- a/src/renderer/src/components/dashboard/build-dashboard-bucket-counts.ts
+++ b/src/renderer/src/components/dashboard/bucket-counts/build-dashboard-bucket-counts.ts
@@ -1,26 +1,26 @@
-import type { DashboardBucket } from '../../../../shared/dashboard-snapshot'
-import type { DashboardSnapshotState } from './build-dashboard-snapshot'
+import type { DashboardBucket } from '../../../../../shared/dashboard-snapshot'
+import type { DashboardSnapshotState } from '../snapshot/build-dashboard-snapshot'
import {
collectActiveDashboardWorkspaces,
type ActiveDashboardWorkspace,
type DashboardWorkspaceState
-} from './dashboard-snapshot-workspaces'
-import { selectDashboardOrchestration } from './dashboard-orchestration-selection'
+} from '../snapshot/dashboard-snapshot-workspaces'
+import { selectDashboardOrchestration } from '../snapshot/dashboard-orchestration-selection'
import {
dashboardRowBucketProjection,
readAgentRowTurnRecords,
type AgentRowUnreadState
-} from './dashboard-row-bucket'
-import type { DashboardAgentRowWithLineage } from './agent-row-lineage'
-import '../sidebar/workspace-card/activity/worktree-agent-orchestration-batch'
-import { EMPTY_WORKTREE_AGENT_ORCHESTRATION } from '../sidebar/workspace-card/activity/worktree-agent-orchestration-index'
+} from '../rows/dashboard-row-bucket'
+import type { DashboardAgentRowWithLineage } from '../rows/agent-row-lineage'
+import '../../sidebar/workspace-card/activity/worktree-agent-orchestration-batch'
+import { EMPTY_WORKTREE_AGENT_ORCHESTRATION } from '../../sidebar/workspace-card/activity/worktree-agent-orchestration-index'
import {
createWorktreeAgentRowsCache,
finishWorktreeAgentRowsCachePass,
selectWorktreeAgentRowsCached,
startWorktreeAgentRowsCachePass,
type WorktreeAgentRowsCache
-} from './worktree-agent-rows-cache'
+} from '../rows/worktree-agent-rows-cache'
import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields'
const EMPTY_COUNTS: Record = {
diff --git a/src/renderer/src/components/dashboard/useAgentBucketCounts.ts b/src/renderer/src/components/dashboard/bucket-counts/useAgentBucketCounts.ts
similarity index 97%
rename from src/renderer/src/components/dashboard/useAgentBucketCounts.ts
rename to src/renderer/src/components/dashboard/bucket-counts/useAgentBucketCounts.ts
index 4beec47fcf6..dc93a7c6833 100644
--- a/src/renderer/src/components/dashboard/useAgentBucketCounts.ts
+++ b/src/renderer/src/components/dashboard/bucket-counts/useAgentBucketCounts.ts
@@ -1,7 +1,7 @@
import { useMemo, useRef } from 'react'
-import { useAppStore } from '@/store'
+import { useAppStore } from '@/store/index'
import type { AppState } from '@/store/types'
-import type { DashboardBucket } from '../../../../shared/dashboard-snapshot'
+import type { DashboardBucket } from '../../../../../shared/dashboard-snapshot'
import {
buildDashboardBucketCounts,
createDashboardBucketCountsCache
diff --git a/src/renderer/src/components/dashboard/build-dashboard-bucket-counts.equivalence.test.ts b/src/renderer/src/components/dashboard/build-dashboard-bucket-counts.equivalence.test.ts
deleted file mode 100644
index a1d0bc2aad8..00000000000
--- a/src/renderer/src/components/dashboard/build-dashboard-bucket-counts.equivalence.test.ts
+++ /dev/null
@@ -1,319 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status/contracts/agent-status-freshness'
-import type { AgentStatusEntry } from '../../../../shared/agent-status/contracts/agent-status-types'
-import type { DashboardBucket } from '../../../../shared/dashboard-snapshot'
-import type { FolderWorkspace } from '../../../../shared/folder-workspaces/folder-workspace-types'
-import type { ProjectGroup } from '../../../../shared/project-group-types'
-import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
-import type { TerminalTab } from '../../../../shared/workspace-layout/terminal-tab-types'
-import type { Worktree } from '../../../../shared/worktree/types'
-import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
-import '../sidebar/workspace-card/activity/worktree-agent-orchestration-batch'
-import { EMPTY_WORKTREE_AGENT_ORCHESTRATION } from '../sidebar/workspace-card/activity/worktree-agent-orchestration-index'
-import type { DashboardSnapshotState } from './build-dashboard-snapshot'
-import {
- buildDashboardBucketCounts,
- createDashboardBucketCountsCache
-} from './build-dashboard-bucket-counts'
-import { selectDashboardOrchestration } from './dashboard-orchestration-selection'
-import { dashboardRowBucketProjection, readAgentRowTurnRecords } from './dashboard-row-bucket'
-import { collectActiveDashboardWorkspaces } from './dashboard-snapshot-workspaces'
-import { selectWorktreeAgentRowsCached } from './worktree-agent-rows-cache'
-
-const BASE = 1_700_000_000_000
-const STALE = AGENT_STATUS_STALE_AFTER_MS
-const LEAF_1 = '11111111-1111-4111-8111-111111111111'
-const LEAF_2 = '22222222-2222-4222-8222-222222222222'
-const LEAF_3 = '33333333-3333-4333-8333-333333333333'
-const PANE_1 = makePaneKey('tab1', LEAF_1)
-const PANE_2 = makePaneKey('tab2', LEAF_2)
-const FOLDER_WORKSPACE_ID = folderWorkspaceKey('folder-1')
-const PANE_3 = makePaneKey('tab3', LEAF_3)
-
-/** Unmemoized reference using shared sidebar primitives, independent of the memoized builder. */
-function oracleBucketCounts(
- state: DashboardSnapshotState,
- now: number
-): Record {
- const counts: Record = {
- attention: 0,
- working: 0,
- done: 0,
- idle: 0
- }
- const activeWorktrees = collectActiveDashboardWorkspaces(state, false)
- const { singletonOrchestration, orchestrationByWorktree } = selectDashboardOrchestration(
- state,
- activeWorktrees
- )
- for (const { worktree } of activeWorktrees) {
- const rows = selectWorktreeAgentRowsCached({
- state,
- worktreeId: worktree.id,
- orchestration:
- singletonOrchestration ??
- orchestrationByWorktree?.get(worktree.id) ??
- EMPTY_WORKTREE_AGENT_ORCHESTRATION,
- now,
- generation: undefined
- })
- for (const row of rows) {
- if (row.rowSource === 'subagent') {
- continue
- }
- counts[dashboardRowBucketProjection(row, readAgentRowTurnRecords(state)).bucket] += 1
- }
- }
- return counts
-}
-
-function worktree(id: string): Worktree {
- return {
- id,
- repoId: 'r1',
- path: `/r1/${id}`,
- head: 'abc123',
- branch: 'main',
- isBare: false,
- isMainWorktree: false,
- displayName: id,
- comment: '',
- linkedIssue: null,
- linkedPR: null,
- linkedLinearIssue: null,
- isArchived: false,
- isUnread: false,
- isPinned: false,
- sortOrder: 0,
- lastActivityAt: BASE
- }
-}
-
-function tab(id: string, worktreeId: string): TerminalTab {
- return {
- id,
- ptyId: `pty-${id}`,
- worktreeId,
- title: 'shell',
- customTitle: null,
- color: null,
- sortOrder: 0,
- createdAt: BASE
- }
-}
-
-function entry(
- paneKey: string,
- tabId: string,
- worktreeId: string,
- overrides: Partial = {}
-): AgentStatusEntry {
- return {
- paneKey,
- state: 'working',
- prompt: 'do the thing',
- updatedAt: BASE,
- stateStartedAt: BASE - 5_000,
- stateHistory: [],
- agentType: 'claude',
- tabId,
- worktreeId,
- ...overrides
- }
-}
-
-function leafLayout(tabId: string, leafId: string) {
- return {
- root: { type: 'leaf', leafId } as const,
- activeLeafId: leafId,
- expandedLeafId: null,
- ptyIdsByLeafId: { [leafId]: `pty-${tabId}` }
- }
-}
-
-function folderWorkspace(): FolderWorkspace {
- return {
- id: 'folder-1',
- projectGroupId: 'group-1',
- name: 'Docs workspace',
- folderPath: '/workspace/docs',
- connectionId: null,
- linkedTask: null,
- comment: '',
- isArchived: false,
- isUnread: false,
- isPinned: false,
- sortOrder: 0,
- lastActivityAt: BASE,
- createdAt: BASE,
- updatedAt: BASE
- }
-}
-
-function projectGroup(): ProjectGroup {
- return {
- id: 'group-1',
- name: 'Documentation',
- parentPath: '/workspace',
- connectionId: null,
- parentGroupId: null,
- createdFrom: 'folder-scan',
- tabOrder: 0,
- isCollapsed: false,
- color: null,
- createdAt: BASE,
- updatedAt: BASE
- }
-}
-
-function baseState(): DashboardSnapshotState {
- return {
- repos: [
- {
- id: 'r1',
- path: '/r1',
- displayName: 'Repo One',
- badgeColor: '#000',
- addedAt: 1
- }
- ],
- worktreesByRepo: { r1: [worktree('w1'), worktree('w2')] },
- folderWorkspaces: [folderWorkspace()],
- projectGroups: [projectGroup()],
- tabsByWorktree: {
- w1: [tab('tab1', 'w1')],
- w2: [tab('tab2', 'w2')],
- [FOLDER_WORKSPACE_ID]: [tab('tab3', FOLDER_WORKSPACE_ID)]
- },
- agentStatusByPaneKey: {
- [PANE_1]: entry(PANE_1, 'tab1', 'w1'),
- [PANE_2]: entry(PANE_2, 'tab2', 'w2', { state: 'done' }),
- [PANE_3]: entry(PANE_3, 'tab3', FOLDER_WORKSPACE_ID, {
- state: 'waiting'
- })
- },
- retainedAgentsByPaneKey: {},
- migrationUnsupportedByPtyId: {},
- runtimeAgentOrchestrationByPaneKey: {},
- terminalLayoutsByTabId: {
- tab1: leafLayout('tab1', LEAF_1),
- tab2: leafLayout('tab2', LEAF_2),
- tab3: leafLayout('tab3', LEAF_3)
- },
- ptyIdsByTabId: {
- tab1: ['pty-tab1'],
- tab2: ['pty-tab2'],
- tab3: ['pty-tab3']
- },
- runtimePaneTitlesByTabId: { tab1: { 0: 'shell' } },
- acknowledgedAgentsByPaneKey: {},
- settings: null
- } as unknown as DashboardSnapshotState
-}
-
-/** Walk one cache across freshness boundaries; generation changes only when decay may shift buckets, and the exact last-fresh instant must still hit. */
-const CLOCK_WALK: { label: string; now: number; generation: number }[] = [
- { label: 'cold', now: BASE, generation: 1 },
- { label: 'last fresh instant (cache hit)', now: BASE + STALE, generation: 1 },
- { label: 'first stale instant', now: BASE + STALE + 1, generation: 2 },
- { label: 'long stale (cache hit)', now: BASE + STALE * 4, generation: 2 },
- { label: 'long stale (recomputed)', now: BASE + STALE * 4, generation: 3 }
-]
-
-type Mutation = {
- label: string
- apply: (state: DashboardSnapshotState) => DashboardSnapshotState
-}
-
-const MUTATIONS: Mutation[] = [
- { label: 'unchanged', apply: (state) => state },
- {
- label: 'acknowledgement written',
- apply: (state) => ({
- ...state,
- acknowledgedAgentsByPaneKey: { [PANE_2]: BASE + STALE * 8 }
- })
- },
- {
- label: 'unrelated pane-title frame',
- apply: (state) => ({
- ...state,
- runtimePaneTitlesByTabId: {
- ...state.runtimePaneTitlesByTabId,
- tab2: { 0: 'sh' }
- }
- })
- },
- {
- label: 'status write on one worktree',
- apply: (state) => ({
- ...state,
- agentStatusByPaneKey: {
- ...state.agentStatusByPaneKey,
- [PANE_1]: entry(PANE_1, 'tab1', 'w1', {
- prompt: 'streamed',
- state: 'blocked'
- })
- }
- })
- },
- {
- label: 'worktree leaves the active set',
- apply: (state) => ({ ...state, worktreesByRepo: { r1: [worktree('w1')] } })
- },
- {
- label: 'folder workspace archived',
- apply: (state) => ({
- ...state,
- folderWorkspaces: [{ ...folderWorkspace(), isArchived: true }]
- })
- },
- {
- label: 'project group renamed',
- apply: (state) => ({
- ...state,
- projectGroups: [{ ...projectGroup(), name: 'Renamed' }]
- })
- },
- {
- label: 'pty goes away',
- apply: (state) => ({
- ...state,
- ptyIdsByTabId: { tab2: ['pty-tab2'], tab3: ['pty-tab3'] }
- })
- }
-]
-
-describe('buildDashboardBucketCounts equivalence with the unmemoized walk', () => {
- it('matches the oracle when mutations are applied cumulatively through one cache', () => {
- const cache = createDashboardBucketCountsCache()
- let state = baseState()
- for (const mutation of MUTATIONS) {
- state = mutation.apply(state)
- for (const step of CLOCK_WALK) {
- expect(
- buildDashboardBucketCounts(state, step.now, cache, step.generation),
- `${mutation.label} @ ${step.label}`
- ).toEqual(oracleBucketCounts(state, step.now))
- }
- }
- })
-
- it('serves the last-fresh instant from a cache hit and still decays one ms later', () => {
- const cache = createDashboardBucketCountsCache()
- const state = baseState()
-
- expect(buildDashboardBucketCounts(state, BASE, cache, 1).working).toBe(1)
- expect(cache.lastComputedWorktreeIds.length).toBe(3)
-
- // At now-observedAt === STALE the cache still hits and the entry remains fresh.
- const atBoundary = buildDashboardBucketCounts(state, BASE + STALE, cache, 1)
- expect(cache.lastComputedWorktreeIds).toEqual([])
- expect(atBoundary.working).toBe(1)
- expect(atBoundary).toEqual(oracleBucketCounts(state, BASE + STALE))
-
- const pastBoundary = buildDashboardBucketCounts(state, BASE + STALE + 1, cache, 2)
- expect(pastBoundary.working).toBe(0)
- expect(pastBoundary).toEqual(oracleBucketCounts(state, BASE + STALE + 1))
- })
-})
diff --git a/src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts b/src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts
deleted file mode 100644
index 3f312091e61..00000000000
--- a/src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts
+++ /dev/null
@@ -1,391 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import {
- buildDashboardSnapshot,
- type DashboardSnapshotInput as DashboardSnapshotState
-} from './build-dashboard-snapshot'
-
-import type {
- AgentStatusEntry,
- AgentStatusOrchestrationContext
-} from '../../../../shared/agent-status/contracts/agent-status-types'
-
-import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
-import type { TerminalTab } from '../../../../shared/workspace-layout/terminal-tab-types'
-import type { Worktree } from '../../../../shared/worktree/types'
-import { selectRuntimeAgentOrchestrationBatch } from '../sidebar/workspace-card/activity/worktree-agent-orchestration-batch'
-import '../sidebar/workspace-card/activity/worktree-agent-row-selectors'
-import { selectWorktreeAgentOrchestration as selectRuntimeAgentOrchestrationForWorktree } from '../sidebar/workspace-card/activity/worktree-agent-orchestration-index'
-import type * as DashboardSnapshotWorkspacesModule from './dashboard-snapshot-workspaces'
-import type * as AgentRowLineageModule from './agent-row-lineage'
-
-const mapMetadataCalls = vi.hoisted(() => ({
- hostKind: vi.fn(),
- parentPaneKey: vi.fn()
-}))
-
-vi.mock('./dashboard-snapshot-workspaces', async (importOriginal) => {
- const actual = await importOriginal()
- return {
- ...actual,
- dashboardCardMapWorkspaceMetadata: (
- ...args: Parameters
- ) => {
- mapMetadataCalls.hostKind()
- return actual.dashboardCardMapWorkspaceMetadata(...args)
- }
- }
-})
-
-vi.mock('./agent-row-lineage', async (importOriginal) => {
- const actual = await importOriginal()
- return {
- ...actual,
- dashboardCardParentPaneKey: (...args: Parameters) => {
- mapMetadataCalls.parentPaneKey()
- return actual.dashboardCardParentPaneKey(...args)
- }
- }
-})
-
-const NOW = 1_000_000_000
-const TAB_ID = 'tab1'
-const LEAF_ID = '11111111-1111-4111-8111-111111111111'
-
-const SPLIT_SIBLING_LEAF_ID = '55555555-5555-4555-8555-555555555555'
-const PANE_KEY = makePaneKey(TAB_ID, LEAF_ID)
-
-function entry(overrides: Partial): AgentStatusEntry {
- return {
- paneKey: PANE_KEY,
- state: 'working',
- prompt: 'do the thing',
- updatedAt: NOW,
- stateStartedAt: NOW - 5000,
- stateHistory: [],
- agentType: 'claude',
- tabId: TAB_ID,
- worktreeId: 'w1',
- ...overrides
- }
-}
-
-function tab(id = TAB_ID, worktreeId = 'w1'): TerminalTab {
- return {
- id,
- ptyId: 'pty1',
- worktreeId,
- title: 'agent',
- customTitle: null,
- color: null,
- sortOrder: 0,
- createdAt: NOW
- }
-}
-
-function worktree(id = 'w1', displayName = 'wt-one'): Worktree {
- return {
- id,
- repoId: 'r1',
- path: `/r1/${id}`,
- head: 'abc123',
- branch: 'main',
- isBare: false,
- isMainWorktree: false,
- displayName,
- comment: '',
- linkedIssue: null,
- linkedPR: null,
- linkedLinearIssue: null,
- isArchived: false,
- isUnread: false,
- isPinned: false,
- sortOrder: 0,
- lastActivityAt: NOW
- }
-}
-
-function baseState(overrides: Partial): DashboardSnapshotState {
- return {
- chatNames: new Map(),
- repos: [{ id: 'r1', path: '/r1', displayName: 'Repo One', badgeColor: '#000', addedAt: NOW }],
- worktreesByRepo: { r1: [worktree()] },
- tabsByWorktree: { w1: [tab()] },
- agentStatusByPaneKey: {},
- retainedAgentsByPaneKey: {},
- migrationUnsupportedByPtyId: {},
- runtimeAgentOrchestrationByPaneKey: {},
- terminalLayoutsByTabId: {
- [TAB_ID]: {
- root: { type: 'leaf', leafId: LEAF_ID },
- activeLeafId: LEAF_ID,
- expandedLeafId: null,
- ptyIdsByLeafId: { [LEAF_ID]: 'pty1' }
- }
- },
- ptyIdsByTabId: { [TAB_ID]: ['pty1'] },
- runtimePaneTitlesByTabId: {},
- acknowledgedAgentsByPaneKey: {},
- settings: null,
- ...overrides
- }
-}
-
-describe('buildDashboardSnapshot', () => {
- it('names each pane of a split tab from its own title', () => {
- const siblingPaneKey = makePaneKey(TAB_ID, SPLIT_SIBLING_LEAF_ID)
- const snapshot = buildDashboardSnapshot(
- baseState({
- agentStatusByPaneKey: {
- [PANE_KEY]: entry({}),
- [siblingPaneKey]: entry({ paneKey: siblingPaneKey })
- },
- // The tab title is whichever pane has focus, so it must not name both.
- tabsByWorktree: { w1: [{ ...tab(), title: '\u2733 Linear work log' }] },
- terminalLayoutsByTabId: {
- [TAB_ID]: {
- root: {
- type: 'split',
- direction: 'horizontal',
- first: { type: 'leaf', leafId: LEAF_ID },
- second: { type: 'leaf', leafId: SPLIT_SIBLING_LEAF_ID }
- },
- activeLeafId: LEAF_ID,
- expandedLeafId: null,
- ptyIdsByLeafId: { [LEAF_ID]: 'pty1', [SPLIT_SIBLING_LEAF_ID]: 'pty2' }
- }
- },
- ptyIdsByTabId: { [TAB_ID]: ['pty1', 'pty2'] },
- // Pane ids are replay-creation-ordered: 1 -> first leaf, 2 -> second.
- runtimePaneTitlesByTabId: {
- [TAB_ID]: { 1: '\u2733 Linear work log', 2: '\u2733 Redis cache strategy' }
- }
- }),
- NOW
- )
-
- const nameByPaneKey = new Map(
- snapshot.cards.map((card) => [card.paneKey, card.conversationName])
- )
- expect(nameByPaneKey.get(PANE_KEY)).toBe('Linear work log')
- expect(nameByPaneKey.get(siblingPaneKey)).toBe('Redis cache strategy')
- })
-
- it('nulls ptyId when the layout entry points at a dead pty', () => {
- const snapshot = buildDashboardSnapshot(
- baseState({
- agentStatusByPaneKey: { [PANE_KEY]: entry({}) },
- // Layouts retain old PTY IDs after restart; only the live set proves preview availability.
- ptyIdsByTabId: { [TAB_ID]: [] }
- }),
- NOW
- )
- expect(snapshot.cards[0].ptyId).toBeNull()
- })
-
- it('labels a waiting agent with its question, not the prompt envelope JSON', () => {
- const interactivePrompt = JSON.stringify({
- questions: [{ question: 'Which database?', options: [{ label: 'Postgres' }] }]
- })
- const snapshot = buildDashboardSnapshot(
- baseState({
- agentStatusByPaneKey: {
- [PANE_KEY]: entry({ state: 'waiting', toolName: 'AskUserQuestion', interactivePrompt })
- }
- }),
- NOW
- )
- expect(snapshot.cards[0].askSummary).toBe('Which database?')
- })
-
- it('skips card-only context for count snapshots', () => {
- mapMetadataCalls.hostKind.mockClear()
- mapMetadataCalls.parentPaneKey.mockClear()
- let linkedReviewReads = 0
- let hostIdentityReads = 0
- const countWorktree = worktree()
- Object.defineProperty(countWorktree, 'linkedPR', {
- enumerable: true,
- get: () => {
- linkedReviewReads += 1
- return null
- }
- })
- const countRepo = {
- id: 'r1',
- path: '/r1',
- displayName: 'Repo One',
- badgeColor: '#000',
- addedAt: 0,
- get connectionId() {
- hostIdentityReads += 1
- return null
- }
- }
- const snapshot = buildDashboardSnapshot(
- baseState({
- repos: [countRepo],
- worktreesByRepo: { r1: [countWorktree] },
- agentStatusByPaneKey: { [PANE_KEY]: entry({}) }
- }),
- NOW,
- { includeCardDetails: false, includeFilterOptions: false }
- )
-
- expect(snapshot.cards[0].workspaceStatusId).toBeUndefined()
- expect(snapshot.cards[0].subagents).toBeUndefined()
- expect(snapshot.cards[0].parentPaneKey).toBeUndefined()
- expect(snapshot.cards[0].parentWorktreeId).toBeUndefined()
- expect(snapshot.cards[0].hostKind).toBeUndefined()
- expect(snapshot.cards[0].workspaceKind).toBeUndefined()
- // A live PTY alone must not trigger host-input resolution on sidebar count rebuilds.
- expect(snapshot.cards[0].ptyId).toBe('pty1')
- expect(snapshot.cards[0].terminalInput).toBeUndefined()
- expect(snapshot.workspaces).toBeUndefined()
- expect(linkedReviewReads).toBe(0)
- expect(hostIdentityReads).toBe(0)
- expect(mapMetadataCalls.hostKind).not.toHaveBeenCalled()
- expect(mapMetadataCalls.parentPaneKey).not.toHaveBeenCalled()
- })
-
- it('indexes runtime host labels once per detailed snapshot', () => {
- let environmentIdReads = 0
- const environmentCount = 24
- const runtimeEnvironments = Array.from({ length: environmentCount }, (_, index) => {
- const environment = { id: `environment-${index}`, name: `Builder ${index}` }
- Object.defineProperty(environment, 'id', {
- enumerable: true,
- get: () => {
- environmentIdReads += 1
- return `environment-${index}`
- }
- })
- return environment
- }) as unknown as DashboardSnapshotState['runtimeEnvironments']
- const executionHostId = `runtime:environment-${environmentCount - 1}` as const
-
- const snapshot = buildDashboardSnapshot(
- baseState({
- repos: [
- {
- id: 'r1',
- path: '/r1',
- displayName: 'Repo One',
- badgeColor: '#000',
- addedAt: 0,
- executionHostId
- }
- ],
- worktreesByRepo: {
- r1: [
- { ...worktree(), hostId: executionHostId },
- { ...worktree('w2', 'wt-two'), hostId: executionHostId }
- ]
- },
- runtimeEnvironments,
- agentStatusByPaneKey: { [PANE_KEY]: entry({}) }
- }),
- NOW
- )
-
- expect(snapshot.cards[0].hostLabel).toBe(`Builder ${environmentCount - 1}`)
- expect(environmentIdReads).toBe(environmentCount)
- })
-
- // Singleton/zero dashboard transitions must not release the shared orchestration index still read by mounted sidebar cards.
- it('keeps batch records live and correct when production moves from multi to singleton to zero', () => {
- const secondLeafId = '77777777-7777-4777-8777-777777777777'
- const firstPaneKey = makePaneKey('tab-w1', LEAF_ID)
- const secondPaneKey = makePaneKey('tab-w2', secondLeafId)
- const runtimeAgentOrchestrationByPaneKey = {
- [firstPaneKey]: {
- taskId: 'task-1',
- dispatchId: 'dispatch-1'
- },
- [secondPaneKey]: {
- taskId: 'task-2',
- dispatchId: 'dispatch-2'
- }
- }
- const multiState = baseState({
- worktreesByRepo: { r1: [worktree('w1'), worktree('w2')] },
- tabsByWorktree: {
- w1: [tab('tab-w1', 'w1')],
- w2: [tab('tab-w2', 'w2')]
- },
- runtimeAgentOrchestrationByPaneKey
- })
- const requested = ['w1', 'w2']
- const firstBatch = selectRuntimeAgentOrchestrationBatch(multiState, requested)
- const firstW1 = firstBatch.get('w1')
-
- buildDashboardSnapshot(
- baseState({
- worktreesByRepo: { r1: [worktree('w1')] },
- tabsByWorktree: multiState.tabsByWorktree,
- runtimeAgentOrchestrationByPaneKey
- }),
- NOW
- )
- const afterSingleton = selectRuntimeAgentOrchestrationBatch(multiState, requested)
- expect(afterSingleton).toBe(firstBatch)
- expect(afterSingleton.get('w1')).toBe(firstW1)
-
- buildDashboardSnapshot(baseState({ repos: [], worktreesByRepo: {} }), NOW)
- const afterZero = selectRuntimeAgentOrchestrationBatch(multiState, requested)
- expect(afterZero).toBe(firstBatch)
- for (const worktreeId of requested) {
- expect(afterZero.get(worktreeId)).toBe(
- selectRuntimeAgentOrchestrationForWorktree(multiState, worktreeId)
- )
- }
- })
-
- it('scans orchestration runtime once for a dashboard snapshot', () => {
- const worktreeCount = 24
- const contextCount = 128
- let runtimeEnumerations = 0
- let contextVisits = 0
- const runtimeRecords: Record = {}
-
- for (let index = 0; index < contextCount; index += 1) {
- const paneKey = makePaneKey(
- `tab-${index % worktreeCount}`,
- `33333333-3333-4333-8333-${index.toString(16).padStart(12, '0')}`
- )
- runtimeRecords[paneKey] = {
- taskId: `task-${index}`,
- dispatchId: `dispatch-${index}`,
- get parentPaneKey() {
- contextVisits += 1
- return undefined
- }
- }
- }
-
- const runtimeAgentOrchestrationByPaneKey = new Proxy(runtimeRecords, {
- ownKeys(target) {
- runtimeEnumerations += 1
- return Reflect.ownKeys(target)
- }
- })
- const worktrees = Array.from({ length: worktreeCount }, (_, index) =>
- worktree(`w${index}`, `wt-${index}`)
- )
- const tabsByWorktree = Object.fromEntries(
- worktrees.map((worktree, index) => [worktree.id, [tab(`tab-${index}`, worktree.id)]])
- )
-
- const snapshot = buildDashboardSnapshot(
- baseState({
- worktreesByRepo: { r1: worktrees },
- tabsByWorktree,
- runtimeAgentOrchestrationByPaneKey
- }),
- NOW
- )
-
- expect(snapshot.cards).toEqual([])
- expect(runtimeEnumerations).toBe(1)
- expect(contextVisits).toBe(contextCount)
- })
-})
diff --git a/src/renderer/src/components/dashboard/navigation/agent-destination-navigation.integration.test.tsx b/src/renderer/src/components/dashboard/navigation/agent-destination-navigation.integration.test.tsx
new file mode 100644
index 00000000000..2469b7e5bd7
--- /dev/null
+++ b/src/renderer/src/components/dashboard/navigation/agent-destination-navigation.integration.test.tsx
@@ -0,0 +1,163 @@
+import type * as RuntimeRpcCallModule from '@/runtime/rpc/calls/runtime-rpc-call'
+// @vitest-environment happy-dom
+
+// @vitest-environment happy-dom
+import { cleanup } from '@testing-library/react'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { useAppStore } from '@/store/index'
+
+import {
+ makeCreatedAgentWorktree,
+ seedAlreadyActiveWorktree
+} from '@/lib/workspace/activation/worktree-activation-created-agent.test-fixture'
+
+import { makePaneKey } from '../../../../../shared/workspace-layout/stable-pane-id'
+
+import type * as WebRuntimeSessionEnvironment from '@/runtime/web-session-tabs-sync/identity/web-runtime-session-environment'
+
+import { activateRetainedAgentRow } from '../../sidebar/workspace-card/activity/activate-retained-agent-row'
+import { activateWorktreeAgentPane } from '../../sidebar/workspace-card/activity/use-worktree-agent-activation'
+import { structuredAgentSessionPaneKey } from '../../../../../shared/agent-session/view/structured-agent-session-projection'
+
+const effects = vi.hoisted(() => ({
+ rpc: vi.fn(async () => ({ ok: true })),
+ composerFocus: vi.fn(),
+ environmentCall: vi.fn(async (_environmentId: string, _args: unknown) => ({
+ id: 'navigation-test',
+ ok: false as const,
+ error: { code: 'unavailable', message: 'Unit-test transport boundary' }
+ }))
+}))
+vi.mock('@/runtime/rpc/calls/runtime-rpc-call', async (importOriginal) => ({
+ ...(await importOriginal()),
+ callRuntimeRpc: effects.rpc
+}))
+vi.mock('@/components/native-chat/composer/focus/native-chat-composer-focus-intent', () => ({
+ requestNativeChatComposerFocus: effects.composerFocus
+}))
+vi.mock('../terminal-preview/AgentTerminalPreview', () => ({ AgentTerminalPreview: () => null }))
+vi.mock(
+ '@/runtime/web-session-tabs-sync/identity/web-runtime-session-environment',
+ async (importOriginal) => ({
+ ...(await importOriginal()),
+ captureRuntimeEnvironmentCall: (environmentId: string) => (args: unknown) =>
+ effects.environmentCall(environmentId, args)
+ })
+)
+
+const initialState = useAppStore.getState()
+const NOW = 1_000_000_000
+const LEAF = '11111111-1111-4111-8111-111111111111'
+const WORKSPACE_HOST = 'runtime:host-A' as const
+const TAB_HOST = 'runtime:host-B' as const
+
+afterEach(() => {
+ cleanup()
+ useAppStore.setState(initialState, true)
+ vi.clearAllMocks()
+ vi.restoreAllMocks()
+})
+
+describe('Dashboard and retained sidebar destination contract', () => {
+ it('notifies the exact live retained terminal owner instead of the workspace default', async () => {
+ const worktree = { ...makeCreatedAgentWorktree(), hostId: WORKSPACE_HOST }
+ seedAlreadyActiveWorktree(worktree, {
+ worktreesByRepo: { [worktree.repoId]: [worktree, { ...worktree, hostId: TAB_HOST }] },
+ activeWorkspaceExecutionHostId: WORKSPACE_HOST,
+ ptyIdsByTabId: { 'tab-1': ['remote:host-B@@pty-B'] },
+ terminalLayoutsByTabId: {
+ 'tab-1': {
+ root: { type: 'leaf', leafId: LEAF },
+ activeLeafId: LEAF,
+ expandedLeafId: null,
+ ptyIdsByLeafId: { [LEAF]: 'remote:host-B@@pty-B' }
+ }
+ }
+ })
+
+ activateRetainedAgentRow(worktree.id, 'tab-1', makePaneKey('tab-1', LEAF))
+ expect(useAppStore.getState().activeWorkspaceExecutionHostId).toBe(TAB_HOST)
+ await vi.waitFor(() =>
+ expect(effects.environmentCall).toHaveBeenCalledWith(
+ 'host-B',
+ expect.objectContaining({
+ method: 'session.tabs.activate',
+ params: expect.objectContaining({ leafId: LEAF })
+ })
+ )
+ )
+ expect(useAppStore.getState().getActiveTab(worktree.id)?.id).toBe('tab-1')
+ })
+})
+
+it('rejects a live terminal row on a different host before workspace activation', () => {
+ const worktree = { ...makeCreatedAgentWorktree(), hostId: WORKSPACE_HOST }
+ seedAlreadyActiveWorktree(worktree, {
+ activeWorktreeId: null,
+ activeWorkspaceExecutionHostId: null,
+ activeTabId: null,
+ ptyIdsByTabId: { 'tab-1': ['remote:host-B@@pty-B'] },
+ terminalLayoutsByTabId: {
+ 'tab-1': {
+ root: { type: 'leaf', leafId: LEAF },
+ activeLeafId: LEAF,
+ expandedLeafId: null,
+ ptyIdsByLeafId: { [LEAF]: 'remote:host-B@@pty-B' }
+ }
+ }
+ })
+
+ activateWorktreeAgentPane(worktree.id, 'tab-1', makePaneKey('tab-1', LEAF), WORKSPACE_HOST)
+
+ expect(useAppStore.getState()).toMatchObject({
+ activeWorktreeId: null,
+ activeWorkspaceExecutionHostId: null,
+ activeTabId: null
+ })
+})
+
+it.each([
+ { label: 'live', activate: activateWorktreeAgentPane },
+ { label: 'retained', activate: activateRetainedAgentRow }
+])('opens a $label paired chat from its physical SSH workspace', ({ activate }) => {
+ const worktree = {
+ ...makeCreatedAgentWorktree(),
+ hostId: 'ssh:server' as const,
+ runtimeOwnerEnvironmentId: 'host-B'
+ }
+ seedAlreadyActiveWorktree(worktree, {
+ activeWorktreeId: null,
+ activeWorkspaceExecutionHostId: null,
+ activeTabType: 'editor',
+ unifiedTabsByWorktree: {
+ [worktree.id]: [
+ {
+ id: 'native-B',
+ worktreeId: worktree.id,
+ executionHostId: TAB_HOST,
+ contentType: 'agent-session',
+ agentSessionAgent: 'claude',
+ entityId: 'session-B',
+ groupId: 'group-1',
+ label: 'Codex',
+ customLabel: null,
+ color: null,
+ sortOrder: 1,
+ createdAt: NOW
+ }
+ ]
+ }
+ })
+ activate(
+ worktree.id,
+ 'native-B',
+ structuredAgentSessionPaneKey('native-B', 'session-B'),
+ 'ssh:server'
+ )
+
+ expect(useAppStore.getState()).toMatchObject({
+ activeWorktreeId: worktree.id,
+ activeWorkspaceExecutionHostId: 'ssh:server',
+ activeTabType: 'agent-session'
+ })
+})
diff --git a/src/renderer/src/components/dashboard/reveal-dashboard-agent.test.ts b/src/renderer/src/components/dashboard/navigation/reveal-dashboard-agent.test.ts
similarity index 72%
rename from src/renderer/src/components/dashboard/reveal-dashboard-agent.test.ts
rename to src/renderer/src/components/dashboard/navigation/reveal-dashboard-agent.test.ts
index fad79235b6f..0f173a821d5 100644
--- a/src/renderer/src/components/dashboard/reveal-dashboard-agent.test.ts
+++ b/src/renderer/src/components/dashboard/navigation/reveal-dashboard-agent.test.ts
@@ -2,7 +2,7 @@
import { cleanup } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import type { Tab } from '../../../../shared/tab-types'
+import type { Tab } from '../../../../../shared/tab-types'
const mocks = vi.hoisted(() => ({
activateAndRevealWorkspace: vi.fn(),
@@ -21,7 +21,7 @@ const getState = () => ({
activateTab: mocks.activateTab,
setActiveTabType: mocks.setActiveTabType
})
-vi.mock('@/store', () => ({ useAppStore: { getState: () => getState() } }))
+vi.mock('@/store/index', () => ({ useAppStore: { getState: () => getState() } }))
vi.mock('@/store/index', () => ({ useAppStore: { getState: () => getState() } }))
vi.mock('@/lib/workspace/activation/worktree-activation', () => ({
activateAndRevealWorkspace: mocks.activateAndRevealWorkspace
@@ -45,7 +45,7 @@ vi.mock('@/runtime/rpc/targets/runtime-worktree-selector', () => ({
toRuntimeWorktreeSelector: (id: string) => `id:${id}`
}))
-vi.mock('../dashboard-popout/AgentTerminalPreview', () => ({ AgentTerminalPreview: () => null }))
+vi.mock('../terminal-preview/AgentTerminalPreview', () => ({ AgentTerminalPreview: () => null }))
import { revealDashboardAgent } from './reveal-dashboard-agent'
@@ -82,21 +82,8 @@ describe('revealDashboardAgent', () => {
mocks.activateAndRevealWorkspace.mockReturnValue(true)
})
- it('does not fall back to a terminal when the current native owner has no runtime', () => {
+ it('refuses a native owner without a runtime', () => {
mocks.tabs = [{ ...nativeTab('codex'), executionHostId: 'ssh:host-C' }]
expect(revealDashboardAgent(args)).toBe(false)
- expect(mocks.activateAndRevealWorkspace).not.toHaveBeenCalled()
- expect(mocks.activateTerminalTabOnOwner).not.toHaveBeenCalled()
- expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
- })
-
- it.each([false, true])('touches no tab when workspace reveal fails (native=%s)', (native) => {
- mocks.tabs = native ? [nativeTab('codex')] : []
- mocks.activateAndRevealWorkspace.mockReturnValue(false)
- expect(revealDashboardAgent(args)).toBe(false)
- expect(mocks.activateTerminalTabOnOwner).not.toHaveBeenCalled()
- expect(mocks.activateTabAndFocusPane).not.toHaveBeenCalled()
- expect(mocks.activateTab).not.toHaveBeenCalled()
- expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
})
})
diff --git a/src/renderer/src/components/dashboard/reveal-dashboard-agent.ts b/src/renderer/src/components/dashboard/navigation/reveal-dashboard-agent.ts
similarity index 91%
rename from src/renderer/src/components/dashboard/reveal-dashboard-agent.ts
rename to src/renderer/src/components/dashboard/navigation/reveal-dashboard-agent.ts
index fbd66384f8a..b3aeb9e963f 100644
--- a/src/renderer/src/components/dashboard/reveal-dashboard-agent.ts
+++ b/src/renderer/src/components/dashboard/navigation/reveal-dashboard-agent.ts
@@ -5,8 +5,8 @@ import {
structuredAgentSessionOwnerForTab,
structuredAgentSessionTargetForTab
} from '@/runtime/native-chat/session-ownership/structured-agent-session-owner'
-import { useAppStore } from '@/store'
-import type { DashboardRevealAgentArgs } from '../../../../shared/dashboard-snapshot'
+import { useAppStore } from '@/store/index'
+import type { DashboardRevealAgentArgs } from '../../../../../shared/dashboard-snapshot'
export function revealDashboardAgent(args: DashboardRevealAgentArgs): boolean {
const state = useAppStore.getState()
diff --git a/src/renderer/src/components/dashboard/DashboardPopoutBridge.tsx b/src/renderer/src/components/dashboard/popout-bridge/DashboardPopoutBridge.tsx
similarity index 100%
rename from src/renderer/src/components/dashboard/DashboardPopoutBridge.tsx
rename to src/renderer/src/components/dashboard/popout-bridge/DashboardPopoutBridge.tsx
diff --git a/src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts b/src/renderer/src/components/dashboard/popout-bridge/useDashboardPopoutBridge.ts
similarity index 95%
rename from src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts
rename to src/renderer/src/components/dashboard/popout-bridge/useDashboardPopoutBridge.ts
index 1bd53b4ff5a..856485c196b 100644
--- a/src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts
+++ b/src/renderer/src/components/dashboard/popout-bridge/useDashboardPopoutBridge.ts
@@ -1,14 +1,17 @@
import { useEffect } from 'react'
-import { useAppStore } from '@/store'
+import { useAppStore } from '@/store/index'
import type { AppState } from '@/store/types'
import {
subscribeStructuredChatTabNames,
getStructuredChatTabNames
} from '@/runtime/native-chat/naming/structured-chat-name-observation'
-import { revealDashboardAgent } from './reveal-dashboard-agent'
-import type { RepoIcon } from '../../../../shared/repo-icon'
-import { buildDashboardSnapshot, type DashboardSnapshotState } from './build-dashboard-snapshot'
-import { createWorktreeAgentRowsCache } from './worktree-agent-rows-cache'
+import { revealDashboardAgent } from '../navigation/reveal-dashboard-agent'
+import type { RepoIcon } from '../../../../../shared/repo-icon'
+import {
+ buildDashboardSnapshot,
+ type DashboardSnapshotState
+} from '../snapshot/build-dashboard-snapshot'
+import { createWorktreeAgentRowsCache } from '../rows/worktree-agent-rows-cache'
import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields'
// Throttle cross-worktree snapshot rebuilds to 4Hz during agent-status bursts.
diff --git a/src/renderer/src/components/dashboard/RetainedAgentsSyncGate.tsx b/src/renderer/src/components/dashboard/retention/RetainedAgentsSyncGate.tsx
similarity index 100%
rename from src/renderer/src/components/dashboard/RetainedAgentsSyncGate.tsx
rename to src/renderer/src/components/dashboard/retention/RetainedAgentsSyncGate.tsx
diff --git a/src/renderer/src/components/dashboard/retention/useRetainedAgents.test.ts b/src/renderer/src/components/dashboard/retention/useRetainedAgents.test.ts
new file mode 100644
index 00000000000..045f39e4758
--- /dev/null
+++ b/src/renderer/src/components/dashboard/retention/useRetainedAgents.test.ts
@@ -0,0 +1,230 @@
+// @vitest-environment happy-dom
+import { act, renderHook } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+
+import { useAppStore } from '@/store/index'
+import { resetAgentPaneAuthorityAliasesForTests } from '@/store/slices/agent-status/agent-pane-authority'
+import type {
+ AgentStatusEntry,
+ AgentStatusState
+} from '../../../../../shared/agent-status/contracts/agent-status-types'
+import { makePaneKey } from '../../../../../shared/workspace-layout/stable-pane-id'
+
+import type { Repo } from '../../../../../shared/repo-types'
+import type { TerminalTab } from '../../../../../shared/workspace-layout/terminal-tab-types'
+import type { Worktree } from '../../../../../shared/worktree/types'
+import { useRetainedAgentsSync } from './useRetainedAgents'
+
+const initialAppState = useAppStore.getInitialState()
+
+beforeEach(() => {
+ useAppStore.setState(initialAppState, true)
+ resetAgentPaneAuthorityAliasesForTests()
+})
+
+afterEach(() => {
+ useAppStore.setState(initialAppState, true)
+ resetAgentPaneAuthorityAliasesForTests()
+})
+
+function makeRepo(): Repo {
+ return {
+ id: 'repo-1',
+ path: '/repo',
+ displayName: 'Repo',
+ badgeColor: '#000',
+ addedAt: 1
+ }
+}
+
+function makeWorktree(overrides?: Partial): Worktree {
+ return {
+ id: 'wt-1',
+ repoId: 'repo-1',
+ path: '/repo/wt-1',
+ head: 'abc123',
+ branch: 'feature',
+ isBare: false,
+ isMainWorktree: false,
+ displayName: 'feature',
+ comment: '',
+ linkedIssue: null,
+ linkedPR: null,
+ linkedLinearIssue: null,
+ isArchived: false,
+ isUnread: false,
+ isPinned: false,
+ sortOrder: 0,
+ lastActivityAt: 1,
+ ...overrides
+ }
+}
+
+function makeTab(overrides: Partial & { id: string }): TerminalTab {
+ return {
+ worktreeId: 'wt-1',
+ title: 'Terminal',
+ ptyId: null,
+ customTitle: null,
+ color: null,
+ sortOrder: 0,
+ createdAt: 1,
+ ...overrides
+ }
+}
+
+function makeAgentRow(args: {
+ paneKey: string
+ state: AgentStatusState
+ interrupted?: boolean
+ mainAgent?: AgentStatusEntry['mainAgent']
+}) {
+ const entry: AgentStatusEntry = {
+ state: args.state,
+ prompt: 'Fix it',
+ updatedAt: 100,
+ stateStartedAt: 100,
+ paneKey: args.paneKey,
+ terminalTitle: 'Claude',
+ stateHistory: [],
+ agentType: 'claude',
+ interrupted: args.interrupted,
+ mainAgent: args.mainAgent
+ }
+
+ return {
+ paneKey: args.paneKey,
+ entry,
+ tab: makeTab({ id: 'tab-1' }),
+ agentType: 'claude' as const,
+ state: args.state,
+ startedAt: 1
+ }
+}
+
+describe('useRetainedAgentsSync', () => {
+ it('uses the freshest tab title when a real agent transition retains a completed row', async () => {
+ const repo = makeRepo()
+ const worktree = makeWorktree()
+ const paneKey = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111')
+ const row = makeAgentRow({ paneKey, state: 'done' })
+ useAppStore.setState({
+ repos: [repo],
+ worktreesByRepo: { [repo.id]: [worktree] },
+ tabsByWorktree: {
+ [worktree.id]: [{ ...row.tab, title: '⠋ Codex is thinking' }]
+ },
+ agentStatusByPaneKey: { [paneKey]: row.entry },
+ agentStatusEpoch: initialAppState.agentStatusEpoch + 1
+ })
+ const hook = renderHook(() => useRetainedAgentsSync())
+ await act(async () => {
+ await Promise.resolve()
+ })
+
+ act(() => {
+ useAppStore.setState((state) => ({
+ tabsByWorktree: {
+ [worktree.id]: [
+ {
+ ...state.tabsByWorktree[worktree.id][0],
+ title: '⠙ Codex is thinking'
+ }
+ ]
+ }
+ }))
+ })
+ act(() => {
+ useAppStore.setState((state) => ({
+ agentStatusByPaneKey: {},
+ agentStatusEpoch: state.agentStatusEpoch + 1
+ }))
+ })
+ await act(async () => {
+ await Promise.resolve()
+ })
+
+ expect(useAppStore.getState().retainedAgentsByPaneKey[paneKey]?.tab.title).toBe(
+ '⠙ Codex is thinking'
+ )
+ hook.unmount()
+ })
+
+ it('does not re-retain when status removal and tab closure commit before the next retention effect', async () => {
+ const repo = makeRepo()
+ const worktree = makeWorktree()
+ const paneKey = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111')
+ const row = makeAgentRow({ paneKey, state: 'done' })
+ useAppStore.setState({
+ repos: [repo],
+ worktreesByRepo: { [repo.id]: [worktree] },
+ tabsByWorktree: { [worktree.id]: [row.tab] },
+ agentStatusByPaneKey: { [row.paneKey]: row.entry },
+ agentStatusEpoch: initialAppState.agentStatusEpoch + 1
+ })
+ const hook = renderHook(() => useRetainedAgentsSync())
+ await act(async () => {
+ await Promise.resolve()
+ })
+
+ // Why: model both teardown writes landing before the next retention effect runs.
+ act(() => {
+ useAppStore.setState((state) => ({
+ tabsByWorktree: { [worktree.id]: [] },
+ agentStatusByPaneKey: {},
+ recentlyClosedAgentStatusTabIds: { [row.tab.id]: true },
+ agentStatusEpoch: state.agentStatusEpoch + 1
+ }))
+ })
+
+ const state = useAppStore.getState()
+ expect(state.retainedAgentsByPaneKey[row.paneKey]).toBeUndefined()
+ hook.unmount()
+ })
+
+ it('does not leave a ghost row when a done pane detaches into another tab', async () => {
+ const repo = makeRepo()
+ const worktree = makeWorktree()
+ const leafId = '11111111-1111-4111-8111-111111111111'
+ const sourceTab = makeTab({ id: 'tab-source', ptyId: 'pty-a' })
+ const targetTab = makeTab({ id: 'tab-target' })
+ const sourcePaneKey = makePaneKey(sourceTab.id, leafId)
+ const targetPaneKey = makePaneKey(targetTab.id, leafId)
+ const row = makeAgentRow({ paneKey: sourcePaneKey, state: 'done' })
+ useAppStore.setState({
+ repos: [repo],
+ worktreesByRepo: { [repo.id]: [worktree] },
+ tabsByWorktree: { [worktree.id]: [sourceTab, targetTab] },
+ ptyIdsByTabId: { [sourceTab.id]: ['pty-a'], [targetTab.id]: [] },
+ agentStatusByPaneKey: { [sourcePaneKey]: { ...row.entry, tabId: sourceTab.id } },
+ agentStatusEpoch: initialAppState.agentStatusEpoch + 1
+ })
+ const hook = renderHook(() => useRetainedAgentsSync())
+ await act(async () => {
+ await Promise.resolve()
+ })
+
+ act(() => {
+ useAppStore.getState().syncPaneDetachPtyOwnership({
+ detachedLeafId: leafId,
+ detachedPtyId: 'pty-a',
+ sourceLayout: {
+ root: null,
+ activeLeafId: null,
+ expandedLeafId: null,
+ ptyIdsByLeafId: {}
+ },
+ sourceTabId: sourceTab.id,
+ targetTabId: targetTab.id
+ })
+ })
+ await act(async () => {
+ await Promise.resolve()
+ })
+
+ const state = useAppStore.getState()
+ expect(state.retainedAgentsByPaneKey[sourcePaneKey]).toBeUndefined()
+ expect(state.retainedAgentsByPaneKey[targetPaneKey]).toBeUndefined()
+ hook.unmount()
+ })
+})
diff --git a/src/renderer/src/components/dashboard/useRetainedAgents.ts b/src/renderer/src/components/dashboard/retention/useRetainedAgents.ts
similarity index 91%
rename from src/renderer/src/components/dashboard/useRetainedAgents.ts
rename to src/renderer/src/components/dashboard/retention/useRetainedAgents.ts
index fa75f806fa6..e3164cd6548 100644
--- a/src/renderer/src/components/dashboard/useRetainedAgents.ts
+++ b/src/renderer/src/components/dashboard/retention/useRetainedAgents.ts
@@ -1,23 +1,23 @@
-import { agentEntrySessionStartedAt } from '../../../../shared/agent-session/lifecycle/agent-session-start-time'
+import { agentEntrySessionStartedAt } from '../../../../../shared/agent-session/lifecycle/agent-session-start-time'
import { useEffect, useRef } from 'react'
import { useShallow } from 'zustand/react/shallow'
-import { useAppStore } from '@/store'
+import { useAppStore } from '@/store/index'
import {
isAgentStatusFresh,
AGENT_STATUS_STALE_AFTER_MS
-} from '../../../../shared/agent-status/contracts/agent-status-freshness'
-import type { DashboardAgentRow } from './useDashboardData'
+} from '../../../../../shared/agent-status/contracts/agent-status-freshness'
+import type { DashboardAgentRow } from '../rows/dashboard-agent-row'
import type { RetainedAgentEntry } from '@/store/slices/agent-status/agent-status-contract'
-import type { FolderWorkspace } from '../../../../shared/folder-workspaces/folder-workspace-types'
-import type { Repo } from '../../../../shared/repo-types'
-import type { TerminalTab } from '../../../../shared/workspace-layout/terminal-tab-types'
-import type { Worktree } from '../../../../shared/worktree/types'
-import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
+import type { FolderWorkspace } from '../../../../../shared/folder-workspaces/folder-workspace-types'
+import type { Repo } from '../../../../../shared/repo-types'
+import type { TerminalTab } from '../../../../../shared/workspace-layout/terminal-tab-types'
+import type { Worktree } from '../../../../../shared/worktree/types'
+import { folderWorkspaceKey } from '../../../../../shared/workspace-scope'
import { resolveAgentPaneAuthorityKey } from '@/store/slices/agent-status/agent-pane-authority'
-import type { AgentStatusEntry } from '../../../../shared/agent-status/contracts/agent-status-types'
-import { parsePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
-import { agentTurnEndedOnPurpose } from '../../../../shared/agent-status/lifecycle/agent-main-agent-verdict'
+import type { AgentStatusEntry } from '../../../../../shared/agent-status/contracts/agent-status-types'
+import { parsePaneKey } from '../../../../../shared/workspace-layout/stable-pane-id'
+import { agentTurnEndedOnPurpose } from '../../../../../shared/agent-status/lifecycle/agent-main-agent-verdict'
import {
createWorktreeTabBucketProjection,
diff --git a/src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.tsx b/src/renderer/src/components/dashboard/rows/DashboardAgentChildDisclosure.tsx
similarity index 100%
rename from src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.tsx
rename to src/renderer/src/components/dashboard/rows/DashboardAgentChildDisclosure.tsx
diff --git a/src/renderer/src/components/dashboard/DashboardAgentRowMessage.tsx b/src/renderer/src/components/dashboard/rows/DashboardAgentRowMessage.tsx
similarity index 100%
rename from src/renderer/src/components/dashboard/DashboardAgentRowMessage.tsx
rename to src/renderer/src/components/dashboard/rows/DashboardAgentRowMessage.tsx
diff --git a/src/renderer/src/components/dashboard/DashboardAgentRowToolStep.tsx b/src/renderer/src/components/dashboard/rows/DashboardAgentRowToolStep.tsx
similarity index 100%
rename from src/renderer/src/components/dashboard/DashboardAgentRowToolStep.tsx
rename to src/renderer/src/components/dashboard/rows/DashboardAgentRowToolStep.tsx
diff --git a/src/renderer/src/components/dashboard/DashboardAgentRowTrailingControls.tsx b/src/renderer/src/components/dashboard/rows/DashboardAgentRowTrailingControls.tsx
similarity index 100%
rename from src/renderer/src/components/dashboard/DashboardAgentRowTrailingControls.tsx
rename to src/renderer/src/components/dashboard/rows/DashboardAgentRowTrailingControls.tsx
diff --git a/src/renderer/src/components/dashboard/agent-finished-timestamp.ts b/src/renderer/src/components/dashboard/rows/agent-finished-timestamp.ts
similarity index 84%
rename from src/renderer/src/components/dashboard/agent-finished-timestamp.ts
rename to src/renderer/src/components/dashboard/rows/agent-finished-timestamp.ts
index 40e07d55d99..f54244598f4 100644
--- a/src/renderer/src/components/dashboard/agent-finished-timestamp.ts
+++ b/src/renderer/src/components/dashboard/rows/agent-finished-timestamp.ts
@@ -1,9 +1,9 @@
-import { agentEntryCompletionAt } from '../../../../shared/agent-status/lifecycle/agent-completion-time'
+import { agentEntryCompletionAt } from '../../../../../shared/agent-status/lifecycle/agent-completion-time'
import {
agentTurnEndedOnPurpose,
agentVerdictDisplayMark
-} from '../../../../shared/agent-status/lifecycle/agent-main-agent-verdict'
-import type { DashboardAgentRow } from './useDashboardData'
+} from '../../../../../shared/agent-status/lifecycle/agent-main-agent-verdict'
+import type { DashboardAgentRow } from './dashboard-agent-row'
/** Share completion age across sidebar and dashboard, including stopped/failed main turns and host-settled child rows. */
export function lastEnteredDoneAt(
diff --git a/src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts b/src/renderer/src/components/dashboard/rows/agent-row-lineage-model.test.ts
similarity index 83%
rename from src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts
rename to src/renderer/src/components/dashboard/rows/agent-row-lineage-model.test.ts
index 75d2e5688dc..d9e616943fc 100644
--- a/src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts
+++ b/src/renderer/src/components/dashboard/rows/agent-row-lineage-model.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
-import type { AgentStatusEntry } from '../../../../shared/agent-status/contracts/agent-status-types'
-import type { TerminalTab } from '../../../../shared/workspace-layout/terminal-tab-types'
-import type { DashboardAgentRow } from './useDashboardData'
+import type { AgentStatusEntry } from '../../../../../shared/agent-status/contracts/agent-status-types'
+import type { TerminalTab } from '../../../../../shared/workspace-layout/terminal-tab-types'
+import type { DashboardAgentRow } from './dashboard-agent-row'
import { buildAgentRowLineageTree } from './agent-row-lineage-model'
function makeTab(id: string): TerminalTab {
@@ -73,16 +73,6 @@ describe('buildAgentRowLineageTree', () => {
expect(tree.childPaneKeys.size).toBe(0)
})
- it('does not use the call stack for a long parent chain', () => {
- const rows = Array.from({ length: 10_000 }, (_, index) =>
- makeRow(`pane-${index}`, index > 0 ? { parentPaneKey: `pane-${index - 1}` } : {})
- )
- const tree = buildAgentRowLineageTree(rows)
- expect(tree.rootRows).toEqual([rows[0]])
- expect(tree.childPaneKeys.size).toBe(rows.length - 1)
- expect(tree.childrenByParentPaneKey.get('pane-9998')?.[0]).toBe(rows[9999])
- })
-
it('terminates a reachable cycle introduced by duplicate pane rows', () => {
const root = makeRow('root')
const first = makeRow('first', { parentPaneKey: 'root' })
diff --git a/src/renderer/src/components/dashboard/agent-row-lineage-model.ts b/src/renderer/src/components/dashboard/rows/agent-row-lineage-model.ts
similarity index 97%
rename from src/renderer/src/components/dashboard/agent-row-lineage-model.ts
rename to src/renderer/src/components/dashboard/rows/agent-row-lineage-model.ts
index d450ea47f11..8a6de14e527 100644
--- a/src/renderer/src/components/dashboard/agent-row-lineage-model.ts
+++ b/src/renderer/src/components/dashboard/rows/agent-row-lineage-model.ts
@@ -1,4 +1,4 @@
-import type { AgentStatusEntry } from '../../../../shared/agent-status/contracts/agent-status-types'
+import type { AgentStatusEntry } from '../../../../../shared/agent-status/contracts/agent-status-types'
/** Minimal lineage input for real dashboard rows and synthetic thread-list rows. */
export type AgentLineageSourceRow = {
diff --git a/src/renderer/src/components/dashboard/agent-row-lineage.ts b/src/renderer/src/components/dashboard/rows/agent-row-lineage.ts
similarity index 97%
rename from src/renderer/src/components/dashboard/agent-row-lineage.ts
rename to src/renderer/src/components/dashboard/rows/agent-row-lineage.ts
index 3d3ac4d9213..eed4d3ad865 100644
--- a/src/renderer/src/components/dashboard/agent-row-lineage.ts
+++ b/src/renderer/src/components/dashboard/rows/agent-row-lineage.ts
@@ -1,4 +1,4 @@
-import type { DashboardAgentRow } from './useDashboardData'
+import type { DashboardAgentRow } from './dashboard-agent-row'
import { buildAgentRowLineageTree } from './agent-row-lineage-model'
export type AgentRowLineagePresentation = {
diff --git a/src/renderer/src/components/dashboard/agent-row-pane-live-title.ts b/src/renderer/src/components/dashboard/rows/agent-row-pane-live-title.ts
similarity index 72%
rename from src/renderer/src/components/dashboard/agent-row-pane-live-title.ts
rename to src/renderer/src/components/dashboard/rows/agent-row-pane-live-title.ts
index 2beaa66b1e1..b9f60a5966d 100644
--- a/src/renderer/src/components/dashboard/agent-row-pane-live-title.ts
+++ b/src/renderer/src/components/dashboard/rows/agent-row-pane-live-title.ts
@@ -1,5 +1,5 @@
-import { resolveRuntimePaneTitleForLeaf } from '../../../../shared/runtime-pane-title-leaf-id'
-import type { TerminalLayoutSnapshot } from '../../../../shared/workspace-layout/terminal-tab-types'
+import { resolveRuntimePaneTitleForLeaf } from '../../../../../shared/runtime-pane-title-leaf-id'
+import type { TerminalLayoutSnapshot } from '../../../../../shared/workspace-layout/terminal-tab-types'
/** Undefined preserves single-pane tab-title fallback; null suppresses unattributable split titles so siblings cannot inherit the focused pane’s name. */
export function resolveAgentRowPaneLiveTitle(
diff --git a/src/renderer/src/components/dashboard/useDashboardData.ts b/src/renderer/src/components/dashboard/rows/dashboard-agent-row.ts
similarity index 68%
rename from src/renderer/src/components/dashboard/useDashboardData.ts
rename to src/renderer/src/components/dashboard/rows/dashboard-agent-row.ts
index 2b07e1d5e90..8905352e711 100644
--- a/src/renderer/src/components/dashboard/useDashboardData.ts
+++ b/src/renderer/src/components/dashboard/rows/dashboard-agent-row.ts
@@ -1,10 +1,10 @@
import type {
AgentStatusEntry,
AgentType
-} from '../../../../shared/agent-status/contracts/agent-status-types'
-import type { AgentRowState } from '../../../../shared/agent-status/presentation/agent-row-decay-state'
-import type { TerminalTab } from '../../../../shared/workspace-layout/terminal-tab-types'
-import type { AgentChildRowModel } from '../../../../shared/agent-status/presentation/agent-child-row-model'
+} from '../../../../../shared/agent-status/contracts/agent-status-types'
+import type { AgentRowState } from '../../../../../shared/agent-status/presentation/agent-row-decay-state'
+import type { TerminalTab } from '../../../../../shared/workspace-layout/terminal-tab-types'
+import type { AgentChildRowModel } from '../../../../../shared/agent-status/presentation/agent-child-row-model'
export type DashboardAgentRow = {
/** Synthetic subagent keys identify rows only; they have no PTY and must never be parsed as pane keys. */
diff --git a/src/renderer/src/components/dashboard/dashboard-row-bucket.ts b/src/renderer/src/components/dashboard/rows/dashboard-row-bucket.ts
similarity index 87%
rename from src/renderer/src/components/dashboard/dashboard-row-bucket.ts
rename to src/renderer/src/components/dashboard/rows/dashboard-row-bucket.ts
index e9e67668441..d226681d4f4 100644
--- a/src/renderer/src/components/dashboard/dashboard-row-bucket.ts
+++ b/src/renderer/src/components/dashboard/rows/dashboard-row-bucket.ts
@@ -1,11 +1,11 @@
-import type { DashboardAgentRow } from './useDashboardData'
+import type { DashboardAgentRow } from './dashboard-agent-row'
import {
dashboardCardBaseDisplayState,
type DashboardBucket,
type DashboardCardDotState
-} from '../../../../shared/dashboard-snapshot'
-import { dashboardBucketForDotState } from './dashboard-card-bucket'
-import type { AgentRowState } from '../../../../shared/agent-status/presentation/agent-row-decay-state'
+} from '../../../../../shared/dashboard-snapshot'
+import { dashboardBucketForDotState } from '../snapshot/dashboard-card-bucket'
+import type { AgentRowState } from '../../../../../shared/agent-status/presentation/agent-row-decay-state'
import {
isAgentSubjectUnread,
type AgentAttentionTurnRecords
diff --git a/src/renderer/src/components/dashboard/use-agent-row-conversation-name.ts b/src/renderer/src/components/dashboard/rows/use-agent-row-conversation-name.ts
similarity index 89%
rename from src/renderer/src/components/dashboard/use-agent-row-conversation-name.ts
rename to src/renderer/src/components/dashboard/rows/use-agent-row-conversation-name.ts
index 1fa9c15b620..76ea9d91563 100644
--- a/src/renderer/src/components/dashboard/use-agent-row-conversation-name.ts
+++ b/src/renderer/src/components/dashboard/rows/use-agent-row-conversation-name.ts
@@ -1,12 +1,12 @@
import { useAppStateValue } from '@/store/retained-views/app-state-view'
-import { getAgentRowConversationName } from '../../../../shared/agent-status/presentation/agent-row-conversation-name'
-import { parsePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
-import { structuredChatDisplayName } from '../../../../shared/structured-chat-row-name'
+import { getAgentRowConversationName } from '../../../../../shared/agent-status/presentation/agent-row-conversation-name'
+import { parsePaneKey } from '../../../../../shared/workspace-layout/stable-pane-id'
+import { structuredChatDisplayName } from '../../../../../shared/structured-chat-row-name'
import { resolveAgentRowPaneLiveTitle } from './agent-row-pane-live-title'
import { structuredChatTabById } from '@/lib/agent-session/tab-identity/structured-chat-tab-index'
import { useStructuredChatTabConversationName } from '@/runtime/native-chat/naming/structured-conversation-name'
import type { AppState } from '@/store/types'
-import type { DashboardAgentRow } from './useDashboardData'
+import type { DashboardAgentRow } from './dashboard-agent-row'
type WorktreeTabs = NonNullable
diff --git a/src/renderer/src/components/dashboard/use-claude-agent-row-view.ts b/src/renderer/src/components/dashboard/rows/use-claude-agent-row-view.ts
similarity index 95%
rename from src/renderer/src/components/dashboard/use-claude-agent-row-view.ts
rename to src/renderer/src/components/dashboard/rows/use-claude-agent-row-view.ts
index 239525d0a38..7eaa195aae9 100644
--- a/src/renderer/src/components/dashboard/use-claude-agent-row-view.ts
+++ b/src/renderer/src/components/dashboard/rows/use-claude-agent-row-view.ts
@@ -1,7 +1,7 @@
import { useCallback, useSyncExternalStore } from 'react'
-import type { DashboardAgentRow } from './useDashboardData'
+import type { DashboardAgentRow } from './dashboard-agent-row'
import { useFocusedAgentPaneKey } from '@/components/sidebar/workspace-card/activity/focused-agent-row-highlight'
-import { useAppStore } from '@/store'
+import { useAppStore } from '@/store/index'
import { runtimeEnvironmentIdOrLocalUnlessTwin } from '@/lib/workspace/ownership/worktree-runtime-owner'
import { runtimeTargetForOwnerEnvironment } from '@/runtime/rpc/targets/runtime-client-target'
import {
diff --git a/src/renderer/src/components/dashboard/worktree-agent-rows-cache.ts b/src/renderer/src/components/dashboard/rows/worktree-agent-rows-cache.ts
similarity index 92%
rename from src/renderer/src/components/dashboard/worktree-agent-rows-cache.ts
rename to src/renderer/src/components/dashboard/rows/worktree-agent-rows-cache.ts
index 26e33938c13..75ba7a2fcf2 100644
--- a/src/renderer/src/components/dashboard/worktree-agent-rows-cache.ts
+++ b/src/renderer/src/components/dashboard/rows/worktree-agent-rows-cache.ts
@@ -2,28 +2,28 @@ import { getStructuredAgentSessionTabsByPaneKey } from '@/components/native-chat
import type {
AgentStatusEntry,
AgentStatusOrchestrationContext
-} from '../../../../shared/agent-status/contracts/agent-status-types'
-import type { MigrationUnsupportedPtyEntry } from '../../../../shared/agent-status/contracts/agent-status-ipc-payload'
+} from '../../../../../shared/agent-status/contracts/agent-status-types'
+import type { MigrationUnsupportedPtyEntry } from '../../../../../shared/agent-status/contracts/agent-status-ipc-payload'
import type {
TerminalLayoutSnapshot,
TerminalTab
-} from '../../../../shared/workspace-layout/terminal-tab-types'
-import { migrationUnsupportedToAgentStatusEntry } from '../../../../shared/migration-unsupported-agent-entry'
+} from '../../../../../shared/workspace-layout/terminal-tab-types'
+import { migrationUnsupportedToAgentStatusEntry } from '../../../../../shared/migration-unsupported-agent-entry'
import type { RetainedAgentEntry } from '@/store/slices/agent-status/agent-status-contract'
import type { AppState } from '@/store/types'
import { applyAgentRowLineage, type DashboardAgentRowWithLineage } from './agent-row-lineage'
-import { buildWorktreeAgentRows } from '../sidebar/workspace-card/activity/worktree-agent-rows'
+import { buildWorktreeAgentRows } from '../../sidebar/workspace-card/activity/worktree-agent-rows'
import {
selectLiveAgentStatusEntriesForWorktree,
selectMigrationUnsupportedEntriesForWorktree,
selectRetainedAgentEntriesForWorktree,
selectTerminalLayoutsForWorktree
-} from '../sidebar/workspace-card/activity/worktree-agent-row-selectors'
+} from '../../sidebar/workspace-card/activity/worktree-agent-row-selectors'
import {
selectLivePtyIdsForWorktree,
selectPaneForegroundAgentsForWorktree,
selectRuntimePaneTitlesForWorktree
-} from '../sidebar/workspace-card/activity/worktree-card-status-inputs'
+} from '../../sidebar/workspace-card/activity/worktree-card-status-inputs'
import type { PaneForegroundAgentEntry } from '@/store/slices/agent-status/pane-foreground-agent'
type WorktreeAgentRowsCacheEntry = {
diff --git a/src/renderer/src/components/dashboard/AgentDashboardExperimentalSetting.tsx b/src/renderer/src/components/dashboard/settings/AgentDashboardExperimentalSetting.tsx
similarity index 90%
rename from src/renderer/src/components/dashboard/AgentDashboardExperimentalSetting.tsx
rename to src/renderer/src/components/dashboard/settings/AgentDashboardExperimentalSetting.tsx
index 4f5f74314ae..d866fbe3d45 100644
--- a/src/renderer/src/components/dashboard/AgentDashboardExperimentalSetting.tsx
+++ b/src/renderer/src/components/dashboard/settings/AgentDashboardExperimentalSetting.tsx
@@ -1,10 +1,10 @@
-import type { GlobalSettings } from '../../../../shared/global-settings-types'
+import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { useTranslate } from '@/i18n/use-translate'
-import { Label } from '../ui/label'
-import { SearchableSetting } from '../settings/section-layout/SearchableSetting'
-import { SettingsSegmentedControl } from '../settings/form-controls/SettingsFormControls'
+import { Label } from '../../ui/label'
+import { SearchableSetting } from '../../settings/section-layout/SearchableSetting'
+import { SettingsSegmentedControl } from '../../settings/form-controls/SettingsFormControls'
import { Switch } from '@/components/ui/switch'
-import { getExperimentalSearchEntry } from '../settings/advanced/experimental-search'
+import { getExperimentalSearchEntry } from '../../settings/advanced/experimental-search'
import { canOpenAgentDashboardPopout } from './agent-dashboard-open-mode'
type AgentDashboardExperimentalSettingProps = {
diff --git a/src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.tsx b/src/renderer/src/components/dashboard/settings/AgentDashboardSettingsMenu.tsx
similarity index 96%
rename from src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.tsx
rename to src/renderer/src/components/dashboard/settings/AgentDashboardSettingsMenu.tsx
index 8057893c52d..ba523f2449e 100644
--- a/src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.tsx
+++ b/src/renderer/src/components/dashboard/settings/AgentDashboardSettingsMenu.tsx
@@ -1,5 +1,5 @@
import { Settings } from 'lucide-react'
-import { useAppStore } from '@/store'
+import { useAppStore } from '@/store/index'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
@@ -8,9 +8,9 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
-import { SettingsSegmentedControl } from '../settings/form-controls/SettingsFormControls'
+import { SettingsSegmentedControl } from '../../settings/form-controls/SettingsFormControls'
import { Switch } from '@/components/ui/switch'
-import type { AgentDashboardMode } from '../../../../shared/ui-chrome-types'
+import type { AgentDashboardMode } from '../../../../../shared/ui-chrome-types'
import { useTranslate } from '@/i18n/use-translate'
import { canOpenAgentDashboardPopout } from './agent-dashboard-open-mode'
diff --git a/src/renderer/src/components/dashboard/agent-dashboard-open-mode.test.ts b/src/renderer/src/components/dashboard/settings/agent-dashboard-open-mode.test.ts
similarity index 64%
rename from src/renderer/src/components/dashboard/agent-dashboard-open-mode.test.ts
rename to src/renderer/src/components/dashboard/settings/agent-dashboard-open-mode.test.ts
index 2c16cf7fe3e..2e9b1f89a5a 100644
--- a/src/renderer/src/components/dashboard/agent-dashboard-open-mode.test.ts
+++ b/src/renderer/src/components/dashboard/settings/agent-dashboard-open-mode.test.ts
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
-import { toggleAgentDashboardFromShortcut } from '../../hooks/ipc-events/agent-dashboard-command'
+import type { toggleAgentDashboardFromShortcut } from '../../../hooks/ipc-events/agent-dashboard-command'
import { resolveAgentDashboardMode } from './agent-dashboard-open-mode'
function dashboardState(): Parameters[0] {
@@ -18,13 +18,9 @@ function dashboardState(): Parameters[0
describe('Agent Dashboard open mode', () => {
afterEach(() => vi.unstubAllGlobals())
- it('opens the in-window board in a web client even when the setting says pop-out', () => {
+ it('resolves a web client to in-window despite a pop-out setting', () => {
vi.stubGlobal('__ORCA_WEB_CLIENT__', true)
const state = dashboardState()
- const openPopout = vi.fn()
expect(resolveAgentDashboardMode(state.settings)).toBe('in-window')
- toggleAgentDashboardFromShortcut(state, openPopout)
- expect(openPopout).not.toHaveBeenCalled()
- expect(state.setAgentDashboardDrawerOpen).toHaveBeenCalledWith(true)
})
})
diff --git a/src/renderer/src/components/dashboard/agent-dashboard-open-mode.ts b/src/renderer/src/components/dashboard/settings/agent-dashboard-open-mode.ts
similarity index 77%
rename from src/renderer/src/components/dashboard/agent-dashboard-open-mode.ts
rename to src/renderer/src/components/dashboard/settings/agent-dashboard-open-mode.ts
index 1e570af3cd7..40957f04ae8 100644
--- a/src/renderer/src/components/dashboard/agent-dashboard-open-mode.ts
+++ b/src/renderer/src/components/dashboard/settings/agent-dashboard-open-mode.ts
@@ -1,5 +1,5 @@
-import type { GlobalSettings } from '../../../../shared/global-settings-types'
-import type { AgentDashboardMode } from '../../../../shared/ui-chrome-types'
+import type { GlobalSettings } from '../../../../../shared/global-settings-types'
+import type { AgentDashboardMode } from '../../../../../shared/ui-chrome-types'
import { isPairedWebClientWindow } from '@/app-shell/chrome/desktop-window-chrome'
/** A browser tab cannot open a second Orca window, so a web client only has the in-window board. */
diff --git a/src/renderer/src/components/dashboard/snapshot/build-dashboard-snapshot.test.ts b/src/renderer/src/components/dashboard/snapshot/build-dashboard-snapshot.test.ts
new file mode 100644
index 00000000000..95d43a52d7a
--- /dev/null
+++ b/src/renderer/src/components/dashboard/snapshot/build-dashboard-snapshot.test.ts
@@ -0,0 +1,196 @@
+import { describe, expect, it, vi } from 'vitest'
+import {
+ buildDashboardSnapshot,
+ type DashboardSnapshotInput as DashboardSnapshotState
+} from './build-dashboard-snapshot'
+
+import type { AgentStatusEntry } from '../../../../../shared/agent-status/contracts/agent-status-types'
+
+import { makePaneKey } from '../../../../../shared/workspace-layout/stable-pane-id'
+import type { TerminalTab } from '../../../../../shared/workspace-layout/terminal-tab-types'
+import type { Worktree } from '../../../../../shared/worktree/types'
+
+import '../../sidebar/workspace-card/activity/worktree-agent-row-selectors'
+
+import type * as DashboardSnapshotWorkspacesModule from './dashboard-snapshot-workspaces'
+import type * as AgentRowLineageModule from '../rows/agent-row-lineage'
+
+const mapMetadataCalls = vi.hoisted(() => ({
+ hostKind: vi.fn(),
+ parentPaneKey: vi.fn()
+}))
+
+vi.mock('./dashboard-snapshot-workspaces', async (importOriginal) => {
+ const actual = await importOriginal()
+ return {
+ ...actual,
+ dashboardCardMapWorkspaceMetadata: (
+ ...args: Parameters
+ ) => {
+ mapMetadataCalls.hostKind()
+ return actual.dashboardCardMapWorkspaceMetadata(...args)
+ }
+ }
+})
+
+vi.mock('../rows/agent-row-lineage', async (importOriginal) => {
+ const actual = await importOriginal()
+ return {
+ ...actual,
+ dashboardCardParentPaneKey: (...args: Parameters) => {
+ mapMetadataCalls.parentPaneKey()
+ return actual.dashboardCardParentPaneKey(...args)
+ }
+ }
+})
+
+const NOW = 1_000_000_000
+const TAB_ID = 'tab1'
+const LEAF_ID = '11111111-1111-4111-8111-111111111111'
+
+const SPLIT_SIBLING_LEAF_ID = '55555555-5555-4555-8555-555555555555'
+const PANE_KEY = makePaneKey(TAB_ID, LEAF_ID)
+
+function entry(overrides: Partial): AgentStatusEntry {
+ return {
+ paneKey: PANE_KEY,
+ state: 'working',
+ prompt: 'do the thing',
+ updatedAt: NOW,
+ stateStartedAt: NOW - 5000,
+ stateHistory: [],
+ agentType: 'claude',
+ tabId: TAB_ID,
+ worktreeId: 'w1',
+ ...overrides
+ }
+}
+
+function tab(id = TAB_ID, worktreeId = 'w1'): TerminalTab {
+ return {
+ id,
+ ptyId: 'pty1',
+ worktreeId,
+ title: 'agent',
+ customTitle: null,
+ color: null,
+ sortOrder: 0,
+ createdAt: NOW
+ }
+}
+
+function worktree(id = 'w1', displayName = 'wt-one'): Worktree {
+ return {
+ id,
+ repoId: 'r1',
+ path: `/r1/${id}`,
+ head: 'abc123',
+ branch: 'main',
+ isBare: false,
+ isMainWorktree: false,
+ displayName,
+ comment: '',
+ linkedIssue: null,
+ linkedPR: null,
+ linkedLinearIssue: null,
+ isArchived: false,
+ isUnread: false,
+ isPinned: false,
+ sortOrder: 0,
+ lastActivityAt: NOW
+ }
+}
+
+function baseState(overrides: Partial): DashboardSnapshotState {
+ return {
+ chatNames: new Map(),
+ repos: [{ id: 'r1', path: '/r1', displayName: 'Repo One', badgeColor: '#000', addedAt: NOW }],
+ worktreesByRepo: { r1: [worktree()] },
+ tabsByWorktree: { w1: [tab()] },
+ agentStatusByPaneKey: {},
+ retainedAgentsByPaneKey: {},
+ migrationUnsupportedByPtyId: {},
+ runtimeAgentOrchestrationByPaneKey: {},
+ terminalLayoutsByTabId: {
+ [TAB_ID]: {
+ root: { type: 'leaf', leafId: LEAF_ID },
+ activeLeafId: LEAF_ID,
+ expandedLeafId: null,
+ ptyIdsByLeafId: { [LEAF_ID]: 'pty1' }
+ }
+ },
+ ptyIdsByTabId: { [TAB_ID]: ['pty1'] },
+ runtimePaneTitlesByTabId: {},
+ acknowledgedAgentsByPaneKey: {},
+ settings: null,
+ ...overrides
+ }
+}
+
+describe('buildDashboardSnapshot', () => {
+ it('names each pane of a split tab from its own title', () => {
+ const siblingPaneKey = makePaneKey(TAB_ID, SPLIT_SIBLING_LEAF_ID)
+ const snapshot = buildDashboardSnapshot(
+ baseState({
+ agentStatusByPaneKey: {
+ [PANE_KEY]: entry({}),
+ [siblingPaneKey]: entry({ paneKey: siblingPaneKey })
+ },
+ // The tab title is whichever pane has focus, so it must not name both.
+ tabsByWorktree: { w1: [{ ...tab(), title: '\u2733 Linear work log' }] },
+ terminalLayoutsByTabId: {
+ [TAB_ID]: {
+ root: {
+ type: 'split',
+ direction: 'horizontal',
+ first: { type: 'leaf', leafId: LEAF_ID },
+ second: { type: 'leaf', leafId: SPLIT_SIBLING_LEAF_ID }
+ },
+ activeLeafId: LEAF_ID,
+ expandedLeafId: null,
+ ptyIdsByLeafId: { [LEAF_ID]: 'pty1', [SPLIT_SIBLING_LEAF_ID]: 'pty2' }
+ }
+ },
+ ptyIdsByTabId: { [TAB_ID]: ['pty1', 'pty2'] },
+ // Pane ids are replay-creation-ordered: 1 -> first leaf, 2 -> second.
+ runtimePaneTitlesByTabId: {
+ [TAB_ID]: { 1: '\u2733 Linear work log', 2: '\u2733 Redis cache strategy' }
+ }
+ }),
+ NOW
+ )
+
+ const nameByPaneKey = new Map(
+ snapshot.cards.map((card) => [card.paneKey, card.conversationName])
+ )
+ expect(nameByPaneKey.get(PANE_KEY)).toBe('Linear work log')
+ expect(nameByPaneKey.get(siblingPaneKey)).toBe('Redis cache strategy')
+ })
+
+ it('nulls ptyId when the layout entry points at a dead pty', () => {
+ const snapshot = buildDashboardSnapshot(
+ baseState({
+ agentStatusByPaneKey: { [PANE_KEY]: entry({}) },
+ // Layouts retain old PTY IDs after restart; only the live set proves preview availability.
+ ptyIdsByTabId: { [TAB_ID]: [] }
+ }),
+ NOW
+ )
+ expect(snapshot.cards[0].ptyId).toBeNull()
+ })
+
+ it('labels a waiting agent with its question, not the prompt envelope JSON', () => {
+ const interactivePrompt = JSON.stringify({
+ questions: [{ question: 'Which database?', options: [{ label: 'Postgres' }] }]
+ })
+ const snapshot = buildDashboardSnapshot(
+ baseState({
+ agentStatusByPaneKey: {
+ [PANE_KEY]: entry({ state: 'waiting', toolName: 'AskUserQuestion', interactivePrompt })
+ }
+ }),
+ NOW
+ )
+ expect(snapshot.cards[0].askSummary).toBe('Which database?')
+ })
+})
diff --git a/src/renderer/src/components/dashboard/build-dashboard-snapshot.ts b/src/renderer/src/components/dashboard/snapshot/build-dashboard-snapshot.ts
similarity index 89%
rename from src/renderer/src/components/dashboard/build-dashboard-snapshot.ts
rename to src/renderer/src/components/dashboard/snapshot/build-dashboard-snapshot.ts
index 3bf830553b8..30ffdca19cb 100644
--- a/src/renderer/src/components/dashboard/build-dashboard-snapshot.ts
+++ b/src/renderer/src/components/dashboard/snapshot/build-dashboard-snapshot.ts
@@ -1,6 +1,6 @@
-import { agentVerdictDisplayMark } from '../../../../shared/agent-status/lifecycle/agent-main-agent-verdict'
-import { interactivePromptLabel } from '../../../../shared/native-chat/input/native-chat-interactive-prompt'
-import { sayNativeChatApprovalTranslated } from '../native-chat/prompts/native-chat-interactive-prompt'
+import { agentVerdictDisplayMark } from '../../../../../shared/agent-status/lifecycle/agent-main-agent-verdict'
+import { interactivePromptLabel } from '../../../../../shared/native-chat/input/native-chat-interactive-prompt'
+import { sayNativeChatApprovalTranslated } from '../../native-chat/prompts/native-chat-interactive-prompt'
import type { StructuredChatTabNames } from '@/runtime/native-chat/naming/structured-chat-tab-name'
import type { AppState } from '@/store/types'
import {
@@ -8,26 +8,26 @@ import {
type DashboardCard,
type DashboardSnapshot,
type DashboardWorkspace
-} from '../../../../shared/dashboard-snapshot'
-import type { RepoIcon } from '../../../../shared/repo-icon'
-import { parsePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
+} from '../../../../../shared/dashboard-snapshot'
+import type { RepoIcon } from '../../../../../shared/repo-icon'
+import { parsePaneKey } from '../../../../../shared/workspace-layout/stable-pane-id'
import {
resolveDashboardCardTerminalInput,
type DashboardCardTerminalInputState
} from './dashboard-card-terminal-input'
import { readDashboardClientHost } from './dashboard-client-host'
-import { dashboardCardParentPaneKey } from './agent-row-lineage'
-import { lastEnteredDoneAt } from './agent-finished-timestamp'
-import { selectTerminalLayoutsForWorktree } from '../sidebar/workspace-card/activity/worktree-agent-row-selectors'
-import '../sidebar/workspace-card/activity/worktree-agent-orchestration-batch'
-import { EMPTY_WORKTREE_AGENT_ORCHESTRATION } from '../sidebar/workspace-card/activity/worktree-agent-orchestration-index'
+import { dashboardCardParentPaneKey } from '../rows/agent-row-lineage'
+import { lastEnteredDoneAt } from '../rows/agent-finished-timestamp'
+import { selectTerminalLayoutsForWorktree } from '../../sidebar/workspace-card/activity/worktree-agent-row-selectors'
+import '../../sidebar/workspace-card/activity/worktree-agent-orchestration-batch'
+import { EMPTY_WORKTREE_AGENT_ORCHESTRATION } from '../../sidebar/workspace-card/activity/worktree-agent-orchestration-index'
import {
finishWorktreeAgentRowsCachePass,
selectWorktreeAgentRowsCached,
startWorktreeAgentRowsCachePass,
type WorktreeAgentRowsCache
-} from './worktree-agent-rows-cache'
-import { selectRuntimePaneTitlesForWorktree } from '../sidebar/workspace-card/activity/worktree-card-status-inputs'
+} from '../rows/worktree-agent-rows-cache'
+import { selectRuntimePaneTitlesForWorktree } from '../../sidebar/workspace-card/activity/worktree-card-status-inputs'
import {
resolveDashboardCardContext,
type DashboardCardContextState
@@ -46,7 +46,7 @@ import {
import { buildDashboardSnapshotFilterOptions } from './dashboard-snapshot-filter-options'
import { groupSubagentsByParentPaneKey } from './dashboard-subagent-cards'
import { selectDashboardOrchestration } from './dashboard-orchestration-selection'
-import { dashboardRowBucketProjection, readAgentRowTurnRecords } from './dashboard-row-bucket'
+import { dashboardRowBucketProjection, readAgentRowTurnRecords } from '../rows/dashboard-row-bucket'
import type { Translate } from '@/i18n/use-translate'
/** Keep builder inputs as a Pick so tests need only construct the store slices read here. */
diff --git a/src/renderer/src/components/dashboard/dashboard-card-bucket.ts b/src/renderer/src/components/dashboard/snapshot/dashboard-card-bucket.ts
similarity index 88%
rename from src/renderer/src/components/dashboard/dashboard-card-bucket.ts
rename to src/renderer/src/components/dashboard/snapshot/dashboard-card-bucket.ts
index 4cc7da66c3c..7b7bb368eff 100644
--- a/src/renderer/src/components/dashboard/dashboard-card-bucket.ts
+++ b/src/renderer/src/components/dashboard/snapshot/dashboard-card-bucket.ts
@@ -1,7 +1,7 @@
import type {
DashboardBucket,
DashboardCardBaseDisplayState
-} from '../../../../shared/dashboard-snapshot'
+} from '../../../../../shared/dashboard-snapshot'
export function dashboardBucketForDotState(state: DashboardCardBaseDisplayState): DashboardBucket {
switch (state) {
diff --git a/src/renderer/src/components/dashboard/dashboard-card-context.ts b/src/renderer/src/components/dashboard/snapshot/dashboard-card-context.ts
similarity index 77%
rename from src/renderer/src/components/dashboard/dashboard-card-context.ts
rename to src/renderer/src/components/dashboard/snapshot/dashboard-card-context.ts
index 7895505d95c..e4c98a07499 100644
--- a/src/renderer/src/components/dashboard/dashboard-card-context.ts
+++ b/src/renderer/src/components/dashboard/snapshot/dashboard-card-context.ts
@@ -1,20 +1,18 @@
import { branchName } from '@/lib/source-control/branches/branch-name'
import { getHostedReviewCacheKey } from '@/store/slices/hosted-review/hosted-review-cache-identity'
import type { AppState } from '@/store/types'
-import type { DashboardCardReview } from '../../../../shared/dashboard-snapshot'
-import { hostedReviewInfoFromGitHubPRInfo } from '../../../../shared/source-control/hosted-review/hosted-review-github'
-import { isPositiveHostedReviewNumber } from '../../../../shared/source-control/hosted-review/hosted-review-contracts'
-import type { Repo } from '../../../../shared/repo-types'
-import type { WorkspaceStatusDefinition, Worktree } from '../../../../shared/worktree/types'
-import {
- DEFAULT_WORKSPACE_STATUSES,
- getWorkspaceStatus
-} from '../../../../shared/workspace-statuses'
+import type { DashboardCardReview } from '../../../../../shared/dashboard-snapshot'
+import { hostedReviewInfoFromGitHubPRInfo } from '../../../../../shared/source-control/hosted-review/hosted-review-github'
+import { isPositiveHostedReviewNumber } from '../../../../../shared/source-control/hosted-review/hosted-review-contracts'
+import type { Repo } from '../../../../../shared/repo-types'
+import type { WorkspaceStatusDefinition, Worktree } from '../../../../../shared/worktree/types'
+import { getWorkspaceStatus } from '../../../../../shared/workspace-statuses'
+import { DEFAULT_WORKSPACE_STATUSES } from '../../../../../shared/workspace-status-defaults'
import {
canUseParentPrChecksGitHubPRCacheEntry,
getParentPrChecksGitHubPRCacheEntry
-} from '../right-sidebar/hosted-review/folder-checks/parent-pr-checks-github-pr-cache'
-import { canUseParentPrChecksHostedReviewCacheEntry } from '../right-sidebar/hosted-review/folder-checks/parent-pr-checks-hosted-review-cache'
+} from '../../right-sidebar/hosted-review/folder-checks/parent-pr-checks-github-pr-cache'
+import { canUseParentPrChecksHostedReviewCacheEntry } from '../../right-sidebar/hosted-review/folder-checks/parent-pr-checks-hosted-review-cache'
import type { Translate } from '@/i18n/use-translate'
export type DashboardCardContextState = Partial<
diff --git a/src/renderer/src/components/dashboard/dashboard-card-labels.ts b/src/renderer/src/components/dashboard/snapshot/dashboard-card-labels.ts
similarity index 80%
rename from src/renderer/src/components/dashboard/dashboard-card-labels.ts
rename to src/renderer/src/components/dashboard/snapshot/dashboard-card-labels.ts
index ce2310922be..c149d38f8df 100644
--- a/src/renderer/src/components/dashboard/dashboard-card-labels.ts
+++ b/src/renderer/src/components/dashboard/snapshot/dashboard-card-labels.ts
@@ -1,12 +1,12 @@
-import { getAgentRowConversationName } from '../../../../shared/agent-status/presentation/agent-row-conversation-name'
-import { DASHBOARD_MAX_LABEL_LENGTH } from '../../../../shared/dashboard-snapshot'
-import { parsePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
-import { resolveAgentRowPaneLiveTitle } from './agent-row-pane-live-title'
+import { getAgentRowConversationName } from '../../../../../shared/agent-status/presentation/agent-row-conversation-name'
+import { DASHBOARD_MAX_LABEL_LENGTH } from '../../../../../shared/dashboard-snapshot'
+import { parsePaneKey } from '../../../../../shared/workspace-layout/stable-pane-id'
+import { resolveAgentRowPaneLiveTitle } from '../rows/agent-row-pane-live-title'
import { structuredChatTabById } from '@/lib/agent-session/tab-identity/structured-chat-tab-index'
import { projectedStructuredChatTabName } from '@/runtime/native-chat/naming/structured-chat-tab-name'
import type { StructuredChatTabNames } from '@/runtime/native-chat/naming/structured-chat-tab-name'
-import type { TerminalLayoutSnapshot } from '../../../../shared/workspace-layout/terminal-tab-types'
-import type { DashboardAgentRow } from './useDashboardData'
+import type { TerminalLayoutSnapshot } from '../../../../../shared/workspace-layout/terminal-tab-types'
+import type { DashboardAgentRow } from '../rows/dashboard-agent-row'
import type { AppState } from '@/store/types'
export function rowTask(row: DashboardAgentRow): string {
diff --git a/src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts b/src/renderer/src/components/dashboard/snapshot/dashboard-card-terminal-input.ts
similarity index 94%
rename from src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts
rename to src/renderer/src/components/dashboard/snapshot/dashboard-card-terminal-input.ts
index 7e4e7f54ae7..53723a67749 100644
--- a/src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts
+++ b/src/renderer/src/components/dashboard/snapshot/dashboard-card-terminal-input.ts
@@ -1,8 +1,11 @@
import type { AppState } from '@/store/types'
-import type { DashboardCardTerminalInput } from '../../../../shared/dashboard-snapshot'
-import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent'
-import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../../../../shared/execution-host'
-import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
+import type { DashboardCardTerminalInput } from '../../../../../shared/dashboard-snapshot'
+import type { TuiAgent } from '../../../../../shared/agent-launch/catalog/tui-agent'
+import {
+ toRuntimeExecutionHostId,
+ toSshExecutionHostId
+} from '../../../../../shared/execution-host'
+import { parseAppSshPtyId } from '../../../../../shared/ssh-pty-id'
import '@/lib/workspace/ownership/connection-context'
import { getConnectionIdFromState } from '@/lib/workspace/ownership/connection-owner-resolution'
import { getExecutionHostIdForWorktree } from '@/lib/workspace/ownership/worktree-runtime-owner'
diff --git a/src/renderer/src/components/dashboard/dashboard-client-host.ts b/src/renderer/src/components/dashboard/snapshot/dashboard-client-host.ts
similarity index 100%
rename from src/renderer/src/components/dashboard/dashboard-client-host.ts
rename to src/renderer/src/components/dashboard/snapshot/dashboard-client-host.ts
diff --git a/src/renderer/src/components/dashboard/dashboard-orchestration-selection.ts b/src/renderer/src/components/dashboard/snapshot/dashboard-orchestration-selection.ts
similarity index 83%
rename from src/renderer/src/components/dashboard/dashboard-orchestration-selection.ts
rename to src/renderer/src/components/dashboard/snapshot/dashboard-orchestration-selection.ts
index bb47495f9a3..6a7c4a2c0fe 100644
--- a/src/renderer/src/components/dashboard/dashboard-orchestration-selection.ts
+++ b/src/renderer/src/components/dashboard/snapshot/dashboard-orchestration-selection.ts
@@ -3,9 +3,9 @@ import type { DashboardSnapshotState } from './build-dashboard-snapshot'
import {
releaseRuntimeAgentOrchestrationBatchCache,
selectRuntimeAgentOrchestrationBatch
-} from '../sidebar/workspace-card/activity/worktree-agent-orchestration-batch'
-import '../sidebar/workspace-card/activity/worktree-agent-row-selectors'
-import { selectWorktreeAgentOrchestration as selectRuntimeAgentOrchestrationForWorktree } from '../sidebar/workspace-card/activity/worktree-agent-orchestration-index'
+} from '../../sidebar/workspace-card/activity/worktree-agent-orchestration-batch'
+import '../../sidebar/workspace-card/activity/worktree-agent-row-selectors'
+import { selectWorktreeAgentOrchestration as selectRuntimeAgentOrchestrationForWorktree } from '../../sidebar/workspace-card/activity/worktree-agent-orchestration-index'
export function selectDashboardOrchestration(
state: DashboardSnapshotState,
diff --git a/src/renderer/src/components/dashboard/dashboard-snapshot-filter-options.ts b/src/renderer/src/components/dashboard/snapshot/dashboard-snapshot-filter-options.ts
similarity index 85%
rename from src/renderer/src/components/dashboard/dashboard-snapshot-filter-options.ts
rename to src/renderer/src/components/dashboard/snapshot/dashboard-snapshot-filter-options.ts
index 828bb029bdf..72a43b3d28b 100644
--- a/src/renderer/src/components/dashboard/dashboard-snapshot-filter-options.ts
+++ b/src/renderer/src/components/dashboard/snapshot/dashboard-snapshot-filter-options.ts
@@ -1,6 +1,6 @@
import type { AppState } from '@/store/types'
-import type { DashboardFilterOptions } from '../../../../shared/dashboard-snapshot'
-import { DEFAULT_WORKSPACE_STATUSES } from '../../../../shared/workspace-statuses'
+import type { DashboardFilterOptions } from '../../../../../shared/dashboard-snapshot'
+import { DEFAULT_WORKSPACE_STATUSES } from '../../../../../shared/workspace-status-defaults'
import { boundedLabel } from './dashboard-card-labels'
import type { ActiveDashboardWorkspace } from './dashboard-snapshot-workspaces'
import { translateWorkspaceBoardStatusLabel } from '@/components/sidebar/workspace-board-status-label'
diff --git a/src/renderer/src/components/dashboard/dashboard-snapshot-workspaces.ts b/src/renderer/src/components/dashboard/snapshot/dashboard-snapshot-workspaces.ts
similarity index 92%
rename from src/renderer/src/components/dashboard/dashboard-snapshot-workspaces.ts
rename to src/renderer/src/components/dashboard/snapshot/dashboard-snapshot-workspaces.ts
index f74338407ed..07de946760e 100644
--- a/src/renderer/src/components/dashboard/dashboard-snapshot-workspaces.ts
+++ b/src/renderer/src/components/dashboard/snapshot/dashboard-snapshot-workspaces.ts
@@ -5,19 +5,19 @@ import {
type DashboardCard,
type DashboardCardHostKind,
type DashboardCardWorkspaceKind
-} from '../../../../shared/dashboard-snapshot'
-import type { RepoIcon } from '../../../../shared/repo-icon'
+} from '../../../../../shared/dashboard-snapshot'
+import type { RepoIcon } from '../../../../../shared/repo-icon'
import {
getWorktreeExecutionHostId,
parseExecutionHostId,
toRuntimeExecutionHostId,
toSshExecutionHostId,
type ExecutionHostId
-} from '../../../../shared/execution-host'
-import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspaces/folder-workspace-worktree'
-import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides'
-import { isFolderRepo } from '../../../../shared/repo-kind'
-import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
+} from '../../../../../shared/execution-host'
+import { folderWorkspaceToWorktree } from '../../../../../shared/folder-workspaces/folder-workspace-worktree'
+import { getHostDisplayLabelOverrides } from '../../../../../shared/host-setting-overrides'
+import { isFolderRepo } from '../../../../../shared/repo-kind'
+import { parseAppSshPtyId } from '../../../../../shared/ssh-pty-id'
import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields'
export type ActiveDashboardWorkspace = {
diff --git a/src/renderer/src/components/dashboard/dashboard-subagent-cards.ts b/src/renderer/src/components/dashboard/snapshot/dashboard-subagent-cards.ts
similarity index 81%
rename from src/renderer/src/components/dashboard/dashboard-subagent-cards.ts
rename to src/renderer/src/components/dashboard/snapshot/dashboard-subagent-cards.ts
index 39dabc28091..d2f4677bff6 100644
--- a/src/renderer/src/components/dashboard/dashboard-subagent-cards.ts
+++ b/src/renderer/src/components/dashboard/snapshot/dashboard-subagent-cards.ts
@@ -1,7 +1,7 @@
-import type { DashboardCardSubagent } from '../../../../shared/dashboard-snapshot'
+import type { DashboardCardSubagent } from '../../../../../shared/dashboard-snapshot'
import { nonEmpty } from './dashboard-card-labels'
-import type { DashboardAgentRow } from './useDashboardData'
-import { dashboardCardDotState } from './dashboard-row-bucket'
+import type { DashboardAgentRow } from '../rows/dashboard-agent-row'
+import { dashboardCardDotState } from '../rows/dashboard-row-bucket'
/** Nest terminal-less subagents on the card of their spawning parent pane. */
export function groupSubagentsByParentPaneKey(
diff --git a/src/renderer/src/components/dashboard/useLiveDashboardSnapshot.ts b/src/renderer/src/components/dashboard/snapshot/useLiveDashboardSnapshot.ts
similarity index 96%
rename from src/renderer/src/components/dashboard/useLiveDashboardSnapshot.ts
rename to src/renderer/src/components/dashboard/snapshot/useLiveDashboardSnapshot.ts
index 481ae3e6d2d..bae0f7d7304 100644
--- a/src/renderer/src/components/dashboard/useLiveDashboardSnapshot.ts
+++ b/src/renderer/src/components/dashboard/snapshot/useLiveDashboardSnapshot.ts
@@ -1,9 +1,9 @@
import { useMemo, useRef } from 'react'
-import { useAppStore } from '@/store'
+import { useAppStore } from '@/store/index'
import { useStructuredChatTabNames } from '@/runtime/native-chat/naming/structured-chat-name-observation'
-import type { DashboardSnapshot } from '../../../../shared/dashboard-snapshot'
+import type { DashboardSnapshot } from '../../../../../shared/dashboard-snapshot'
import { buildDashboardSnapshot } from './build-dashboard-snapshot'
-import { createWorktreeAgentRowsCache } from './worktree-agent-rows-cache'
+import { createWorktreeAgentRowsCache } from '../rows/worktree-agent-rows-cache'
import {
selectDetectedWorktreesByRepo,
selectFolderWorkspaces,
diff --git a/src/renderer/src/components/dashboard-popout/AgentTerminalDialog.tsx b/src/renderer/src/components/dashboard/terminal-preview/AgentTerminalDialog.tsx
similarity index 95%
rename from src/renderer/src/components/dashboard-popout/AgentTerminalDialog.tsx
rename to src/renderer/src/components/dashboard/terminal-preview/AgentTerminalDialog.tsx
index 47ae4dff869..77c9a229d00 100644
--- a/src/renderer/src/components/dashboard-popout/AgentTerminalDialog.tsx
+++ b/src/renderer/src/components/dashboard/terminal-preview/AgentTerminalDialog.tsx
@@ -1,24 +1,21 @@
import { useEffect, useId, useRef } from 'react'
import { SquareArrowOutUpRight, XIcon } from 'lucide-react'
import { AgentTypeIcon } from '@/components/agent/icons/agent-type-icon'
-import { formatAgentTypeLabel } from '../../../../shared/agent-status/presentation/agent-type-label'
+import { formatAgentTypeLabel } from '../../../../../shared/agent-status/presentation/agent-type-label'
import { agentStateLabel } from '@/components/AgentStateDot'
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import {
dashboardCardDisplayState,
type DashboardCard,
- type DashboardRevealAgentArgs
-} from '../../../../shared/dashboard-snapshot'
+ type DashboardRevealAgentArgs as AgentRevealArgs
+} from '../../../../../shared/dashboard-snapshot'
import { AgentTerminalPreview } from './AgentTerminalPreview'
-import { AgentCardLastMessages, hasAgentCardLastMessages } from './AgentCardLastMessages'
+import { AgentCardLastMessages, hasAgentCardLastMessages } from '../board/AgentCardLastMessages'
import { terminalPreviewUnavailableMessage } from './terminal-preview-unavailable-message'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/class-names'
-/** Routing payload for focusing an agent's pane in the main window. */
-export type AgentRevealArgs = DashboardRevealAgentArgs
-
type AgentTerminalDialogProps = {
/** The agent shown in the dialog; null renders the dialog closed. */
card: DashboardCard | null
diff --git a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.tsx b/src/renderer/src/components/dashboard/terminal-preview/AgentTerminalPreview.tsx
similarity index 91%
rename from src/renderer/src/components/dashboard-popout/AgentTerminalPreview.tsx
rename to src/renderer/src/components/dashboard/terminal-preview/AgentTerminalPreview.tsx
index 952a64d9b1f..43601de9520 100644
--- a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.tsx
+++ b/src/renderer/src/components/dashboard/terminal-preview/AgentTerminalPreview.tsx
@@ -2,35 +2,35 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { composeActiveTerminalTheme } from '@/components/terminal-pane/appearance/terminal-appearance'
import { useSystemPrefersDark } from '@/hooks/theme/use-system-prefers-dark'
-import { TerminalKittyKeyboardModeTracker } from '../../../../shared/terminal/protocol/input-modes/terminal-kitty-keyboard-mode-tracker'
-import { replayPreviewConnectionSnapshot } from './preview-terminal-snapshot-replay'
+import { TerminalKittyKeyboardModeTracker } from '../../../../../shared/terminal/protocol/input-modes/terminal-kitty-keyboard-mode-tracker'
+import { replayPreviewConnectionSnapshot } from './transport/preview-terminal-snapshot-replay'
import { useEffectiveMacOptionAsAlt } from '@/lib/keyboard-layout/use-effective-mac-option-as-alt'
import {
buildPreviewAppearanceOptions,
buildPreviewTerminalOptions,
previewAdvertisesKittyKeyboard
-} from './preview-terminal-options'
-import { syncPreviewTerminalLigatures } from './preview-terminal-ligatures'
+} from '../../standalone-terminal/rendering/preview-terminal-options'
+import { syncPreviewTerminalLigatures } from '../../standalone-terminal/rendering/preview-terminal-ligatures'
import { terminalPreviewUnavailableMessage } from './terminal-preview-unavailable-message'
import { resolveEffectiveTerminalAppearance } from '@/lib/terminal/appearance/terminal-theme'
import { cn } from '@/lib/class-names'
import { useAppStore } from '@/store/index'
import { usePluginFontReady } from '@/store/plugins/appearance/plugin-fonts'
-import { createPreviewGridClaim } from './preview-grid-claim'
-import { createPreviewBoxFit } from './preview-terminal-box-fit'
-import { openStandaloneTerminal } from '../standalone-terminal/open-standalone-terminal'
-import type { StandaloneTerminal } from '../standalone-terminal/open-standalone-terminal'
-import { terminalPreviewApiFor } from './terminal-preview-api'
+import { createPreviewGridClaim } from './geometry/preview-grid-claim'
+import { createPreviewBoxFit } from './geometry/preview-terminal-box-fit'
+import { openStandaloneTerminal } from '../../standalone-terminal/open-standalone-terminal'
+import type { StandaloneTerminal } from '../../standalone-terminal/open-standalone-terminal'
+import { terminalPreviewApiFor } from './transport/terminal-preview-api'
import { LinkActionPopover } from '@/components/link-actions/LinkActionPopover'
import { closeLinkActionRequest } from '@/components/link-actions/link-action-request'
import type { LinkActionRequest } from '@/components/link-actions/link-action-request'
-import { installDashboardPreviewLinkInteraction } from './dashboard-preview-link-interaction'
+import { installDashboardPreviewLinkInteraction } from './links/dashboard-preview-link-interaction'
import { useTranslate } from '@/i18n/use-translate'
-import type { PreviewTerminalLinkHandler } from './preview-terminal-links'
+import type { PreviewTerminalLinkHandler } from '../../standalone-terminal/rendering/preview-terminal-links'
import type { ITheme, Terminal } from '@xterm/xterm'
-import type { DashboardCardTerminalInput } from '../../../../shared/dashboard-snapshot'
-import type { TerminalPreviewDataPayload } from '../../../../shared/terminal/replay/terminal-preview'
-import type { DashboardPreviewLinkSource } from './dashboard-preview-http-actions'
+import type { DashboardCardTerminalInput } from '../../../../../shared/dashboard-snapshot'
+import type { TerminalPreviewDataPayload } from '../../../../../shared/terminal/replay/terminal-preview'
+import type { DashboardPreviewLinkSource } from './links/dashboard-preview-http-actions'
const PREVIEW_SCROLLBACK_ROWS = 24
// Preview snapshots bound history; pane scrollback would only cost memory.
diff --git a/src/renderer/src/components/dashboard-popout/agent-terminal-preview-target.ts b/src/renderer/src/components/dashboard/terminal-preview/agent-terminal-preview-target.ts
similarity index 100%
rename from src/renderer/src/components/dashboard-popout/agent-terminal-preview-target.ts
rename to src/renderer/src/components/dashboard/terminal-preview/agent-terminal-preview-target.ts
diff --git a/src/renderer/src/components/dashboard-popout/preview-grid-claim.ts b/src/renderer/src/components/dashboard/terminal-preview/geometry/preview-grid-claim.ts
similarity index 92%
rename from src/renderer/src/components/dashboard-popout/preview-grid-claim.ts
rename to src/renderer/src/components/dashboard/terminal-preview/geometry/preview-grid-claim.ts
index 46bd7eaf985..c5fcd9b1613 100644
--- a/src/renderer/src/components/dashboard-popout/preview-grid-claim.ts
+++ b/src/renderer/src/components/dashboard/terminal-preview/geometry/preview-grid-claim.ts
@@ -1,6 +1,6 @@
import type { Terminal } from '@xterm/xterm'
-import { clampTerminalViewport } from '../../../../shared/terminal/viewport/terminal-viewport'
-import { terminalPreviewApiFor } from './terminal-preview-api'
+import { clampTerminalViewport } from '../../../../../../shared/terminal/viewport/terminal-viewport'
+import { terminalPreviewApiFor } from '../transport/terminal-preview-api'
const FIT_REQUEST_DEBOUNCE_MS = 200
/** Claim the dialog-sized PTY grid once per target size; unchanged requests must not fight a host or phone reclaiming geometry. */
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-box-fit.ts b/src/renderer/src/components/dashboard/terminal-preview/geometry/preview-terminal-box-fit.ts
similarity index 100%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-box-fit.ts
rename to src/renderer/src/components/dashboard/terminal-preview/geometry/preview-terminal-box-fit.ts
diff --git a/src/renderer/src/components/dashboard-popout/dashboard-preview-http-actions.ts b/src/renderer/src/components/dashboard/terminal-preview/links/dashboard-preview-http-actions.ts
similarity index 94%
rename from src/renderer/src/components/dashboard-popout/dashboard-preview-http-actions.ts
rename to src/renderer/src/components/dashboard/terminal-preview/links/dashboard-preview-http-actions.ts
index 91bd1aef3a0..ae94a132a18 100644
--- a/src/renderer/src/components/dashboard-popout/dashboard-preview-http-actions.ts
+++ b/src/renderer/src/components/dashboard/terminal-preview/links/dashboard-preview-http-actions.ts
@@ -1,11 +1,11 @@
import { toast } from 'sonner'
import { intentPresentation } from '@/lib/browser/workspace/workspace-browser-tab-presentation'
-import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
-import { parseRoutableExecutionHostId } from '../../../../shared/execution-host'
+import { parseAppSshPtyId } from '../../../../../../shared/ssh-pty-id'
+import { parseRoutableExecutionHostId } from '../../../../../../shared/execution-host'
import {
parseRemoteRuntimePtyId,
isRemoteRuntimePtyId
-} from '../../../../shared/remote-runtime/remote-runtime-pty-id'
+} from '../../../../../../shared/remote-runtime/remote-runtime-pty-id'
import { useAppStore } from '@/store/index'
import { resolveOwner } from '@/lib/workspace/ownership/resolve-owner'
import { resolveWorktreeOperationRoute } from '@/lib/workspace/ownership/worktree-operation-route'
@@ -24,7 +24,7 @@ import type {
HttpLinkDestination,
HttpLinkActionDestinations
} from '@/lib/browser/navigation/http-link-destinations'
-import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
+import type { DashboardCard } from '../../../../../../shared/dashboard-snapshot'
import type { HttpLinkSourceOwner } from '@/lib/browser/navigation/http-link-routing'
export type DashboardPreviewLinkSource = Pick<
diff --git a/src/renderer/src/components/dashboard-popout/dashboard-preview-link-interaction.ts b/src/renderer/src/components/dashboard/terminal-preview/links/dashboard-preview-link-interaction.ts
similarity index 96%
rename from src/renderer/src/components/dashboard-popout/dashboard-preview-link-interaction.ts
rename to src/renderer/src/components/dashboard/terminal-preview/links/dashboard-preview-link-interaction.ts
index a75ae43fad9..a4344c581f0 100644
--- a/src/renderer/src/components/dashboard-popout/dashboard-preview-link-interaction.ts
+++ b/src/renderer/src/components/dashboard/terminal-preview/links/dashboard-preview-link-interaction.ts
@@ -1,4 +1,4 @@
-import type { PreviewTerminalLinkHandler } from './preview-terminal-links'
+import type { PreviewTerminalLinkHandler } from '../../../standalone-terminal/rendering/preview-terminal-links'
import type { Terminal } from '@xterm/xterm'
import type { LinkActionRequest } from '@/components/link-actions/link-action-request'
import { installTerminalLinkifierClickPriming } from '@/components/terminal-pane/links/gestures/terminal-linkifier-click-priming'
@@ -12,10 +12,10 @@ import {
} from '@/components/terminal-pane/links/gestures/terminal-link-activation'
import { getTerminalBufferPositionForMouseEvent } from '@/components/terminal-pane/input/pointer/terminal-mouse-buffer-position'
import { findHttpLinkAtTerminalMouseEvent } from '@/components/terminal-pane/links/http/terminal-url-link-hit-testing'
-import { findTerminalHttpLinkAtBufferPosition } from '../../../../shared/terminal/links/terminal-link-buffer-hit-testing'
+import { findTerminalHttpLinkAtBufferPosition } from '../../../../../../shared/terminal/links/terminal-link-buffer-hit-testing'
import { httpLinkDirectClickDestination } from '@/lib/browser/navigation/http-link-destinations'
import { isWindowsUserAgent } from '@/app-shell/platform/user-agent-platform'
-import type { GlobalSettings } from '../../../../shared/global-settings-types'
+import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
import {
dashboardPreviewHttpActions,
type DashboardPreviewLinkSource
diff --git a/src/renderer/src/components/dashboard-popout/terminal-preview-unavailable-message.ts b/src/renderer/src/components/dashboard/terminal-preview/terminal-preview-unavailable-message.ts
similarity index 79%
rename from src/renderer/src/components/dashboard-popout/terminal-preview-unavailable-message.ts
rename to src/renderer/src/components/dashboard/terminal-preview/terminal-preview-unavailable-message.ts
index 255dd4362eb..d024306fd3c 100644
--- a/src/renderer/src/components/dashboard-popout/terminal-preview-unavailable-message.ts
+++ b/src/renderer/src/components/dashboard/terminal-preview/terminal-preview-unavailable-message.ts
@@ -1,8 +1,8 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
-import type { DashboardCardHostKind } from '../../../../shared/dashboard-snapshot'
-import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
-import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime/remote-runtime-pty-id'
+import type { DashboardCardHostKind } from '../../../../../shared/dashboard-snapshot'
+import { parseAppSshPtyId } from '../../../../../shared/ssh-pty-id'
+import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime/remote-runtime-pty-id'
/** Remote snapshot absence cannot prove pane exit because the client does not own the PTY; see docs/reference/ssh-execution-boundary.md. */
export function terminalPreviewUnavailableMessage(
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-snapshot-replay.ts b/src/renderer/src/components/dashboard/terminal-preview/transport/preview-terminal-snapshot-replay.ts
similarity index 80%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-snapshot-replay.ts
rename to src/renderer/src/components/dashboard/terminal-preview/transport/preview-terminal-snapshot-replay.ts
index ae659aa593f..afc128389fa 100644
--- a/src/renderer/src/components/dashboard-popout/preview-terminal-snapshot-replay.ts
+++ b/src/renderer/src/components/dashboard/terminal-preview/transport/preview-terminal-snapshot-replay.ts
@@ -1,10 +1,10 @@
-import type { TerminalKittyKeyboardModeTracker } from '../../../../shared/terminal/protocol/input-modes/terminal-kitty-keyboard-mode-tracker'
-import { parseTerminalKittyKeyboardFlags } from '../../../../shared/terminal/protocol/input-modes/terminal-kitty-keyboard-flags'
-import { buildKittyKeyboardRestore } from '../../../../shared/terminal/protocol/input-modes/terminal-mode-reset-profiles'
+import type { TerminalKittyKeyboardModeTracker } from '../../../../../../shared/terminal/protocol/input-modes/terminal-kitty-keyboard-mode-tracker'
+import { parseTerminalKittyKeyboardFlags } from '../../../../../../shared/terminal/protocol/input-modes/terminal-kitty-keyboard-flags'
+import { buildKittyKeyboardRestore } from '../../../../../../shared/terminal/protocol/input-modes/terminal-mode-reset-profiles'
import type {
TerminalPreviewReplayChunk,
TerminalPreviewSnapshot
-} from '../../../../shared/terminal/replay/terminal-preview'
+} from '../../../../../../shared/terminal/replay/terminal-preview'
/** Restore proven kitty flags after kitty-free snapshot ANSI; remain synchronous so events cannot observe the temporary reset. */
export function replayPreviewConnectionSnapshot(args: {
diff --git a/src/renderer/src/components/dashboard-popout/remote-runtime-terminal-preview-api.ts b/src/renderer/src/components/dashboard/terminal-preview/transport/remote-runtime-terminal-preview-api.ts
similarity index 98%
rename from src/renderer/src/components/dashboard-popout/remote-runtime-terminal-preview-api.ts
rename to src/renderer/src/components/dashboard/terminal-preview/transport/remote-runtime-terminal-preview-api.ts
index 0ee2a2934ab..2f4819e81b7 100644
--- a/src/renderer/src/components/dashboard-popout/remote-runtime-terminal-preview-api.ts
+++ b/src/renderer/src/components/dashboard/terminal-preview/transport/remote-runtime-terminal-preview-api.ts
@@ -1,16 +1,16 @@
-import { createNonSecureContextUuid as createBrowserUuid } from '../../../../shared/non-secure-context-uuid'
+import { createNonSecureContextUuid as createBrowserUuid } from '../../../../../../shared/non-secure-context-uuid'
import { takeCurrentTerminalDeliveryCredit } from '@/lib/pane-manager/terminal-output/terminal-delivery-credit'
import { getRemoteRuntimeTerminalMultiplexer } from '@/runtime/hosted-terminals/streams/registry/remote-runtime-terminal-multiplexer-registry'
import type {
RemoteRuntimeMultiplexedTerminal,
RemoteRuntimeMultiplexedTerminalCallbacks
} from '@/runtime/hosted-terminals/streams/remote-terminal-stream-contracts'
-import { parseRemoteRuntimePtyId } from '../../../../shared/remote-runtime/remote-runtime-pty-id'
+import { parseRemoteRuntimePtyId } from '../../../../../../shared/remote-runtime/remote-runtime-pty-id'
import type {
TerminalPreviewConnectResult,
TerminalPreviewDataPayload,
TerminalPreviewReplayChunk
-} from '../../../../shared/terminal/replay/terminal-preview'
+} from '../../../../../../shared/terminal/replay/terminal-preview'
type TerminalPreviewApi = typeof window.api.terminalPreview
diff --git a/src/renderer/src/components/dashboard-popout/terminal-preview-api.ts b/src/renderer/src/components/dashboard/terminal-preview/transport/terminal-preview-api.ts
similarity index 79%
rename from src/renderer/src/components/dashboard-popout/terminal-preview-api.ts
rename to src/renderer/src/components/dashboard/terminal-preview/transport/terminal-preview-api.ts
index a5784935a97..798eccb2782 100644
--- a/src/renderer/src/components/dashboard-popout/terminal-preview-api.ts
+++ b/src/renderer/src/components/dashboard/terminal-preview/transport/terminal-preview-api.ts
@@ -1,4 +1,4 @@
-import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime/remote-runtime-pty-id'
+import { isRemoteRuntimePtyId } from '../../../../../../shared/remote-runtime/remote-runtime-pty-id'
import { remoteRuntimeTerminalPreviewApi } from './remote-runtime-terminal-preview-api'
/** A paired host's pane previews from that host; everything else from this app's main process. */
diff --git a/src/renderer/src/components/dashboard/useRetainedAgents.test.ts b/src/renderer/src/components/dashboard/useRetainedAgents.test.ts
deleted file mode 100644
index 0f6ff754d57..00000000000
--- a/src/renderer/src/components/dashboard/useRetainedAgents.test.ts
+++ /dev/null
@@ -1,389 +0,0 @@
-// @vitest-environment happy-dom
-import { act, renderHook } from '@testing-library/react'
-import { afterEach, beforeEach, describe, expect, it } from 'vitest'
-import { buildWorktreeAgentRows } from '../sidebar/workspace-card/activity/worktree-agent-rows'
-import { useAppStore } from '@/store'
-import { resetAgentPaneAuthorityAliasesForTests } from '@/store/slices/agent-status/agent-pane-authority'
-import type {
- AgentStatusEntry,
- AgentStatusState
-} from '../../../../shared/agent-status/contracts/agent-status-types'
-import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
-import { applyWebSessionTabsSnapshot } from '@/runtime/web-session-tabs-sync/snapshot-api'
-import {
- ENV,
- LEAF_ID,
- WT,
- makeSnapshot,
- resetWebSessionTabsSyncTestState
-} from '@/runtime/web-session-tabs-sync/testing/web-session-tabs-sync.test-fixture'
-import type { Repo } from '../../../../shared/repo-types'
-import type { TerminalTab } from '../../../../shared/workspace-layout/terminal-tab-types'
-import type { Worktree } from '../../../../shared/worktree/types'
-import {
- buildRetainedAgentsSyncSnapshot,
- collectRetainedAgentsOnDisappear,
- useRetainedAgentsSync
-} from './useRetainedAgents'
-
-const initialAppState = useAppStore.getInitialState()
-
-beforeEach(() => {
- useAppStore.setState(initialAppState, true)
- resetAgentPaneAuthorityAliasesForTests()
-})
-
-afterEach(() => {
- useAppStore.setState(initialAppState, true)
- resetAgentPaneAuthorityAliasesForTests()
-})
-
-function makeRepo(): Repo {
- return {
- id: 'repo-1',
- path: '/repo',
- displayName: 'Repo',
- badgeColor: '#000',
- addedAt: 1
- }
-}
-
-function makeWorktree(overrides?: Partial): Worktree {
- return {
- id: 'wt-1',
- repoId: 'repo-1',
- path: '/repo/wt-1',
- head: 'abc123',
- branch: 'feature',
- isBare: false,
- isMainWorktree: false,
- displayName: 'feature',
- comment: '',
- linkedIssue: null,
- linkedPR: null,
- linkedLinearIssue: null,
- isArchived: false,
- isUnread: false,
- isPinned: false,
- sortOrder: 0,
- lastActivityAt: 1,
- ...overrides
- }
-}
-
-function makeTab(overrides: Partial & { id: string }): TerminalTab {
- return {
- worktreeId: 'wt-1',
- title: 'Terminal',
- ptyId: null,
- customTitle: null,
- color: null,
- sortOrder: 0,
- createdAt: 1,
- ...overrides
- }
-}
-
-function makeAgentRow(args: {
- paneKey: string
- state: AgentStatusState
- interrupted?: boolean
- mainAgent?: AgentStatusEntry['mainAgent']
-}) {
- const entry: AgentStatusEntry = {
- state: args.state,
- prompt: 'Fix it',
- updatedAt: 100,
- stateStartedAt: 100,
- paneKey: args.paneKey,
- terminalTitle: 'Claude',
- stateHistory: [],
- agentType: 'claude',
- interrupted: args.interrupted,
- mainAgent: args.mainAgent
- }
-
- return {
- paneKey: args.paneKey,
- entry,
- tab: makeTab({ id: 'tab-1' }),
- agentType: 'claude' as const,
- state: args.state,
- startedAt: 1
- }
-}
-
-describe('collectRetainedAgentsOnDisappear', () => {
- it('carries paired retained order through same-session return and resets known replacement', () => {
- resetWebSessionTabsSyncTestState()
- useAppStore.setState({
- repos: [makeRepo()],
- worktreesByRepo: { 'repo-1': [makeWorktree({ id: WT })] }
- })
- const entry: AgentStatusEntry = {
- ...makeAgentRow({ paneKey: makePaneKey('host-tab-1', LEAF_ID), state: 'done' }).entry,
- worktreeId: WT,
- providerSession: { key: 'session_id', id: 'A' }
- }
- const quiet: AgentStatusEntry = {
- ...entry,
- paneKey: makePaneKey('host-tab-2', LEAF_ID),
- state: 'working',
- updatedAt: 2000,
- stateStartedAt: 2000,
- providerSession: { key: 'session_id', id: 'quiet' }
- }
- const update = (agentStatus: AgentStatusEntry | undefined, version: number) => {
- const tabs = [agentStatus, quiet].map((status, index) => ({
- type: 'terminal' as const,
- id: `host-tab-${index + 1}::${LEAF_ID}`,
- title: 'Claude',
- parentTabId: `host-tab-${index + 1}`,
- leafId: LEAF_ID,
- isActive: index === 0,
- status: 'ready' as const,
- terminal: `terminal-${index + 1}`,
- agentStatus: status
- }))
- useAppStore.setState(
- applyWebSessionTabsSnapshot(
- useAppStore.getState(),
- makeSnapshot(tabs, { snapshotVersion: version }),
- ENV,
- 1_000_000 + version
- )
- )
- }
- const snapshot = () =>
- buildRetainedAgentsSyncSnapshot({
- repos: [makeRepo()],
- worktreesByRepo: { 'repo-1': [makeWorktree({ id: WT })] },
- folderWorkspaces: [],
- tabsByWorktree: useAppStore.getState().tabsByWorktree,
- agentStatusByPaneKey: useAppStore.getState().agentStatusByPaneKey,
- now: 1_000_000
- })
- const rows = () =>
- buildWorktreeAgentRows({
- tabs: useAppStore.getState().tabsByWorktree[WT] ?? [],
- entries: Object.values(useAppStore.getState().agentStatusByPaneKey),
- retained: Object.values(useAppStore.getState().retainedAgentsByPaneKey),
- now: 1_000_000
- })
- update({ ...entry, updatedAt: 1000, stateStartedAt: 1000, state: 'working' }, 1)
- update({ ...entry, updatedAt: 5000, stateStartedAt: 5000 }, 2)
- const previous = snapshot()
- update(undefined, 3)
- const current = snapshot()
- const result = collectRetainedAgentsOnDisappear({
- previousAgents: previous.currentAgents,
- currentAgents: current.currentAgents,
- retainedAgentsByPaneKey: useAppStore.getState().retainedAgentsByPaneKey,
- retentionSuppressedPaneKeys: {},
- recentlyClosedAgentStatusTabIds: {},
- recentlyRetiredAgentStatusPaneKeys: {},
- tabIndex: current.tabIndex
- })
- expect(result.toRetain).toHaveLength(1)
- useAppStore.getState().retainAgents(result.toRetain)
- expect(rows().map((row) => row.startedAt)).toEqual([1000, 2000])
- update({ ...entry, updatedAt: 6000, stateStartedAt: 6000, state: 'working' }, 4)
- expect(rows().map((row) => row.entry.providerSession?.id)).toEqual(['A', 'quiet'])
- expect(rows().map((row) => row.startedAt)).toEqual([1000, 2000])
- update(undefined, 5)
- update(
- {
- ...entry,
- updatedAt: 7000,
- stateStartedAt: 6000,
- state: 'working',
- providerSession: { key: 'session_id', id: 'B' }
- },
- 6
- )
- expect(rows().map((row) => row.entry.providerSession?.id)).toEqual(['quiet', 'B'])
- expect(rows().map((row) => row.startedAt)).toEqual([2000, 7000])
- })
-})
-
-describe('useRetainedAgentsSync', () => {
- it('uses the freshest tab title when a real agent transition retains a completed row', async () => {
- const repo = makeRepo()
- const worktree = makeWorktree()
- const paneKey = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111')
- const row = makeAgentRow({ paneKey, state: 'done' })
- useAppStore.setState({
- repos: [repo],
- worktreesByRepo: { [repo.id]: [worktree] },
- tabsByWorktree: {
- [worktree.id]: [{ ...row.tab, title: '⠋ Codex is thinking' }]
- },
- agentStatusByPaneKey: { [paneKey]: row.entry },
- agentStatusEpoch: initialAppState.agentStatusEpoch + 1
- })
- const hook = renderHook(() => useRetainedAgentsSync())
- await act(async () => {
- await Promise.resolve()
- })
-
- act(() => {
- useAppStore.setState((state) => ({
- tabsByWorktree: {
- [worktree.id]: [
- {
- ...state.tabsByWorktree[worktree.id][0],
- title: '⠙ Codex is thinking'
- }
- ]
- }
- }))
- })
- act(() => {
- useAppStore.setState((state) => ({
- agentStatusByPaneKey: {},
- agentStatusEpoch: state.agentStatusEpoch + 1
- }))
- })
- await act(async () => {
- await Promise.resolve()
- })
-
- expect(useAppStore.getState().retainedAgentsByPaneKey[paneKey]?.tab.title).toBe(
- '⠙ Codex is thinking'
- )
- hook.unmount()
- })
-
- it('does not re-retain when status removal and tab closure commit before the next retention effect', async () => {
- const repo = makeRepo()
- const worktree = makeWorktree()
- const paneKey = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111')
- const row = makeAgentRow({ paneKey, state: 'done' })
- useAppStore.setState({
- repos: [repo],
- worktreesByRepo: { [repo.id]: [worktree] },
- tabsByWorktree: { [worktree.id]: [row.tab] },
- agentStatusByPaneKey: { [row.paneKey]: row.entry },
- agentStatusEpoch: initialAppState.agentStatusEpoch + 1
- })
- const hook = renderHook(() => useRetainedAgentsSync())
- await act(async () => {
- await Promise.resolve()
- })
- expect(useAppStore.getState().retentionSuppressedPaneKeys[row.paneKey]).toBeUndefined()
-
- // Why: model both teardown writes landing before the next retention effect runs.
- act(() => {
- useAppStore.setState((state) => ({
- tabsByWorktree: { [worktree.id]: [] },
- agentStatusByPaneKey: {},
- recentlyClosedAgentStatusTabIds: { [row.tab.id]: true },
- agentStatusEpoch: state.agentStatusEpoch + 1
- }))
- })
-
- const state = useAppStore.getState()
- expect(state.recentlyClosedAgentStatusTabIds[row.tab.id]).toBe(true)
- expect(state.retainedAgentsByPaneKey[row.paneKey]).toBeUndefined()
- hook.unmount()
- })
-
- it('does not leave a ghost row when a done pane detaches into another tab', async () => {
- const repo = makeRepo()
- const worktree = makeWorktree()
- const leafId = '11111111-1111-4111-8111-111111111111'
- const sourceTab = makeTab({ id: 'tab-source', ptyId: 'pty-a' })
- const targetTab = makeTab({ id: 'tab-target' })
- const sourcePaneKey = makePaneKey(sourceTab.id, leafId)
- const targetPaneKey = makePaneKey(targetTab.id, leafId)
- const row = makeAgentRow({ paneKey: sourcePaneKey, state: 'done' })
- useAppStore.setState({
- repos: [repo],
- worktreesByRepo: { [repo.id]: [worktree] },
- tabsByWorktree: { [worktree.id]: [sourceTab, targetTab] },
- ptyIdsByTabId: { [sourceTab.id]: ['pty-a'], [targetTab.id]: [] },
- agentStatusByPaneKey: { [sourcePaneKey]: { ...row.entry, tabId: sourceTab.id } },
- agentStatusEpoch: initialAppState.agentStatusEpoch + 1
- })
- const hook = renderHook(() => useRetainedAgentsSync())
- await act(async () => {
- await Promise.resolve()
- })
-
- act(() => {
- useAppStore.getState().syncPaneDetachPtyOwnership({
- detachedLeafId: leafId,
- detachedPtyId: 'pty-a',
- sourceLayout: {
- root: null,
- activeLeafId: null,
- expandedLeafId: null,
- ptyIdsByLeafId: {}
- },
- sourceTabId: sourceTab.id,
- targetTabId: targetTab.id
- })
- })
- await act(async () => {
- await Promise.resolve()
- })
-
- const state = useAppStore.getState()
- expect(state.agentStatusByPaneKey[targetPaneKey]?.state).toBe('done')
- expect(state.retainedAgentsByPaneKey[sourcePaneKey]).toBeUndefined()
- expect(state.retainedAgentsByPaneKey[targetPaneKey]).toBeUndefined()
- expect(state.retentionSuppressedPaneKeys[sourcePaneKey]).toBeUndefined()
- hook.unmount()
- })
-
- it('keeps a wrapped-history completion in position through disappearance retention', () => {
- const paneKey = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111')
- const store = useAppStore.getState()
- store.setAgentStatus(
- paneKey,
- { state: 'working', prompt: 'task', agentType: 'pi' },
- undefined,
- {
- updatedAt: 1000
- }
- )
- for (let i = 0; i < 23; i++) {
- store.setAgentStatus(
- paneKey,
- { state: i % 2 ? 'working' : 'done', prompt: 'task', agentType: 'pi' },
- undefined,
- { updatedAt: 3000 + i * 1000 }
- )
- }
- const entry = useAppStore.getState().agentStatusByPaneKey[paneKey]
- expect(entry.stateHistory[0].startedAt).toBeGreaterThan(2000)
- const previous = buildRetainedAgentsSyncSnapshot({
- repos: [makeRepo()],
- worktreesByRepo: { 'repo-1': [makeWorktree()] },
- folderWorkspaces: [],
- tabsByWorktree: { 'wt-1': [makeTab({ id: 'tab-1' })] },
- agentStatusByPaneKey: { [paneKey]: entry },
- now: 30000
- })
- const result = collectRetainedAgentsOnDisappear({
- previousAgents: previous.currentAgents,
- currentAgents: new Map(),
- retainedAgentsByPaneKey: {},
- retentionSuppressedPaneKeys: {},
- recentlyClosedAgentStatusTabIds: {},
- recentlyRetiredAgentStatusPaneKeys: {}
- })
- expect(result.toRetain[0]?.startedAt).toBe(1000)
- const quiet = makeAgentRow({
- paneKey: makePaneKey('tab-2', '22222222-2222-4222-8222-222222222222'),
- state: 'working'
- }).entry
- const rows = buildWorktreeAgentRows({
- tabs: [makeTab({ id: 'tab-2' })],
- entries: [{ ...quiet, stateStartedAt: 2000 }],
- retained: result.toRetain,
- now: 30000
- })
- expect(rows.map((row) => row.paneKey)).toEqual([paneKey, quiet.paneKey])
- })
-})
diff --git a/src/renderer/src/components/editor/review-note-delivery/ReviewNotesSendMenuContent.tsx b/src/renderer/src/components/editor/review-note-delivery/ReviewNotesSendMenuContent.tsx
index 40bc1576fec..0bd8e83bb6f 100644
--- a/src/renderer/src/components/editor/review-note-delivery/ReviewNotesSendMenuContent.tsx
+++ b/src/renderer/src/components/editor/review-note-delivery/ReviewNotesSendMenuContent.tsx
@@ -43,10 +43,10 @@ import { agentKindForAgentType } from '@/lib/agent-status/aggregation/worktree-a
import { formatAgentTypeLabel } from '../../../../../shared/agent-status/presentation/agent-type-label'
import { track } from '@/diagnostics/telemetry/telemetry'
import { useNow } from '@/hooks/use-now'
-import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
-import { lastEnteredDoneAt } from '@/components/dashboard/agent-finished-timestamp'
+import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/rows/dashboard-agent-row'
+import { lastEnteredDoneAt } from '@/components/dashboard/rows/agent-finished-timestamp'
import { useWorktreeAgentRows } from '@/components/sidebar/workspace-card/activity/useWorktreeAgentRows'
-import { useAgentRowConversationName } from '@/components/dashboard/use-agent-row-conversation-name'
+import { useAgentRowConversationName } from '@/components/dashboard/rows/use-agent-row-conversation-name'
import { getAgentRowPrimaryText } from '../../../../../shared/agent-status/presentation/agent-row-primary-text'
import type { LaunchSource } from '../../../../../shared/telemetry-property-schemas'
import { agentRowDisplayDotState } from '@/lib/agent-status/presentation/agent-row-dot-state'
diff --git a/src/renderer/src/components/editor/review-note-delivery/review-notes-send-target-order.ts b/src/renderer/src/components/editor/review-note-delivery/review-notes-send-target-order.ts
index 274d678fc80..4845618d1d3 100644
--- a/src/renderer/src/components/editor/review-note-delivery/review-notes-send-target-order.ts
+++ b/src/renderer/src/components/editor/review-note-delivery/review-notes-send-target-order.ts
@@ -1,4 +1,4 @@
-import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/rows/dashboard-agent-row'
import type { NotesSendAgentTarget } from '@/lib/agent-session/messaging/notes-send-agent-targets'
export type OrderedSendTarget = {
diff --git a/src/renderer/src/components/native-chat/messages/agent-senders/native-chat-agent-message-sender-source.ts b/src/renderer/src/components/native-chat/messages/agent-senders/native-chat-agent-message-sender-source.ts
index 010560ee33f..8920d62bfdb 100644
--- a/src/renderer/src/components/native-chat/messages/agent-senders/native-chat-agent-message-sender-source.ts
+++ b/src/renderer/src/components/native-chat/messages/agent-senders/native-chat-agent-message-sender-source.ts
@@ -5,7 +5,7 @@ import {
createWorktreeAgentRowsCache,
startWorktreeAgentRowsCachePass,
selectWorktreeAgentRowsCached
-} from '@/components/dashboard/worktree-agent-rows-cache'
+} from '@/components/dashboard/rows/worktree-agent-rows-cache'
import { createWorktreeAgentFreshnessSelector } from '@/components/sidebar/workspace-card/activity/worktree-agent-freshness-selector'
import '@/components/sidebar/workspace-card/activity/worktree-agent-row-selectors'
import { selectWorktreeAgentOrchestration as selectRuntimeAgentOrchestrationForWorktree } from '@/components/sidebar/workspace-card/activity/worktree-agent-orchestration-index'
diff --git a/src/renderer/src/components/plain-ssh-terminal/PlainSshTerminalView.tsx b/src/renderer/src/components/plain-ssh-terminal/PlainSshTerminalView.tsx
index 76d6fe4c6bc..8d82cba11d1 100644
--- a/src/renderer/src/components/plain-ssh-terminal/PlainSshTerminalView.tsx
+++ b/src/renderer/src/components/plain-ssh-terminal/PlainSshTerminalView.tsx
@@ -8,11 +8,11 @@ import { composeActiveTerminalTheme } from '@/components/terminal-pane/appearanc
import { resolveEffectiveTerminalAppearance } from '@/lib/terminal/appearance/terminal-theme'
import { useSystemPrefersDark } from '@/hooks/theme/use-system-prefers-dark'
import { resolveTerminalMinimumContrastRatio } from '../../../../shared/terminal-themes/terminal-contrast-correction'
-import { syncPreviewTerminalLigatures } from '@/components/dashboard-popout/preview-terminal-ligatures'
+import { syncPreviewTerminalLigatures } from '@/components/standalone-terminal/rendering/preview-terminal-ligatures'
import {
buildPreviewAppearanceOptions,
buildPreviewTerminalOptions
-} from '@/components/dashboard-popout/preview-terminal-options'
+} from '@/components/standalone-terminal/rendering/preview-terminal-options'
import { openStandaloneTerminal } from '@/components/standalone-terminal/open-standalone-terminal'
import { removePlainSshPanel } from '@/ssh/plain-ssh-terminal-panels'
import { usePluginFontReady } from '@/store/plugins/appearance/plugin-fonts'
diff --git a/src/renderer/src/components/settings/advanced/ExperimentalPane.tsx b/src/renderer/src/components/settings/advanced/ExperimentalPane.tsx
index e04aafc4feb..e0c64da6566 100644
--- a/src/renderer/src/components/settings/advanced/ExperimentalPane.tsx
+++ b/src/renderer/src/components/settings/advanced/ExperimentalPane.tsx
@@ -9,7 +9,7 @@ import { getExperimentalSearchEntry } from './experimental-search'
import { HiddenExperimentalGroup } from './HiddenExperimentalGroup'
import { NumberField, SettingsSegmentedControl } from '../form-controls/SettingsFormControls'
import { useTranslate } from '@/i18n/use-translate'
-import { AgentDashboardExperimentalSetting } from '../../dashboard/AgentDashboardExperimentalSetting'
+import { AgentDashboardExperimentalSetting } from '../../dashboard/settings/AgentDashboardExperimentalSetting'
import { EphemeralVmsExperimentalSetting } from '../virtual-machines/recipes/EphemeralVmsExperimentalSetting'
import { WslHostsExperimentalSetting } from '../wsl/hosts/WslHostsExperimentalSetting'
import { wslHostsAvailable } from '../../sidebar/use-wsl-hosts-enabled'
diff --git a/src/renderer/src/components/sidebar/AgentDashboardSidebarEntry.tsx b/src/renderer/src/components/sidebar/AgentDashboardSidebarEntry.tsx
index 020775a907b..3977f79f28c 100644
--- a/src/renderer/src/components/sidebar/AgentDashboardSidebarEntry.tsx
+++ b/src/renderer/src/components/sidebar/AgentDashboardSidebarEntry.tsx
@@ -3,8 +3,8 @@ import { useAppStore } from '@/store/index'
import { cn } from '@/lib/class-names'
import { AgentQuestionIcon } from '@/components/AgentQuestionIcon'
import { DASHBOARD_BUCKET_ORDER, type DashboardBucket } from '../../../../shared/dashboard-snapshot'
-import { useAgentBucketCounts } from '@/components/dashboard/useAgentBucketCounts'
-import { resolveAgentDashboardMode } from '@/components/dashboard/agent-dashboard-open-mode'
+import { useAgentBucketCounts } from '@/components/dashboard/bucket-counts/useAgentBucketCounts'
+import { resolveAgentDashboardMode } from '@/components/dashboard/settings/agent-dashboard-open-mode'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/WorkerLineage.fixture.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/WorkerLineage.fixture.tsx
index 263a3ce5c47..69584c7581a 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/WorkerLineage.fixture.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/WorkerLineage.fixture.tsx
@@ -1,6 +1,6 @@
import { defineFixture } from '../../../../component-fixtures/component-fixture'
import WorktreeCardAgents from './WorktreeCardAgents'
-import { applyAgentRowLineage } from '../../../dashboard/agent-row-lineage'
+import { applyAgentRowLineage } from '../../../dashboard/rows/agent-row-lineage'
import { buildWorktreeAgentRows } from './worktree-agent-rows'
import { structuredAgentSessionPaneKey } from '../../../../../../shared/agent-session/view/structured-agent-session-projection'
import type { Tab } from '../../../../../../shared/tab-types'
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/WorktreeCardAgents.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/WorktreeCardAgents.tsx
index 63567d83a79..f2728c5177e 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/WorktreeCardAgents.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/WorktreeCardAgents.tsx
@@ -3,7 +3,7 @@ import DashboardAgentRow from '@/components/dashboard/DashboardAgentRow'
import { useNow } from '@/hooks/use-now'
import { useWorktreeAgentRows } from './useWorktreeAgentRows'
import { cn } from '@/lib/class-names'
-import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/rows/dashboard-agent-row'
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
import { CompactAgentExpansion, CompactAgentSummaryButton } from './worktree-card-compact-agents'
import { CompactAgentRow } from './worktree-card-compact-agent-row'
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/mirrored-terminal-lineage.test-fixture.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/mirrored-terminal-lineage.test-fixture.tsx
deleted file mode 100644
index dde0c977cac..00000000000
--- a/src/renderer/src/components/sidebar/workspace-card/activity/mirrored-terminal-lineage.test-fixture.tsx
+++ /dev/null
@@ -1,359 +0,0 @@
-import { act } from 'react'
-import { expect, vi } from 'vitest'
-import type { RuntimeAgentLineageTestModule } from '../../../../../../shared/runtime-agent-lineage-consumer.test-fixture'
-import { isStructuredTab } from '../../../../../../shared/agent-session/view/structured-agent-session-tab'
-import { useAppStore } from '@/store'
-import type { AppState } from '@/store/types'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
-import { makePaneKey } from '../../../../../../shared/workspace-layout/stable-pane-id'
-import type { AgentStatusEntry } from '../../../../../../shared/agent-status/contracts/agent-status-types'
-import { makeTab } from '@/store/terminals/testing/terminal-tab.test-fixture'
-
-// Load the native graph during collection; fixture state stays per test.
-const {
- default: { publicLineageFixture, canonicalChatRow, LINEAGE_PARENT, LINEAGE_CHILD }
-} = await vi.importActual<{ default: RuntimeAgentLineageTestModule }>(
- '../../../../../../main/runtime/workspaces/process-listing/runtime-agent-lineage.test-fixture'
-)
-
-const LEAF = '11111111-1111-4111-8111-111111111111'
-
-export async function verifyMirroredTerminalLineage(
- kind: 'default' | 'reserved' | 'collision',
- {
- patch,
- entry,
- mount,
- getRows
- }: {
- patch: (update: Partial) => void
- entry: (key: string, worktree: string) => AgentStatusEntry
- mount: () => void
- getRows: () => DashboardAgentRow[]
- }
-): Promise {
- const { makeUnifiedTab } = await import('@/store/slices/tabs/tab-group.test-fixture'),
- { makeWorktree } = await import('@/store/slices/worktrees/worktree.test-fixture'),
- { TEST_REPO } = await import('@/store/repos/repo.test-fixture')
- const { selectDashboardOrchestration } =
- await import('@/components/dashboard/dashboard-orchestration-selection')
- const { selectWorktreeAgentRowsCached } =
- await import('@/components/dashboard/worktree-agent-rows-cache')
- const { buildActivityEvents } = await import('@/components/activity/activity-event-builder')
- const { buildMirroredAgentTabs } =
- await import('@/runtime/web-session-tabs-sync/terminal-surfaces')
- const { buildMirroredAgentStatusPatch } =
- await import('@/runtime/web-session-tabs-sync/agent-status-patch')
- const { buildTerminalUnifiedTab } = await import('@/runtime/web-session-tabs-sync/tab-builders')
- const { toRemoteRuntimePtyId } =
- await import('../../../../../../shared/remote-runtime/remote-runtime-pty-id')
- const { toWebTerminalSurfaceTabId } =
- await import('../../../../../../shared/workspace-layout/terminal-surface-id')
- const { projectStatus } =
- await import('@/components/native-chat/session/status/structured-agent-session-status-projection')
- const { structuredAgentSessionPaneKey: pane, structuredAgentSessionTabId: tabId } =
- await import('../../../../../../shared/agent-session/view/structured-agent-session-projection')
- const { selectVisibleAgentOrchestration } =
- await import('@/runtime/agent-orchestration/visible-agent-orchestration')
- const worktree = makeWorktree({ id: 'folder-a', repoId: TEST_REPO.id })
- patch({ repos: [TEST_REPO], worktreesByRepo: { [TEST_REPO.id]: [worktree] } })
- const f = publicLineageFixture('folder-a')
- try {
- const context = f.projection.buildByPaneKey()?.[canonicalChatRow(LINEAGE_CHILD)]
- if (!context) {
- throw new Error('Missing production context')
- }
- const native = buildMirroredAgentTabs(
- {
- worktree: 'folder-a',
- publicationEpoch: 'test',
- snapshotVersion: 1,
- activeGroupId: 'g',
- activeTabId: null,
- activeTabType: 'agent-session',
- tabs: [
- {
- type: 'agent-session',
- id: 'host-parent',
- title: 'Parent',
- sessionId: LINEAGE_PARENT,
- replacesSessionId: 'previous',
- agent: 'codex',
- isActive: false
- }
- ]
- },
- 'runtime:paired',
- new Map(),
- 'g',
- 0,
- kind === 'default'
- ? []
- : [
- makeUnifiedTab({
- id: kind === 'reserved' ? 'reserved-parent' : tabId(LINEAGE_PARENT),
- entityId: kind === 'reserved' ? 'previous' : 'occupied',
- worktreeId: 'folder-a',
- groupId: 'g',
- contentType: 'agent-session'
- })
- ],
- Date.now()
- )[0].unifiedTab
- if (!isStructuredTab(native)) {
- throw new Error('Expected the mirrored native session tab')
- }
- const actualParent = pane(native.id, LINEAGE_PARENT)
- const tab = makeTab({
- id: toWebTerminalSurfaceTabId('host-terminal'),
- worktreeId: 'folder-a',
- ptyId: toRemoteRuntimePtyId('terminal', 'paired')
- })
- const worker = makePaneKey(tab.id, LEAF)
- const terminal = buildTerminalUnifiedTab(tab, 'g', 'paired')
- patch({
- tabsByWorktree: { 'folder-a': [tab] },
- unifiedTabsByWorktree: {},
- ptyIdsByTabId: {},
- agentStatusByPaneKey: {},
- retainedAgentsByPaneKey: {},
- runtimeAgentOrchestrationByPaneKey: {}
- })
- const produced = buildMirroredAgentStatusPatch(
- useAppStore.getState(),
- [],
- [
- {
- type: 'terminal',
- id: 'surface',
- title: 'Worker',
- parentTabId: 'host-terminal',
- leafId: LEAF,
- terminal: 'terminal',
- status: 'ready',
- isActive: false,
- agentStatus: {
- ...entry(makePaneKey('host-terminal', LEAF), 'folder-a'),
- orchestration: context
- }
- }
- ],
- [],
- 'paired',
- 'folder-a',
- new Set(),
- Date.now()
- )
- if (!produced) {
- throw new Error('Missing mirrored terminal status')
- }
- patch(produced)
- expect(useAppStore.getState().agentStatusByPaneKey[worker].connectionId).toBe('paired')
- // Status before graph: the missing parent stays rooted, then tab hydration alone joins it.
- expect(selectVisibleAgentOrchestration(useAppStore.getState())[worker]?.parentPaneKey).not.toBe(
- actualParent
- )
- patch({ unifiedTabsByWorktree: { 'folder-a': [native, terminal] } })
- act(() =>
- useAppStore.getState().transactAgentStatuses((transaction) =>
- projectStatus(
- transaction,
- native,
- {
- sessionId: LINEAGE_PARENT,
- workspaceId: 'folder-a',
- agent: 'codex',
- status: 'working',
- latestPrompt: 'Parent',
- updatedAt: Date.now(),
- hostExecutionOwned: true
- },
- 'live',
- null
- )
- )
- )
- mount()
- const assertNested = (parent = actualParent) => {
- expect(getRows().map((row) => row.paneKey)).toEqual([parent, worker])
- expect(getRows().map((row) => row.lineage?.depth)).toEqual([0, 1])
- expect(getRows()[1].entry.orchestration?.parentPaneKey).toBe(parent)
- expect(
- useAppStore.getState().agentStatusByPaneKey[worker]?.orchestration?.parentPaneKey ??
- context.parentPaneKey
- ).toBe(canonicalChatRow(LINEAGE_PARENT))
- const state = useAppStore.getState()
- const orchestration =
- selectDashboardOrchestration(state, [{ worktree }]).singletonOrchestration ?? {}
- const dashboard = selectWorktreeAgentRowsCached({
- state,
- worktreeId: 'folder-a',
- orchestration,
- now: Date.now(),
- generation: 1
- })
- expect(dashboard.map((row) => [row.paneKey, row.lineage.depth])).toEqual([
- [parent, 0],
- [worker, 1]
- ])
- const activity = buildActivityEvents({
- ...state,
- chatNames: new Map(),
- worktreeMap: new Map([[worktree.id, worktree]]),
- repoMap: new Map([[TEST_REPO.id, TEST_REPO]]),
- runtimeAgentOrchestrationByPaneKey: selectVisibleAgentOrchestration(state),
- now: Date.now()
- })
- expect(activity.paneEntryByPaneKey[worker]?.orchestration?.parentPaneKey).toBe(parent)
- expect(activity.paneEntryByPaneKey[parent]).toBeDefined()
- }
- assertNested()
- // A host-supplied legacy connection spelling does not override actual producer proof.
- patch({
- agentStatusByPaneKey: {
- ...useAppStore.getState().agentStatusByPaneKey,
- [worker]: {
- ...useAppStore.getState().agentStatusByPaneKey[worker],
- connectionId: 'ssh-legacy'
- }
- }
- })
- assertNested()
- const canonical = useAppStore.getState().agentStatusByPaneKey[worker]
- const epoch = useAppStore.getState().agentStatusEpoch
- // Graph before parent hydration; matching task/Dispatch producer remains valid with nonempty connectionId.
- const parentEntry = useAppStore.getState().agentStatusByPaneKey[actualParent]
- patch({
- unifiedTabsByWorktree: { 'folder-a': [terminal] },
- agentStatusByPaneKey: { [worker]: canonical }
- })
- act(() =>
- useAppStore
- .getState()
- .setRuntimeAgentOrchestrationByPaneKey(
- { [worker]: structuredClone(context) },
- 'runtime:paired'
- )
- )
- expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
- patch({
- unifiedTabsByWorktree: { 'folder-a': [native, terminal] },
- agentStatusByPaneKey: { [worker]: canonical, [actualParent]: parentEntry }
- })
- assertNested()
- act(() => useAppStore.getState().setRuntimeAgentOrchestrationByPaneKey({}))
- // PTY-only hydration is an independent cache input; disagreeing tab/PTY facts must not join.
- patch({
- unifiedTabsByWorktree: { 'folder-a': [native] },
- tabsByWorktree: { 'folder-a': [{ ...tab, ptyId: null }] }
- })
- expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
- patch({ ptyIdsByTabId: { [tab.id]: [tab.ptyId ?? ''] } })
- assertNested()
- patch({
- unifiedTabsByWorktree: {
- 'folder-a': [native, { ...terminal, executionHostId: 'runtime:other' }]
- }
- })
- expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
- patch({
- unifiedTabsByWorktree: {
- 'folder-a': [{ ...native, executionHostId: 'runtime:other' }, terminal]
- },
- ptyIdsByTabId: {}
- })
- expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
- patch({ unifiedTabsByWorktree: { 'folder-a': [native, terminal] } })
- assertNested()
- // Retained snapshot proves the runtime after the current terminal tab/PTY/unified row disappears.
- act(() =>
- useAppStore.getState().retainAgents([
- {
- entry: { ...canonical, state: 'done' },
- worktreeId: 'folder-a',
- tab,
- agentType: 'claude',
- startedAt: Date.now()
- }
- ])
- )
- patch({
- agentStatusByPaneKey: {
- [actualParent]: useAppStore.getState().agentStatusByPaneKey[actualParent]
- },
- tabsByWorktree: {},
- unifiedTabsByWorktree: { 'folder-a': [native] },
- ptyIdsByTabId: {}
- })
- assertNested()
- patch({ unifiedTabsByWorktree: { 'folder-a': [{ ...native, id: 'remapped-parent' }] } })
- // The parent status arrives independently; canonical retained context did not change or need an RPC.
- patch({
- agentStatusByPaneKey: {
- [pane('remapped-parent', LINEAGE_PARENT)]: {
- ...useAppStore.getState().agentStatusByPaneKey[actualParent],
- paneKey: pane('remapped-parent', LINEAGE_PARENT),
- tabId: 'remapped-parent'
- }
- }
- })
- assertNested(pane('remapped-parent', LINEAGE_PARENT))
- expect(
- useAppStore.getState().retainedAgentsByPaneKey[worker].entry.orchestration?.parentPaneKey
- ).toBe(canonicalChatRow(LINEAGE_PARENT))
- expect(useAppStore.getState().agentStatusEpoch).toBeGreaterThanOrEqual(epoch)
- // Removing the only retained PTY proof cannot infer a runtime from its connection spelling.
- patch({
- retainedAgentsByPaneKey: {
- [worker]: {
- ...useAppStore.getState().retainedAgentsByPaneKey[worker],
- tab: { ...tab, ptyId: null }
- }
- }
- })
- expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
- expect(new Set(getRows().map((row) => row.paneKey)).size).toBe(getRows().length)
- // Matching graph context identifies its producer even when terminal routing names another host.
- const renamedParent = pane('remapped-parent', LINEAGE_PARENT)
- patch({
- retainedAgentsByPaneKey: {},
- tabsByWorktree: { 'folder-a': [tab] },
- unifiedTabsByWorktree: {
- 'folder-a': [{ ...native, id: 'remapped-parent', executionHostId: 'local' }, terminal]
- },
- agentStatusByPaneKey: {
- [renamedParent]: useAppStore.getState().agentStatusByPaneKey[renamedParent],
- [worker]: canonical
- }
- })
- act(() =>
- useAppStore
- .getState()
- .setRuntimeAgentOrchestrationByPaneKey({ [worker]: structuredClone(context) })
- )
- assertNested(renamedParent)
- act(() =>
- useAppStore
- .getState()
- .setRuntimeAgentOrchestrationByPaneKey(
- { [worker]: structuredClone(context) },
- 'runtime:other'
- )
- )
- expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
- act(() => useAppStore.getState().setRuntimeAgentOrchestrationByPaneKey({}))
- patch({
- tabsByWorktree: { 'folder-a': [{ ...tab, ptyId: 'remote:%zz@@terminal' }] },
- unifiedTabsByWorktree: {
- 'folder-a': [{ ...native, id: 'remapped-parent', executionHostId: 'local' }]
- },
- agentStatusByPaneKey: {
- ...useAppStore.getState().agentStatusByPaneKey,
- [worker]: { ...canonical, connectionId: undefined }
- }
- })
- expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
- } finally {
- f.db.close()
- }
-}
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/use-worktree-agent-list-model.ts b/src/renderer/src/components/sidebar/workspace-card/activity/use-worktree-agent-list-model.ts
index 3f0f72965d8..c3e3c5bed09 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/use-worktree-agent-list-model.ts
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/use-worktree-agent-list-model.ts
@@ -4,8 +4,8 @@ import { useCallback, useMemo } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { useAppStore } from '@/store'
import { deriveRunningAgentSendTargets } from '@/lib/agent-session/messaging/running-agent-targets'
-import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
-import { buildAgentRowLineageTree } from '@/components/dashboard/agent-row-lineage-model'
+import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/rows/dashboard-agent-row'
+import { buildAgentRowLineageTree } from '@/components/dashboard/rows/agent-row-lineage-model'
import { useTranslate } from '@/i18n/use-translate'
import { agentSendDisabledReasonLabel } from '@/lib/agent-session/messaging/agent-send-disabled-reason-label'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/use-worktree-agent-rows-subscriptions.test.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/use-worktree-agent-rows-subscriptions.test.tsx
index bb451230f5d..9083934fd71 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/use-worktree-agent-rows-subscriptions.test.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/use-worktree-agent-rows-subscriptions.test.tsx
@@ -6,7 +6,7 @@ import { useAppStore } from '@/store'
import { makeTab } from '@/store/terminals/testing/terminal-tab.test-fixture'
import type { AppState } from '@/store/types'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
import { makePaneKey } from '../../../../../../shared/workspace-layout/stable-pane-id'
import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../../../shared/agent-status/contracts/agent-status-freshness'
import type { AgentStatusEntry } from '../../../../../../shared/agent-status/contracts/agent-status-types'
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-row-order.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-row-order.ts
index 053a2dd6511..3066218529f 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-row-order.ts
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-row-order.ts
@@ -1,4 +1,4 @@
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
function comparableNumber(value: number | undefined, fallback = 0): number {
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-rows-projection.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-rows-projection.ts
index 156e9f0848a..1a33b9fd97d 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-rows-projection.ts
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-rows-projection.ts
@@ -3,8 +3,8 @@ import { isExecutionHostAliasForWorktree } from '@/lib/workspace/ownership/workt
import { structuredAgentSessionOwnerInputs } from '@/runtime/native-chat/session-ownership/structured-agent-session-owner'
import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields'
import { getStructuredAgentSessionTabsByPaneKey } from '@/components/native-chat/session/status/structured-agent-session-tabs'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
-import { applyAgentRowLineage } from '@/components/dashboard/agent-row-lineage'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
+import { applyAgentRowLineage } from '@/components/dashboard/rows/agent-row-lineage'
import { migrationUnsupportedToAgentStatusEntry } from '../../../../../../shared/migration-unsupported-agent-entry'
import type { AppState } from '@/store/types'
import {
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-rows.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-rows.ts
index bba87b3a15b..756342b70c8 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-rows.ts
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-rows.ts
@@ -1,6 +1,6 @@
import type { Tab } from '../../../../../../shared/tab-types'
import { parseAgentStatusPaneIdentity } from '../../../../../../shared/agent-status/identity/agent-status-worktree-attribution'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
import {
isAgentStatusFresh,
AGENT_STATUS_STALE_AFTER_MS
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-ack-inputs.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-ack-inputs.ts
index 2bd4b99b67f..96387186ebe 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-ack-inputs.ts
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-ack-inputs.ts
@@ -1,9 +1,9 @@
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
import { isAgentSubjectUnread } from '@/attention/agent-attention-acknowledgement'
import {
readAgentRowTurnRecords,
type AgentRowUnreadState
-} from '@/components/dashboard/dashboard-row-bucket'
+} from '@/components/dashboard/rows/dashboard-row-bucket'
export function selectAgentRowsUnread(
state: AgentRowUnreadState,
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-summary.test.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-summary.test.ts
index 1fbb314f034..4eaeeac70ea 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-summary.test.ts
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-summary.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
import { buildSummaryAgentGroups } from './worktree-card-agent-summary'
function row(paneKey: string, state: 'done' | 'working' | 'waiting'): DashboardAgentRow {
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-summary.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-summary.ts
index 6f3fe9f40f8..12d542f826b 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-summary.ts
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-agent-summary.ts
@@ -1,5 +1,5 @@
import type { AgentDotState } from '@/components/AgentStateDot'
-import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/rows/dashboard-agent-row'
import { formatAgentTypeLabel } from '../../../../../../shared/agent-status/presentation/agent-type-label'
import { agentRowDisplayDotState as getAgentDotState } from '@/lib/agent-status/presentation/agent-row-dot-state'
import { translate } from '@/i18n/i18n'
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-line-order.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-line-order.ts
index b11cfa12ef1..8838e5ed25c 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-line-order.ts
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-line-order.ts
@@ -1,4 +1,4 @@
-import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/rows/dashboard-agent-row'
import { agentRowStoppingLabel } from '@/lib/agent-status/presentation/agent-row-stopping-label'
import type { Translate } from '@/i18n/use-translate'
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.stable-message.test.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.stable-message.test.tsx
index d323e01c3f7..01861d6323c 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.stable-message.test.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.stable-message.test.tsx
@@ -2,13 +2,13 @@
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it, vi } from 'vitest'
-import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/rows/dashboard-agent-row'
import { TooltipProvider } from '@/components/ui/tooltip'
import { CompactAgentRow } from './worktree-card-compact-agent-row'
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
-vi.mock('@/components/dashboard/use-agent-row-conversation-name', () => ({
+vi.mock('@/components/dashboard/rows/use-agent-row-conversation-name', () => ({
useAgentRowConversationName: () => null
}))
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.tsx
index 6f544aff741..612199be03c 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.tsx
@@ -1,20 +1,20 @@
import React, { useCallback, useEffect, useRef } from 'react'
-import { DashboardAgentChildDisclosure } from '@/components/dashboard/DashboardAgentChildDisclosure'
+import { DashboardAgentChildDisclosure } from '@/components/dashboard/rows/DashboardAgentChildDisclosure'
import { AgentStateDot, agentStateLabel, type AgentDotState } from '@/components/AgentStateDot'
import { AgentChildRowContent } from '@/components/AgentChildRowContent'
-import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/rows/dashboard-agent-row'
import { AgentTypeIcon } from '@/components/agent/icons/agent-type-icon'
import { formatAgentTypeLabel } from '../../../../../../shared/agent-status/presentation/agent-type-label'
import { cn } from '@/lib/class-names'
import { viewClaudeAgentFromRow } from '@/lib/agent-session/navigation/claude-agent-row-view'
-import { useClaudeAgentRowView } from '@/components/dashboard/use-claude-agent-row-view'
+import { useClaudeAgentRowView } from '@/components/dashboard/rows/use-claude-agent-row-view'
import './worktree-card-agent-summary'
import { agentRowDisplayDotState as getAgentDotState } from '../../../../lib/agent-status/presentation/agent-row-dot-state'
import { getAgentRowPrimaryText } from '../../../../../../shared/agent-status/presentation/agent-row-primary-text'
import { formatAgentToolPreview } from '@/lib/agent-status/presentation/agent-row-tool-preview'
import { agentNoUpdateLabel } from '../../../../../../shared/agent-status/presentation/agent-row-decay-state'
-import { useAgentRowConversationName } from '@/components/dashboard/use-agent-row-conversation-name'
-import { lastEnteredDoneAt } from '@/components/dashboard/agent-finished-timestamp'
+import { useAgentRowConversationName } from '@/components/dashboard/rows/use-agent-row-conversation-name'
+import { lastEnteredDoneAt } from '@/components/dashboard/rows/agent-finished-timestamp'
import CacheTimer, { usePromptCacheCountdownForPane } from './CacheTimer'
import { formatShortTimeAgo } from '@/lib/time-format/short-time-ago'
import { agentVerdictStatusLine } from '@/lib/agent-status/presentation/agent-verdict-status-line'
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agents.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agents.tsx
index 9eca5cbb793..8231c89173e 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agents.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agents.tsx
@@ -1,7 +1,7 @@
import React, { useCallback, useRef } from 'react'
import { ChevronDown } from 'lucide-react'
import { AgentStateDot } from '@/components/AgentStateDot'
-import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/rows/dashboard-agent-row'
import { AgentTypeIcon } from '@/components/agent/icons/agent-type-icon'
import { cn } from '@/lib/class-names'
import {
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.test.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.test.ts
index ccab467020a..8a2cdcd0874 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.test.ts
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
import { buildCollapsedChildAgentSignalStates } from './worktree-card-idle-child-agents'
function row(paneKey: string, state: DashboardAgentRow['state']): DashboardAgentRow {
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.tsx
index 549b47770eb..f5ecf1eb1e1 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.tsx
@@ -1,7 +1,7 @@
import React, { useCallback } from 'react'
import { ChevronDown } from 'lucide-react'
import { AgentStateDot } from '@/components/AgentStateDot'
-import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/rows/dashboard-agent-row'
import { cn } from '@/lib/class-names'
import { useTranslate } from '@/i18n/use-translate'
import { CompactAgentExpansion } from './worktree-card-compact-agents'
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-subagent-child-rows.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-subagent-child-rows.ts
index 25a4b6236df..903df04a7ac 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-subagent-child-rows.ts
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-subagent-child-rows.ts
@@ -1,4 +1,4 @@
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
import type { AgentRowState } from '../../../../../../shared/agent-status/presentation/agent-row-decay-state'
import type { AgentStatusEntry } from '../../../../../../shared/agent-status/contracts/agent-status-types'
import {
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-title-derived-agent-rows.ts b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-title-derived-agent-rows.ts
index 97db8a57b2a..6615e626e71 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-title-derived-agent-rows.ts
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-title-derived-agent-rows.ts
@@ -1,5 +1,5 @@
import type { TitleDerivedAgentRowsInput } from './title-derived-agent-row-input'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
import { formatAgentTypeLabel } from '../../../../../../shared/agent-status/presentation/agent-type-label'
import {
isClaudeManagementTitle,
diff --git a/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-title-status.tsx b/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-title-status.tsx
index 3cef077640d..79a46c5cedc 100644
--- a/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-title-status.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-title-status.tsx
@@ -1,7 +1,7 @@
import React, { createContext, useCallback, useContext, useMemo } from 'react'
import { useShallow } from 'zustand/react/shallow'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
import { useNow } from '@/hooks/use-now'
import { useAppStore } from '@/store'
import type { WorktreeStatus } from '../../../../../../shared/worktree-status-rollup'
diff --git a/src/renderer/src/components/sidebar/worktree-card-slot-agent-signals.ts b/src/renderer/src/components/sidebar/worktree-card-slot-agent-signals.ts
index 86cbc3c69e6..02ea212e0fe 100644
--- a/src/renderer/src/components/sidebar/worktree-card-slot-agent-signals.ts
+++ b/src/renderer/src/components/sidebar/worktree-card-slot-agent-signals.ts
@@ -1,7 +1,7 @@
import type { AgentRowState } from '../../../../shared/agent-status/presentation/agent-row-decay-state'
import type { AgentChildRowModel } from '../../../../shared/agent-status/presentation/agent-child-row-model'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
-import { buildAgentRowLineageTree } from '@/components/dashboard/agent-row-lineage-model'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
+import { buildAgentRowLineageTree } from '@/components/dashboard/rows/agent-row-lineage-model'
import { agentRowDisplayDotState } from '@/lib/agent-status/presentation/agent-row-dot-state'
import { resolveRowProgramStatus } from '@/lib/program-status/agent-row-presentation'
import { classifySlotLine, slotLineAt, type SlotLineSignal } from './worktree-card-slot-signal'
diff --git a/src/renderer/src/components/sidebar/worktree-card-slot-rollup.ts b/src/renderer/src/components/sidebar/worktree-card-slot-rollup.ts
index cc5000e9625..da386900f10 100644
--- a/src/renderer/src/components/sidebar/worktree-card-slot-rollup.ts
+++ b/src/renderer/src/components/sidebar/worktree-card-slot-rollup.ts
@@ -1,6 +1,6 @@
import { getResolvedExecutionHostIdForWorktree } from '@/lib/workspace/ownership/resolved-worktree-execution-host'
import { createLineageSlotReader, type WorkspaceSlot } from './worktree-card-slot-range'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields'
import {
toLineageSlotRecord,
diff --git a/src/renderer/src/components/sidebar/worktree-card-slot-signal.ts b/src/renderer/src/components/sidebar/worktree-card-slot-signal.ts
index 2ab2f50af36..da591ed69cc 100644
--- a/src/renderer/src/components/sidebar/worktree-card-slot-signal.ts
+++ b/src/renderer/src/components/sidebar/worktree-card-slot-signal.ts
@@ -1,6 +1,6 @@
import type { AgentDotState } from '@/components/AgentStateDot'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
-import { lastEnteredDoneAt } from '@/components/dashboard/agent-finished-timestamp'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
+import { lastEnteredDoneAt } from '@/components/dashboard/rows/agent-finished-timestamp'
/** What one agent line means for the experimental card, in slot priority order. */
export type SlotLineKind = 'needs' | 'failed' | 'unread' | 'working' | 'seen' | 'idle'
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-app-menu-clipboard.ts b/src/renderer/src/components/standalone-terminal/input/preview-terminal-app-menu-clipboard.ts
similarity index 100%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-app-menu-clipboard.ts
rename to src/renderer/src/components/standalone-terminal/input/preview-terminal-app-menu-clipboard.ts
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge.ts b/src/renderer/src/components/standalone-terminal/input/preview-terminal-ime-bridge.ts
similarity index 100%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge.ts
rename to src/renderer/src/components/standalone-terminal/input/preview-terminal-ime-bridge.ts
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-key-handler.ts b/src/renderer/src/components/standalone-terminal/input/preview-terminal-key-handler.ts
similarity index 98%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-key-handler.ts
rename to src/renderer/src/components/standalone-terminal/input/preview-terminal-key-handler.ts
index f46fa9d48e3..951ee33aa8e 100644
--- a/src/renderer/src/components/dashboard-popout/preview-terminal-key-handler.ts
+++ b/src/renderer/src/components/standalone-terminal/input/preview-terminal-key-handler.ts
@@ -1,7 +1,7 @@
import type { Terminal } from '@xterm/xterm'
import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform'
-import { keybindingMatchesAction } from '../../../../shared/keybindings'
-import { useAppStore } from '@/store'
+import { keybindingMatchesAction } from '../../../../../shared/keybindings/matching'
+import { useAppStore } from '@/store/index'
import { isCurrentPlatformIosWeb } from '@/app-shell/platform/ios-web-platform'
import {
getLayoutCharacterForCode,
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-paste.ts b/src/renderer/src/components/standalone-terminal/input/preview-terminal-paste.ts
similarity index 97%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-paste.ts
rename to src/renderer/src/components/standalone-terminal/input/preview-terminal-paste.ts
index aa757c946b3..38c0eb8cb50 100644
--- a/src/renderer/src/components/dashboard-popout/preview-terminal-paste.ts
+++ b/src/renderer/src/components/standalone-terminal/input/preview-terminal-paste.ts
@@ -6,7 +6,7 @@ import { resolveTerminalPasteRuntime } from '@/components/terminal-pane/input/pa
import { TERMINAL_PASTE_MAX_BYTES } from '@/components/terminal-pane/input/paste/terminal-paste-limits'
import { pasteTerminalText } from '@/components/terminal-pane/input/paste/terminal-bracketed-paste'
import { isTerminalPanePasteFocusCurrent } from '@/components/terminal-pane/input/paste/terminal-paste-target-state'
-import type { DashboardCardTerminalInput } from '../../../../shared/dashboard-snapshot'
+import type { DashboardCardTerminalInput } from '../../../../../shared/dashboard-snapshot'
export type PreviewTerminalPasteSource = 'keyboard' | 'app-menu' | 'right-click'
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-right-click-paste.ts b/src/renderer/src/components/standalone-terminal/input/preview-terminal-right-click-paste.ts
similarity index 100%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-right-click-paste.ts
rename to src/renderer/src/components/standalone-terminal/input/preview-terminal-right-click-paste.ts
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-shortcuts.ts b/src/renderer/src/components/standalone-terminal/input/preview-terminal-shortcuts.ts
similarity index 89%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-shortcuts.ts
rename to src/renderer/src/components/standalone-terminal/input/preview-terminal-shortcuts.ts
index 60384f71492..e4a08d0e0dc 100644
--- a/src/renderer/src/components/dashboard-popout/preview-terminal-shortcuts.ts
+++ b/src/renderer/src/components/standalone-terminal/input/preview-terminal-shortcuts.ts
@@ -3,12 +3,12 @@ import type { MacOptionAsAlt } from '@/components/terminal-pane/input/keyboard/t
import type { TerminalShortcutAction } from '@/components/terminal-pane/input/keyboard/terminal-shortcut-policy'
import { getLayoutCharacterForCode } from '@/lib/keyboard-layout/layout-base-character'
import type { OptionKeyLocationState } from '@/lib/keyboard-layout/option-key-location-state'
-import type { DashboardCardTerminalInput } from '../../../../shared/dashboard-snapshot'
-import {
- normalizeTerminalShortcutPolicy,
- type KeybindingOverrides,
- type TerminalShortcutPolicy
-} from '../../../../shared/keybindings'
+import type { DashboardCardTerminalInput } from '../../../../../shared/dashboard-snapshot'
+import { normalizeTerminalShortcutPolicy } from '../../../../../shared/keybindings/effective'
+import type {
+ KeybindingOverrides,
+ TerminalShortcutPolicy
+} from '../../../../../shared/keybindings/types'
export type PreviewShortcutContext = {
clientPlatform: NodeJS.Platform
diff --git a/src/renderer/src/components/standalone-terminal/open-standalone-terminal.ts b/src/renderer/src/components/standalone-terminal/open-standalone-terminal.ts
index ff2aa6e25d4..96947d81bed 100644
--- a/src/renderer/src/components/standalone-terminal/open-standalone-terminal.ts
+++ b/src/renderer/src/components/standalone-terminal/open-standalone-terminal.ts
@@ -10,13 +10,13 @@ import type { MacOptionAsAlt } from '@/components/terminal-pane/input/keyboard/t
import { subscribeToTerminalUserInput } from '@/components/terminal-pane/input/terminal-user-input-signal'
import { installTerminalNativeCopyGutterTrim } from '@/components/terminal-pane/clipboard/terminal-native-copy-gutter'
import { isWindowsUserAgent } from '@/app-shell/platform/user-agent-platform'
-import type { PreviewTerminalLinkHandlers } from '../dashboard-popout/preview-terminal-links'
-import { installPreviewTerminalCompatibility } from '../dashboard-popout/preview-terminal-compatibility'
-import { createPreviewClipboardPaster } from '../dashboard-popout/preview-terminal-paste'
-import { installPreviewImeBridge } from '../dashboard-popout/preview-terminal-ime-bridge'
-import { installPreviewTerminalKeyHandler } from '../dashboard-popout/preview-terminal-key-handler'
-import { installPreviewTerminalAppMenuClipboard } from '../dashboard-popout/preview-terminal-app-menu-clipboard'
-import { installPreviewTerminalRightClickPaste } from '../dashboard-popout/preview-terminal-right-click-paste'
+import type { PreviewTerminalLinkHandlers } from './rendering/preview-terminal-links'
+import { installPreviewTerminalCompatibility } from './rendering/preview-terminal-compatibility'
+import { createPreviewClipboardPaster } from './input/preview-terminal-paste'
+import { installPreviewImeBridge } from './input/preview-terminal-ime-bridge'
+import { installPreviewTerminalKeyHandler } from './input/preview-terminal-key-handler'
+import { installPreviewTerminalAppMenuClipboard } from './input/preview-terminal-app-menu-clipboard'
+import { installPreviewTerminalRightClickPaste } from './input/preview-terminal-right-click-paste'
export type StandaloneTerminalArgs = {
container: HTMLElement
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-compatibility.ts b/src/renderer/src/components/standalone-terminal/rendering/preview-terminal-compatibility.ts
similarity index 94%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-compatibility.ts
rename to src/renderer/src/components/standalone-terminal/rendering/preview-terminal-compatibility.ts
index 00640ae9cdc..15909195f5f 100644
--- a/src/renderer/src/components/dashboard-popout/preview-terminal-compatibility.ts
+++ b/src/renderer/src/components/standalone-terminal/rendering/preview-terminal-compatibility.ts
@@ -1,7 +1,7 @@
import type { Terminal } from '@xterm/xterm'
import { Unicode11Addon } from '@xterm/addon-unicode11'
-import type { GlobalSettings } from '../../../../shared/global-settings-types'
-import { activateOrcaTerminalUnicodeProvider } from '../../../../shared/terminal/appearance/terminal-unicode-provider'
+import type { GlobalSettings } from '../../../../../shared/global-settings-types'
+import { activateOrcaTerminalUnicodeProvider } from '../../../../../shared/terminal/appearance/terminal-unicode-provider'
import { installWindowsCtrlAltChordRepair } from '@/lib/pane-manager/terminal-protocol/terminal-windows-ctrl-alt-chord-classification'
import { attachTerminalMouseWheelMultiplier } from '@/lib/pane-manager/terminal-input/pane-terminal-mouse-wheel'
import { configureLazyArabicShapingJoiner } from '@/lib/pane-manager/terminal-rendering/terminal-arabic-shaping-joiner'
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-ligatures.ts b/src/renderer/src/components/standalone-terminal/rendering/preview-terminal-ligatures.ts
similarity index 85%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-ligatures.ts
rename to src/renderer/src/components/standalone-terminal/rendering/preview-terminal-ligatures.ts
index 1da2f26a134..eb20de73c90 100644
--- a/src/renderer/src/components/dashboard-popout/preview-terminal-ligatures.ts
+++ b/src/renderer/src/components/standalone-terminal/rendering/preview-terminal-ligatures.ts
@@ -1,6 +1,6 @@
import type { Terminal } from '@xterm/xterm'
-import type { GlobalSettings } from '../../../../shared/global-settings-types'
-import { resolveTerminalLigaturesEnabled } from '../../../../shared/terminal/appearance/terminal-ligatures'
+import type { GlobalSettings } from '../../../../../shared/global-settings-types'
+import { resolveTerminalLigaturesEnabled } from '../../../../../shared/terminal/appearance/terminal-ligatures'
import { TerminalLigaturesAddon } from '@/lib/pane-manager/terminal-rendering/terminal-ligatures-addon'
const ligatureAddonsByTerminal = new WeakMap()
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-links.ts b/src/renderer/src/components/standalone-terminal/rendering/preview-terminal-links.ts
similarity index 95%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-links.ts
rename to src/renderer/src/components/standalone-terminal/rendering/preview-terminal-links.ts
index a129ef4de62..f582eca27cf 100644
--- a/src/renderer/src/components/dashboard-popout/preview-terminal-links.ts
+++ b/src/renderer/src/components/standalone-terminal/rendering/preview-terminal-links.ts
@@ -2,7 +2,7 @@ import type { IBufferRange, ILinkHandler, Terminal } from '@xterm/xterm'
import { WebLinksAddon } from '@xterm/addon-web-links'
import { installGuardedLinkProviderRegistration } from '@/lib/pane-manager/terminal-input/terminal-link-provider-guard'
import { isTerminalLinkDirectActivation as isTerminalHttpLinkActivation } from '@/components/terminal-pane/links/gestures/terminal-link-activation'
-import { TERMINAL_HTTP_URL_REGEX } from '../../../../shared/terminal/links/terminal-http-url-extraction'
+import { TERMINAL_HTTP_URL_REGEX } from '../../../../../shared/terminal/links/terminal-http-url-extraction'
export type PreviewTerminalLinkHandler = Omit & {
activate(event: MouseEvent, uri: string, range?: IBufferRange): void
diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-options.ts b/src/renderer/src/components/standalone-terminal/rendering/preview-terminal-options.ts
similarity index 87%
rename from src/renderer/src/components/dashboard-popout/preview-terminal-options.ts
rename to src/renderer/src/components/standalone-terminal/rendering/preview-terminal-options.ts
index 6726e0dd912..0db68001c3a 100644
--- a/src/renderer/src/components/dashboard-popout/preview-terminal-options.ts
+++ b/src/renderer/src/components/standalone-terminal/rendering/preview-terminal-options.ts
@@ -1,8 +1,8 @@
import type { ITerminalInitOnlyOptions, ITerminalOptions, ITheme } from '@xterm/xterm'
-import type { GlobalSettings } from '../../../../shared/global-settings-types'
-import type { DashboardCardTerminalInput } from '../../../../shared/dashboard-snapshot'
-import { resolveTerminalFontWeights } from '../../../../shared/terminal/appearance/terminal-fonts'
-import { normalizeTerminalLineHeight } from '../../../../shared/terminal/appearance/terminal-line-height-settings'
+import type { GlobalSettings } from '../../../../../shared/global-settings-types'
+import type { DashboardCardTerminalInput } from '../../../../../shared/dashboard-snapshot'
+import { resolveTerminalFontWeights } from '../../../../../shared/terminal/appearance/terminal-fonts'
+import { normalizeTerminalLineHeight } from '../../../../../shared/terminal/appearance/terminal-line-height-settings'
import {
buildDefaultTerminalOptions,
normalizeTerminalFastScrollSensitivity,
@@ -11,9 +11,9 @@ import {
resolveTerminalScrollOnUserInput
} from '@/lib/pane-manager/pane-terminal-options'
import { buildLocalConptyTerminalOptions } from '@/lib/pane-manager/terminal-protocol/windows-pty-compatibility'
-import { buildFontFamily } from '../../../../shared/monospace-font-family'
+import { buildFontFamily } from '../../../../../shared/monospace-font-family'
import { pluginFontFamilyWhenLoaded } from '@/store/plugins/appearance/plugin-fonts'
-import { resolveTerminalMinimumContrastRatio } from '../../../../shared/terminal-themes/terminal-contrast-correction'
+import { resolveTerminalMinimumContrastRatio } from '../../../../../shared/terminal-themes/terminal-contrast-correction'
/** Options a live settings change can write onto an open preview terminal. */
export function buildPreviewAppearanceOptions(
diff --git a/src/renderer/src/components/terminal-workspace-keydown.ts b/src/renderer/src/components/terminal-workspace-keydown.ts
index b7c46169f85..de8665dc4d6 100644
--- a/src/renderer/src/components/terminal-workspace-keydown.ts
+++ b/src/renderer/src/components/terminal-workspace-keydown.ts
@@ -23,7 +23,7 @@ import {
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
import { translate } from '@/i18n/i18n'
import { getKeybindingContext } from './terminal-workspace-model'
-import { isAgentTerminalPreviewTarget } from './dashboard-popout/agent-terminal-preview-target'
+import { isAgentTerminalPreviewTarget } from './dashboard/terminal-preview/agent-terminal-preview-target'
import { resolveTerminalAgentTabShortcut } from './terminal-agent-tab-shortcut'
import { getDetectionTargetIdentityForWorktree } from '@/lib/agent-session/launch/inventory/agent-detection-target-inventory'
import {
diff --git a/src/renderer/src/hooks/ipc-events/agent-dashboard-command.ts b/src/renderer/src/hooks/ipc-events/agent-dashboard-command.ts
index 75882470f1b..6153cd051d1 100644
--- a/src/renderer/src/hooks/ipc-events/agent-dashboard-command.ts
+++ b/src/renderer/src/hooks/ipc-events/agent-dashboard-command.ts
@@ -1,6 +1,6 @@
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { AppState } from '../../store/types'
-import { resolveAgentDashboardMode } from '../../components/dashboard/agent-dashboard-open-mode'
+import { resolveAgentDashboardMode } from '../../components/dashboard/settings/agent-dashboard-open-mode'
export function toggleAgentDashboardFromShortcut(
state: Pick<
diff --git a/src/renderer/src/lib/agent-session/navigation/claude-agent-row-view.test.ts b/src/renderer/src/lib/agent-session/navigation/claude-agent-row-view.test.ts
index 8a6a8754261..415501523a9 100644
--- a/src/renderer/src/lib/agent-session/navigation/claude-agent-row-view.test.ts
+++ b/src/renderer/src/lib/agent-session/navigation/claude-agent-row-view.test.ts
@@ -2,7 +2,7 @@
import { act, renderHook } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
import { useNativeChatOpenSubagent } from '@/components/native-chat/subagents/native-chat-open-subagent'
import { viewClaudeAgentFromRow } from './claude-agent-row-view'
diff --git a/src/renderer/src/lib/agent-session/navigation/claude-agent-row-view.ts b/src/renderer/src/lib/agent-session/navigation/claude-agent-row-view.ts
index 155a24a4270..dfb5f825d07 100644
--- a/src/renderer/src/lib/agent-session/navigation/claude-agent-row-view.ts
+++ b/src/renderer/src/lib/agent-session/navigation/claude-agent-row-view.ts
@@ -1,6 +1,6 @@
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
-import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
+import type { DashboardAgentRow } from '@/components/dashboard/rows/dashboard-agent-row'
import { useAppStore } from '@/store/index'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
import { hostRouteForAuthority } from '@/runtime/rpc/targets/runtime-client-target'
diff --git a/src/renderer/src/runtime/web-session-tabs-sync/agent-status/web-session-tabs-sync-host-tab-retraction-ghost-rows.test.ts b/src/renderer/src/runtime/web-session-tabs-sync/agent-status/web-session-tabs-sync-host-tab-retraction-ghost-rows.test.ts
deleted file mode 100644
index 5617d3f934d..00000000000
--- a/src/renderer/src/runtime/web-session-tabs-sync/agent-status/web-session-tabs-sync-host-tab-retraction-ghost-rows.test.ts
+++ /dev/null
@@ -1,349 +0,0 @@
-/** Host tab retraction must sweep renderer status and plant close markers like local close (STA-4593). Client-owned rows bypass normal mirror deletion and lose their only reachable pane keys at retraction; done rows otherwise become retained ghosts. Exercise real mirror, retention and sidebar logic with fake clocks. */
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import type { RuntimeMobileSessionTabsResult } from '../../../../../shared/runtime-session-contracts'
-import { makePaneKey } from '../../../../../shared/workspace-layout/stable-pane-id'
-import { toWebTerminalSurfaceTabId } from '../../../../../shared/workspace-layout/terminal-surface-id'
-import { getDefaultSettings } from '../../../../../shared/constants'
-import type { AppState } from '../../../store/types'
-import { createTestStore, seedStore } from '../../../store/testing/app-store-test-fixture'
-import { makeWorktree } from '../../../store/slices/worktrees/worktree.test-fixture'
-import {
- markRendererOwnedAgentStatusWrite,
- registerRendererOwnedAgentStatusPane,
- resetRendererOwnedAgentStatusPanesForTests
-} from '../../../components/terminal-pane/agent-status/renderer-owned-agent-status-registry'
-import { applyFreshWebSessionTabsSnapshot } from '../snapshot-api'
-import { resetWebSessionTabsSnapshotFreshnessForTests } from '../tracking-lifecycle'
-import {
- buildRetainedAgentsSyncSnapshot,
- collectRetainedAgentsOnDisappear
-} from '../../../components/dashboard/useRetainedAgents'
-import { buildWorktreeAgentRows } from '../../../components/sidebar/workspace-card/activity/worktree-agent-rows'
-import {
- selectLiveAgentStatusEntriesForWorktree,
- selectRetainedAgentEntriesForWorktree
-} from '../../../components/sidebar/workspace-card/activity/worktree-agent-row-selectors'
-import {
- selectLivePtyIdsForWorktree,
- selectRuntimePaneTitlesForWorktree
-} from '../../../components/sidebar/workspace-card/activity/worktree-card-status-inputs'
-
-// Mirror imports app store singleton; redirect it to this createTestStore harness.
-vi.mock('../../../store/index', () => ({
- useAppStore: {
- setState: vi.fn(),
- getState: vi.fn(() => ({})),
- subscribe: vi.fn(() => () => {})
- }
-}))
-
-const WT = 'repo1::/path/wt1'
-const ENV = 'web-env-1'
-const HOST_EPOCH = 'host-epoch-1'
-const T0 = 1_700_000_000_000
-
-const KEEP_TAB = 'host-tab-keep'
-const RETRACTED_TAB = 'host-tab-reviewer'
-const KEEP_LEAF = '11111111-1111-4111-8111-111111111111'
-const RETRACTED_LEAF = '22222222-2222-4222-8222-222222222222'
-
-type TestStore = ReturnType
-type RetainedSyncAgents = ReturnType['currentAgents']
-
-function mirrorTabId(hostTabId: string): string {
- return toWebTerminalSurfaceTabId(hostTabId)
-}
-
-function mirrorPaneKey(hostTabId: string, leafId: string): string {
- return makePaneKey(mirrorTabId(hostTabId), leafId)
-}
-
-const GHOST_PANE_KEY = mirrorPaneKey(RETRACTED_TAB, RETRACTED_LEAF)
-
-/** hostAgentStatus=true models a host-hook pane; otherwise status comes from client bytes. */
-function makeHostSnapshot(args: {
- snapshotVersion: number
- hostTabIds: readonly string[]
- hostNow: number
- hostAgentStatusTabIds?: readonly string[]
- hostAgentStatusState?: 'working' | 'done'
-}): RuntimeMobileSessionTabsResult {
- const leafByTab: Record = {
- [KEEP_TAB]: KEEP_LEAF,
- [RETRACTED_TAB]: RETRACTED_LEAF
- }
- return {
- worktree: WT,
- publicationEpoch: HOST_EPOCH,
- snapshotVersion: args.snapshotVersion,
- activeGroupId: 'host-group-1',
- activeTabId: `${args.hostTabIds[0]}::${leafByTab[args.hostTabIds[0]!]}`,
- activeTabType: 'terminal',
- tabs: args.hostTabIds.map((hostTabId, index) => ({
- type: 'terminal' as const,
- id: `${hostTabId}::${leafByTab[hostTabId]}`,
- title: hostTabId === RETRACTED_TAB ? 'Review origin/main' : 'Codex',
- parentTabId: hostTabId,
- leafId: leafByTab[hostTabId]!,
- isActive: index === 0,
- launchAgent: 'codex',
- status: 'ready' as const,
- terminal: `terminal-${index + 1}`,
- ...(args.hostAgentStatusTabIds?.includes(hostTabId)
- ? {
- agentStatus: {
- state: args.hostAgentStatusState ?? ('working' as const),
- prompt: `work on ${hostTabId}`,
- updatedAt: args.hostNow,
- stateStartedAt: args.hostNow - 60_000,
- agentType: 'codex',
- paneKey: makePaneKey(hostTabId, leafByTab[hostTabId]!),
- tabId: hostTabId,
- worktreeId: WT,
- stateHistory: []
- }
- }
- : {})
- }))
- }
-}
-
-/** Mirrors applyWebSessionTabsStorePatch: build the patch from live state, then set it. */
-function applyHostSnapshot(
- store: TestStore,
- snapshot: RuntimeMobileSessionTabsResult,
- now: number,
- opts?: { expectChange?: boolean }
-): void {
- vi.setSystemTime(now)
- const state = store.getState()
- const patch = applyFreshWebSessionTabsSnapshot(state, snapshot, ENV, now)
- if (opts?.expectChange !== false) {
- expect(patch, 'host snapshot must pass the freshness gate').not.toBe(state)
- }
- store.setState(patch as Partial)
-}
-
-/** Replay paired pty-connection ownership: claim on transport creation, verify on byte writes, release on unmount. */
-function replayClientByteStatus(
- store: TestStore,
- hostTabId: string,
- leafId: string,
- state: 'working' | 'done',
- clientNow: number
-): () => void {
- vi.setSystemTime(clientNow)
- const paneKey = mirrorPaneKey(hostTabId, leafId)
- const release = registerRendererOwnedAgentStatusPane(paneKey, ENV)
- markRendererOwnedAgentStatusWrite(paneKey)
- store
- .getState()
- .setAgentStatus(
- paneKey,
- { state, prompt: `review on ${hostTabId}`, agentType: 'codex' },
- 'codex',
- undefined,
- {
- tabId: mirrorTabId(hostTabId),
- worktreeId: WT
- }
- )
- return release
-}
-
-/** Byte-identical replay of useRetainedAgentsSync's effect body, minus React. */
-function replayRetainedAgentsSync(
- store: TestStore,
- previousAgents: RetainedSyncAgents,
- now: number
-): RetainedSyncAgents {
- const state = store.getState()
- const { currentAgents, existingWorktreeIds, tabIndex } = buildRetainedAgentsSyncSnapshot({
- repos: state.repos,
- worktreesByRepo: state.worktreesByRepo,
- folderWorkspaces: state.folderWorkspaces,
- tabsByWorktree: state.tabsByWorktree,
- agentStatusByPaneKey: state.agentStatusByPaneKey,
- now
- })
- const { toRetain, consumedSuppressedPaneKeys } = collectRetainedAgentsOnDisappear({
- previousAgents,
- currentAgents,
- retainedAgentsByPaneKey: state.retainedAgentsByPaneKey,
- retentionSuppressedPaneKeys: state.retentionSuppressedPaneKeys,
- recentlyClosedAgentStatusTabIds: state.recentlyClosedAgentStatusTabIds,
- recentlyRetiredAgentStatusPaneKeys: state.recentlyRetiredAgentStatusPaneKeys,
- tabIndex
- })
- store.getState().retainAgents(toRetain)
- store.getState().pruneRetainedAgents(existingWorktreeIds)
- if (consumedSuppressedPaneKeys.length > 0) {
- store.getState().clearRetentionSuppressedPaneKeys(consumedSuppressedPaneKeys)
- }
- return currentAgents
-}
-
-function seedPairedClientStore(): TestStore {
- const store = createTestStore()
- seedStore(store, {
- settings: { ...getDefaultSettings('/tmp'), tabAutoGenerateTitle: true },
- worktreesByRepo: { repo1: [makeWorktree({ id: WT, repoId: 'repo1', path: '/path/wt1' })] },
- activeWorktreeId: WT
- } as Partial)
- return store
-}
-
-type SidebarObservation = {
- tabIds: string[]
- rowPaneKeys: string[]
- retainedPaneKeys: string[]
-}
-
-/** Byte-identical to useWorktreeAgentRows' inputs, minus React. */
-function observeSidebar(store: TestStore, now: number): SidebarObservation {
- const state = store.getState()
- const tabs = state.tabsByWorktree[WT] ?? []
- const rows = buildWorktreeAgentRows({
- tabs,
- entries: selectLiveAgentStatusEntriesForWorktree(state, WT),
- retained: selectRetainedAgentEntriesForWorktree(state, WT),
- runtimePaneTitlesByTabId: selectRuntimePaneTitlesForWorktree(state, WT),
- ptyIdsByTabId: selectLivePtyIdsForWorktree(state, WT),
- terminalLayoutsByTabId: Object.fromEntries(
- tabs.map((tab) => [tab.id, state.terminalLayoutsByTabId[tab.id]])
- ),
- now
- })
- return {
- tabIds: tabs.map((tab) => tab.id),
- rowPaneKeys: rows.filter((row) => row.rowSource !== 'subagent').map((row) => row.paneKey),
- retainedPaneKeys: Object.keys(state.retainedAgentsByPaneKey)
- }
-}
-
-describe('a host-retracted paired tab leaves no ghost agent row behind', () => {
- beforeEach(() => {
- vi.useFakeTimers()
- vi.setSystemTime(T0)
- resetWebSessionTabsSnapshotFreshnessForTests()
- resetRendererOwnedAgentStatusPanesForTests()
- })
-
- afterEach(() => {
- vi.useRealTimers()
- resetRendererOwnedAgentStatusPanesForTests()
- })
-
- it('Mechanism C: a done agent whose tab the host retracted must not become a permanent retained row', () => {
- const store = seedPairedClientStore()
- let previousAgents: RetainedSyncAgents = new Map()
- applyHostSnapshot(
- store,
- makeHostSnapshot({
- snapshotVersion: 1,
- hostTabIds: [KEEP_TAB, RETRACTED_TAB],
- hostNow: T0 - 1_000
- }),
- T0
- )
- const release = replayClientByteStatus(store, RETRACTED_TAB, RETRACTED_LEAF, 'done', T0)
- previousAgents = replayRetainedAgentsSync(store, previousAgents, T0 + 500)
- expect([...previousAgents.keys()]).toContain(GHOST_PANE_KEY)
-
- applyHostSnapshot(
- store,
- makeHostSnapshot({ snapshotVersion: 2, hostTabIds: [KEEP_TAB], hostNow: T0 + 1_000 }),
- T0 + 2_000
- )
- // Require closeTab’s named marker, not merely an arbitrary retention suppressor.
- expect(
- store.getState().recentlyClosedAgentStatusTabIds[mirrorTabId(RETRACTED_TAB)],
- 'the paired retraction did not plant the closed-tab suppressor'
- ).toBeDefined()
- release()
- previousAgents = replayRetainedAgentsSync(store, previousAgents, T0 + 2_500)
-
- const observed = observeSidebar(store, T0 + 3_000)
- const evidence = JSON.stringify(
- { observed, retained: store.getState().retainedAgentsByPaneKey[GHOST_PANE_KEY] },
- null,
- 2
- )
- // Host retraction needs the same closed-tab retention suppression as local close.
- expect(
- store.getState().retainedAgentsByPaneKey[GHOST_PANE_KEY],
- `a host-retracted done agent was promoted into permanent retained state\n${evidence}`
- ).toBeUndefined()
- expect(
- observed.rowPaneKeys.filter((paneKey) => paneKey === GHOST_PANE_KEY),
- `the sidebar renders a retained ghost row for a tab that no longer exists\n${evidence}`
- ).toEqual([])
- })
-
- it('a retract-and-return flap must not strand a suppressor that eats a later real retention', () => {
- const store = seedPairedClientStore()
- let previousAgents: RetainedSyncAgents = new Map()
- applyHostSnapshot(
- store,
- makeHostSnapshot({
- snapshotVersion: 1,
- hostTabIds: [KEEP_TAB, RETRACTED_TAB],
- hostNow: T0 - 1_000
- }),
- T0
- )
- const release = replayClientByteStatus(store, RETRACTED_TAB, RETRACTED_LEAF, 'working', T0)
- previousAgents = replayRetainedAgentsSync(store, previousAgents, T0 + 500)
-
- // React can batch retraction/return into one retention interval while the pane stays mounted. Its next fresh write must lift the unconsumed one-shot suppressor or a later real completion is lost.
- applyHostSnapshot(
- store,
- makeHostSnapshot({ snapshotVersion: 2, hostTabIds: [KEEP_TAB], hostNow: T0 + 1_000 }),
- T0 + 2_000
- )
- applyHostSnapshot(
- store,
- makeHostSnapshot({
- snapshotVersion: 3,
- hostTabIds: [KEEP_TAB, RETRACTED_TAB],
- hostNow: T0 + 2_500
- }),
- T0 + 3_000
- )
- vi.setSystemTime(T0 + 3_500)
- markRendererOwnedAgentStatusWrite(GHOST_PANE_KEY)
- store
- .getState()
- .setAgentStatus(
- GHOST_PANE_KEY,
- { state: 'done', prompt: 'review finished', agentType: 'codex' },
- 'codex',
- undefined,
- { tabId: mirrorTabId(RETRACTED_TAB), worktreeId: WT }
- )
- previousAgents = replayRetainedAgentsSync(store, previousAgents, T0 + 4_000)
- expect(store.getState().agentStatusByPaneKey[GHOST_PANE_KEY]?.state).toBe('done')
-
- // After unmount, ordinary disappearance of non-client-owned done status must remain retainable.
- release()
- applyHostSnapshot(
- store,
- makeHostSnapshot({
- snapshotVersion: 4,
- hostTabIds: [KEEP_TAB, RETRACTED_TAB],
- hostNow: T0 + 600_000
- }),
- T0 + 601_000
- )
- expect(
- store.getState().agentStatusByPaneKey[GHOST_PANE_KEY],
- 'precondition: the released done row must be removed by the mirror delete loop'
- ).toBeUndefined()
- previousAgents = replayRetainedAgentsSync(store, previousAgents, T0 + 601_500)
-
- expect(
- store.getState().retainedAgentsByPaneKey[GHOST_PANE_KEY],
- 'a stranded retraction suppressor ate a legitimate done-agent retention'
- ).toBeDefined()
- })
-})