Files
orca/mobile/src/worktree/server-workspaces.ts
T
Jinwoo-H 64a48c39f8 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
2026-10-11 22:36:12 -04:00

110 lines
3.9 KiB
TypeScript

import { z } from 'zod'
import type { ExecutionHostId } from '../../../src/shared/execution-host'
import type { ExecutionHostHealth } from '../../../src/shared/execution-host-health'
import type { MobileRelayHost } from '../../../src/shared/mobile-relay-hosts-contract'
import type { RpcOperationSender } from '../transport/rpc-operation-sender'
import type { RpcClient } from '../transport/rpc-client'
import { relayHostsFeed } from './relay-hosts-feed'
import { relayHostWorktreesRead } from './server-workspace-operations'
import type { Worktree } from './workspace-list-types'
import { areWorktreeListsEqual } from './worktree-list-snapshot'
/** The desktop's servers and their rows, each stamped by the desktop with the server's host id. */
export type ServerWorkspaces = { hosts: readonly MobileRelayHost[]; worktrees: readonly Worktree[] }
export const NO_SERVER_WORKSPACES: ServerWorkspaces = { hosts: [], worktrees: [] }
/** A server that needs an update reads as blocked, which the shared table labels "Update needed". */
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<Value>(
desktop: ReadonlyMap<ExecutionHostId, Value>,
hosts: readonly MobileRelayHost[],
read: (host: MobileRelayHost) => Value
): ReadonlyMap<ExecutionHostId, Value> {
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<Worktree>(
(row) =>
typeof row === 'object' &&
row !== null &&
'hostId' in row &&
row.hostId === hostId &&
'worktreeId' in row &&
typeof row.worktreeId === 'string' &&
'repoId' in row &&
typeof row.repoId === 'string'
)
}
async function fetchHostRows(
client: RpcOperationSender,
hostId: ExecutionHostId
): Promise<Worktree[] | null> {
try {
const reply = relayHostWorktreesRead.interpret(
await relayHostWorktreesRead.request(client, { hostId })
)
if (!reply.accepted || !reply.value.worktrees) {
return null
}
const schema = serverRowSchema(hostId)
return reply.value.worktrees.flatMap((row) => {
const parsed = schema.safeParse(row)
return parsed.success ? [parsed.data] : []
})
} catch {
return null
}
}
/** One listing round: the desktop's servers, and per server its rows or null when unread. */
export type FetchedServerWorkspaces = {
hosts: readonly MobileRelayHost[]
rows: readonly (readonly Worktree[] | null)[]
}
/** The servers' rows through the desktop's one listing path; null when it did not answer. */
export async function fetchServerWorkspaces(
client: RpcClient
): Promise<FetchedServerWorkspaces | null> {
const hosts = await relayHostsFeed(client).read()
if (!hosts) {
return null
}
return { hosts, rows: await Promise.all(hosts.map((host) => fetchHostRows(client, host.hostId))) }
}
/**
* A server whose rows were not read keeps those shown; one the desktop dropped loses its rows.
* An unchanged poll returns `previous` itself, so the list does not rebuild every few seconds.
*/
export function applyServerWorkspaces(
previous: ServerWorkspaces,
fetched: FetchedServerWorkspaces
): ServerWorkspaces {
const worktrees = fetched.hosts.flatMap(
(host, index) =>
fetched.rows[index] ?? previous.worktrees.filter((row) => row.hostId === host.hostId)
)
// Why kept: screens key per-server clients on `hosts`, which rows changing must not rebuild.
const hosts =
JSON.stringify(fetched.hosts) === JSON.stringify(previous.hosts)
? previous.hosts
: fetched.hosts
const unchanged = hosts === previous.hosts && areWorktreeListsEqual(worktrees, previous.worktrees)
return unchanged ? previous : { hosts, worktrees }
}