preserve experimental dashboard agent launch

This commit is contained in:
Brennan Benson
2026-08-02 18:34:00 -07:00
parent 95c431f5c3
commit 47829cb226
15 changed files with 426 additions and 5 deletions
@@ -0,0 +1,38 @@
import type { DashboardSpawnAgentArgs } from '../../shared/dashboard-snapshot'
import { isTuiAgent } from '../../shared/tui-agent-config'
const MAX_ID_LENGTH = 4_096
const MAX_WORKTREES = 500
const MAX_AGENTS_PER_WORKTREE = 64
function isBoundedId(value: unknown): value is string {
return typeof value === 'string' && value.length > 0 && value.length <= MAX_ID_LENGTH
}
export function isDashboardSpawnAgentArgs(value: unknown): value is DashboardSpawnAgentArgs {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false
}
const args = value as Record<string, unknown>
return isBoundedId(args.worktreeId) && isTuiAgent(args.agent)
}
export function isDashboardLaunchOptions(value: unknown): boolean {
if (value === undefined) {
return true
}
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false
}
const entries = Object.entries(value as Record<string, unknown>)
return (
entries.length <= MAX_WORKTREES &&
entries.every(
([worktreeId, agents]) =>
isBoundedId(worktreeId) &&
Array.isArray(agents) &&
agents.length <= MAX_AGENTS_PER_WORKTREE &&
agents.every(isTuiAgent)
)
)
}
@@ -19,6 +19,7 @@ vi.mock('../../shared/repo-icon', async (importOriginal) => {
import {
admitDashboardSnapshot,
isDashboardRevealAgentArgs,
isDashboardSpawnAgentArgs,
isDashboardSnapshot
} from './dashboard-payload-validation'
@@ -162,6 +163,26 @@ describe('dashboard payload validation', () => {
).toBe(false)
})
it('validates bounded launch choices and spawn requests', () => {
expect(
isDashboardSnapshot({
...SNAPSHOT,
launchableAgentsByWorktreeId: { 'worktree-1': ['codex', 'claude'] }
})
).toBe(true)
expect(
isDashboardSnapshot({
...SNAPSHOT,
launchableAgentsByWorktreeId: { 'worktree-1': ['not-an-agent'] }
})
).toBe(false)
expect(isDashboardSnapshot({ ...SNAPSHOT, launchableAgentsByWorktreeId: [] })).toBe(false)
expect(isDashboardSpawnAgentArgs({ worktreeId: 'worktree-1', agent: 'codex' })).toBe(true)
expect(isDashboardSpawnAgentArgs({ worktreeId: '', agent: 'codex' })).toBe(false)
expect(isDashboardSpawnAgentArgs({ worktreeId: 'worktree-1', agent: 'unknown' })).toBe(false)
})
it('bounds the conversation name', () => {
expect(
isDashboardSnapshot({
@@ -11,6 +11,8 @@ import {
AGENT_STATUS_MAX_FIELD_LENGTH,
AGENT_TYPE_MAX_LENGTH
} from '../../shared/agent-status-types'
import { isDashboardLaunchOptions } from './dashboard-agent-launch-validation'
export { isDashboardSpawnAgentArgs } from './dashboard-agent-launch-validation'
const MAX_DASHBOARD_CARDS = 1_000
const MAX_DASHBOARD_SUBAGENTS = 100
@@ -85,6 +87,7 @@ export function isDashboardSnapshot(value: unknown): value is DashboardSnapshot
snapshot.cards.every(isDashboardCard) &&
(snapshot.showIdle === undefined || typeof snapshot.showIdle === 'boolean') &&
isDashboardFilterOptions(snapshot.filterOptions) &&
isDashboardLaunchOptions(snapshot.launchableAgentsByWorktreeId) &&
isDashboardRepoIcons(snapshot.repoIconsByRepoId)
)
}
@@ -113,6 +116,7 @@ export function admitDashboardSnapshot(value: unknown): DashboardSnapshotAdmissi
snapshot.cards.length > MAX_DASHBOARD_CARDS ||
(snapshot.showIdle !== undefined && typeof snapshot.showIdle !== 'boolean') ||
!isDashboardFilterOptions(snapshot.filterOptions) ||
!isDashboardLaunchOptions(snapshot.launchableAgentsByWorktreeId) ||
!isDashboardRepoIcons(snapshot.repoIconsByRepoId)
) {
return null
+13
View File
@@ -240,6 +240,19 @@ describe('registerDashboardPopoutHandlers', () => {
expect(sendToTrustedMock).toHaveBeenCalledWith('ui:ackDashboardAgent', 'tab1:leaf1')
})
it('relays only valid agent launches from the popout', () => {
const args = { worktreeId: 'worktree-1', agent: 'codex' }
handlers.get('dashboardPopout:spawnAgent')!({ sender: untrustedSender } as never, args)
handlers.get('dashboardPopout:spawnAgent')!({ sender: popoutSender } as never, {
...args,
agent: 'unknown'
})
expect(sendToTrustedMock).not.toHaveBeenCalled()
handlers.get('dashboardPopout:spawnAgent')!({ sender: popoutSender } as never, args)
expect(sendToTrustedMock).toHaveBeenCalledWith('ui:spawnDashboardAgent', args)
})
it('reveals an agent in only the trusted main window', () => {
const main = makeWindow(mainSender)
getTrustedWindowMock.mockReturnValue(main)
+14 -1
View File
@@ -14,7 +14,8 @@ import { getTrustedUIRendererWindow, isTrustedUIRenderer, sendToTrustedUIRendere
import {
admitDashboardSnapshot,
isDashboardPaneKey,
isDashboardRevealAgentArgs
isDashboardRevealAgentArgs,
isDashboardSpawnAgentArgs
} from './dashboard-payload-validation'
// The most recent snapshot the main renderer published, replayed to the popout
@@ -36,6 +37,7 @@ export function registerDashboardPopoutHandlers(
ipcMain.removeHandler('dashboard:getPopoutOpen')
ipcMain.removeHandler('dashboardPopout:revealAgent')
ipcMain.removeHandler('dashboardPopout:ackAgent')
ipcMain.removeHandler('dashboardPopout:spawnAgent')
onDashboardPopoutOpenChanged((open) => {
if (!open) {
@@ -141,4 +143,15 @@ export function registerDashboardPopoutHandlers(
// Best-effort; the per-window focus above may still bring it forward.
}
})
ipcMain.handle('dashboardPopout:spawnAgent', (event, args: unknown): void => {
if (
!isDashboardPopoutRenderer(event.sender) ||
!isDashboardEnabled(store) ||
!isDashboardSpawnAgentArgs(args)
) {
return
}
sendToTrustedUIRenderer('ui:spawnDashboardAgent', args)
})
}
+7 -1
View File
@@ -10,7 +10,11 @@ import type {
} from '../shared/hosted-review'
import type { NativeFileDropPayload } from '../shared/native-file-drop'
import type { BrowserFindSource } from '../shared/browser-find-source'
import type { DashboardSnapshot, DashboardRevealAgentArgs } from '../shared/dashboard-snapshot'
import type {
DashboardRevealAgentArgs,
DashboardSnapshot,
DashboardSpawnAgentArgs
} from '../shared/dashboard-snapshot'
import type {
TerminalPreviewConnectResult,
TerminalPreviewDataPayload
@@ -2470,10 +2474,12 @@ export type PreloadApi = {
onSnapshotRequested: (callback: () => void) => () => void
onRevealAgent: (callback: (args: DashboardRevealAgentArgs) => void) => () => void
onAckAgent: (callback: (paneKey: string) => void) => () => void
onSpawnAgent: (callback: (args: DashboardSpawnAgentArgs) => void) => () => void
requestSnapshot: () => Promise<void>
onSnapshot: (callback: (snapshot: DashboardSnapshot) => void) => () => void
revealAgent: (args: DashboardRevealAgentArgs) => Promise<void>
ackAgent: (paneKey: string) => Promise<void>
spawnAgent: (args: DashboardSpawnAgentArgs) => Promise<void>
}
terminalPreview: {
connect: (
+14 -2
View File
@@ -4,7 +4,11 @@ import { electronAPI } from '@electron-toolkit/preload'
import { preloadE2EConfig } from './e2e-config'
import { glApi } from './gitlab'
import type { AppIdentity } from '../shared/app-identity'
import type { DashboardSnapshot, DashboardRevealAgentArgs } from '../shared/dashboard-snapshot'
import type {
DashboardRevealAgentArgs,
DashboardSnapshot,
DashboardSpawnAgentArgs
} from '../shared/dashboard-snapshot'
import type {
TerminalPreviewConnectResult,
TerminalPreviewDataPayload
@@ -2290,6 +2294,12 @@ const api = {
ipcRenderer.on('ui:ackDashboardAgent', listener)
return () => ipcRenderer.removeListener('ui:ackDashboardAgent', listener)
},
onSpawnAgent: (callback: (args: DashboardSpawnAgentArgs) => void): (() => void) => {
const listener = (_event: Electron.IpcRendererEvent, args: DashboardSpawnAgentArgs): void =>
callback(args)
ipcRenderer.on('ui:spawnDashboardAgent', listener)
return () => ipcRenderer.removeListener('ui:spawnDashboardAgent', listener)
},
// ── Consumer side (pop-out window) ───────────────────────────────────
requestSnapshot: (): Promise<void> => ipcRenderer.invoke('dashboard:requestSnapshot'),
@@ -2302,7 +2312,9 @@ const api = {
revealAgent: (args: DashboardRevealAgentArgs): Promise<void> =>
ipcRenderer.invoke('dashboardPopout:revealAgent', args),
ackAgent: (paneKey: string): Promise<void> =>
ipcRenderer.invoke('dashboardPopout:ackAgent', { paneKey })
ipcRenderer.invoke('dashboardPopout:ackAgent', { paneKey }),
spawnAgent: (args: DashboardSpawnAgentArgs): Promise<void> =>
ipcRenderer.invoke('dashboardPopout:spawnAgent', args)
},
terminalPreview: {
@@ -41,6 +41,10 @@ import {
resolveDashboardCardContext,
type DashboardCardContextState
} from './dashboard-card-context'
import {
buildDashboardWorktreeLaunchOptions,
type DashboardLaunchDetectionState
} from './dashboard-worktree-launch-options'
/** The store slices the snapshot builder reads. Kept as a Pick so unit tests
* can pass a partial store without constructing the whole AppState. */
@@ -60,7 +64,7 @@ export type DashboardSnapshotState = Pick<
| 'settings'
> &
DashboardCardContextState &
Partial<DashboardCardTerminalInputState>
Partial<DashboardCardTerminalInputState & DashboardLaunchDetectionState>
function bucketForState(state: DashboardAgentRow['state']): DashboardBucket {
switch (state) {
@@ -330,6 +334,7 @@ export function buildDashboardSnapshot(
cards,
showIdle: state.settings?.experimentalAgentDashboardShowIdle === true,
filterOptions,
launchableAgentsByWorktreeId: buildDashboardWorktreeLaunchOptions(state, cards),
repoIconsByRepoId
}
}
@@ -0,0 +1,99 @@
import { describe, expect, it } from 'vitest'
import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
import { buildDashboardWorktreeLaunchOptions } from './dashboard-worktree-launch-options'
type LaunchState = Parameters<typeof buildDashboardWorktreeLaunchOptions>[0]
function state(overrides: Partial<LaunchState> = {}): LaunchState {
return {
repos: [],
worktreesByRepo: {},
folderWorkspaces: [],
projectGroups: [],
detectedAgentIds: [],
remoteDetectedAgentIds: {},
runtimeDetectedAgentIds: {},
settings: null,
...overrides
}
}
function card(overrides: Partial<DashboardCard> = {}): DashboardCard {
return {
paneKey: 'pane-1',
ptyId: 'pty-1',
agentType: 'codex',
bucket: 'working',
dotState: 'working',
task: 'Ship it',
repoId: 'repo-1',
worktreeId: 'worktree-1',
tabId: 'tab-1',
leafId: 'leaf-1',
repoName: 'Orca',
worktreeName: 'Dashboard',
startedAt: 1,
finishedAt: null,
stateChangedAt: 1,
unseen: false,
...overrides
}
}
describe('buildDashboardWorktreeLaunchOptions', () => {
it('combines local detection with proven providers, honoring defaults and disabled agents', () => {
const options = buildDashboardWorktreeLaunchOptions(
state({
detectedAgentIds: ['claude', 'codex'],
settings: {
defaultTuiAgent: 'codex',
disabledTuiAgents: ['claude']
} as LaunchState['settings']
}),
[card(), card({ paneKey: 'pane-2', agentType: 'gemini' })]
)
expect(options).toEqual({ 'worktree-1': ['codex', 'gemini'] })
})
it('uses each git workspace execution host instead of local detection', () => {
const options = buildDashboardWorktreeLaunchOptions(
state({
repos: [
{ id: 'repo-ssh', connectionId: 'ssh-1' },
{ id: 'repo-runtime', executionHostId: 'runtime:hub-1' }
] as LaunchState['repos'],
worktreesByRepo: {
'repo-ssh': [{ id: 'ssh-worktree', repoId: 'repo-ssh' }],
'repo-runtime': [{ id: 'runtime-worktree', repoId: 'repo-runtime' }]
} as unknown as LaunchState['worktreesByRepo'],
detectedAgentIds: ['claude'],
remoteDetectedAgentIds: { 'ssh-1': ['grok'] },
runtimeDetectedAgentIds: { 'hub-1': ['aider'] }
}),
[
card({ repoId: 'repo-ssh', worktreeId: 'ssh-worktree', agentType: 'grok' }),
card({ repoId: 'repo-runtime', worktreeId: 'runtime-worktree', agentType: 'aider' })
]
)
expect(options).toEqual({ 'ssh-worktree': ['grok'], 'runtime-worktree': ['aider'] })
})
it('resolves folder workspace detection through its project host', () => {
const worktreeId = folderWorkspaceKey('folder-1')
const options = buildDashboardWorktreeLaunchOptions(
state({
folderWorkspaces: [
{ id: 'folder-1', projectGroupId: 'group-1', connectionId: 'ssh-folder' }
] as LaunchState['folderWorkspaces'],
projectGroups: [{ id: 'group-1' }] as LaunchState['projectGroups'],
remoteDetectedAgentIds: { 'ssh-folder': ['goose'] }
}),
[card({ repoId: 'folder-workspace:group-1', worktreeId })]
)
expect(options).toEqual({ [worktreeId]: ['codex', 'goose'] })
})
})
@@ -0,0 +1,112 @@
import type { AppState } from '@/store/types'
import { parseExecutionHostId } from '../../../../shared/execution-host'
import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
import { isTuiAgent } from '../../../../shared/tui-agent-config'
import {
filterEnabledTuiAgents,
TUI_AGENT_AUTO_PICK_ORDER
} from '../../../../shared/tui-agent-selection'
import type { TuiAgent } from '../../../../shared/types'
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
export type DashboardLaunchDetectionState = Pick<
AppState,
| 'detectedAgentIds'
| 'folderWorkspaces'
| 'projectGroups'
| 'remoteDetectedAgentIds'
| 'runtimeDetectedAgentIds'
>
type DashboardLaunchOptionState = Pick<AppState, 'repos' | 'settings' | 'worktreesByRepo'> &
Partial<DashboardLaunchDetectionState>
type DashboardLaunchCatalog = {
foldersById: Map<string, AppState['folderWorkspaces'][number]>
groupsById: Map<string, AppState['projectGroups'][number]>
reposById: Map<string, AppState['repos'][number]>
worktreesByRepoAndId: Map<string, Map<string, AppState['worktreesByRepo'][string][number]>>
}
function buildDashboardLaunchCatalog(state: DashboardLaunchOptionState): DashboardLaunchCatalog {
return {
foldersById: new Map((state.folderWorkspaces ?? []).map((folder) => [folder.id, folder])),
groupsById: new Map((state.projectGroups ?? []).map((group) => [group.id, group])),
reposById: new Map((state.repos ?? []).map((repo) => [repo.id, repo])),
worktreesByRepoAndId: new Map(
Object.entries(state.worktreesByRepo ?? {}).map(([repoId, worktrees]) => [
repoId,
new Map(worktrees.map((worktree) => [worktree.id, worktree]))
])
)
}
}
function detectedAgentsForWorktree(
state: DashboardLaunchOptionState,
worktreeId: string,
repoId: string,
catalog: DashboardLaunchCatalog
): readonly TuiAgent[] {
const workspaceScope = parseWorkspaceKey(worktreeId)
if (workspaceScope?.type === 'folder') {
const folder = catalog.foldersById.get(workspaceScope.folderWorkspaceId)
const group = folder ? catalog.groupsById.get(folder.projectGroupId) : undefined
const host = parseExecutionHostId(group?.executionHostId)
if (host?.kind === 'runtime') {
return state.runtimeDetectedAgentIds?.[host.environmentId] ?? []
}
const connectionId = folder?.connectionId ?? group?.connectionId
return connectionId ? (state.remoteDetectedAgentIds?.[connectionId] ?? []) : []
}
const worktree = catalog.worktreesByRepoAndId.get(repoId)?.get(worktreeId)
const repo = catalog.reposById.get(worktree?.repoId ?? repoId)
const host = parseExecutionHostId(worktree?.hostId ?? repo?.executionHostId)
if (host?.kind === 'runtime') {
return state.runtimeDetectedAgentIds?.[host.environmentId] ?? []
}
const connectionId = host?.kind === 'ssh' ? host.targetId : repo?.connectionId
return connectionId
? (state.remoteDetectedAgentIds?.[connectionId] ?? [])
: (state.detectedAgentIds ?? [])
}
/** Host-detected choices plus providers already proven to run in the workspace. */
export function buildDashboardWorktreeLaunchOptions(
state: DashboardLaunchOptionState,
cards: readonly DashboardCard[]
): Record<string, TuiAgent[]> {
const catalog = buildDashboardLaunchCatalog(state)
const cardsByWorktreeId = new Map<string, DashboardCard[]>()
for (const card of cards) {
const existing = cardsByWorktreeId.get(card.worktreeId)
if (existing) {
existing.push(card)
} else {
cardsByWorktreeId.set(card.worktreeId, [card])
}
}
const result: Record<string, TuiAgent[]> = {}
for (const [worktreeId, worktreeCards] of cardsByWorktreeId) {
const available = new Set<TuiAgent>(
detectedAgentsForWorktree(state, worktreeId, worktreeCards[0].repoId, catalog)
)
for (const card of worktreeCards) {
if (isTuiAgent(card.agentType)) {
available.add(card.agentType)
}
}
const enabled = filterEnabledTuiAgents(
TUI_AGENT_AUTO_PICK_ORDER.filter((agent) => available.has(agent)),
state.settings?.disabledTuiAgents
)
const preferred = state.settings?.defaultTuiAgent
result[worktreeId] =
preferred && preferred !== 'blank' && enabled.includes(preferred)
? [preferred, ...enabled.filter((agent) => agent !== preferred)]
: enabled
}
return result
}
@@ -0,0 +1,46 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
getKnownWorktreeById: vi.fn(),
setActiveWorktree: vi.fn(),
launchAgentInNewTab: vi.fn(),
getExecutionHostIdForWorktree: vi.fn()
}))
vi.mock('@/store', () => ({
useAppStore: {
getState: () => ({
settings: null,
getKnownWorktreeById: mocks.getKnownWorktreeById,
setActiveWorktree: mocks.setActiveWorktree
})
}
}))
vi.mock('@/lib/launch-agent-in-new-tab', () => ({
launchAgentInNewTab: mocks.launchAgentInNewTab
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getExecutionHostIdForWorktree: mocks.getExecutionHostIdForWorktree
}))
import { launchDashboardAgent } from './launch-dashboard-agent'
describe('launchDashboardAgent', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getExecutionHostIdForWorktree.mockReturnValue('ssh:docs')
mocks.getKnownWorktreeById.mockReturnValue({ id: 'folder:docs' })
mocks.launchAgentInNewTab.mockReturnValue({ tabId: 'tab-1' })
})
it('activates a folder or git workspace on its execution host before launching', () => {
expect(launchDashboardAgent({ worktreeId: 'folder:docs', agent: 'codex' })).toBe(true)
expect(mocks.getKnownWorktreeById).toHaveBeenCalledWith('folder:docs', 'ssh:docs')
expect(mocks.setActiveWorktree).toHaveBeenCalledWith('folder:docs', 'ssh:docs')
expect(mocks.launchAgentInNewTab).toHaveBeenCalledWith({
agent: 'codex',
worktreeId: 'folder:docs',
launchSource: 'unknown'
})
})
})
@@ -0,0 +1,23 @@
import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab'
import { useAppStore } from '@/store'
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
import type { DashboardSpawnAgentArgs } from '../../../../shared/dashboard-snapshot'
import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection'
/** Starts the requested agent through the same host-aware tab path as Quick Launch. */
export function launchDashboardAgent({ worktreeId, agent }: DashboardSpawnAgentArgs): boolean {
const state = useAppStore.getState()
const executionHostId = getExecutionHostIdForWorktree(state, worktreeId)
const worktree = state.getKnownWorktreeById(worktreeId, executionHostId)
if (!worktree || !isTuiAgentEnabled(agent, state.settings?.disabledTuiAgents)) {
return false
}
state.setActiveWorktree(worktreeId, executionHostId)
return (
launchAgentInNewTab({
agent,
worktreeId,
launchSource: 'unknown'
}) !== null
)
}
@@ -3,6 +3,7 @@ import { useAppStore, type AppState } from '@/store'
import { activateTabAndFocusPane } from '@/lib/activate-tab-and-focus-pane'
import type { RepoIcon } from '../../../../shared/repo-icon'
import { buildDashboardSnapshot, type DashboardSnapshotState } from './build-dashboard-snapshot'
import { launchDashboardAgent } from './launch-dashboard-agent'
// Why: cap snapshot rebuilds during bursts of agent-status pings. The board is a
// glanceable surface, so ~4 updates/sec is plenty and keeps the cross-worktree
@@ -57,6 +58,9 @@ export function dashboardSnapshotInputsChanged(
// Why: settings controls idle visibility and generated conversation names.
state.settings !== previousState.settings ||
state.workspaceStatuses !== previousState.workspaceStatuses ||
state.detectedAgentIds !== previousState.detectedAgentIds ||
state.remoteDetectedAgentIds !== previousState.remoteDetectedAgentIds ||
state.runtimeDetectedAgentIds !== previousState.runtimeDetectedAgentIds ||
// Why: freshness can change a bucket without replacing any backing map.
state.agentStatusEpoch !== previousState.agentStatusEpoch ||
// Why: each card carries the host-input profile its preview terminal keys
@@ -103,6 +107,13 @@ function watchSnapshotInputs(onChanged: () => void): () => void {
* the agent's worktree and focus its pane in this (main) window.
*/
export function useDashboardPopoutBridge(enabled: boolean): void {
useEffect(() => {
if (!enabled) {
return
}
return window.api.dashboard.onSpawnAgent?.(launchDashboardAgent)
}, [enabled])
useEffect(() => {
if (!enabled) {
return
@@ -51,6 +51,9 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot {
const runtimeEnvironmentCatalogHydrated = useAppStore((s) => s.runtimeEnvironmentCatalogHydrated)
const removedRuntimeEnvironmentIds = useAppStore((s) => s.removedRuntimeEnvironmentIds)
const paneForegroundAgentByPaneKey = useAppStore((s) => s.paneForegroundAgentByPaneKey)
const detectedAgentIds = useAppStore((s) => s.detectedAgentIds)
const remoteDetectedAgentIds = useAppStore((s) => s.remoteDetectedAgentIds)
const runtimeDetectedAgentIds = useAppStore((s) => s.runtimeDetectedAgentIds)
// Why: freshness can flip a bucket without any backing map changing; the epoch
// ticks on the freshness boundary so the memo re-derives stale-decayed cards.
const agentStatusEpoch = useAppStore((s) => s.agentStatusEpoch)
@@ -87,6 +90,9 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot {
runtimeEnvironmentCatalogHydrated,
removedRuntimeEnvironmentIds,
paneForegroundAgentByPaneKey,
detectedAgentIds,
remoteDetectedAgentIds,
runtimeDetectedAgentIds,
// Why: read non-reactively — resolveWindowsShiftEnterEncoding takes
// launch identity but never routes on it, so subscribing would only
// rebuild the board. Matches the bridge's republish gate.
@@ -122,6 +128,9 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot {
runtimeEnvironmentCatalogHydrated,
removedRuntimeEnvironmentIds,
paneForegroundAgentByPaneKey,
detectedAgentIds,
remoteDetectedAgentIds,
runtimeDetectedAgentIds,
agentStatusEpoch
]
)
+9
View File
@@ -1,5 +1,6 @@
import type { AgentType } from './agent-status-types'
import type { RepoIcon } from './repo-icon'
import type { TuiAgent } from './types'
/**
* Serializable contract for the pop-out agent dashboard. The main renderer owns
@@ -128,6 +129,8 @@ export type DashboardSnapshot = {
/** Available filter dimensions are store-derived so zero-card projects and
* statuses remain selectable. Optional for preload-version compatibility. */
filterOptions?: DashboardFilterOptions
/** Launch choices resolved on each workspace's execution host. */
launchableAgentsByWorktreeId?: Record<string, TuiAgent[]>
/** Icons for the repos the cards belong to. Keyed by repoId rather than
* carried per card: image icons are data URLs up to 400KB, and the snapshot
* is republished several times a second. Optional so a pop-out running
@@ -139,6 +142,7 @@ export const EMPTY_DASHBOARD_SNAPSHOT: DashboardSnapshot = {
generatedAt: 0,
cards: [],
filterOptions: { projects: [], workspaceStatuses: [] },
launchableAgentsByWorktreeId: {},
repoIconsByRepoId: {}
}
@@ -151,3 +155,8 @@ export type DashboardRevealAgentArgs = {
tabId: string
leafId: string | null
}
export type DashboardSpawnAgentArgs = {
worktreeId: string
agent: TuiAgent
}