feat(dashboard): identify SSH and remote hosts

This commit is contained in:
Brennan Benson
2026-08-12 19:57:24 -07:00
parent bc2e30000b
commit 330a20dcab
20 changed files with 426 additions and 29 deletions
@@ -45,6 +45,7 @@ const SNAPSHOT = {
worktreeName: 'Dashboard',
hostKind: 'ssh',
executionHostId: 'ssh:build-box',
hostLabel: 'Build box',
workspaceKind: 'worktree',
workspaceStatusId: 'in-review',
workspaceStatusLabel: 'In review',
@@ -68,6 +69,7 @@ const SNAPSHOT = {
parentWorktreeId: 'parent-worktree-1',
hostKind: 'ssh',
executionHostId: 'ssh:build-box',
hostLabel: 'Build box',
workspaceKind: 'worktree',
workspaceStatusId: 'in-review',
workspaceStatusLabel: 'In review',
@@ -143,6 +145,12 @@ describe('dashboard payload validation', () => {
cards: [{ ...SNAPSHOT.cards[0], executionHostId: `ssh:${'x'.repeat(4_097)}` }]
})
).toBe(false)
expect(
isDashboardSnapshot({
...SNAPSHOT,
cards: [{ ...SNAPSHOT.cards[0], hostLabel: 'x'.repeat(1_025) }]
})
).toBe(false)
expect(
isDashboardSnapshot({
...SNAPSHOT,
@@ -236,6 +244,12 @@ describe('dashboard payload validation', () => {
workspaces: [{ ...SNAPSHOT.workspaces[0], worktreeName: 'x'.repeat(1_025) }]
})
).toBe(false)
expect(
isDashboardSnapshot({
...SNAPSHOT,
workspaces: [{ ...SNAPSHOT.workspaces[0], hostLabel: 'x'.repeat(1_025) }]
})
).toBe(false)
})
it('validates bounded launch choices and spawn requests', () => {
@@ -272,6 +272,7 @@ function isDashboardCard(value: unknown): boolean {
(card.executionHostId === undefined ||
(isBoundedString(card.executionHostId, MAX_ID_LENGTH) &&
normalizeExecutionHostId(card.executionHostId) !== null)) &&
isOptionalBoundedString(card.hostLabel, MAX_LABEL_LENGTH) &&
(card.workspaceKind === undefined ||
(typeof card.workspaceKind === 'string' &&
DASHBOARD_WORKSPACE_KINDS.has(card.workspaceKind))) &&
@@ -50,6 +50,7 @@ export function isDashboardWorkspace(value: unknown): value is DashboardWorkspac
HOST_KINDS.has(workspace.hostKind) &&
isString(workspace.executionHostId, MAX_ID_LENGTH) &&
normalizeExecutionHostId(workspace.executionHostId) !== null &&
isOptionalString(workspace.hostLabel, DASHBOARD_MAX_LABEL_LENGTH) &&
typeof workspace.workspaceKind === 'string' &&
WORKSPACE_KINDS.has(workspace.workspaceKind) &&
isOptionalString(workspace.workspaceStatusId, MAX_ID_LENGTH) &&
@@ -102,6 +102,23 @@ describe('AgentKanbanCard', () => {
expect(container.querySelector('.lucide-message-circle-question-mark')).toBeNull()
})
it('shows the saved SSH host beside the repository metadata', () => {
const { container } = renderCard({
card: card({
hostKind: 'ssh',
executionHostId: 'ssh:opaque-target',
hostLabel: 'openclaw'
}),
now: 2_000
})
expect(screen.getByLabelText('SSH host · openclaw')).toHaveAttribute(
'data-dashboard-host-badge',
'ssh'
)
expect(container.querySelector('.lucide-server')).toBeInTheDocument()
})
it('shows review metadata and expands grouped subagents without opening the terminal', () => {
const onOpenTerminal = vi.fn()
renderCard({
@@ -20,6 +20,7 @@ import {
} from '../../../../shared/dashboard-snapshot'
import type { RepoIcon } from '../../../../shared/repo-icon'
import { translate } from '@/i18n/i18n'
import { DashboardHostBadge } from './DashboardHostBadge'
/** Compact "started N ago" (the card is glanceable — coarse units are fine). */
function formatStartedAgo(startedAt: number, now: number): string {
@@ -95,6 +96,9 @@ function sameCard(a: DashboardCard, b: DashboardCard): boolean {
a.leafId === b.leafId &&
a.repoName === b.repoName &&
a.worktreeName === b.worktreeName &&
a.hostKind === b.hostKind &&
a.executionHostId === b.executionHostId &&
a.hostLabel === b.hostLabel &&
a.hasReview === b.hasReview &&
a.review?.number === b.review?.number &&
a.review?.state === b.review?.state &&
@@ -327,6 +331,12 @@ export const AgentKanbanCard = memo(
{card.repoName}
</TooltipContent>
</Tooltip>
<DashboardHostBadge
hostKind={card.hostKind}
executionHostId={card.executionHostId}
hostLabel={card.hostLabel}
className="size-[18px] rounded-[5px] bg-muted-foreground/10 transition-colors group-hover:text-foreground"
/>
{worktreeInFooter ? <span className="truncate">{card.worktreeName}</span> : null}
<ReviewPill card={card} />
{displayTimestamp(card) > 0 ? (
@@ -5,6 +5,7 @@ import { render } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import type { AgentMapLayout } from './agent-map-layout'
import { AgentMapScene } from './AgentMapScene'
import { TooltipProvider } from '@/components/ui/tooltip'
const LAYOUT: AgentMapLayout = {
projects: [
@@ -60,4 +61,56 @@ describe('AgentMapScene project labels', () => {
expect(container.querySelector('.agent-map-project-label-frame')).toHaveAttribute('x', '-48')
expect(container.querySelector('.agent-map-project-label-frame')).toHaveAttribute('width', '96')
})
it('labels an SSH-backed project ring with its saved host', () => {
const sshLayout: AgentMapLayout = {
...LAYOUT,
projects: [
{
...LAYOUT.projects[0],
worktrees: [
{
id: 'worktree-1:openclaw',
worktreeId: 'worktree-1',
executionHostId: 'ssh:opaque-target',
hostKind: 'ssh',
hostLabel: 'openclaw',
name: 'humpback',
workspaceKind: 'worktree',
x: 120,
y: 120,
radius: 48,
agents: [],
statusCounts: { working: 0, blocked: 0, waiting: 0, done: 0, idle: 0 },
quiet: true
}
]
}
]
}
const { container } = render(
<TooltipProvider>
<svg>
<AgentMapScene
layout={sshLayout}
zoom={1}
labelScale={1}
mapScale={0.5}
heldProjectId={null}
heldWorktreeId={null}
selectedPaneKey={null}
allowAggregation
showOrchestrationLinks
nodeRefs={{ current: new Map() }}
onSelectAgent={vi.fn()}
onAgentKeyDown={vi.fn()}
/>
</svg>
</TooltipProvider>
)
const badge = container.querySelector('[data-dashboard-host-badge="ssh"]')
expect(badge).toHaveAccessibleName('SSH host · openclaw')
expect(badge).toHaveClass('agent-map-project-host-badge', 'pointer-events-auto')
})
})
@@ -15,6 +15,7 @@ import { selectVisibleAgentMapLabels } from './agent-map-label-declutter'
import { agentMapDirectLineageChevronPath } from './agent-map-lineage-chevron-path'
import { AgentMapWorktreeLabel } from './AgentMapWorktreeLabel'
import { AgentMapWorktreeRingNode } from './AgentMapWorktreeRingNode'
import { DashboardHostBadge } from './DashboardHostBadge'
type AgentMapSceneProps = {
layout: AgentMapLayout
@@ -129,6 +130,13 @@ export const AgentMapScene = memo(function AgentMapScene({
{layout.projects.map((project) => {
const worktreesById = new Map(project.worktrees.map((worktree) => [worktree.id, worktree]))
const projectLabelHalfWidth = project.radius * mapScale
const projectHostsById = new Map<string, AgentMapWorktreeRing>()
for (const worktree of project.worktrees) {
if (worktree.hostKind === 'ssh' || worktree.hostKind === 'remote') {
projectHostsById.set(`${worktree.hostKind}:${worktree.executionHostId ?? ''}`, worktree)
}
}
const projectHosts = [...projectHostsById.values()]
const projectCountText = translate(
'dashboardPopout.map.projectCount',
'{{agents}} agents · {{workspaces}} workspaces',
@@ -253,6 +261,16 @@ export const AgentMapScene = memo(function AgentMapScene({
<span className="agent-map-project-name min-w-0 truncate">
{project.name.toUpperCase()}
</span>
{projectHosts.map((host) => (
<DashboardHostBadge
key={`${host.hostKind}:${host.executionHostId ?? ''}`}
hostKind={host.hostKind}
executionHostId={host.executionHostId}
hostLabel={host.hostLabel}
keyboardFocusable
className="agent-map-project-host-badge"
/>
))}
</div>
</foreignObject>
{visibleLabels.projectCountIds.has(project.id) ? (
@@ -0,0 +1,55 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import { DashboardHostBadge } from './DashboardHostBadge'
afterEach(cleanup)
describe('DashboardHostBadge', () => {
it('names a saved SSH host in its focusable tooltip', async () => {
render(
<TooltipProvider>
<DashboardHostBadge
hostKind="ssh"
executionHostId="ssh:opaque-target"
hostLabel="openclaw"
keyboardFocusable
/>
</TooltipProvider>
)
const badge = screen.getByLabelText('SSH host · openclaw')
expect(badge).toHaveAttribute('data-dashboard-host-badge', 'ssh')
expect(badge.querySelector('.lucide-server')).toBeInTheDocument()
fireEvent.focus(badge)
expect(await screen.findByRole('tooltip')).toHaveTextContent('SSH host · openclaw')
})
it('distinguishes paired Orca hosts and omits local hosts', () => {
const { rerender } = render(
<TooltipProvider>
<DashboardHostBadge
hostKind="remote"
executionHostId="runtime:server-1"
hostLabel="Build Mac"
/>
</TooltipProvider>
)
expect(screen.getByLabelText('Remote Orca host · Build Mac')).toHaveAttribute(
'data-dashboard-host-badge',
'remote'
)
rerender(
<TooltipProvider>
<DashboardHostBadge hostKind="local" executionHostId="local" />
</TooltipProvider>
)
expect(screen.queryByLabelText(/host/i)).not.toBeInTheDocument()
})
})
@@ -0,0 +1,82 @@
import { RadioTower, Server } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import type { DashboardCardHostKind } from '../../../../shared/dashboard-snapshot'
import { parseExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
type DashboardHostBadgeProps = {
hostKind?: DashboardCardHostKind
executionHostId?: ExecutionHostId
hostLabel?: string
keyboardFocusable?: boolean
className?: string
iconClassName?: string
}
function fallbackHostLabel(executionHostId: ExecutionHostId | undefined): string | null {
const parsed = parseExecutionHostId(executionHostId)
if (parsed?.kind === 'ssh') {
return parsed.targetId
}
if (parsed?.kind === 'runtime') {
return parsed.environmentId
}
return null
}
export function dashboardHostTooltipLabel({
hostKind,
executionHostId,
hostLabel
}: Pick<DashboardHostBadgeProps, 'hostKind' | 'executionHostId' | 'hostLabel'>): string | null {
if (hostKind !== 'ssh' && hostKind !== 'remote') {
return null
}
const label = hostLabel?.trim() || fallbackHostLabel(executionHostId)
if (hostKind === 'ssh') {
return label
? translate('dashboardPopout.host.sshNamed', 'SSH host · {{host}}', { host: label })
: translate('dashboardPopout.host.ssh', 'SSH host')
}
return label
? translate('dashboardPopout.host.remoteNamed', 'Remote Orca host · {{host}}', { host: label })
: translate('dashboardPopout.host.remote', 'Remote Orca host')
}
export function DashboardHostBadge({
hostKind,
executionHostId,
hostLabel,
keyboardFocusable = false,
className,
iconClassName
}: DashboardHostBadgeProps): React.JSX.Element | null {
const tooltipLabel = dashboardHostTooltipLabel({ hostKind, executionHostId, hostLabel })
if (!tooltipLabel) {
return null
}
const Icon = hostKind === 'ssh' ? Server : RadioTower
return (
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
'inline-flex shrink-0 items-center justify-center text-muted-foreground',
keyboardFocusable &&
'pointer-events-auto focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none',
className
)}
data-dashboard-host-badge={hostKind}
aria-label={tooltipLabel}
tabIndex={keyboardFocusable ? 0 : undefined}
>
<Icon className={cn('size-3', iconClassName)} aria-hidden />
</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{tooltipLabel}
</TooltipContent>
</Tooltip>
)
}
@@ -1,5 +1,6 @@
import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
import type { DashboardCard, DashboardWorkspace } from '../../../../shared/dashboard-snapshot'
import type { AgentMapLayout, AgentMapStatusCounts } from './agent-map-layout'
import { agentMapWorkspaceIdentity } from './agent-map-workspace-identity'
import { agentMapDurationMinutes, agentMapNodeStatus } from './agent-map-node-metadata'
function emptyStatusCounts(): AgentMapStatusCounts {
@@ -9,15 +10,25 @@ function emptyStatusCounts(): AgentMapStatusCounts {
export function refreshAgentMapMetadata(
geometry: AgentMapLayout,
cards: DashboardCard[],
workspaces: DashboardWorkspace[],
now: number
): AgentMapLayout {
const cardsByPaneKey = new Map(cards.map((card) => [card.paneKey, card]))
const workspacesById = new Map(
workspaces.map((workspace) => [agentMapWorkspaceIdentity(workspace), workspace])
)
const projects = geometry.projects.map((project) => {
let projectName = project.name
let agentCount = 0
const worktrees = project.worktrees.map((worktree) => {
let worktreeName = worktree.name
let workspaceKind = worktree.workspaceKind
const workspace = workspacesById.get(worktree.id)
if (workspace) {
projectName = workspace.repoName
}
let worktreeName = workspace?.worktreeName ?? worktree.name
let workspaceKind = workspace?.workspaceKind ?? worktree.workspaceKind
let hostKind = workspace?.hostKind ?? worktree.hostKind
let hostLabel = workspace?.hostLabel ?? worktree.hostLabel
const statusCounts = emptyStatusCounts()
const agents = worktree.agents.flatMap((agent) => {
const card = cardsByPaneKey.get(agent.card.paneKey)
@@ -27,6 +38,8 @@ export function refreshAgentMapMetadata(
projectName = card.repoName
worktreeName = card.worktreeName
workspaceKind = card.workspaceKind ?? 'worktree'
hostKind = card.hostKind ?? hostKind
hostLabel = card.hostLabel ?? hostLabel
agentCount += 1
statusCounts[agentMapNodeStatus(card)] += 1
return [
@@ -42,6 +55,8 @@ export function refreshAgentMapMetadata(
...worktree,
name: worktreeName,
workspaceKind,
hostKind,
hostLabel,
agents,
statusCounts,
quiet: statusCounts.idle === agents.length
@@ -142,6 +142,25 @@ describe('agent map layout', () => {
)
})
it('preserves remote host presentation on its workspace ring', () => {
const layout = deriveAgentMapLayout(
[
card({
executionHostId: 'ssh:opaque-target',
hostKind: 'ssh',
hostLabel: 'openclaw'
})
],
NOW
)
expect(layout.projects[0].worktrees[0]).toMatchObject({
executionHostId: 'ssh:opaque-target',
hostKind: 'ssh',
hostLabel: 'openclaw'
})
})
it('reserves project and workspace header bands above dense ring contents', () => {
const layout = deriveAgentMapLayout(
Array.from({ length: 24 }, (_unused, index) =>
@@ -13,6 +13,7 @@ import {
agentMapWorktreeIdentityFromParts
} from './agent-map-workspace-identity'
import { layoutAgentMapWorktreeLineage } from './agent-map-worktree-lineage-layout'
import { agentMapWorktreeHost } from './agent-map-worktree-host'
type DashboardCard = DashboardSnapshotTypes.DashboardCard
type DashboardCardDotState = DashboardSnapshotTypes.DashboardCardDotState
@@ -58,6 +59,8 @@ export type AgentMapWorktreeRing = {
clusterParentId?: string
worktreeId: string
executionHostId: DashboardCard['executionHostId']
hostKind?: DashboardCard['hostKind']
hostLabel?: string
name: string
workspaceKind: NonNullable<DashboardCard['workspaceKind']>
x: number
@@ -154,7 +157,8 @@ function buildLocalWorktree(
for (const card of cards) {
statusCounts[agentMapNodeStatus(card)] += 1
}
const executionHostId = workspace?.executionHostId ?? cards[0]?.executionHostId
const host = agentMapWorktreeHost(cards, workspace)
const executionHostId = host.executionHostId
const parentWorktreeId = workspace?.parentWorktreeId ?? cards[0]?.parentWorktreeId
return {
id,
@@ -162,7 +166,7 @@ function buildLocalWorktree(
? agentMapWorktreeIdentityFromParts(parentWorktreeId, executionHostId)
: undefined,
worktreeId: workspace?.worktreeId ?? cards[0]?.worktreeId ?? id,
executionHostId,
...host,
name: workspace?.worktreeName ?? cards[0]?.worktreeName ?? id,
workspaceKind: workspace?.workspaceKind ?? cards[0]?.workspaceKind ?? 'worktree',
x: 0,
@@ -316,6 +320,6 @@ export function updateAgentMapLayout(
layout: geometry
}
}
const layout = refreshAgentMapMetadata(cache.geometry, cards, now)
const layout = refreshAgentMapMetadata(cache.geometry, cards, workspaces, now)
return { cache, layout }
}
@@ -0,0 +1,25 @@
import type { DashboardCard, DashboardWorkspace } from '../../../../shared/dashboard-snapshot'
import { parseExecutionHostId } from '../../../../shared/execution-host'
export function agentMapWorktreeHost(
cards: DashboardCard[],
workspace?: DashboardWorkspace
): {
executionHostId: DashboardCard['executionHostId']
hostKind: DashboardCard['hostKind']
hostLabel: DashboardCard['hostLabel']
} {
const executionHostId = workspace?.executionHostId ?? cards[0]?.executionHostId
const parsedHost = parseExecutionHostId(executionHostId)
const hostKind =
parsedHost?.kind === 'ssh'
? 'ssh'
: parsedHost?.kind === 'runtime'
? 'remote'
: (workspace?.hostKind ?? cards[0]?.hostKind)
return {
executionHostId,
hostKind,
hostLabel: workspace?.hostLabel ?? cards[0]?.hostLabel
}
}
@@ -100,7 +100,9 @@ function state(): DashboardSnapshotState {
describe('buildDashboardSnapshot folder workspaces', () => {
it('places folder-workspace agents in their real project group without git assumptions', () => {
const snapshot = buildDashboardSnapshot(state(), NOW)
const sshState = state()
sshState.sshTargetLabels = new Map([['ssh-1', 'openclaw']])
const snapshot = buildDashboardSnapshot(sshState, NOW)
expect(snapshot.cards).toHaveLength(1)
expect(snapshot.cards[0]).toMatchObject({
@@ -111,7 +113,8 @@ describe('buildDashboardSnapshot folder workspaces', () => {
worktreeName: 'Docs workspace',
workspaceKind: 'folder',
hostKind: 'ssh',
executionHostId: 'ssh:ssh-1'
executionHostId: 'ssh:ssh-1',
hostLabel: 'openclaw'
})
expect(snapshot.filterOptions?.projects).toEqual([
{ id: 'folder-workspace:group-1', label: 'Documentation' }
@@ -124,7 +127,8 @@ describe('buildDashboardSnapshot folder workspaces', () => {
worktreeName: 'Docs workspace',
workspaceKind: 'folder',
hostKind: 'ssh',
executionHostId: 'ssh:ssh-1'
executionHostId: 'ssh:ssh-1',
hostLabel: 'openclaw'
})
])
})
@@ -135,10 +139,14 @@ describe('buildDashboardSnapshot folder workspaces', () => {
{ ...folderWorkspace(), connectionId: null, executionHostId: 'runtime:environment-1' }
]
runtimeState.projectGroups = [{ ...projectGroup(), connectionId: null }]
runtimeState.runtimeEnvironments = [
{ id: 'environment-1', name: 'Build Mac' }
] as DashboardSnapshotState['runtimeEnvironments']
const snapshot = buildDashboardSnapshot(runtimeState, NOW)
expect(snapshot.cards[0].hostKind).toBe('remote')
expect(snapshot.cards[0].executionHostId).toBe('runtime:environment-1')
expect(snapshot.cards[0].hostLabel).toBe('Build Mac')
})
})
@@ -9,23 +9,23 @@ import { DASHBOARD_MAX_LABEL_LENGTH } from '../../../../shared/dashboard-snapsho
import { makePaneKey } from '../../../../shared/stable-pane-id'
import type { TerminalTab, Worktree } from '../../../../shared/types'
import { selectRuntimeAgentOrchestrationBatch } from '../sidebar/worktree-agent-orchestration-batch'
import type * as DashboardSnapshotWorkspacesModule from './dashboard-snapshot-workspaces'
import type * as AgentRowLineageModule from './agent-row-lineage'
import type * as DashboardCardHostMetadataModule from './dashboard-card-host-metadata'
const mapMetadataCalls = vi.hoisted(() => ({
hostKind: vi.fn(),
parentPaneKey: vi.fn()
}))
vi.mock('./dashboard-snapshot-workspaces', async (importOriginal) => {
const actual = await importOriginal<typeof DashboardSnapshotWorkspacesModule>()
vi.mock('./dashboard-card-host-metadata', async (importOriginal) => {
const actual = await importOriginal<typeof DashboardCardHostMetadataModule>()
return {
...actual,
dashboardCardMapWorkspaceMetadata: (
...args: Parameters<typeof actual.dashboardCardMapWorkspaceMetadata>
resolveDashboardCardHostMetadata: (
...args: Parameters<typeof actual.resolveDashboardCardHostMetadata>
) => {
mapMetadataCalls.hostKind()
return actual.dashboardCardMapWorkspaceMetadata(...args)
return actual.resolveDashboardCardHostMetadata(...args)
}
}
})
@@ -39,10 +39,8 @@ import {
resolveDashboardCardContext,
type DashboardCardContextState
} from './dashboard-card-context'
import {
dashboardCardMapWorkspaceMetadata,
collectActiveDashboardWorkspaces
} from './dashboard-snapshot-workspaces'
import { collectActiveDashboardWorkspaces } from './dashboard-snapshot-workspaces'
import { resolveDashboardCardHostMetadata } from './dashboard-card-host-metadata'
import {
boundedLabel,
boundedLabelOrUndefined,
@@ -75,7 +73,11 @@ export type DashboardSnapshotState = Pick<
| 'settings'
> &
DashboardCardContextState &
Partial<DashboardCardTerminalInputState & DashboardLaunchDetectionState>
Partial<
DashboardCardTerminalInputState &
DashboardLaunchDetectionState &
Pick<AppState, 'runtimeEnvironments' | 'sshTargetLabels'>
>
/**
* Derive the serializable dashboard snapshot from the live renderer store.
@@ -185,13 +187,20 @@ export function buildDashboardSnapshot(
? resolveDashboardCardContext(state, repo, worktree)
: undefined
if (workspaces && workspaces.length < DASHBOARD_MAX_MAP_WORKSPACES) {
const hostMetadata = resolveDashboardCardHostMetadata(
state,
workspace,
null,
undefined,
clientHost.platform
)
workspaces.push({
repoId: workspace.projectId,
worktreeId,
repoName: boundedLabel(workspace.projectName),
worktreeName: boundedLabel(worktree.displayName),
...(parentWorktreeId ? { parentWorktreeId } : {}),
...dashboardCardMapWorkspaceMetadata(workspace, null, undefined, clientHost.platform),
...hostMetadata,
workspaceStatusId: context?.workspaceStatus.id,
workspaceStatusLabel: context?.workspaceStatus.label,
workspaceStatusColor: context?.workspaceStatus.color,
@@ -246,6 +255,15 @@ export function buildDashboardSnapshot(
})
: null
const finishedAt = lastEnteredDoneAt(row)
const hostMetadata = includeCardDetails
? resolveDashboardCardHostMetadata(
state,
workspace,
ptyId,
terminalInput ?? undefined,
clientHost.platform
)
: undefined
// Only repos that actually contribute a card ship their icon.
repoIconsByRepoId[workspace.projectId] = workspace.repoIcon
@@ -266,12 +284,7 @@ export function buildDashboardSnapshot(
? {
parentPaneKey: dashboardCardParentPaneKey(row),
...(parentWorktreeId ? { parentWorktreeId } : {}),
...dashboardCardMapWorkspaceMetadata(
workspace,
ptyId,
terminalInput ?? undefined,
clientHost.platform
)
...hostMetadata
}
: {}),
workspaceStatusId: context?.workspaceStatus.id,
@@ -0,0 +1,29 @@
import type { AppState } from '@/store/types'
import type { DashboardCard } from '../../../../shared/dashboard-snapshot'
import type { ActiveDashboardWorkspace } from './dashboard-snapshot-workspaces'
import {
dashboardCardMapWorkspaceMetadata,
dashboardExecutionHostLabel
} from './dashboard-snapshot-workspaces'
import { boundedLabelOrUndefined } from './dashboard-card-labels'
type DashboardHostMetadataState = Partial<Pick<AppState, 'runtimeEnvironments' | 'sshTargetLabels'>>
export function resolveDashboardCardHostMetadata(
state: DashboardHostMetadataState,
workspace: ActiveDashboardWorkspace,
ptyId: string | null,
terminalInput: DashboardCard['terminalInput'],
clientPlatform: NodeJS.Platform
): ReturnType<typeof dashboardCardMapWorkspaceMetadata> & { hostLabel?: string } {
const metadata = dashboardCardMapWorkspaceMetadata(
workspace,
ptyId,
terminalInput,
clientPlatform
)
const hostLabel = boundedLabelOrUndefined(
dashboardExecutionHostLabel(state, metadata.executionHostId)
)
return { ...metadata, ...(hostLabel ? { hostLabel } : {}) }
}
@@ -6,7 +6,11 @@ import type {
DashboardCardWorkspaceKind
} from '../../../../shared/dashboard-snapshot'
import type { RepoIcon } from '../../../../shared/repo-icon'
import { getWorktreeExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
import {
getWorktreeExecutionHostId,
parseExecutionHostId,
type ExecutionHostId
} from '../../../../shared/execution-host'
import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree'
import { isFolderRepo } from '../../../../shared/repo-kind'
import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
@@ -24,6 +28,25 @@ export type ActiveDashboardWorkspace = {
type DashboardWorkspaceState = Pick<AppState, 'repos' | 'worktreesByRepo'> &
Partial<Pick<AppState, 'folderWorkspaces' | 'projectGroups'>>
type DashboardHostLabelState = Partial<Pick<AppState, 'runtimeEnvironments' | 'sshTargetLabels'>>
export function dashboardExecutionHostLabel(
state: DashboardHostLabelState,
executionHostId: ExecutionHostId
): string | undefined {
const parsed = parseExecutionHostId(executionHostId)
if (parsed?.kind === 'ssh') {
return state.sshTargetLabels?.get(parsed.targetId) ?? parsed.targetId
}
if (parsed?.kind === 'runtime') {
return (
state.runtimeEnvironments?.find((environment) => environment.id === parsed.environmentId)
?.name ?? parsed.environmentId
)
}
return undefined
}
function remoteHostKind(
connectionId: string | null | undefined,
executionHostId: string | null | undefined
+7 -1
View File
@@ -15418,7 +15418,13 @@
"clear": "Clear search",
"results": "{{shown}} of {{total}} shown"
},
"settingsLabel": "Agent Dashboard settings"
"settingsLabel": "Agent Dashboard settings",
"host": {
"sshNamed": "SSH host · {{host}}",
"ssh": "SSH host",
"remoteNamed": "Remote Orca host · {{host}}",
"remote": "Remote Orca host"
}
},
"dashboard": {
"sidebar": {
+4
View File
@@ -65,6 +65,8 @@ export type DashboardWorkspace = {
parentWorktreeId?: string
hostKind: DashboardCardHostKind
executionHostId: ExecutionHostId
/** Friendly saved-host name for compact host tooltips. */
hostLabel?: string
workspaceKind: DashboardCardWorkspaceKind
workspaceStatusId?: string
workspaceStatusLabel?: string
@@ -103,6 +105,8 @@ export type DashboardCard = {
hostKind?: DashboardCardHostKind
/** Exact owner used by in-window workspace actions when IDs collide across hosts. */
executionHostId?: ExecutionHostId
/** Friendly saved-host name for compact host tooltips. */
hostLabel?: string
/** Folder workspaces share the ring hierarchy without pretending to be git worktrees. */
workspaceKind?: DashboardCardWorkspaceKind
workspaceStatusId?: string