mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
fix(mobile): name the desktop's servers in a new workspace opened before the list loads
The previous fix offered the servers but still named them from the host list's rows, so a sheet opened from Home showed a server's raw id under Run on. The sheet now takes both the servers and their names from the server-list feed while it is open. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -4,7 +4,7 @@ import { Pressable, Text, View } from 'react-native'
|
||||
import { Check, Moon } from 'lucide-react-native'
|
||||
import { buildWorktreeNavigationActions } from '../agent-history/worktree-navigation-actions'
|
||||
import { workspaceRouteHref } from '../navigation/workspace-execution-host'
|
||||
import { useReachableServerClients, useWorkspaceServer } from '../transport/workspace-server'
|
||||
import { useWorkspaceServer } from '../transport/workspace-server'
|
||||
import { ActionSheetContent } from '../components/ActionSheetModal'
|
||||
import { BottomDrawer } from '../components/BottomDrawer'
|
||||
import { ConfirmModal } from '../components/ConfirmModal'
|
||||
@@ -19,6 +19,7 @@ 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 }) {
|
||||
@@ -43,12 +44,13 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr
|
||||
desktopCapabilities: hostCapabilities,
|
||||
executionHost: actionTarget?.hostId
|
||||
})
|
||||
// Why its own read: the host list's rows stop refreshing while the sheet is open.
|
||||
const [newWorktreeVisible, setNewWorktreeVisible] = useState(false)
|
||||
const serverClients = useReachableServerClients(
|
||||
newWorktreeVisible ? client : null,
|
||||
hostCapabilities
|
||||
)
|
||||
const newWorkspaceServers = useNewWorkspaceServers({
|
||||
client,
|
||||
hostCapabilities,
|
||||
hostLabelById: hostLabels.hostLabelById,
|
||||
open: newWorktreeVisible
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -229,9 +231,9 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr
|
||||
ref={state.newWorktreeModalRef}
|
||||
routeVisible={showNewWorktree}
|
||||
client={client}
|
||||
serverClients={serverClients}
|
||||
serverClients={newWorkspaceServers.serverClients}
|
||||
hostId={hostId}
|
||||
hostLabelById={hostLabels.hostLabelById}
|
||||
hostLabelById={newWorkspaceServers.hostLabelById}
|
||||
existingWorktreePaths={existingWorktreePaths}
|
||||
existingWorktrees={state.worktrees}
|
||||
// The seam, not react-native's `Linking`: this screen is in the tasks page closure, and
|
||||
|
||||
+16
-11
@@ -3,18 +3,17 @@ import { act, create } from 'react-test-renderer'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('react-native', () => ({ AppState: { currentState: 'active' } }))
|
||||
// The server status gates beside this hook persist what a desktop reports; nothing here reads them.
|
||||
vi.mock('./host-app-version-store', () => ({ recordHostAppVersion: async () => {} }))
|
||||
vi.mock('./host-descriptor-recorder', () => ({ recordHostDescriptorFromStatus: () => {} }))
|
||||
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 './mobile-endpoint-supervisor-test-fakes'
|
||||
import type { RpcClient } from './rpc-client'
|
||||
import { useReachableServerClients } from './workspace-server'
|
||||
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<ExecutionHostId, string> = new Map()
|
||||
|
||||
/** A desktop that lists one ready server. */
|
||||
function desktop(): FakeSession {
|
||||
@@ -38,11 +37,16 @@ function listCalls(session: FakeSession): number {
|
||||
}
|
||||
|
||||
describe('the servers a new workspace can run on', () => {
|
||||
it('are offered when the sheet opens, whether or not the host list has loaded their rows', async () => {
|
||||
it('are offered, by name, when the sheet opens before the host list has loaded them', async () => {
|
||||
const client = desktop()
|
||||
const seen: { clients: ReadonlyMap<ExecutionHostId, RpcClient> | null } = { clients: null }
|
||||
const seen: { value: ReturnType<typeof useNewWorkspaceServers> | null } = { value: null }
|
||||
function Sheet({ open }: { open: boolean }): null {
|
||||
seen.clients = useReachableServerClients(open ? client : null, RELAYS)
|
||||
seen.value = useNewWorkspaceServers({
|
||||
client,
|
||||
hostCapabilities: RELAYS,
|
||||
hostLabelById: DESKTOP_LABELS,
|
||||
open
|
||||
})
|
||||
return null
|
||||
}
|
||||
const rendered: { tree: ReturnType<typeof create> | null } = { tree: null }
|
||||
@@ -51,10 +55,11 @@ describe('the servers a new workspace can run on', () => {
|
||||
})
|
||||
// Closed, it asks nothing of the desktop.
|
||||
expect(listCalls(client)).toBe(0)
|
||||
expect([...(seen.clients?.keys() ?? [])]).toEqual([])
|
||||
expect([...(seen.value?.serverClients.keys() ?? [])]).toEqual([])
|
||||
await act(async () => {
|
||||
rendered.tree?.update(createElement(Sheet, { open: true }))
|
||||
})
|
||||
expect([...(seen.clients?.keys() ?? [])]).toEqual(['runtime:vm'])
|
||||
expect([...(seen.value?.serverClients.keys() ?? [])]).toEqual(['runtime:vm'])
|
||||
expect(seen.value?.hostLabelById.get('runtime:vm')).toBe('vm')
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,10 @@
|
||||
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 { filterVisibleHostRows } from '../worktree/visible-host-rows'
|
||||
import {
|
||||
@@ -40,6 +43,30 @@ export function useHostLabelSources(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<ExecutionHostId, string>
|
||||
open: boolean
|
||||
}): {
|
||||
serverClients: ReadonlyMap<ExecutionHostId, RpcClient>
|
||||
hostLabelById: ReadonlyMap<ExecutionHostId, string>
|
||||
} {
|
||||
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,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { useCallback, useMemo, useSyncExternalStore } from 'react'
|
||||
import type { ExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import type { MobileRelayHost } from '../../../src/shared/mobile-relay-hosts-contract'
|
||||
import { relayHostsFeed } from '../worktree/relay-hosts-feed'
|
||||
import { useRelayHosts } from '../worktree/relay-hosts-feed'
|
||||
import { serverHostHealth } from '../worktree/server-workspaces'
|
||||
import {
|
||||
reachableServerClients,
|
||||
scopeRpcClientToExecutionHost,
|
||||
workspaceRouteExecutionHost
|
||||
} from './execution-host-scoped-rpc-client'
|
||||
@@ -34,19 +31,6 @@ export function useWorkspaceServer(args: {
|
||||
return { routeHost, server: routeHost === undefined ? null : { client: serverClient, gates } }
|
||||
}
|
||||
|
||||
const noSubscription = (): (() => void) => () => {}
|
||||
|
||||
/** The desktop's servers from the shared feed while `client` is set; null before it answers. */
|
||||
function useRelayHosts(client: RpcClient | null): readonly MobileRelayHost[] | null {
|
||||
const feed = client ? relayHostsFeed(client) : null
|
||||
const subscribe = useCallback(
|
||||
(listener: () => void) => (feed ? feed.subscribe(listener) : noSubscription()),
|
||||
[feed]
|
||||
)
|
||||
const readHosts = () => feed?.getSnapshot() ?? null
|
||||
return useSyncExternalStore(subscribe, readHosts, readHosts)
|
||||
}
|
||||
|
||||
/**
|
||||
* The connection state a server workspace's screens see: the desktop's, unless the desktop reports
|
||||
* that server down. Desktop-owned workspaces (`routeHost` absent) keep the desktop's state untouched.
|
||||
@@ -76,18 +60,3 @@ export function useWorkspaceServerConnState(args: {
|
||||
return 'disconnected'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The servers a new workspace can run on, read from the desktop's server-list feed while `client`
|
||||
* is set (the sheet is open), not from the host list's rows, which pause while the sheet is open.
|
||||
*/
|
||||
export function useReachableServerClients(
|
||||
client: RpcClient | null,
|
||||
hostCapabilities: readonly string[]
|
||||
): ReadonlyMap<ExecutionHostId, RpcClient> {
|
||||
const hosts = useRelayHosts(client)
|
||||
return useMemo(
|
||||
() => reachableServerClients(client, hostCapabilities, hosts ?? []),
|
||||
[client, hostCapabilities, hosts]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// list and every open server workspace share one poll and one answer, so a row's badge and its
|
||||
// workspace screen never disagree and two readers never double the desktop's load.
|
||||
|
||||
import { useCallback, useSyncExternalStore } from 'react'
|
||||
import { AppState } from 'react-native'
|
||||
import type { MobileRelayHost } from '../../../src/shared/mobile-relay-hosts-contract'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
@@ -127,3 +128,16 @@ export function relayHostsFeed(client: RpcClient): RelayHostsFeed {
|
||||
}
|
||||
return feed
|
||||
}
|
||||
|
||||
const noSubscription = (): (() => void) => () => {}
|
||||
|
||||
/** The desktop's servers from the shared feed while `client` is set; null before it answers. */
|
||||
export function useRelayHosts(client: RpcClient | null): readonly MobileRelayHost[] | null {
|
||||
const feed = client ? relayHostsFeed(client) : null
|
||||
const subscribe = useCallback(
|
||||
(listener: () => void) => (feed ? feed.subscribe(listener) : noSubscription()),
|
||||
[feed]
|
||||
)
|
||||
const readHosts = () => feed?.getSnapshot() ?? null
|
||||
return useSyncExternalStore(subscribe, readHosts, readHosts)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user