mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +00:00
fix(mobile): offer the desktop's servers in a new workspace opened before the list loads
The sheet's run targets came from the host list's server rows, which never load while the sheet is open (the list deliberately pauses its refresh behind it). From Home's New Workspace that left only the desktop's repos. The sheet now reads the desktop's server-list feed itself while it is open. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { openExternalLink } from '../platform/external-link'
|
||||
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 { reachableServerClients } from '../transport/execution-host-scoped-rpc-client'
|
||||
import { useWorkspaceServer } from '../transport/workspace-server'
|
||||
import { useReachableServerClients, useWorkspaceServer } from '../transport/workspace-server'
|
||||
import { ActionSheetContent } from '../components/ActionSheetModal'
|
||||
import { BottomDrawer } from '../components/BottomDrawer'
|
||||
import { ConfirmModal } from '../components/ConfirmModal'
|
||||
@@ -44,9 +43,11 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr
|
||||
desktopCapabilities: hostCapabilities,
|
||||
executionHost: actionTarget?.hostId
|
||||
})
|
||||
const serverClients = useMemo(
|
||||
() => reachableServerClients(client, hostCapabilities, state.serverWorkspaces.hosts),
|
||||
[client, hostCapabilities, state.serverWorkspaces.hosts]
|
||||
// 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
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -238,6 +239,7 @@ 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 })
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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' } }))
|
||||
// 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: () => {} }))
|
||||
|
||||
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'
|
||||
|
||||
const RELAYS = [MOBILE_DESKTOP_RELAY_RUNTIME_CAPABILITY]
|
||||
|
||||
/** 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 when the sheet opens, whether or not the host list has loaded their rows', async () => {
|
||||
const client = desktop()
|
||||
const seen: { clients: ReadonlyMap<ExecutionHostId, RpcClient> | null } = { clients: null }
|
||||
function Sheet({ open }: { open: boolean }): null {
|
||||
seen.clients = useReachableServerClients(open ? client : null, RELAYS)
|
||||
return null
|
||||
}
|
||||
const rendered: { tree: ReturnType<typeof create> | 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.clients?.keys() ?? [])]).toEqual([])
|
||||
await act(async () => {
|
||||
rendered.tree?.update(createElement(Sheet, { open: true }))
|
||||
})
|
||||
expect([...(seen.clients?.keys() ?? [])]).toEqual(['runtime:vm'])
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useCallback, useSyncExternalStore } from 'react'
|
||||
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 { serverHostHealth } from '../worktree/server-workspaces'
|
||||
import {
|
||||
reachableServerClients,
|
||||
scopeRpcClientToExecutionHost,
|
||||
workspaceRouteExecutionHost
|
||||
} from './execution-host-scoped-rpc-client'
|
||||
@@ -34,6 +36,17 @@ export function useWorkspaceServer(args: {
|
||||
|
||||
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.
|
||||
@@ -45,13 +58,7 @@ export function useWorkspaceServerConnState(args: {
|
||||
}): ConnectionState {
|
||||
const { client, connState, routeHost } = args
|
||||
// The same feed the host list reads, so a row's badge and its screens agree.
|
||||
const feed = client && routeHost ? relayHostsFeed(client) : null
|
||||
const subscribe = useCallback(
|
||||
(listener: () => void) => (feed ? feed.subscribe(listener) : noSubscription()),
|
||||
[feed]
|
||||
)
|
||||
const readHosts = () => feed?.getSnapshot() ?? null
|
||||
const hosts = useSyncExternalStore(subscribe, readHosts, readHosts)
|
||||
const hosts = useRelayHosts(routeHost ? client : null)
|
||||
if (connState !== 'connected' || !routeHost || !hosts) {
|
||||
return connState
|
||||
}
|
||||
@@ -69,3 +76,18 @@ 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]
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user