From 64a48c39f807a6f723d63095ec935abc9c2686df Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sun, 11 Oct 2026 22:36:12 -0400 Subject: [PATCH] refactor(mobile): let the new-workspace sheet read its servers where it knows it is open The sheet's controller already owns whether it is open, so it now reads the desktop's server list and names itself; the overlay's mirrored visibility state and the list-side hook go away. A server row no longer offers Sleep, which would sleep it on the desktop. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- .../src/components/NewWorktreeModal.test.tsx | 57 ++++++++++++ .../components/NewWorktreeModalController.tsx | 29 +++++- .../host-screen/host-screen-overlays.test.tsx | 83 +++++++++++++++++ .../src/host-screen/host-screen-overlays.tsx | 36 +++---- .../host-server-workspaces-wiring.test.ts | 4 +- .../host-ssh-health-badge-wiring.test.ts | 4 +- .../new-workspace-servers.test.tsx | 65 ------------- .../host-screen/use-host-display-worktrees.ts | 93 ++++--------------- .../host-screen/use-host-screen-controller.ts | 6 +- mobile/src/worktree/server-workspaces.ts | 16 ++++ 10 files changed, 221 insertions(+), 172 deletions(-) create mode 100644 mobile/src/host-screen/host-screen-overlays.test.tsx delete mode 100644 mobile/src/host-screen/new-workspace-servers.test.tsx diff --git a/mobile/src/components/NewWorktreeModal.test.tsx b/mobile/src/components/NewWorktreeModal.test.tsx index 5d1bbf450ff..bc5bc7ba483 100644 --- a/mobile/src/components/NewWorktreeModal.test.tsx +++ b/mobile/src/components/NewWorktreeModal.test.tsx @@ -11,6 +11,7 @@ const asyncStorage = vi.hoisted(() => ({ vi.mock('@react-native-async-storage/async-storage', () => ({ default: asyncStorage })) vi.mock('react-native', () => ({ ActivityIndicator: 'ActivityIndicator', + AppState: { currentState: 'active' }, FlatList: 'FlatList', Image: 'Image', Keyboard: { dismiss: vi.fn() }, @@ -45,8 +46,12 @@ vi.mock('./TaskProviderLogo', () => ({ TaskProviderLogo: 'TaskProviderLogo' })) import { setCachedRepos } from '../cache/repo-cache' import { recordHostDescriptor } from '../transport/host-descriptor-store' +import { FakeSession } from '../transport/mobile-endpoint-supervisor-test-fakes' import { getLocalExecutionHostLabel } from '../../../src/shared/execution-host' +import { MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY } from '../../../src/shared/mobile-desktop-relay-contract' +import { MOBILE_RELAY_HOSTS_LIST_METHOD } from '../../../src/shared/mobile-relay-hosts-contract' import { NewWorktreeModal } from './NewWorktreeModal' +import { NewWorktreeModalController } from './NewWorktreeModalController' const LOCAL_HOST_LABEL = getLocalExecutionHostLabel('darwin') @@ -397,6 +402,58 @@ describe('NewWorktreeModal project targets', () => { expect(onCreated).toHaveBeenCalledWith('srv-wt', expect.any(String), undefined, 'runtime:env-1') }) + it("offers the desktop's servers, by name, in a sheet opened before the list has them", async () => { + const serverRepo = { ...repos[0]!, id: 'server-repo', path: '/srv/orca' } + // One desktop: its own repos, its server list, and a server's repos relayed through it. + const desktop = new FakeSession('connected') + desktop.sendRequest.mockImplementation(async (method: string, ...rest: unknown[]) => { + const options = rest[1] + const relayed = + typeof options === 'object' && options !== null && 'executionHost' in options + ? options.executionHost + : undefined + const result = + method === MOBILE_RELAY_HOSTS_LIST_METHOD + ? { + hosts: [ + { hostId: 'runtime:vm', label: 'scratch-vm', health: 'available', relay: 'ready' } + ] + } + : method === 'repo.list' + ? { repos: relayed === 'runtime:vm' ? [serverRepo] : repos } + : {} + return { id: 'reply', ok: true, result, _meta: { runtimeId: 'runtime-1' } } + }) + const listCalls = () => + desktop.sendRequest.mock.calls.filter(([method]) => method === MOBILE_RELAY_HOSTS_LIST_METHOD) + // The host list's own names: no server rows have loaded behind a sheet opened from Home. + const sheet = (routeVisible: boolean) => + createElement(NewWorktreeModalController, { + routeVisible, + client: desktop, + hostCapabilities: [MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY], + hostId: 'host-1', + hostLabelById: new Map(), + openExternalUrl: () => {}, + onCreated: () => {}, + onRouteVisibleChange: () => {} + }) + + await act(async () => { + renderer = create(sheet(false)) + }) + await flushUpdates() + // Closed, it asks nothing of the desktop. + expect(listCalls()).toHaveLength(0) + + await act(async () => renderer.update(sheet(true))) + await flushUpdates() + expect(pickerItems(renderer, 'Run on').map((item) => [item.label, item.detail])).toEqual([ + [expect.any(String), '/src/orca'], + ['scratch-vm', '/srv/orca'] + ]) + }) + it("lists the desktop's repos at once while a server has not answered", async () => { const desktop = vi.fn((method: string) => method === 'repo.list' diff --git a/mobile/src/components/NewWorktreeModalController.tsx b/mobile/src/components/NewWorktreeModalController.tsx index c00dfc03e9e..3210c67e209 100644 --- a/mobile/src/components/NewWorktreeModalController.tsx +++ b/mobile/src/components/NewWorktreeModalController.tsx @@ -1,5 +1,9 @@ -import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react' +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' +import type { ExecutionHostId } from '../../../src/shared/execution-host' +import { reachableServerClients } from '../transport/execution-host-scoped-rpc-client' +import { useRelayHosts } from '../worktree/relay-hosts-feed' +import { withServerHosts } from '../worktree/server-workspaces' import { NewWorktreeModal } from './NewWorktreeModal' import type { NewWorktreeModalProps } from './new-worktree-modal-types' @@ -7,7 +11,14 @@ export type NewWorktreeModalControllerHandle = { open: () => void } -type Props = Omit & { +type Props = Omit< + NewWorktreeModalProps, + 'visible' | 'onClose' | 'serverClients' | 'hostLabelById' +> & { + /** The desktop's capabilities, which say whether its servers are reachable through it. */ + hostCapabilities: readonly string[] + /** The desktop's own host names; the sheet adds its servers' from the feed. */ + hostLabelById: ReadonlyMap routeVisible: boolean onVisibleChange?: (visible: boolean) => void onRouteVisibleChange: (visible: boolean) => void @@ -18,7 +29,7 @@ export const NewWorktreeModalController = forwardRef reachableServerClients(client, hostCapabilities, servers ?? []), + [client, hostCapabilities, servers] + ) + const serverLabelById = useMemo( + () => withServerHosts(hostLabelById, servers ?? [], (host) => host.label), + [hostLabelById, servers] + ) useImperativeHandle( ref, @@ -58,7 +79,7 @@ export const NewWorktreeModalController = forwardRef ({ + AppState: { currentState: 'active' }, + Linking: { openURL: vi.fn() }, + Platform: { OS: 'ios', select: (options: { ios?: unknown }) => options.ios }, + Pressable: 'Pressable', + StyleSheet: { create: (styles: T) => styles }, + Text: 'Text', + View: 'View' +})) +vi.mock('expo-secure-store', () => ({ WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'when-unlocked' })) +vi.mock('lucide-react-native', () => ({ Check: 'Check', Moon: 'Moon' })) +vi.mock('../agent-history/worktree-navigation-actions', () => ({ + buildWorktreeNavigationActions: () => [] +})) +vi.mock('../components/ActionSheetModal', () => ({ ActionSheetContent: 'ActionSheetContent' })) +vi.mock('../components/BottomDrawer', () => ({ BottomDrawer: 'BottomDrawer' })) +vi.mock('../components/ConfirmModal', () => ({ ConfirmModal: 'ConfirmModal' })) +vi.mock('../components/NewWorktreeModalController', () => ({ + NewWorktreeModalController: 'NewWorktreeModalController' +})) +vi.mock('../components/PickerModal', () => ({ PickerModal: 'PickerModal' })) + +import { MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY } from '../../../src/shared/mobile-desktop-relay-contract' +import { FakeSession } from '../transport/mobile-endpoint-supervisor-test-fakes' +import type { Worktree } from '../worktree/workspace-list-types' +import { HostScreenOverlays } from './host-screen-overlays' + +let renderer: ReactTestRenderer | null = null + +afterEach(() => { + act(() => renderer?.unmount()) + renderer = null +}) + +/** The long-press sheet's action labels for `row`. */ +async function actionLabels(row: Partial): Promise { + const controller = { + actions: {}, + catalog: {}, + client: new FakeSession('connected'), + connState: 'connected', + existingWorktreePaths: [], + hostCapabilities: [MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY], + hostId: 'host-1', + sectionsResult: { uniqueRepos: [] }, + settings: { activeFilterCount: 0 }, + showNewWorktree: false, + state: { + actionTarget: { worktreeId: 'wt', repo: 'orca', branch: 'main', ...row }, + confirmDelete: null, + filters: { filterRepoIds: new Set() }, + hostLabelById: new Map(), + pinnedIds: new Set() + } + } + await act(async () => { + renderer = create( + createElement(HostScreenOverlays, { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the action sheet reads only these members; the drawers and pickers that read the rest are mocked. + controller: controller as unknown as Parameters[0]['controller'] + }) + ) + }) + const sheet = renderer!.root.find((node) => String(node.type) === 'ActionSheetContent') + return sheet.props.actions.map((action: { label: string }) => action.label) +} + +describe('the workspace long-press sheet', () => { + it("offers Sleep for the desktop's own workspace", async () => { + expect(await actionLabels({})).toContain('Sleep') + }) + + it("does not offer Sleep for a server's workspace, which the desktop would sleep by its own id", async () => { + expect(await actionLabels({ worktreeId: 'runtime:vm-wt', hostId: 'runtime:vm' })).toEqual([ + 'Pin', + 'Delete' + ]) + }) +}) diff --git a/mobile/src/host-screen/host-screen-overlays.tsx b/mobile/src/host-screen/host-screen-overlays.tsx index a4bd353c0f0..f02078e0e3a 100644 --- a/mobile/src/host-screen/host-screen-overlays.tsx +++ b/mobile/src/host-screen/host-screen-overlays.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react' import { openExternalLink } from '../platform/external-link' import { Pressable, Text, View } from 'react-native' import { Check, Moon } from 'lucide-react-native' @@ -19,7 +18,6 @@ import { } from '../worktree/workspace-list-picker-options' import { isWorktreePinned } from '../worktree/workspace-list-sections' import { hostScreenStyles as styles } from './host-screen-styles' -import { useNewWorkspaceServers } from './use-host-display-worktrees' import type { HostScreenController } from './use-host-screen-controller' export function HostScreenOverlays({ controller }: { controller: HostScreenController }) { @@ -31,7 +29,6 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr existingWorktreePaths, hostCapabilities, hostId, - hostLabels, settings, showNewWorktree, state @@ -44,13 +41,6 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr desktopCapabilities: hostCapabilities, executionHost: actionTarget?.hostId }) - const [newWorktreeVisible, setNewWorktreeVisible] = useState(false) - const newWorkspaceServers = useNewWorkspaceServers({ - client, - hostCapabilities, - hostLabelById: hostLabels.hostLabelById, - open: newWorktreeVisible - }) return ( <> @@ -189,14 +179,19 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr navigate: actions.navigateFromHostList, onDone: () => state.setActionTarget(null) }), - { - label: 'Sleep', - icon: Moon, - onPress: () => { - actions.putWorktreeToSleep(actionTarget) - state.setActionTarget(null) - } - }, + // Server sleep is not offered yet; the desktop would sleep its own id. + ...(actionServer.server + ? [] + : [ + { + label: 'Sleep', + icon: Moon, + onPress: () => { + actions.putWorktreeToSleep(actionTarget) + state.setActionTarget(null) + } + } + ]), { label: isWorktreePinned(actionTarget, state.pinnedIds) ? 'Unpin' : 'Pin', onPress: () => { @@ -231,9 +226,9 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr ref={state.newWorktreeModalRef} routeVisible={showNewWorktree} client={client} - serverClients={newWorkspaceServers.serverClients} + hostCapabilities={hostCapabilities} hostId={hostId} - hostLabelById={newWorkspaceServers.hostLabelById} + hostLabelById={state.hostLabelById} existingWorktreePaths={existingWorktreePaths} existingWorktrees={state.worktrees} // The seam, not react-native's `Linking`: this screen is in the tasks page closure, and @@ -241,7 +236,6 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr openExternalUrl={openExternalLink} onVisibleChange={(visible) => { state.newWorktreeModalVisibleRef.current = visible - setNewWorktreeVisible(visible) }} onCreated={(worktreeId, worktreeName, warning, executionHost) => { void catalog.fetchWorktrees({ allowDuringModal: true }) diff --git a/mobile/src/host-screen/host-server-workspaces-wiring.test.ts b/mobile/src/host-screen/host-server-workspaces-wiring.test.ts index 9813d2a6a5e..1d0aada05c5 100644 --- a/mobile/src/host-screen/host-server-workspaces-wiring.test.ts +++ b/mobile/src/host-screen/host-server-workspaces-wiring.test.ts @@ -26,7 +26,7 @@ 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, useHostLabelSources } from './use-host-display-worktrees' +import { useHostDisplayWorktrees } 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' @@ -149,7 +149,7 @@ async function mountScreen(first: FakeSession, hostCapabilities: string[]): Prom }) held.state = state held.fetch = catalog.fetchWorktrees - held.rows = useHostDisplayWorktrees('connected', state, useHostLabelSources('host-1', state)) + held.rows = useHostDisplayWorktrees('connected', state, 'host-1') return null } await act(async () => { diff --git a/mobile/src/host-screen/host-ssh-health-badge-wiring.test.ts b/mobile/src/host-screen/host-ssh-health-badge-wiring.test.ts index 5984b4df144..9f47bafa747 100644 --- a/mobile/src/host-screen/host-ssh-health-badge-wiring.test.ts +++ b/mobile/src/host-screen/host-ssh-health-badge-wiring.test.ts @@ -13,7 +13,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, useHostLabelSources } from './use-host-display-worktrees' +import { useHostDisplayWorktrees } from './use-host-display-worktrees' import { useHostRepoMetadata } from './use-host-repo-metadata' import { useHostScreenState } from './use-host-screen-state' @@ -70,7 +70,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, useHostLabelSources('host-1', state)) + seen.rows = useHostDisplayWorktrees('connected', state, 'host-1') return null } await act(async () => { diff --git a/mobile/src/host-screen/new-workspace-servers.test.tsx b/mobile/src/host-screen/new-workspace-servers.test.tsx deleted file mode 100644 index 704d76f359c..00000000000 --- a/mobile/src/host-screen/new-workspace-servers.test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { createElement } from 'react' -import { act, create } from 'react-test-renderer' -import { describe, expect, it, vi } from 'vitest' - -vi.mock('react-native', () => ({ AppState: { currentState: 'active' } })) -vi.mock('../transport/host-descriptor-store', () => ({ useHostDescriptor: () => null })) - -import type { ExecutionHostId } from '../../../src/shared/execution-host' -import { MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY } from '../../../src/shared/mobile-desktop-relay-contract' -import { MOBILE_RELAY_HOSTS_LIST_METHOD } from '../../../src/shared/mobile-relay-hosts-contract' -import { FakeSession } from '../transport/mobile-endpoint-supervisor-test-fakes' -import { useNewWorkspaceServers } from './use-host-display-worktrees' - -const RELAYS = [MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY] -// The host list's own labels: what a sheet opened from Home sees before the list loads a server. -const DESKTOP_LABELS: ReadonlyMap = new Map() - -/** A desktop that lists one ready server. */ -function desktop(): FakeSession { - const session = new FakeSession('connected') - session.sendRequest.mockImplementation(async (method: string) => ({ - id: 'reply', - ok: true, - result: - method === MOBILE_RELAY_HOSTS_LIST_METHOD - ? { hosts: [{ hostId: 'runtime:vm', label: 'vm', health: 'available', relay: 'ready' }] } - : {}, - _meta: { runtimeId: 'runtime-1' } - })) - return session -} - -function listCalls(session: FakeSession): number { - return session.sendRequest.mock.calls.filter( - ([method]) => method === MOBILE_RELAY_HOSTS_LIST_METHOD - ).length -} - -describe('the servers a new workspace can run on', () => { - it('are offered, by name, when the sheet opens before the host list has loaded them', async () => { - const client = desktop() - const seen: { value: ReturnType | null } = { value: null } - function Sheet({ open }: { open: boolean }): null { - seen.value = useNewWorkspaceServers({ - client, - hostCapabilities: RELAYS, - hostLabelById: DESKTOP_LABELS, - open - }) - return null - } - const rendered: { tree: ReturnType | null } = { tree: null } - await act(async () => { - rendered.tree = create(createElement(Sheet, { open: false })) - }) - // Closed, it asks nothing of the desktop. - expect(listCalls(client)).toBe(0) - expect([...(seen.value?.serverClients.keys() ?? [])]).toEqual([]) - await act(async () => { - rendered.tree?.update(createElement(Sheet, { open: true })) - }) - expect([...(seen.value?.serverClients.keys() ?? [])]).toEqual(['runtime:vm']) - expect(seen.value?.hostLabelById.get('runtime:vm')).toBe('vm') - }) -}) diff --git a/mobile/src/host-screen/use-host-display-worktrees.ts b/mobile/src/host-screen/use-host-display-worktrees.ts index 1fbb0070689..a4081a1b7fa 100644 --- a/mobile/src/host-screen/use-host-display-worktrees.ts +++ b/mobile/src/host-screen/use-host-display-worktrees.ts @@ -1,24 +1,18 @@ import { useMemo } from 'react' -import type { ExecutionHostId } from '../../../src/shared/execution-host' -import { reachableServerClients } from '../transport/execution-host-scoped-rpc-client' import { useHostDescriptor } from '../transport/host-descriptor-store' -import type { RpcClient } from '../transport/rpc-client' import type { ConnectionState } from '../transport/types' -import { useRelayHosts } from '../worktree/relay-hosts-feed' -import { serverHostHealth } from '../worktree/server-workspaces' +import { serverHostHealth, withServerHosts } from '../worktree/server-workspaces' import { filterVisibleHostRows } from '../worktree/visible-host-rows' -import { - applyWorktreeHostContextLabels, - type HostLabelSources -} from '../worktree/worktree-host-context-labels' +import { applyWorktreeHostContextLabels } from '../worktree/worktree-host-context-labels' import { applyWorktreeRowDisplayState } from '../worktree/worktree-host-row-identity' import type { HostScreenState } from './use-host-screen-state' -/** 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 { +/** 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, + hostId: string | undefined +) { const { serverWorkspaces } = state // Why: a server's label and health come with its rows, alongside the desktop's SSH ones. const hostLabelById = useMemo( @@ -32,84 +26,35 @@ export function useHostLabelSources( 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 servers the new-workspace sheet offers, and their names, read from the desktop's server-list - * feed while it is open: the list's own server rows stop refreshing behind the sheet. - */ -export function useNewWorkspaceServers(args: { - client: RpcClient | null - hostCapabilities: readonly string[] - hostLabelById: ReadonlyMap - open: boolean -}): { - serverClients: ReadonlyMap - hostLabelById: ReadonlyMap -} { - const { client, hostCapabilities, hostLabelById, open } = args - const hosts = useRelayHosts(open ? client : null) - return useMemo(() => { - const listed = hosts ?? [] - return { - serverClients: reachableServerClients(client, hostCapabilities, listed), - hostLabelById: withServerHosts(hostLabelById, listed, (host) => host.label) - } - }, [client, hostCapabilities, hostLabelById, hosts]) -} - -/** 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, ...state.serverWorkspaces.worktrees], + [...base, ...serverWorkspaces.worktrees], state.visibleHostIds, state.repoHostIdByRepoId ) return applyWorktreeHostContextLabels( applyWorktreeRowDisplayState(rows, state.sleptIds, state.optimisticActiveWorktreeIdentity), - labels + { + repoHostIdByRepoId: state.repoHostIdByRepoId, + hostLabelById, + hostPlatform, + hostHealthById + } ) }, [ connState, state.worktrees, state.lastKnownWorktrees, - state.serverWorkspaces.worktrees, + serverWorkspaces.worktrees, state.visibleHostIds, state.sleptIds, state.optimisticActiveWorktreeIdentity, state.repoHostIdByRepoId, - labels + hostLabelById, + hostPlatform, + hostHealthById ]) } - -function withServerHosts( - desktop: ReadonlyMap, - hosts: HostScreenState['serverWorkspaces']['hosts'], - read: (host: HostScreenState['serverWorkspaces']['hosts'][number]) => Value -): ReadonlyMap { - if (hosts.length === 0) { - return desktop - } - const merged = new Map(desktop) - for (const host of hosts) { - merged.set(host.hostId, read(host)) - } - return merged -} diff --git a/mobile/src/host-screen/use-host-screen-controller.ts b/mobile/src/host-screen/use-host-screen-controller.ts index cbb91f89b3f..3842161f333 100644 --- a/mobile/src/host-screen/use-host-screen-controller.ts +++ b/mobile/src/host-screen/use-host-screen-controller.ts @@ -15,7 +15,7 @@ import { useRelayRecoveryStatus } from '../transport/client-context-connection-metrics' import { useWorkspaceSections } from '../worktree/use-workspace-sections' -import { useHostDisplayWorktrees, useHostLabelSources } from './use-host-display-worktrees' +import { useHostDisplayWorktrees } 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,8 +103,7 @@ export function useHostScreenController({ } const showNewWorktree = resolvedRouteActionState.showNewWorktree - const hostLabels = useHostLabelSources(hostId, state) - const displayWorktrees = useHostDisplayWorktrees(connState, state, hostLabels) + const displayWorktrees = useHostDisplayWorktrees(connState, state, hostId) const sectionsResult = useWorkspaceSections({ displayWorktrees, sortMode: state.sortMode, @@ -136,7 +135,6 @@ export function useHostScreenController({ hostCapabilities, hostDisplay, hostId, - hostLabels, insets, isReadOnly: connState === 'auth-failed', isWideLayout, diff --git a/mobile/src/worktree/server-workspaces.ts b/mobile/src/worktree/server-workspaces.ts index 12c0553631d..29f0e54cbfa 100644 --- a/mobile/src/worktree/server-workspaces.ts +++ b/mobile/src/worktree/server-workspaces.ts @@ -19,6 +19,22 @@ export function serverHostHealth(host: MobileRelayHost): ExecutionHostHealth { return host.relay === 'update-needed' ? 'blocked' : host.health } +/** `desktop`'s per-host values with each server's read from its feed entry. */ +export function withServerHosts( + desktop: ReadonlyMap, + hosts: readonly MobileRelayHost[], + read: (host: MobileRelayHost) => Value +): ReadonlyMap { + if (hosts.length === 0) { + return desktop + } + const merged = new Map(desktop) + for (const host of hosts) { + merged.set(host.hostId, read(host)) + } + return merged +} + function serverRowSchema(hostId: ExecutionHostId) { return z.custom( (row) =>