Merge pull request #313 from nwparker/cloud/w2-cloud-one-host

feat(cloud): Orca Cloud is one host in the sidebar and Hosts filter (RA-2, RA-6)
This commit is contained in:
Neil
2026-11-10 17:00:32 -08:00
committed by GitHub
12 changed files with 465 additions and 15 deletions
@@ -1,5 +1,6 @@
import React from 'react'
import { useAppStore } from '@/store'
import { findWorktreeById } from '@/store/github/refresh/worktree-refresh'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/utils'
import { HostGlyph, vmStateToDot } from '@/components/host-row-icon'
@@ -87,6 +88,8 @@ export function WorktreeCardHostStateWord({
? vmStateToDot(selectEphemeralVmWorkspaceState(state, worktreeId))
: null
)
// The separator only when line 2 goes on (a branch); "Asleep ·" alone reads as cut off.
const hasBranch = useAppStore((state) => Boolean(findWorktreeById(state, worktreeId)?.branch))
const t = useTranslate()
const state = cloud.isCloud ? cloud.state : vmDot
// Unknown is not a state to name (the panel shows a placeholder until the first read).
@@ -101,7 +104,8 @@ export function WorktreeCardHostStateWord({
PROBLEM.has(state) ? 'text-destructive' : 'text-muted-foreground'
)}
>
{cloudStateLabel(state, t)} ·
{cloudStateLabel(state, t)}
{hasBranch ? ' ·' : null}
</span>
)
}
@@ -34,7 +34,8 @@ type SidebarHostScopeMenuSectionProps = {
function getHostMetadata(host: SidebarHostOption, t: Translate = translate): string {
const healthLabel = getSidebarHostHealthLabel(host.health, host.blockedReason, t, host.vmState)
if (host.kind === 'local') {
// Local and Orca Cloud name themselves: "This computer", "1 running · 1 asleep".
if (host.kind === 'local' || host.orcaCloud) {
return host.detail
}
if (host.kind === 'ssh') {
@@ -1,4 +1,5 @@
import { shallow } from 'zustand/shallow'
import { collapseOrcaCloudHosts } from './sidebar-orca-cloud-host'
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
import type { Repo } from '../../../../../../shared/repo-types'
import type { WorkspaceHostScope } from '../../../../../../shared/ui-chrome-types'
@@ -62,6 +63,8 @@ export type SidebarHostOption = {
wslDistro?: string
/** Why a listed WSL host cannot run: the experiment is off, or this build lacks its server files. */
wslUnavailable?: 'turned-off' | 'server-files-missing'
/** On the one row standing for every Orca Cloud sandbox: how many run and sleep. */
orcaCloud?: { running: number; asleep: number }
}
export type SidebarHostScopeOption = {
@@ -117,7 +120,7 @@ export function buildSidebarHostOptions(
const activeRuntimeHostId = args.settings?.activeRuntimeEnvironmentId?.trim()
? toRuntimeExecutionHostId(args.settings.activeRuntimeEnvironmentId.trim())
: null
return buildExecutionHostRegistry({
const hosts = buildExecutionHostRegistry({
repos: args.repos,
settings: args.settings,
sshTargetLabels: args.sshTargetLabels,
@@ -126,7 +129,7 @@ export function buildSidebarHostOptions(
runtimeStatusByEnvironmentId: args.runtimeStatusByEnvironmentId,
hostLabelOverrides: args.hostLabelOverrides,
ephemeralVmHosts: args.ephemeralVmHosts
}).map((host) => {
}).map((host): SidebarHostOption => {
if (host.kind === 'local') {
return { ...host, presence: 'local' }
}
@@ -174,6 +177,7 @@ export function buildSidebarHostOptions(
presence: host.id === activeRuntimeHostId ? 'active' : 'project'
}
})
return collapseOrcaCloudHosts(hosts, args.runtimeEnvironments ?? [], t)
}
export function shouldShowHostScopeControls(hosts: readonly SidebarHostOption[]): boolean {
@@ -0,0 +1,138 @@
import { describe, expect, it } from 'vitest'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
import { planSidebarRows } from '../../../../../../shared/sidebar-list/sidebar-row-plan'
import { addHostSectionRows } from '../../../../../../shared/sidebar-list/host-section-rows'
import { createHostScopedRepoOf } from '../../../../../../shared/sidebar-list/sidebar-feed-index'
import { cloneDefaultWorkspaceStatuses } from '../../../../../../shared/workspace-statuses'
import { expandEquivalentExecutionHostIds } from '../../../../../../shared/execution-host/managed-orcad-execution-host'
import { repo, worktree } from '../grouping/worktree-list-groups-test-fixtures'
import { buildSidebarHostOptions, buildSidebarHostScopeOptions } from './sidebar-host-options'
// English defaults with {{slots}} filled, like i18next with no catalog.
const t = (_key: string, fallback: string, params?: Record<string, unknown>): string =>
fallback.replace(/\{\{(\w+)\}\}/g, (_slot, name: string) => String(params?.[name] ?? ''))
// Two Orca Cloud sandboxes (one running, one asleep) and this computer, all for one project.
const cloudA: Repo = { ...repo, path: '/workspace/hello', executionHostId: 'runtime:env-a' }
const cloudB: Repo = { ...repo, path: '/workspace/hello', executionHostId: 'runtime:env-b' }
const environments = [
{ id: 'env-a', name: 'hello sandbox aaaaaaaa', source: 'ephemeral-vm' as const },
{ id: 'env-b', name: 'hello sandbox bbbbbbbb', source: 'ephemeral-vm' as const }
]
function hostOptions() {
return buildSidebarHostOptions(
{
repos: [repo, cloudA, cloudB],
sshTargetLabels: new Map(),
settings: null,
runtimeEnvironments: environments,
ephemeralVmHosts: [
{ runtimeEnvironmentId: 'env-a', status: 'running', waking: false },
{ runtimeEnvironmentId: 'env-b', status: 'suspended', waking: false }
]
},
t
)
}
const worktrees: Worktree[] = [
{ ...worktree, id: 'wt-local', displayName: 'local work' },
{ ...worktree, id: 'wt-a', hostId: 'runtime:env-a', displayName: 'fix login' },
{ ...worktree, id: 'wt-b', hostId: 'runtime:env-b', displayName: 'docs refresh' }
]
function groupedByHost() {
const hosts = hostOptions()
const repoMap = new Map([[repo.id, repo]])
const rows = planSidebarRows({
groupBy: 'repo',
sortBy: 'smart',
worktrees,
repoMap,
repoOfWorktree: createHostScopedRepoOf([repo, cloudA, cloudB], repoMap),
prGroupOf: () => 'in-progress',
workspaceStatuses: cloneDefaultWorkspaceStatuses(),
projectOrderBy: 'manual',
lineageById: {},
worktreeMap: new Map(worktrees.map((row) => [row.id, row])),
nestLineage: false,
projectGroups: [],
placeholderRepoIds: new Set(),
importedWorktreesByRepo: new Map(),
newExternalWorktreesInboxByRepo: new Map(),
pendingCreations: [],
folderWorkspaces: [],
defaultHostId: 'local',
pinnedDisplayPolicy: 'single-location',
filterRepoIds: []
}).render(new Set())
const everyHost: ExecutionHostId[] = expandEquivalentExecutionHostIds(
hosts,
hosts.map((host) => host.id)
)
return addHostSectionRows({
rows,
hostOptions: hosts,
workspaceHostScope: 'all',
// The Hosts filter with every host checked: the view that draws host headers.
visibleWorkspaceHostIds: everyHost,
defaultHostId: 'local',
localHostLabel: 'Local Mac',
preferProjectGrouping: true
})
}
describe('Orca Cloud is one host (RA-2)', () => {
it('collapses two sandboxes into one "Orca Cloud" section with an aggregate state', () => {
const rows = groupedByHost()
const hostHeaders = rows.filter((row) => row.type === 'host-header')
expect(hostHeaders.map((row) => row.label)).toEqual(['Local Mac', 'Orca Cloud'])
const cloud = hostHeaders[1]
expect(cloud?.count).toBe(2)
// Never one sandbox's state.
expect(cloud?.vmState).toBeUndefined()
expect(cloud?.detail).toBe('1 running · 1 asleep')
// One project header under Orca Cloud, holding both sandboxes' workspaces.
const cloudIndex = rows.indexOf(cloud!)
const underCloud = rows.slice(cloudIndex + 1)
expect(underCloud.filter((row) => row.type === 'header')).toHaveLength(1)
expect(
underCloud.flatMap((row) => (row.type === 'item' ? [row.worktree.id] : [])).sort()
).toEqual(['wt-a', 'wt-b'])
})
it('lists Orca Cloud once in the Hosts filter, and checking it selects every sandbox', () => {
const hosts = hostOptions()
const scope = buildSidebarHostScopeOptions(hosts, t)
expect(scope.map((option) => option.label)).toEqual(['All hosts', 'Local Mac', 'Orca Cloud'])
const cloudRow = scope.find((option) => option.label === 'Orca Cloud')
expect(cloudRow?.detail).toBe('1 running · 1 asleep')
expect(
expandEquivalentExecutionHostIds(hosts, [
hosts.find((host) => host.id === cloudRow?.id)!.id
]).sort()
).toEqual(['runtime:env-a', 'runtime:env-b'])
})
it('says "Asleep" when every sandbox sleeps, never "Suspended"', () => {
const hosts = buildSidebarHostOptions(
{
repos: [repo, cloudA, cloudB],
sshTargetLabels: new Map(),
settings: null,
runtimeEnvironments: environments,
ephemeralVmHosts: [
{ runtimeEnvironmentId: 'env-a', status: 'suspended', waking: false },
{ runtimeEnvironmentId: 'env-b', status: 'suspended', waking: false }
]
},
t
)
const cloud = hosts.find((host) => host.orcaCloud)
expect(cloud?.detail).toBe('Asleep')
expect(cloud?.vmState).toBeUndefined()
})
})
@@ -0,0 +1,74 @@
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
import { toRuntimeExecutionHostId } from '../../../../../../shared/execution-host'
import {
ORCA_CLOUD_HOST_LABEL,
isOrcaCloudRuntimeEnvironment
} from '../../../../../../shared/remote-runtime/host-catalog/orca-cloud-environment-name'
import type { PublicKnownRuntimeEnvironment } from '../../../../../../shared/remote-runtime/host-catalog/runtime-environments'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { SidebarHostOption } from './sidebar-host-options'
/**
* Orca Cloud is one logical host. Each sandbox is its own runtime environment underneath, so
* without this every sandbox drew its own "Orca Cloud" header and Hosts entry (RA-2). The first
* sandbox's row stands for all of them (aliasHostIds); the rest merge into it, so filters and
* host sections treat every cloud workspace as the same host. The shown row carries an aggregate
* state, never one sandbox's.
*/
export function collapseOrcaCloudHosts(
hosts: SidebarHostOption[],
runtimeEnvironments: readonly Pick<PublicKnownRuntimeEnvironment, 'id' | 'name' | 'source'>[],
t: Translate = translate
): SidebarHostOption[] {
const cloudIds = new Set<ExecutionHostId>(
runtimeEnvironments
.filter((environment) => isOrcaCloudRuntimeEnvironment(environment))
.map((environment) => toRuntimeExecutionHostId(environment.id.trim()))
)
const cloudHosts = hosts.filter((host) => cloudIds.has(host.id) && !host.mergedIntoHostId)
if (cloudHosts.length === 0) {
return hosts
}
const shown = cloudHosts[0]
const asleep = cloudHosts.filter((host) => host.vmState === 'suspended').length
const failed = cloudHosts.filter(
(host) => host.vmState === 'resume_failed' || host.vmState === 'provision_failed'
).length
const running = cloudHosts.length - asleep - failed
const aliasIds = new Set(cloudHosts.slice(1).map((host) => host.id))
return hosts.map((host): SidebarHostOption => {
if (host.id === shown.id) {
const { vmState: _vmState, ...rest } = host
return {
...rest,
label: ORCA_CLOUD_HOST_LABEL,
detail: orcaCloudAggregateDetail(running, asleep, t),
// Dimmed like a sleeping host only when nothing is running.
health: running > 0 ? 'available' : 'suspended',
orcaCloud: { running, asleep },
aliasHostIds: [...(host.aliasHostIds ?? []), ...aliasIds]
}
}
return aliasIds.has(host.id)
? { ...host, label: ORCA_CLOUD_HOST_LABEL, mergedIntoHostId: shown.id }
: host
})
}
/** "Asleep", "2 running", or "1 running · 1 asleep": the cloud host as a whole. */
export function orcaCloudAggregateDetail(running: number, asleep: number, t: Translate): string {
if (running === 0 && asleep > 0) {
return t('orcaCloudWorkspace.state.asleep', 'Asleep')
}
if (asleep === 0) {
return t('auto.components.sidebar.orcaCloudHost.running', '{{count}} running', {
count: running
})
}
return t(
'auto.components.sidebar.orcaCloudHost.runningAsleep',
'{{running}} running · {{asleep}} asleep',
{ running, asleep }
)
}
@@ -134,6 +134,10 @@ export function HostSectionHeader({
>
{row.vmState ? (
<VmHostIcon state={row.vmState} isCloud={isCloudHost} />
) : isCloudHost ? (
<HostGlyph kind="orca-cloud" size={14} />
) : row.kind === 'local' ? (
<HostGlyph kind="local" size={14} />
) : isDisconnected ? (
<ServerOff className="size-3.5 shrink-0 text-muted-foreground/80" />
) : (
@@ -144,7 +148,8 @@ export function HostSectionHeader({
<div className="flex min-w-0 flex-1 items-center gap-1.5">
<span
className={cn(
'min-w-0 truncate text-[12px] font-semibold leading-none',
// The host name wins over its detail: "Orca Cloud" stays whole, "1 running · 1 asleep" truncates.
'min-w-0 max-w-[70%] shrink-0 truncate text-[12px] font-semibold leading-none',
isDisconnected || isSuspended ? 'text-muted-foreground' : 'text-foreground'
)}
>
@@ -153,7 +158,7 @@ export function HostSectionHeader({
{detail ? (
<span
className={cn(
'shrink-0 truncate text-[10px] leading-none',
'min-w-0 truncate text-[10px] leading-none',
detail.isWarning ? 'text-destructive' : 'text-muted-foreground/70'
)}
>
@@ -13,7 +13,11 @@ export function getHostHeaderDetail(
/** Orca Cloud hosts use the cloud glossary: Asleep, Waking, Couldn't wake, never "Suspended". */
isOrcaCloud = false
): { text: string; isWarning: boolean } | null {
if (isOrcaCloud && row.vmState) {
if (isOrcaCloud) {
// One header for every sandbox: its detail is the aggregate ("1 running · 1 asleep").
if (!row.vmState) {
return row.detail ? { text: row.detail, isWarning: false } : null
}
const dot = vmStateToDot(row.vmState)
return { text: cloudStateLabel(dot, t), isWarning: dot === 'failed' || dot === 'wakeFailed' }
}
@@ -0,0 +1,71 @@
// @vitest-environment happy-dom
import { act, cleanup, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const toast = vi.hoisted(() => Object.assign(vi.fn(), { success: vi.fn(), error: vi.fn() }))
vi.mock('sonner', () => ({ toast }))
vi.mock('@/lib/desktop-window-chrome', () => ({ isPairedWebClientWindow: () => false }))
import { useAppStore } from '@/store/index'
import { seedCloudWorkspaceSnapshot } from '@/lib/orca-cloud/orca-cloud-workspace-store'
import { useClaudeThemeSyncOffer } from './claude-theme-sync-offer'
import { getDefaultSettings } from '../../../../../../shared/constants'
import { makeTerminalTab } from '../../../worktree-jump/worktree-jump-palette-test-fixtures'
const initial = useAppStore.getState()
type State = ReturnType<typeof useAppStore.getState>
function seed(active: { id: string; host: State['activeWorkspaceExecutionHostId'] }): void {
const settings = initial.settings ?? getDefaultSettings('/home/test')
useAppStore.setState({
settings: { ...settings, claudeCodeThemeSyncOffer: 'pending' },
updateSettings: vi.fn<State['updateSettings']>(async () => undefined),
activeWorktreeId: active.id,
activeWorkspaceExecutionHostId: active.host,
activePendingCreationId: null,
tabsByWorktree: {
[active.id]: [{ ...makeTerminalTab('tab-1', active.id, 'Claude'), launchAgent: 'claude' }]
}
})
}
beforeEach(() => toast.mockClear())
afterEach(() => {
cleanup()
seedCloudWorkspaceSnapshot({})
useAppStore.setState(initial, true)
})
describe('Claude Code theme-sync offer (RA-6)', () => {
it('holds the offer while Claude runs in an Orca Cloud workspace, then makes it on a local one', () => {
seedCloudWorkspaceSnapshot({ ids: new Set(['cloud-ws']), idsLoaded: true })
seed({ id: 'cloud-ws', host: 'runtime:env-a' })
renderHook(() => useClaudeThemeSyncOffer())
expect(toast).not.toHaveBeenCalled()
act(() => {
useAppStore.setState({
activeWorktreeId: 'local-ws',
activeWorkspaceExecutionHostId: 'local'
})
})
expect(toast).toHaveBeenCalledTimes(1)
expect(toast.mock.calls[0]?.[0]).toBe('Match Claude Code to your Orca theme?')
})
it('holds it for a brand-new cloud workspace that is not in the cloud id list yet', () => {
seedCloudWorkspaceSnapshot({ ids: new Set(), idsLoaded: true })
seed({ id: 'new-cloud-ws', host: 'runtime:env-b' })
renderHook(() => useClaudeThemeSyncOffer())
expect(toast).not.toHaveBeenCalled()
})
it('offers right away when Claude runs on this computer', () => {
seedCloudWorkspaceSnapshot({ ids: new Set(), idsLoaded: true })
seed({ id: 'local-ws', host: 'local' })
renderHook(() => useClaudeThemeSyncOffer())
expect(toast).toHaveBeenCalledTimes(1)
})
})
@@ -6,6 +6,8 @@ import type { AppState } from '@/store/types'
import { isPairedWebClientWindow } from '@/lib/desktop-window-chrome'
import { TRAILING_ACTIONS_CLASS_NAMES } from '@/components/ui/sonner'
import { whenAgentTerminalAppears } from '../agent-presence/agent-terminal-presence'
import { getCloudWorkspaceSnapshot } from '@/lib/orca-cloud/orca-cloud-workspace-store'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../../../shared/execution-host'
const OFFER_TOAST_ID = 'claude-theme-sync-offer'
@@ -64,6 +66,48 @@ function showClaudeThemeSyncOffer(): void {
})
}
/**
* The sync changes Claude Code on THIS computer. Hold the offer while the active workspace runs
* elsewhere (Orca Cloud, a VM, SSH, an Orca server) or is still being created: a brand-new cloud
* workspace is not in the cloud id list yet when its Claude terminal appears (RA-6).
*/
function isAwayFromThisComputer(state: AppState): boolean {
if (state.activePendingCreationId) {
return true
}
const id = state.activeWorktreeId
if (!id) {
return false
}
if (getCloudWorkspaceSnapshot().ids.has(id) || state.ephemeralVmRuntimesByWorkspaceId?.[id]) {
return true
}
const host = state.activeWorkspaceExecutionHostId
return Boolean(host && host !== LOCAL_EXECUTION_HOST_ID)
}
/**
* The sync changes Claude Code on this computer, which an Orca Cloud workspace's Claude doesn't
* use: hold the offer while one is active and make it once a local workspace is (RA-6).
*/
function offerOnLocalWorkspace(): () => void {
if (!isAwayFromThisComputer(useAppStore.getState())) {
showClaudeThemeSyncOffer()
return () => {}
}
const unsubscribe = useAppStore.subscribe((state, previous) => {
const moved =
state.activeWorktreeId !== previous.activeWorktreeId ||
state.activePendingCreationId !== previous.activePendingCreationId ||
state.activeWorkspaceExecutionHostId !== previous.activeWorkspaceExecutionHostId
if (moved && !isAwayFromThisComputer(state)) {
unsubscribe()
showClaudeThemeSyncOffer()
}
})
return unsubscribe
}
/** Offers theme sync once, when Claude Code first runs in an Orca terminal. */
export function useClaudeThemeSyncOffer(): void {
const due = useAppStore(isOfferDue)
@@ -73,6 +117,13 @@ export function useClaudeThemeSyncOffer(): void {
if (!due || isPairedWebClientWindow()) {
return
}
return whenAgentTerminalAppears('claude', showClaudeThemeSyncOffer)
let stopWaitingForLocal: () => void = () => {}
const stopWatching = whenAgentTerminalAppears('claude', () => {
stopWaitingForLocal = offerOnLocalWorkspace()
})
return () => {
stopWatching()
stopWaitingForLocal()
}
}, [due])
}
+4
View File
@@ -8087,6 +8087,10 @@
"delete": "Delete workspace",
"uncommitted_one": "{{count}} uncommitted change",
"uncommitted_other": "{{count}} uncommitted changes"
},
"orcaCloudHost": {
"running": "{{count}} running",
"runningAsleep": "{{running}} running · {{asleep}} asleep"
}
},
"shared": {
@@ -0,0 +1,86 @@
import type { ExecutionHostId } from '../execution-host'
import type { Repo } from '../repo-types'
import type { Row } from './row-types'
type MergedHost = { id: ExecutionHostId; mergedIntoHostId?: ExecutionHostId }
/**
* Host ids that share one section: a merged-away host (a managed server's SSH route, or one Orca
* Cloud sandbox among several) resolves to the row that stands for it. Null when nothing merges.
*/
export function createHostSectionResolver(
hosts: readonly MergedHost[]
): ((hostId: ExecutionHostId) => ExecutionHostId) | null {
const shownById = new Map<ExecutionHostId, ExecutionHostId>()
for (const host of hosts) {
if (host.mergedIntoHostId) {
shownById.set(host.id, host.mergedIntoHostId)
}
}
return shownById.size > 0 ? (hostId) => shownById.get(hostId) ?? hostId : null
}
function rekeyCounts(
map: ReadonlyMap<ExecutionHostId, number> | undefined,
resolve: (hostId: ExecutionHostId) => ExecutionHostId
): ReadonlyMap<ExecutionHostId, number> | undefined {
if (!map) {
return map
}
const next = new Map<ExecutionHostId, number>()
for (const [hostId, count] of map) {
const shown = resolve(hostId)
next.set(shown, (next.get(shown) ?? 0) + count)
}
return next
}
function rekeyIds(
map: ReadonlyMap<ExecutionHostId, readonly string[]> | undefined,
resolve: (hostId: ExecutionHostId) => ExecutionHostId
): ReadonlyMap<ExecutionHostId, readonly string[]> | undefined {
if (!map) {
return map
}
const next = new Map<ExecutionHostId, readonly string[]>()
for (const [hostId, ids] of map) {
const shown = resolve(hostId)
next.set(shown, [...(next.get(shown) ?? []), ...ids])
}
return next
}
function rekeyRepos(
map: ReadonlyMap<ExecutionHostId, Repo>,
resolve: (hostId: ExecutionHostId) => ExecutionHostId
): ReadonlyMap<ExecutionHostId, Repo> {
const next = new Map<ExecutionHostId, Repo>()
for (const [hostId, repo] of map) {
const shown = resolve(hostId)
// The shown host's own registration names the header; else the first merged one's.
if (!next.has(shown) || hostId === shown) {
next.set(shown, repo)
}
}
return next
}
/** A header's per-host maps keyed by section: one project header per section, counts summed. */
export function mergeHeaderHostsIntoSections(
rows: readonly Row[],
resolve: (hostId: ExecutionHostId) => ExecutionHostId
): Row[] {
return rows.map((row) => {
if (row.type !== 'header' || (!row.hostRepos && !row.hostWorktreeCounts)) {
return row
}
return {
...row,
...(row.hostRepos ? { hostRepos: rekeyRepos(row.hostRepos, resolve) } : {}),
...(row.hostWorktreeCounts
? { hostWorktreeCounts: rekeyCounts(row.hostWorktreeCounts, resolve) }
: {}),
...(row.hostWorktreeIds ? { hostWorktreeIds: rekeyIds(row.hostWorktreeIds, resolve) } : {})
}
})
}
+15 -7
View File
@@ -17,6 +17,7 @@ import type { SshConnectionStatus } from '../ssh-types'
import type { Row } from './row-types'
import { getFolderWorkspaceHostId } from './folder-workspace-host-id'
import { countWorkspaceRows } from './host-section-workspace-count'
import { createHostSectionResolver, mergeHeaderHostsIntoSections } from './host-section-aliases'
import { localizeProjectHeader, localizeRowsToHost } from './project-header-localization'
export type HostHeaderRow = {
@@ -50,6 +51,8 @@ export type HostSectionOption = {
blockedReason?: RuntimeHostBlockedReason
vmState?: ExecutionHostVmState
connectionStatus?: SshConnectionStatus
/** Shares the section of the host it merged into (see host-section-aliases). */
mergedIntoHostId?: ExecutionHostId
}
function getRowHostId(row: Row, defaultHostId: ExecutionHostId): ExecutionHostId | null {
@@ -146,6 +149,13 @@ export function addHostSectionRows(args: {
return localizeRowsToHost(args.rows, args.hostOptions[0]?.id)
}
// Merged-away hosts (each Orca Cloud sandbox after the first) share their shown host's section.
const resolveHost = createHostSectionResolver(args.hostOptions)
const inputRows = resolveHost ? mergeHeaderHostsIntoSections(args.rows, resolveHost) : args.rows
const sectionHostOf = (row: Row): ExecutionHostId | null => {
const hostId = getRowHostId(row, args.defaultHostId)
return hostId && resolveHost ? resolveHost(hostId) : hostId
}
const hostOptionsById = new Map(args.hostOptions.map((host) => [host.id, host]))
const rowsByHostId = new Map<ExecutionHostId, Row[]>()
const globalRows: Row[] = []
@@ -214,7 +224,7 @@ export function addHostSectionRows(args: {
// A nested child stays in its lineage root's section, even when it runs on another machine.
let lineageRootHostId: ExecutionHostId | null = null
for (const row of args.rows) {
for (const row of inputRows) {
if (row.type === 'header' && row.repo) {
retainPendingAncestors(row.projectGroupDepth ?? 0)
}
@@ -225,7 +235,7 @@ export function addHostSectionRows(args: {
}
continue
}
const ownHostId = getRowHostId(row, args.defaultHostId)
const ownHostId = sectionHostOf(row)
if ((row.type === 'item' || row.type === 'folder-workspace') && row.depth === 0) {
lineageRootHostId = ownHostId
}
@@ -249,7 +259,7 @@ export function addHostSectionRows(args: {
const hostOrder: ExecutionHostId[] = []
for (const host of args.hostOptions) {
if (rowsByHostId.has(host.id)) {
if (rowsByHostId.has(host.id) && !host.mergedIntoHostId) {
hostOrder.push(host.id)
}
}
@@ -261,7 +271,7 @@ export function addHostSectionRows(args: {
// Show host headers only for mixed hosts; registered empty hosts remain in the scope picker.
if (rowsByHostId.size <= 1) {
return localizeRowsToHost(args.rows, rowsByHostId.keys().next().value)
return localizeRowsToHost(inputRows, rowsByHostId.keys().next().value)
}
const result: HostSectionRow[] = [...globalRows]
@@ -302,9 +312,7 @@ export function addHostSectionRows(args: {
return { ...row, hostId: row.hostId ?? hostId, ...(repeated ? { repeated: true } : {}) }
}
// The enclosing host header already names the owner, unless a nested child runs elsewhere.
return 'hostContextLabel' in row &&
row.hostContextLabel &&
getRowHostId(row, args.defaultHostId) === hostId
return 'hostContextLabel' in row && row.hostContextLabel && sectionHostOf(row) === hostId
? { ...row, hostContextLabel: undefined }
: row
})