mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* feat(mobile): show SSH host health on the workspace row host badge
Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now
adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared
function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): name an unhealthy SSH host even in a single-host list
The desktop card shows SSH status on any host count, so the phone now reads target summaries
whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one
target-to-host keying, and a hook-level test pins the reply-to-row wiring.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): clear stale SSH host badges and announce them without the dot
A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps,
so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox,
Disconnected") instead of reading the middle dot.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): type the single-host health fixtures inline so the test tsc passes
Fixtures hoisted into consts widened their health maps to string, which the now-required
hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): re-record the repo-metadata goldens for SSH host health
The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the
target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
import {
|
|
getExecutionHostLabel,
|
|
getLocalExecutionHostLabel,
|
|
normalizeExecutionHostId,
|
|
parseExecutionHostId,
|
|
toSshExecutionHostId,
|
|
type ExecutionHostId
|
|
} from '../execution-host'
|
|
import type { GlobalSettings } from '../global-settings-types'
|
|
import { getHostDisplayLabelOverrides } from '../host-setting-overrides'
|
|
|
|
/** Inputs used by every client when spelling a host in a workspace row. */
|
|
export type HostContextLabelSources = {
|
|
/** Explicit labels (SSH target names and per-host display overrides). */
|
|
hostLabelById?: ReadonlyMap<string, string>
|
|
/** The execution host's platform; clients must not use the device platform. */
|
|
hostPlatform?: NodeJS.Platform | null
|
|
}
|
|
|
|
/** Canonical user-facing host label used by desktop and mobile workspace rows. */
|
|
export function getHostContextLabel(
|
|
hostId: ExecutionHostId,
|
|
sources: HostContextLabelSources = {}
|
|
): string {
|
|
const override = sources.hostLabelById?.get(hostId)?.trim()
|
|
if (override) {
|
|
return override
|
|
}
|
|
if (parseExecutionHostId(hostId)?.kind === 'local') {
|
|
// An explicit null means the paired host platform is unknown (mobile); an
|
|
// omitted platform means use the current process (desktop).
|
|
if (sources.hostPlatform === null) {
|
|
return 'This computer'
|
|
}
|
|
return sources.hostPlatform !== undefined
|
|
? getLocalExecutionHostLabel(sources.hostPlatform)
|
|
: getExecutionHostLabel(hostId)
|
|
}
|
|
return getExecutionHostLabel(hostId)
|
|
}
|
|
|
|
/**
|
|
* The host id an SSH target summary names. Summaries have carried both raw target ids and
|
|
* canonical `ssh:` ids across protocol versions, so accept either representation.
|
|
*/
|
|
export function sshTargetSummaryHostId(targetId: string): ExecutionHostId {
|
|
return normalizeExecutionHostId(targetId) ?? toSshExecutionHostId(targetId)
|
|
}
|
|
|
|
/** Build labels for SSH targets and apply persisted display-name overrides. */
|
|
export function buildHostLabelById(args: {
|
|
sshTargets: readonly { id: string; label: string }[]
|
|
hostSettingOverrides: unknown
|
|
}): Map<ExecutionHostId, string> {
|
|
const labels = new Map<ExecutionHostId, string>()
|
|
for (const target of args.sshTargets) {
|
|
const label = target.label.trim()
|
|
if (!target.id.trim() || !label) {
|
|
continue
|
|
}
|
|
labels.set(sshTargetSummaryHostId(target.id), label)
|
|
}
|
|
const overrides =
|
|
args.hostSettingOverrides && typeof args.hostSettingOverrides === 'object'
|
|
? getHostDisplayLabelOverrides({
|
|
hostSettingOverrides: args.hostSettingOverrides as GlobalSettings['hostSettingOverrides']
|
|
})
|
|
: new Map<ExecutionHostId, string>()
|
|
for (const [hostId, label] of overrides) {
|
|
const normalized = normalizeExecutionHostId(hostId) ?? toSshExecutionHostId(hostId)
|
|
if (normalized) {
|
|
labels.set(normalized, label)
|
|
}
|
|
}
|
|
return labels
|
|
}
|
|
|
|
/** Generic mixed-host projection shared by desktop grouping and mobile sections. */
|
|
export function getMixedHostContextLabels<T>(
|
|
items: readonly T[],
|
|
args: {
|
|
getHostId: (item: T) => ExecutionHostId
|
|
getIdentity: (item: T) => string
|
|
sources?: HostContextLabelSources
|
|
}
|
|
): Map<string, string> | undefined {
|
|
// Why two passes: on a single-host install the map is built for every item and then discarded.
|
|
// Deciding first costs one getHostId per item and skips the label work entirely.
|
|
let firstHostId: ExecutionHostId | undefined
|
|
let isMixed = false
|
|
for (const item of items) {
|
|
const hostId = args.getHostId(item)
|
|
if (firstHostId === undefined) {
|
|
firstHostId = hostId
|
|
continue
|
|
}
|
|
if (hostId !== firstHostId) {
|
|
isMixed = true
|
|
break
|
|
}
|
|
}
|
|
if (!isMixed) {
|
|
return undefined
|
|
}
|
|
const labelsByIdentity = new Map<string, string>()
|
|
for (const item of items) {
|
|
labelsByIdentity.set(
|
|
args.getIdentity(item),
|
|
getHostContextLabel(args.getHostId(item), args.sources)
|
|
)
|
|
}
|
|
return labelsByIdentity
|
|
}
|