mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
fix(mobile): name run targets and the desktop's own row as the host list does
The Run on picker showed a server's raw environment id behind "Remote ·" and always a monitor glyph; it now takes the host list's badge labels and icon, as the desktop's picker does. Server rows also made the desktop row read "This computer" because its platform was only read for a mixed desktop catalog. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { Monitor, Server } from 'lucide-react-native'
|
||||
import { parseExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
|
||||
/** The desktop's own machine is a monitor, any other host a server, as on the desktop's run targets. */
|
||||
export function HostKindIcon({ hostId, size }: { hostId: string | undefined; size: number }) {
|
||||
// Rows from hosts that predate hostId stamping are local: a remote row always carries one.
|
||||
const Icon = (parseExecutionHostId(hostId)?.kind ?? 'local') === 'local' ? Monitor : Server
|
||||
return <Icon size={size} color={colors.textMuted} />
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export function NewWorktreeFormSheet(props: {
|
||||
loading: boolean
|
||||
hasRepos: boolean
|
||||
project: Selection | null
|
||||
runTarget: Selection | null
|
||||
runTarget: (Selection & { hostId: string }) | null
|
||||
projectBadgeColor: string | null
|
||||
selectedRepoIsGit: boolean
|
||||
selectedRepoConnectionId: string | null
|
||||
|
||||
@@ -184,7 +184,7 @@ describe('NewWorktreeModal project targets', () => {
|
||||
expect(pickerItems(renderer, 'Run on')).toEqual([
|
||||
expect.objectContaining({ label: LOCAL_HOST_LABEL, detail: '/src/orca' }),
|
||||
expect.objectContaining({
|
||||
label: 'SSH · build-server',
|
||||
label: 'build-server',
|
||||
detail: '/home/dev/orca'
|
||||
})
|
||||
])
|
||||
@@ -349,6 +349,7 @@ describe('NewWorktreeModal project targets', () => {
|
||||
client: requestsOnly(desktop),
|
||||
serverClients: new Map([['runtime:env-1', requestsOnly(server)]]),
|
||||
hostId: 'host-1',
|
||||
hostLabelById: new Map([['runtime:env-1', 'scratch-server']]),
|
||||
onCreated,
|
||||
onClose: () => {}
|
||||
})
|
||||
@@ -357,10 +358,26 @@ describe('NewWorktreeModal project targets', () => {
|
||||
await flushUpdates()
|
||||
|
||||
const runTargets = pickerItems(renderer, 'Run on')
|
||||
expect(runTargets.map((item) => item.detail)).toEqual(['/src/orca', '/srv/orca'])
|
||||
expect(runTargets.map((item) => [item.label, item.detail])).toEqual([
|
||||
[LOCAL_HOST_LABEL, '/src/orca'],
|
||||
['scratch-server', '/srv/orca']
|
||||
])
|
||||
const serverTarget = runTargetPicker(renderer)
|
||||
const iconOf = (item: unknown) => {
|
||||
let icon: ReactTestRenderer | undefined
|
||||
act(() => {
|
||||
icon = create(serverTarget.props.renderIcon(item))
|
||||
})
|
||||
return icon?.toJSON()
|
||||
}
|
||||
expect(serverTarget.props.items.map(iconOf)).toMatchObject([
|
||||
{ type: 'Monitor' },
|
||||
{ type: 'Server' }
|
||||
])
|
||||
await act(async () => serverTarget.props.onSelect(serverTarget.props.items[1]))
|
||||
await flushUpdates()
|
||||
// The Run on field shows the picked server's glyph, as its row does.
|
||||
expect(renderer.root.findAll((node) => node.type === 'Server')).toHaveLength(1)
|
||||
expect(pickerItems(renderer, 'Run on')[0]?.label).toBe(LOCAL_HOST_LABEL)
|
||||
act(() => sourceInputs(renderer)[0]!.props.onChangeText('srv-wt'))
|
||||
const form = renderer.root.find((node) => typeof node.props.onCreate === 'function')
|
||||
|
||||
@@ -69,6 +69,7 @@ function NewWorktreeModalContent(props: NewWorktreeModalProps) {
|
||||
client: desktopClient,
|
||||
serverClients,
|
||||
hostId,
|
||||
hostLabelById,
|
||||
existingWorktreePaths,
|
||||
existingWorktrees,
|
||||
openExternalUrl,
|
||||
@@ -182,11 +183,11 @@ function NewWorktreeModalContent(props: NewWorktreeModalProps) {
|
||||
const selectedProject =
|
||||
projectPickerItems.find((project) => project.id === selectedProjectId) ?? null
|
||||
const runTargetPickerItems = useMemo(
|
||||
() => buildNewWorkspaceRunTargetOptions(repos, selectedProjectId, hostPlatform),
|
||||
[hostPlatform, repos, selectedProjectId]
|
||||
() => buildNewWorkspaceRunTargetOptions(repos, selectedProjectId, hostPlatform, hostLabelById),
|
||||
[hostLabelById, hostPlatform, repos, selectedProjectId]
|
||||
)
|
||||
const selectedRunTarget = selectedRepo
|
||||
? getNewWorkspaceRunTarget(selectedRepo, hostPlatform)
|
||||
? getNewWorkspaceRunTarget(selectedRepo, hostPlatform, hostLabelById)
|
||||
: null
|
||||
const needsSetupChoice = Boolean(setupScript.setupCommand) && setupScript.setupRunPolicy === 'ask'
|
||||
const canCreate =
|
||||
|
||||
@@ -20,6 +20,7 @@ export const NewWorktreeModalController = forwardRef<NewWorktreeModalControllerH
|
||||
client,
|
||||
serverClients,
|
||||
hostId,
|
||||
hostLabelById,
|
||||
existingWorktreePaths,
|
||||
existingWorktrees,
|
||||
openExternalUrl,
|
||||
@@ -57,6 +58,7 @@ export const NewWorktreeModalController = forwardRef<NewWorktreeModalControllerH
|
||||
client={client}
|
||||
serverClients={serverClients}
|
||||
hostId={hostId}
|
||||
hostLabelById={hostLabelById}
|
||||
existingWorktreePaths={existingWorktreePaths}
|
||||
existingWorktrees={existingWorktrees}
|
||||
openExternalUrl={openExternalUrl}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { View } from 'react-native'
|
||||
import { getRepoExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import { Monitor } from 'lucide-react-native'
|
||||
import type { SmartModeAvailabilityInput } from '../tasks/mobile-smart-source-modes'
|
||||
import type { PasteRepoCandidate } from '../tasks/smart-source-paste-intent'
|
||||
import type { useMobileComposerSource } from '../tasks/use-mobile-composer-source'
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
import { HostKindIcon } from './HostKindIcon'
|
||||
import { MobileAgentIcon } from './MobileAgentIcon'
|
||||
import type { NewWorktreeAgentOption } from './new-worktree-agent-selection'
|
||||
import { newWorktreeFormStyles as styles } from './new-worktree-form-styles'
|
||||
@@ -90,7 +89,7 @@ export function NewWorktreeModalDrawers(props: {
|
||||
selectedId={props.selectedRepo ? getRepoExecutionHostId(props.selectedRepo) : ''}
|
||||
onSelect={(item) => props.onRepoChange(item.repo)}
|
||||
onClose={props.onTransitionToForm}
|
||||
renderIcon={() => <Monitor size={16} color={colors.textMuted} />}
|
||||
renderIcon={(item) => <HostKindIcon hostId={item.hostId} size={16} />}
|
||||
/>
|
||||
|
||||
<PickerListDrawer
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { View, Text, Pressable, StyleSheet, Platform } from 'react-native'
|
||||
import { ChevronDown, Monitor } from 'lucide-react-native'
|
||||
import { ChevronDown } from 'lucide-react-native'
|
||||
import { HostKindIcon } from './HostKindIcon'
|
||||
import { colors, radii, spacing, typography } from '../theme/mobile-theme'
|
||||
|
||||
type Selection = { label: string; detail?: string }
|
||||
@@ -12,7 +13,7 @@ export function NewWorktreeProjectTargetFields({
|
||||
onOpenRunTarget
|
||||
}: {
|
||||
project: Selection | null
|
||||
runTarget: Selection | null
|
||||
runTarget: (Selection & { hostId: string }) | null
|
||||
projectBadgeColor: string | null
|
||||
onOpenProject: () => void
|
||||
onOpenRunTarget: () => void
|
||||
@@ -33,7 +34,7 @@ export function NewWorktreeProjectTargetFields({
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>Run on</Text>
|
||||
<Pressable style={styles.fieldButton} onPress={onOpenRunTarget}>
|
||||
<Monitor size={14} color={colors.textMuted} />
|
||||
<HostKindIcon hostId={runTarget?.hostId} size={14} />
|
||||
<SelectionCopy selection={runTarget} placeholder="Select a run target" />
|
||||
<ChevronDown size={14} color={colors.textMuted} />
|
||||
</Pressable>
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
Bell,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
GitBranch,
|
||||
GitPullRequest,
|
||||
Monitor,
|
||||
Server
|
||||
} from 'lucide-react-native'
|
||||
import { Bell, ChevronDown, ChevronRight, GitBranch, GitPullRequest } from 'lucide-react-native'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { parseExecutionHostId, type ExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import type { ExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import type { AgentWorkingMode } from '../../../src/shared/agent-status-types'
|
||||
import type { RuntimeWorktreeAgentRow } from '../../../src/shared/runtime-types'
|
||||
import type { MobileRenderableRepoIcon } from '../host-screen/host-screen-reply-schema'
|
||||
import { triggerMediumImpact } from '../platform/haptics'
|
||||
import { colors, radii, spacing, typography } from '../theme/mobile-theme'
|
||||
import { AgentSpinner } from './AgentSpinner'
|
||||
import { HostKindIcon } from './HostKindIcon'
|
||||
import { MobileRepoIcon } from './MobileRepoIcon'
|
||||
import { WorktreeAgentList } from './WorktreeAgentList'
|
||||
import { WorktreeMetaGlyphs, prStateColor } from './WorktreeMetaGlyphs'
|
||||
@@ -173,13 +166,7 @@ function WorktreeListRowComponent<T extends WorktreeListRowItem>({
|
||||
.filter(Boolean)
|
||||
.join(', ')}
|
||||
>
|
||||
{/* Rows from hosts that predate hostId stamping are local: a remote row always carries one. */}
|
||||
{(parseExecutionHostId(item.hostContextHostId ?? item.hostId)?.kind ?? 'local') ===
|
||||
'local' ? (
|
||||
<Monitor size={10} color={colors.textMuted} />
|
||||
) : (
|
||||
<Server size={10} color={colors.textMuted} />
|
||||
)}
|
||||
<HostKindIcon hostId={item.hostContextHostId ?? item.hostId} size={10} />
|
||||
<Text style={[styles.childBadgeText, styles.hostBadgeText]} numberOfLines={1}>
|
||||
{item.hostContextLabel}
|
||||
</Text>
|
||||
|
||||
@@ -43,16 +43,16 @@ describe('new workspace project targets', () => {
|
||||
expect(options[0]).toMatchObject({ label: 'orca', detail: 'stablyai/orca' })
|
||||
})
|
||||
|
||||
it('labels local, SSH, and paired runtime targets', () => {
|
||||
it('labels local, SSH, and paired runtime targets as the host list names them', () => {
|
||||
expect(
|
||||
getNewWorkspaceRunTarget({ id: 'local', displayName: 'orca', path: '/src/orca' }, 'darwin')
|
||||
).toEqual({ label: LOCAL_HOST_LABEL, detail: '/src/orca' })
|
||||
).toEqual({ hostId: 'local', label: LOCAL_HOST_LABEL, detail: '/src/orca' })
|
||||
expect(
|
||||
getNewWorkspaceRunTarget({ id: 'local', displayName: 'orca', path: 'C:\\src\\orca' })
|
||||
).toEqual({ label: 'This computer', detail: 'C:\\src\\orca' })
|
||||
).toMatchObject({ label: 'This computer' })
|
||||
expect(
|
||||
getNewWorkspaceRunTarget({ id: 'local', displayName: 'orca', path: 'C:\\src\\orca' }, 'win32')
|
||||
).toEqual({ label: 'Local Windows', detail: 'C:\\src\\orca' })
|
||||
).toMatchObject({ label: 'Local Windows' })
|
||||
expect(
|
||||
getNewWorkspaceRunTarget({
|
||||
id: 'ssh',
|
||||
@@ -60,15 +60,19 @@ describe('new workspace project targets', () => {
|
||||
path: 'C:\\src\\orca',
|
||||
executionHostId: 'ssh:Windows%20VM'
|
||||
})
|
||||
).toEqual({ label: 'SSH · Windows VM', detail: 'C:\\src\\orca' })
|
||||
expect(
|
||||
getNewWorkspaceRunTarget({
|
||||
id: 'runtime',
|
||||
displayName: 'orca',
|
||||
path: '/src/orca',
|
||||
executionHostId: 'runtime:devbox'
|
||||
})
|
||||
).toEqual({ label: 'Remote · devbox', detail: '/src/orca' })
|
||||
).toEqual({ hostId: 'ssh:Windows%20VM', label: 'Windows VM', detail: 'C:\\src\\orca' })
|
||||
const server = {
|
||||
id: 'runtime',
|
||||
displayName: 'orca',
|
||||
path: '/src/orca',
|
||||
executionHostId: 'runtime:20269ff3-60aa-4e79-a581-513a3f8e394b'
|
||||
}
|
||||
const labels = new Map([[server.executionHostId, 'scratch-server']] as const)
|
||||
expect(getNewWorkspaceRunTarget(server, 'darwin', labels)).toEqual({
|
||||
hostId: server.executionHostId,
|
||||
label: 'scratch-server',
|
||||
detail: '/src/orca'
|
||||
})
|
||||
})
|
||||
|
||||
it('shows one target per host when the project has multiple local worktrees', () => {
|
||||
@@ -90,7 +94,7 @@ describe('new workspace project targets', () => {
|
||||
expect.objectContaining({ id: 'local', label: LOCAL_HOST_LABEL, detail: '/src/orca-a' }),
|
||||
expect.objectContaining({
|
||||
id: 'ssh:build-server',
|
||||
label: 'SSH · build-server',
|
||||
label: 'build-server',
|
||||
detail: '/home/dev/orca'
|
||||
})
|
||||
])
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import type { Repo } from '../../../src/shared/repo-types'
|
||||
import {
|
||||
getExecutionHostLabel,
|
||||
getLocalExecutionHostLabel,
|
||||
getRepoExecutionHostId,
|
||||
parseExecutionHostId
|
||||
} from '../../../src/shared/execution-host'
|
||||
import { getRepoExecutionHostId, type ExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import {
|
||||
getProjectIdentityKey,
|
||||
getProjectProviderIdentity
|
||||
} from '../../../src/shared/project-host-setup-projection'
|
||||
import { getHostContextLabel } from '../../../src/shared/worktree/host-context-labels'
|
||||
|
||||
type WorkspaceRepo = Pick<Repo, 'id' | 'displayName' | 'path'> &
|
||||
Partial<
|
||||
@@ -24,6 +20,7 @@ export type NewWorkspaceProjectOption<TRepo extends WorkspaceRepo> = {
|
||||
|
||||
export type NewWorkspaceRunTargetOption<TRepo extends WorkspaceRepo> = {
|
||||
id: string
|
||||
hostId: ExecutionHostId
|
||||
label: string
|
||||
detail: string
|
||||
repo: TRepo
|
||||
@@ -58,24 +55,23 @@ export function buildNewWorkspaceProjectOptions<TRepo extends WorkspaceRepo>(
|
||||
})
|
||||
}
|
||||
|
||||
/** Named like the host list's badge; the desktop's picker shows the bare host label too. */
|
||||
export function getNewWorkspaceRunTarget(
|
||||
repo: WorkspaceRepo,
|
||||
localPlatform: NodeJS.Platform | null = null
|
||||
localPlatform: NodeJS.Platform | null = null,
|
||||
hostLabelById?: ReadonlyMap<ExecutionHostId, string>
|
||||
): {
|
||||
hostId: ExecutionHostId
|
||||
label: string
|
||||
detail: string
|
||||
} {
|
||||
const hostId = getRepoExecutionHostId(repo)
|
||||
const host = parseExecutionHostId(hostId)
|
||||
const hostLabel = getExecutionHostLabel(hostId)
|
||||
if (host?.kind === 'ssh') {
|
||||
return { label: `SSH · ${hostLabel}`, detail: repo.path }
|
||||
}
|
||||
if (host?.kind === 'runtime') {
|
||||
return { label: `Remote · ${hostLabel}`, detail: repo.path }
|
||||
}
|
||||
return {
|
||||
label: localPlatform ? getLocalExecutionHostLabel(localPlatform) : 'This computer',
|
||||
hostId,
|
||||
label: getHostContextLabel(hostId, {
|
||||
hostLabelById,
|
||||
hostPlatform: localPlatform
|
||||
}),
|
||||
detail: repo.path
|
||||
}
|
||||
}
|
||||
@@ -83,7 +79,8 @@ export function getNewWorkspaceRunTarget(
|
||||
export function buildNewWorkspaceRunTargetOptions<TRepo extends WorkspaceRepo>(
|
||||
repos: readonly TRepo[],
|
||||
projectId: string | null,
|
||||
localPlatform: NodeJS.Platform | null = null
|
||||
localPlatform: NodeJS.Platform | null = null,
|
||||
hostLabelById?: ReadonlyMap<ExecutionHostId, string>
|
||||
): NewWorkspaceRunTargetOption<TRepo>[] {
|
||||
if (!projectId) {
|
||||
return []
|
||||
@@ -98,7 +95,7 @@ export function buildNewWorkspaceRunTargetOptions<TRepo extends WorkspaceRepo>(
|
||||
// Keyed by host: a repo id names one repo per host, so two hosts can share one.
|
||||
options.set(hostId, {
|
||||
id: hostId,
|
||||
...getNewWorkspaceRunTarget(repo, localPlatform),
|
||||
...getNewWorkspaceRunTarget(repo, localPlatform, hostLabelById),
|
||||
repo
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ export type NewWorktreeModalProps = {
|
||||
/** The desktop's servers this phone can reach now; their repos are offered and created there. */
|
||||
serverClients?: ReadonlyMap<ExecutionHostId, RpcClient>
|
||||
hostId?: string
|
||||
/** The host list's badge labels, so a run target reads as its row does. */
|
||||
hostLabelById?: ReadonlyMap<ExecutionHostId, string>
|
||||
existingWorktreePaths?: readonly string[]
|
||||
existingWorktrees?: readonly { repoId: string; branch: string }[]
|
||||
openExternalUrl: (url: string) => void
|
||||
|
||||
@@ -31,6 +31,7 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr
|
||||
existingWorktreePaths,
|
||||
hostCapabilities,
|
||||
hostId,
|
||||
hostLabels,
|
||||
settings,
|
||||
showNewWorktree,
|
||||
state
|
||||
@@ -229,6 +230,7 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr
|
||||
client={client}
|
||||
serverClients={serverClients}
|
||||
hostId={hostId}
|
||||
hostLabelById={hostLabels.hostLabelById}
|
||||
existingWorktreePaths={existingWorktreePaths}
|
||||
existingWorktrees={state.worktrees}
|
||||
// The seam, not react-native's `Linking`: this screen is in the tasks page closure, and
|
||||
|
||||
@@ -18,11 +18,15 @@ vi.mock('../transport/use-worktree-resync', () => ({
|
||||
}))
|
||||
|
||||
import { MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY } from '../../../src/shared/mobile-desktop-relay-contract'
|
||||
import {
|
||||
recordHostDescriptor,
|
||||
resetHostDescriptorStoreForTests
|
||||
} from '../transport/host-descriptor-store'
|
||||
import { FakeSession } from '../transport/mobile-endpoint-supervisor-test-fakes'
|
||||
import type { RpcResponse } from '../transport/types'
|
||||
import { readVisibleHostIds } from '../worktree/visible-host-rows'
|
||||
import type { Worktree } from '../worktree/workspace-list-types'
|
||||
import { useHostDisplayWorktrees } from './use-host-display-worktrees'
|
||||
import { useHostDisplayWorktrees, useHostLabelSources } from './use-host-display-worktrees'
|
||||
import { useHostScreenState } from './use-host-screen-state'
|
||||
import { useHostWorktreeActions } from './use-host-worktree-actions'
|
||||
import { useHostWorktreeCatalog } from './use-host-worktree-catalog'
|
||||
@@ -145,7 +149,7 @@ async function mountScreen(first: FakeSession, hostCapabilities: string[]): Prom
|
||||
})
|
||||
held.state = state
|
||||
held.fetch = catalog.fetchWorktrees
|
||||
held.rows = useHostDisplayWorktrees('connected', state)
|
||||
held.rows = useHostDisplayWorktrees('connected', state, useHostLabelSources('host-1', state))
|
||||
return null
|
||||
}
|
||||
await act(async () => {
|
||||
@@ -213,6 +217,19 @@ describe("a desktop's server workspaces on the phone", () => {
|
||||
])
|
||||
})
|
||||
|
||||
it("names the desktop's own row for the desktop's platform once servers mix the list", async () => {
|
||||
recordHostDescriptor('host-1', { machineName: null, platform: 'darwin' })
|
||||
try {
|
||||
const screen = await mountScreen(desktop(), RELAYS)
|
||||
expect(screen.rows()[0]).toMatchObject({
|
||||
worktreeId: 'mac-wt',
|
||||
hostContextLabel: 'Local Mac'
|
||||
})
|
||||
} finally {
|
||||
resetHostDescriptorStoreForTests()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps showing today’s view under an older desktop or an older shell', async () => {
|
||||
const oldDesktop = desktop()
|
||||
const oldShell = desktop({ carriesExecutionHost: false })
|
||||
|
||||
@@ -11,7 +11,7 @@ vi.mock('../cache/repo-cache', () => ({ setCachedRepos: () => {} }))
|
||||
import { FakeSession } from '../transport/mobile-endpoint-supervisor-test-fakes'
|
||||
import type { RpcResponse } from '../transport/types'
|
||||
import type { Worktree } from '../worktree/workspace-list-types'
|
||||
import { useHostDisplayWorktrees } from './use-host-display-worktrees'
|
||||
import { useHostDisplayWorktrees, useHostLabelSources } from './use-host-display-worktrees'
|
||||
import { useHostRepoMetadata } from './use-host-repo-metadata'
|
||||
import { useHostScreenState } from './use-host-screen-state'
|
||||
|
||||
@@ -68,7 +68,7 @@ async function displayedRows(args: {
|
||||
state.clientRef.current = client
|
||||
setWorktrees = state.setWorktrees
|
||||
seen.refresh = useHostRepoMetadata({ client, connState: 'connected', hostId: 'host-1', state })
|
||||
seen.rows = useHostDisplayWorktrees('connected', state)
|
||||
seen.rows = useHostDisplayWorktrees('connected', state, useHostLabelSources('host-1', state))
|
||||
return null
|
||||
}
|
||||
await act(async () => {
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { ExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import { useHostDescriptor } from '../transport/host-descriptor-store'
|
||||
import type { ConnectionState } from '../transport/types'
|
||||
import { serverHostHealth } from '../worktree/server-workspaces'
|
||||
import { filterVisibleHostRows } from '../worktree/visible-host-rows'
|
||||
import { applyWorktreeHostContextLabels } from '../worktree/worktree-host-context-labels'
|
||||
import {
|
||||
applyWorktreeHostContextLabels,
|
||||
type HostLabelSources
|
||||
} from '../worktree/worktree-host-context-labels'
|
||||
import { applyWorktreeRowDisplayState } from '../worktree/worktree-host-row-identity'
|
||||
import type { HostScreenState } from './use-host-screen-state'
|
||||
|
||||
/** The rows the host list renders: the live or last-known catalog with display state and host badges. */
|
||||
export function useHostDisplayWorktrees(connState: ConnectionState, state: HostScreenState) {
|
||||
/** What names each host on this screen: the row badges and the new-workspace run targets. */
|
||||
export function useHostLabelSources(
|
||||
hostId: string | undefined,
|
||||
state: HostScreenState
|
||||
): HostLabelSources {
|
||||
const { serverWorkspaces } = state
|
||||
// Why: a server's label and health come with its rows, alongside the desktop's SSH ones.
|
||||
const hostLabelById = useMemo(
|
||||
@@ -19,36 +26,49 @@ export function useHostDisplayWorktrees(connState: ConnectionState, state: HostS
|
||||
() => withServerHosts(state.hostHealthById, serverWorkspaces.hosts, serverHostHealth),
|
||||
[state.hostHealthById, serverWorkspaces.hosts]
|
||||
)
|
||||
const descriptorPlatform = useHostDescriptor(hostId)?.platform ?? null
|
||||
// Why: metadata reads the platform only for a mixed desktop catalog; server rows mix it too.
|
||||
const hostPlatform = state.hostPlatform ?? descriptorPlatform
|
||||
return useMemo(
|
||||
() => ({
|
||||
repoHostIdByRepoId: state.repoHostIdByRepoId,
|
||||
hostLabelById,
|
||||
hostPlatform,
|
||||
hostHealthById
|
||||
}),
|
||||
[state.repoHostIdByRepoId, hostLabelById, hostPlatform, hostHealthById]
|
||||
)
|
||||
}
|
||||
|
||||
/** The rows the host list renders: the live or last-known catalog with display state and host badges. */
|
||||
export function useHostDisplayWorktrees(
|
||||
connState: ConnectionState,
|
||||
state: HostScreenState,
|
||||
labels: HostLabelSources
|
||||
) {
|
||||
return useMemo(() => {
|
||||
// Why: live `worktrees` is authoritative only while connected; under the amber
|
||||
// mount default, connecting/handshaking must keep the pre-reconnect list too.
|
||||
const base = connState === 'connected' ? state.worktrees : state.lastKnownWorktrees
|
||||
const rows = filterVisibleHostRows(
|
||||
[...base, ...serverWorkspaces.worktrees],
|
||||
[...base, ...state.serverWorkspaces.worktrees],
|
||||
state.visibleHostIds,
|
||||
state.repoHostIdByRepoId
|
||||
)
|
||||
return applyWorktreeHostContextLabels(
|
||||
applyWorktreeRowDisplayState(rows, state.sleptIds, state.optimisticActiveWorktreeIdentity),
|
||||
{
|
||||
repoHostIdByRepoId: state.repoHostIdByRepoId,
|
||||
hostLabelById,
|
||||
hostPlatform: state.hostPlatform,
|
||||
hostHealthById
|
||||
}
|
||||
labels
|
||||
)
|
||||
}, [
|
||||
connState,
|
||||
state.worktrees,
|
||||
state.lastKnownWorktrees,
|
||||
serverWorkspaces.worktrees,
|
||||
state.serverWorkspaces.worktrees,
|
||||
state.visibleHostIds,
|
||||
state.sleptIds,
|
||||
state.optimisticActiveWorktreeIdentity,
|
||||
state.repoHostIdByRepoId,
|
||||
hostLabelById,
|
||||
state.hostPlatform,
|
||||
hostHealthById
|
||||
labels
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useRelayRecoveryStatus
|
||||
} from '../transport/client-context-connection-metrics'
|
||||
import { useWorkspaceSections } from '../worktree/use-workspace-sections'
|
||||
import { useHostDisplayWorktrees } from './use-host-display-worktrees'
|
||||
import { useHostDisplayWorktrees, useHostLabelSources } from './use-host-display-worktrees'
|
||||
import { useHostRepoMetadata } from './use-host-repo-metadata'
|
||||
import { useHostScreenIdentity } from './use-host-screen-identity'
|
||||
import { useHostScreenState } from './use-host-screen-state'
|
||||
@@ -103,7 +103,8 @@ export function useHostScreenController({
|
||||
}
|
||||
const showNewWorktree = resolvedRouteActionState.showNewWorktree
|
||||
|
||||
const displayWorktrees = useHostDisplayWorktrees(connState, state)
|
||||
const hostLabels = useHostLabelSources(hostId, state)
|
||||
const displayWorktrees = useHostDisplayWorktrees(connState, state, hostLabels)
|
||||
const sectionsResult = useWorkspaceSections({
|
||||
displayWorktrees,
|
||||
sortMode: state.sortMode,
|
||||
@@ -135,6 +136,7 @@ export function useHostScreenController({
|
||||
hostCapabilities,
|
||||
hostDisplay,
|
||||
hostId,
|
||||
hostLabels,
|
||||
insets,
|
||||
isReadOnly: connState === 'auth-failed',
|
||||
isWideLayout,
|
||||
|
||||
Reference in New Issue
Block a user