mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
feat(dashboard): identify SSH and remote hosts (#14177)
* feat(dashboard): identify SSH and remote hosts * fix(dashboard): resolve host labels consistently * fix(dashboard): reuse host server icon * test(dashboard): guard host label refresh cost * test(dashboard): satisfy runtime environment shape
This commit is contained in:
@@ -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,54 @@
|
||||
// @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>
|
||||
)
|
||||
|
||||
const badge = screen.getByLabelText('Remote Orca host · Build Mac')
|
||||
expect(badge).toHaveAttribute('data-dashboard-host-badge', 'remote')
|
||||
expect(badge.querySelector('.lucide-server')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<TooltipProvider>
|
||||
<DashboardHostBadge hostKind="local" executionHostId="local" />
|
||||
</TooltipProvider>
|
||||
)
|
||||
expect(screen.queryByLabelText(/host/i)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import { 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
|
||||
}
|
||||
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}
|
||||
>
|
||||
<Server 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) =>
|
||||
@@ -448,6 +467,38 @@ describe('agent map layout', () => {
|
||||
expect(topologyChanged.cache.packingGeneration).toBe(2)
|
||||
})
|
||||
|
||||
it('refreshes saved host labels without repacking geometry', () => {
|
||||
const cards = [
|
||||
card({
|
||||
executionHostId: 'ssh:builder',
|
||||
hostKind: 'ssh',
|
||||
hostLabel: 'Builder'
|
||||
})
|
||||
]
|
||||
const workspaces = [
|
||||
workspace({
|
||||
worktreeId: 'worktree-1',
|
||||
executionHostId: 'ssh:builder',
|
||||
hostKind: 'ssh',
|
||||
hostLabel: 'Builder'
|
||||
})
|
||||
]
|
||||
const initial = updateAgentMapLayout(null, cards, NOW, workspaces)
|
||||
packWorktrees.mockClear()
|
||||
|
||||
const updated = updateAgentMapLayout(
|
||||
initial.cache,
|
||||
cards.map((candidate) => ({ ...candidate, hostLabel: 'CI Builder' })),
|
||||
NOW,
|
||||
workspaces.map((candidate) => ({ ...candidate, hostLabel: 'CI Builder' }))
|
||||
)
|
||||
|
||||
expect(updated.cache).toBe(initial.cache)
|
||||
expect(updated.cache.packingGeneration).toBe(1)
|
||||
expect(packWorktrees).not.toHaveBeenCalled()
|
||||
expect(updated.layout.projects[0].worktrees[0].hostLabel).toBe('CI Builder')
|
||||
})
|
||||
|
||||
it('packs worktree rings tightly without a square grid', () => {
|
||||
const layout = deriveAgentMapLayout(
|
||||
Array.from({ length: 36 }, (_, 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
|
||||
}
|
||||
}
|
||||
+32
-3
@@ -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,35 @@ 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 unknown 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')
|
||||
})
|
||||
|
||||
it('uses the user-facing host label override', () => {
|
||||
const runtimeState = state()
|
||||
runtimeState.folderWorkspaces = [
|
||||
{ ...folderWorkspace(), connectionId: null, executionHostId: 'runtime:environment-1' }
|
||||
]
|
||||
runtimeState.projectGroups = [{ ...projectGroup(), connectionId: null }]
|
||||
runtimeState.runtimeEnvironments = [
|
||||
{ id: 'environment-1', name: 'Build Mac' }
|
||||
] as unknown as DashboardSnapshotState['runtimeEnvironments']
|
||||
runtimeState.settings = {
|
||||
hostSettingOverrides: {
|
||||
'runtime:environment-1': { displayLabel: 'CI Builder' }
|
||||
}
|
||||
} as unknown as DashboardSnapshotState['settings']
|
||||
|
||||
const snapshot = buildDashboardSnapshot(runtimeState, NOW)
|
||||
|
||||
expect(snapshot.cards[0].hostLabel).toBe('CI Builder')
|
||||
expect(snapshot.workspaces?.[0].hostLabel).toBe('CI Builder')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -603,6 +603,50 @@ describe('buildDashboardSnapshot', () => {
|
||||
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)
|
||||
})
|
||||
|
||||
it("resolves a live pty's host-input profile for card snapshots", () => {
|
||||
const snapshot = buildDashboardSnapshot(
|
||||
baseState({ agentStatusByPaneKey: { [PANE_KEY]: entry({}) } }),
|
||||
|
||||
@@ -40,8 +40,8 @@ import {
|
||||
type DashboardCardContextState
|
||||
} from './dashboard-card-context'
|
||||
import {
|
||||
dashboardCardMapWorkspaceMetadata,
|
||||
collectActiveDashboardWorkspaces
|
||||
collectActiveDashboardWorkspaces,
|
||||
dashboardCardMapWorkspaceMetadata
|
||||
} from './dashboard-snapshot-workspaces'
|
||||
import {
|
||||
boundedLabel,
|
||||
@@ -75,7 +75,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 +189,19 @@ export function buildDashboardSnapshot(
|
||||
? resolveDashboardCardContext(state, repo, worktree)
|
||||
: undefined
|
||||
if (workspaces && workspaces.length < DASHBOARD_MAX_MAP_WORKSPACES) {
|
||||
const hostMetadata = dashboardCardMapWorkspaceMetadata(
|
||||
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 +256,14 @@ export function buildDashboardSnapshot(
|
||||
})
|
||||
: null
|
||||
const finishedAt = lastEnteredDoneAt(row)
|
||||
const hostMetadata = includeCardDetails
|
||||
? dashboardCardMapWorkspaceMetadata(
|
||||
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,
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import type { AppState } from '@/store/types'
|
||||
import { getRemoteRuntimePtyEnvironmentId } from '@/runtime/runtime-terminal-stream'
|
||||
import type {
|
||||
DashboardCard,
|
||||
DashboardCardHostKind,
|
||||
DashboardCardWorkspaceKind
|
||||
import {
|
||||
DASHBOARD_MAX_LABEL_LENGTH,
|
||||
type DashboardCard,
|
||||
type DashboardCardHostKind,
|
||||
type DashboardCardWorkspaceKind
|
||||
} from '../../../../shared/dashboard-snapshot'
|
||||
import type { RepoIcon } from '../../../../shared/repo-icon'
|
||||
import { getWorktreeExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import {
|
||||
getWorktreeExecutionHostId,
|
||||
parseExecutionHostId,
|
||||
toRuntimeExecutionHostId,
|
||||
toSshExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree'
|
||||
import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides'
|
||||
import { isFolderRepo } from '../../../../shared/repo-kind'
|
||||
import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
|
||||
|
||||
@@ -19,10 +27,32 @@ export type ActiveDashboardWorkspace = {
|
||||
worktree: AppState['worktreesByRepo'][string][number] & { parentWorktreeId?: string | null }
|
||||
workspaceKind: DashboardCardWorkspaceKind
|
||||
remoteHostKind: Extract<DashboardCardHostKind, 'ssh' | 'remote'> | null
|
||||
hostLabel?: string
|
||||
}
|
||||
|
||||
type DashboardWorkspaceState = Pick<AppState, 'repos' | 'worktreesByRepo'> &
|
||||
Partial<Pick<AppState, 'folderWorkspaces' | 'projectGroups'>>
|
||||
Partial<
|
||||
Pick<
|
||||
AppState,
|
||||
'folderWorkspaces' | 'projectGroups' | 'runtimeEnvironments' | 'settings' | 'sshTargetLabels'
|
||||
>
|
||||
>
|
||||
|
||||
function buildHostLabelLookup(
|
||||
state: DashboardWorkspaceState
|
||||
): ReadonlyMap<ExecutionHostId, string> {
|
||||
const labels = new Map<ExecutionHostId, string>()
|
||||
for (const [targetId, label] of state.sshTargetLabels ?? []) {
|
||||
labels.set(toSshExecutionHostId(targetId), label)
|
||||
}
|
||||
for (const environment of state.runtimeEnvironments ?? []) {
|
||||
labels.set(toRuntimeExecutionHostId(environment.id), environment.name)
|
||||
}
|
||||
for (const [hostId, label] of getHostDisplayLabelOverrides(state.settings)) {
|
||||
labels.set(hostId, label)
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
function remoteHostKind(
|
||||
connectionId: string | null | undefined,
|
||||
@@ -40,6 +70,20 @@ export function collectActiveDashboardWorkspaces(
|
||||
): ActiveDashboardWorkspace[] {
|
||||
const workspaces: ActiveDashboardWorkspace[] = []
|
||||
const seenWorkspaceIds = new Set<string>()
|
||||
let hostLabels: ReadonlyMap<ExecutionHostId, string> | null = null
|
||||
const resolveHostLabel = (executionHostId: ExecutionHostId): string | undefined => {
|
||||
const parsed = includeMapMetadata ? parseExecutionHostId(executionHostId) : null
|
||||
if (parsed?.kind !== 'ssh' && parsed?.kind !== 'runtime') {
|
||||
return undefined
|
||||
}
|
||||
hostLabels ??= buildHostLabelLookup(state)
|
||||
const label =
|
||||
hostLabels.get(executionHostId) ??
|
||||
(parsed.kind === 'ssh' ? parsed.targetId : parsed.environmentId)
|
||||
return label.length > DASHBOARD_MAX_LABEL_LENGTH
|
||||
? label.slice(0, DASHBOARD_MAX_LABEL_LENGTH)
|
||||
: label
|
||||
}
|
||||
|
||||
for (const repo of state.repos ?? []) {
|
||||
for (const worktree of state.worktreesByRepo?.[repo.id] ?? []) {
|
||||
@@ -47,6 +91,9 @@ export function collectActiveDashboardWorkspaces(
|
||||
continue
|
||||
}
|
||||
seenWorkspaceIds.add(worktree.id)
|
||||
const workspaceHostLabel = includeMapMetadata
|
||||
? resolveHostLabel(getWorktreeExecutionHostId(worktree, repo))
|
||||
: undefined
|
||||
workspaces.push({
|
||||
projectId: repo.id,
|
||||
projectName: repo.displayName,
|
||||
@@ -56,7 +103,8 @@ export function collectActiveDashboardWorkspaces(
|
||||
workspaceKind: includeMapMetadata && isFolderRepo(repo) ? 'folder' : 'worktree',
|
||||
remoteHostKind: includeMapMetadata
|
||||
? remoteHostKind(repo.connectionId, worktree.hostId ?? repo.executionHostId)
|
||||
: null
|
||||
: null,
|
||||
...(workspaceHostLabel ? { hostLabel: workspaceHostLabel } : {})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -70,6 +118,9 @@ export function collectActiveDashboardWorkspaces(
|
||||
continue
|
||||
}
|
||||
const projectGroup = projectGroupsById.get(folderWorkspace.projectGroupId)
|
||||
const workspaceHostLabel = includeMapMetadata
|
||||
? resolveHostLabel(getWorktreeExecutionHostId(worktree, undefined))
|
||||
: undefined
|
||||
workspaces.push({
|
||||
projectId: `folder-workspace:${folderWorkspace.projectGroupId}`,
|
||||
projectName: projectGroup?.name ?? folderWorkspace.name,
|
||||
@@ -82,7 +133,8 @@ export function collectActiveDashboardWorkspaces(
|
||||
folderWorkspace.connectionId ?? projectGroup?.connectionId,
|
||||
worktree.hostId ?? projectGroup?.executionHostId
|
||||
)
|
||||
: null
|
||||
: null,
|
||||
...(workspaceHostLabel ? { hostLabel: workspaceHostLabel } : {})
|
||||
})
|
||||
}
|
||||
return workspaces
|
||||
@@ -115,10 +167,12 @@ export function dashboardCardMapWorkspaceMetadata(
|
||||
hostKind: DashboardCardHostKind
|
||||
executionHostId: ExecutionHostId
|
||||
workspaceKind: DashboardCardWorkspaceKind
|
||||
hostLabel?: string
|
||||
} {
|
||||
return {
|
||||
hostKind: dashboardCardHostKind(workspace, ptyId, terminalInput, clientPlatform),
|
||||
executionHostId: getWorktreeExecutionHostId(workspace.worktree, workspace.repo ?? undefined),
|
||||
workspaceKind: workspace.workspaceKind
|
||||
workspaceKind: workspace.workspaceKind,
|
||||
...(workspace.hostLabel ? { hostLabel: workspace.hostLabel } : {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ function makeSnapshotWatchState(): DashboardSnapshotWatchState {
|
||||
detectedWorktreesByRepo: {},
|
||||
folderWorkspaces: [],
|
||||
projectGroups: [],
|
||||
sshTargetLabels: new Map(),
|
||||
restoredRuntimeHostIdByWorkspaceSessionKey: {},
|
||||
runtimeEnvironments: [],
|
||||
runtimeEnvironmentCatalogHydrated: false,
|
||||
@@ -207,6 +208,12 @@ describe('useDashboardPopoutBridge', () => {
|
||||
expect(
|
||||
dashboardSnapshotInputsChanged({ ...previousState, agentStatusEpoch: 1 }, previousState)
|
||||
).toBe(false)
|
||||
expect(
|
||||
dashboardSnapshotInputsChanged(
|
||||
{ ...previousState, sshTargetLabels: new Map([['target-1', 'Builder']]) },
|
||||
previousState
|
||||
)
|
||||
).toBe(true)
|
||||
|
||||
// Why: each card's preview terminal keys against a host-input profile
|
||||
// derived from these. Not republishing leaves the pop-out encoding bytes
|
||||
|
||||
@@ -78,6 +78,7 @@ export function dashboardSnapshotInputsChanged(
|
||||
// these two instead of worktreesByRepo.
|
||||
state.folderWorkspaces !== previousState.folderWorkspaces ||
|
||||
state.projectGroups !== previousState.projectGroups ||
|
||||
state.sshTargetLabels !== previousState.sshTargetLabels ||
|
||||
state.restoredRuntimeHostIdByWorkspaceSessionKey !==
|
||||
previousState.restoredRuntimeHostIdByWorkspaceSessionKey ||
|
||||
state.runtimeEnvironments !== previousState.runtimeEnvironments ||
|
||||
|
||||
@@ -161,4 +161,17 @@ describe('useLiveDashboardSnapshot', () => {
|
||||
rerender()
|
||||
expect(result.current.cards[0].terminalInput?.hostPlatform).toBe('win32')
|
||||
})
|
||||
|
||||
it('re-derives saved SSH labels when the target catalog changes', () => {
|
||||
seed({ tabAutoGenerateTitle: false })
|
||||
useAppStore.setState({
|
||||
repos: [{ ...repo(), connectionId: 'target-1', executionHostId: 'ssh:target-1' }]
|
||||
})
|
||||
const { result, rerender } = renderHook(() => useLiveDashboardSnapshot())
|
||||
expect(result.current.cards[0].hostLabel).toBe('target-1')
|
||||
|
||||
useAppStore.setState({ sshTargetLabels: new Map([['target-1', 'Builder']]) })
|
||||
rerender()
|
||||
expect(result.current.cards[0].hostLabel).toBe('Builder')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,6 +41,7 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot {
|
||||
// these two instead of worktreesByRepo.
|
||||
const folderWorkspaces = useAppStore((s) => s.folderWorkspaces)
|
||||
const projectGroups = useAppStore((s) => s.projectGroups)
|
||||
const sshTargetLabels = useAppStore((s) => s.sshTargetLabels)
|
||||
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
|
||||
const sshStateByEnvironment = useAppStore((s) => s.sshStateByEnvironment)
|
||||
const runtimeStatusByEnvironmentId = useAppStore((s) => s.runtimeStatusByEnvironmentId)
|
||||
@@ -82,6 +83,7 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot {
|
||||
detectedWorktreesByRepo,
|
||||
folderWorkspaces,
|
||||
projectGroups,
|
||||
sshTargetLabels,
|
||||
sshConnectionStates,
|
||||
sshStateByEnvironment,
|
||||
runtimeStatusByEnvironmentId,
|
||||
@@ -120,6 +122,7 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot {
|
||||
detectedWorktreesByRepo,
|
||||
folderWorkspaces,
|
||||
projectGroups,
|
||||
sshTargetLabels,
|
||||
sshConnectionStates,
|
||||
sshStateByEnvironment,
|
||||
runtimeStatusByEnvironmentId,
|
||||
|
||||
@@ -15453,7 +15453,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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user