fix(dashboard): resolve host labels consistently

This commit is contained in:
Brennan Benson
2026-08-12 20:21:01 -07:00
parent 330a20dcab
commit d7902a1f59
9 changed files with 155 additions and 64 deletions
@@ -149,4 +149,25 @@ describe('buildDashboardSnapshot folder workspaces', () => {
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 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')
})
})
@@ -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-card-host-metadata', async (importOriginal) => {
const actual = await importOriginal<typeof DashboardCardHostMetadataModule>()
vi.mock('./dashboard-snapshot-workspaces', async (importOriginal) => {
const actual = await importOriginal<typeof DashboardSnapshotWorkspacesModule>()
return {
...actual,
resolveDashboardCardHostMetadata: (
...args: Parameters<typeof actual.resolveDashboardCardHostMetadata>
dashboardCardMapWorkspaceMetadata: (
...args: Parameters<typeof actual.dashboardCardMapWorkspaceMetadata>
) => {
mapMetadataCalls.hostKind()
return actual.resolveDashboardCardHostMetadata(...args)
return actual.dashboardCardMapWorkspaceMetadata(...args)
}
}
})
@@ -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 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({}) } }),
@@ -39,8 +39,10 @@ import {
resolveDashboardCardContext,
type DashboardCardContextState
} from './dashboard-card-context'
import { collectActiveDashboardWorkspaces } from './dashboard-snapshot-workspaces'
import { resolveDashboardCardHostMetadata } from './dashboard-card-host-metadata'
import {
collectActiveDashboardWorkspaces,
dashboardCardMapWorkspaceMetadata
} from './dashboard-snapshot-workspaces'
import {
boundedLabel,
boundedLabelOrUndefined,
@@ -187,8 +189,7 @@ export function buildDashboardSnapshot(
? resolveDashboardCardContext(state, repo, worktree)
: undefined
if (workspaces && workspaces.length < DASHBOARD_MAX_MAP_WORKSPACES) {
const hostMetadata = resolveDashboardCardHostMetadata(
state,
const hostMetadata = dashboardCardMapWorkspaceMetadata(
workspace,
null,
undefined,
@@ -256,8 +257,7 @@ export function buildDashboardSnapshot(
: null
const finishedAt = lastEnteredDoneAt(row)
const hostMetadata = includeCardDetails
? resolveDashboardCardHostMetadata(
state,
? dashboardCardMapWorkspaceMetadata(
workspace,
ptyId,
terminalInput ?? undefined,
@@ -1,29 +0,0 @@
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 } : {}) }
}
@@ -1,17 +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,
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'
@@ -23,28 +27,31 @@ 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'
>
>
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
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)
}
if (parsed?.kind === 'runtime') {
return (
state.runtimeEnvironments?.find((environment) => environment.id === parsed.environmentId)
?.name ?? parsed.environmentId
)
for (const environment of state.runtimeEnvironments ?? []) {
labels.set(toRuntimeExecutionHostId(environment.id), environment.name)
}
return undefined
for (const [hostId, label] of getHostDisplayLabelOverrides(state.settings)) {
labels.set(hostId, label)
}
return labels
}
function remoteHostKind(
@@ -63,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] ?? []) {
@@ -70,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,
@@ -79,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 } : {})
})
}
}
@@ -93,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,
@@ -105,7 +133,8 @@ export function collectActiveDashboardWorkspaces(
folderWorkspace.connectionId ?? projectGroup?.connectionId,
worktree.hostId ?? projectGroup?.executionHostId
)
: null
: null,
...(workspaceHostLabel ? { hostLabel: workspaceHostLabel } : {})
})
}
return workspaces
@@ -138,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,