mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 16:03:45 +00:00
refactor: align worktree host labels across clients
This commit is contained in:
@@ -155,6 +155,53 @@ describe('buildRows with pinned worktrees', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('uses the registered SSH target label for openclaw rows', () => {
|
||||
const sshRepo: Repo = {
|
||||
...remoteRepo,
|
||||
id: 'repo-openclaw',
|
||||
connectionId: 'openclaw',
|
||||
executionHostId: 'ssh:openclaw'
|
||||
}
|
||||
const sshWorktree: Worktree = {
|
||||
...remoteWorktree,
|
||||
id: 'wt-openclaw',
|
||||
repoId: sshRepo.id
|
||||
}
|
||||
const rows = buildRows(
|
||||
'workspace-status',
|
||||
[worktree, sshWorktree],
|
||||
new Map([
|
||||
[repo.id, repo],
|
||||
[sshRepo.id, sshRepo]
|
||||
]),
|
||||
null,
|
||||
new Set(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{},
|
||||
new Map([
|
||||
[worktree.id, worktree],
|
||||
[sshWorktree.id, sshWorktree]
|
||||
]),
|
||||
false,
|
||||
undefined,
|
||||
[],
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Map(),
|
||||
[],
|
||||
undefined,
|
||||
[],
|
||||
new Map([['ssh:openclaw', 'openclaw']])
|
||||
)
|
||||
|
||||
expect(rows.filter((row) => row.type === 'item')).toMatchObject([
|
||||
{ worktree: { id: worktree.id }, hostContextLabel: LOCAL_HOST_LABEL },
|
||||
{ worktree: { id: sshWorktree.id }, hostContextLabel: 'openclaw' }
|
||||
])
|
||||
})
|
||||
|
||||
it('shows distinct Orca server names when status grouping mixes runtime hosts', () => {
|
||||
const firstRepo: Repo = {
|
||||
...repo,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import type { Worktree } from '../../../../../../shared/worktree/types'
|
||||
import {
|
||||
getExecutionHostLabel,
|
||||
getRepoExecutionHostId,
|
||||
getWorktreeExecutionHostId
|
||||
} from '../../../../../../shared/execution-host'
|
||||
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import {
|
||||
getHostContextLabel,
|
||||
getMixedHostContextLabels as getSharedMixedHostContextLabels
|
||||
} from '../../../../../../shared/worktree/host-context-labels'
|
||||
import { getWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity'
|
||||
import {
|
||||
getProjectGroupingForRepo,
|
||||
@@ -15,7 +18,7 @@ import {
|
||||
import { getFolderWorkspaceHostId } from '../../folder-workspace-host-id'
|
||||
import type { RenderableFolderWorkspace } from './folder-workspace-lanes'
|
||||
|
||||
function getRepoHostId(repoId: string, repoMap: Map<string, Repo>): string | null {
|
||||
function getRepoHostId(repoId: string, repoMap: Map<string, Repo>): ExecutionHostId | null {
|
||||
const repo = repoMap.get(repoId)
|
||||
return repo ? getRepoExecutionHostId(repo) : null
|
||||
}
|
||||
@@ -28,14 +31,14 @@ function getRepoHostLabel(
|
||||
): string | null {
|
||||
const setup = projectIndex?.setupByRepoId.get(repoId)
|
||||
if (setup) {
|
||||
return hostLabelById?.get(setup.hostId) ?? getExecutionHostLabel(setup.hostId)
|
||||
return getHostContextLabel(setup.hostId, { hostLabelById })
|
||||
}
|
||||
const repo = repoMap.get(repoId)
|
||||
if (!repo) {
|
||||
return null
|
||||
}
|
||||
const hostId = getRepoExecutionHostId(repo)
|
||||
return hostLabelById?.get(hostId) ?? getExecutionHostLabel(hostId)
|
||||
return getHostContextLabel(hostId, { hostLabelById })
|
||||
}
|
||||
|
||||
export function getMixedHostContextLabels(
|
||||
@@ -45,16 +48,22 @@ export function getMixedHostContextLabels(
|
||||
hostLabelById: ReadonlyMap<string, string> | undefined
|
||||
): Map<string, string> | undefined {
|
||||
const labelsByRepoId = new Map<string, string>()
|
||||
const uniqueLabels = new Set<string>()
|
||||
// Host identity, not the rendered label, determines whether rows are ambiguous:
|
||||
// two hosts can intentionally share a user-facing label.
|
||||
const uniqueHostIds = new Set<ExecutionHostId>()
|
||||
for (const repoId of group.repoIds) {
|
||||
const label = getRepoHostLabel(repoId, repoMap, projectIndex, hostLabelById)
|
||||
if (!label) {
|
||||
continue
|
||||
}
|
||||
labelsByRepoId.set(repoId, label)
|
||||
uniqueLabels.add(label)
|
||||
const setup = projectIndex?.setupByRepoId.get(repoId)
|
||||
const hostId = setup?.hostId ?? getRepoHostId(repoId, repoMap)
|
||||
if (hostId) {
|
||||
uniqueHostIds.add(hostId)
|
||||
}
|
||||
}
|
||||
return uniqueLabels.size > 1 ? labelsByRepoId : undefined
|
||||
return uniqueHostIds.size > 1 ? labelsByRepoId : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,17 +137,12 @@ export function getMixedWorktreeHostContextLabels(
|
||||
hostLabelById: ReadonlyMap<string, string> | undefined,
|
||||
defaultHostId: ExecutionHostId
|
||||
): Map<string, string> | undefined {
|
||||
const labelsByIdentity = new Map<string, string>()
|
||||
const uniqueHostIds = new Set<ExecutionHostId>()
|
||||
for (const worktree of worktrees) {
|
||||
const hostId = getWorktreeExecutionHostId(worktree, repoMap.get(worktree.repoId), defaultHostId)
|
||||
uniqueHostIds.add(hostId)
|
||||
labelsByIdentity.set(
|
||||
getWorktreeHostIdentity(worktree),
|
||||
hostLabelById?.get(hostId) ?? getExecutionHostLabel(hostId)
|
||||
)
|
||||
}
|
||||
return uniqueHostIds.size > 1 ? labelsByIdentity : undefined
|
||||
return getSharedMixedHostContextLabels(worktrees, {
|
||||
getHostId: (worktree) =>
|
||||
getWorktreeExecutionHostId(worktree, repoMap.get(worktree.repoId), defaultHostId),
|
||||
getIdentity: getWorktreeHostIdentity,
|
||||
sources: { hostLabelById }
|
||||
})
|
||||
}
|
||||
|
||||
export function getHostWorktreeCounts(
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build labels for SSH targets and apply persisted display-name overrides.
|
||||
* Target summaries have appeared both as raw target ids and canonical `ssh:` ids
|
||||
* across protocol versions, so accept either representation.
|
||||
*/
|
||||
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
|
||||
}
|
||||
const hostId = normalizeExecutionHostId(target.id) ?? toSshExecutionHostId(target.id)
|
||||
if (hostId) {
|
||||
labels.set(hostId, 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 {
|
||||
const labelsByIdentity = new Map<string, string>()
|
||||
const hostIds = new Set<ExecutionHostId>()
|
||||
for (const item of items) {
|
||||
const hostId = args.getHostId(item)
|
||||
hostIds.add(hostId)
|
||||
labelsByIdentity.set(args.getIdentity(item), getHostContextLabel(hostId, args.sources))
|
||||
}
|
||||
return hostIds.size > 1 ? labelsByIdentity : undefined
|
||||
}
|
||||
Reference in New Issue
Block a user