mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
fix(dashboard): resolve host labels consistently
This commit is contained in:
+21
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user