refactor(cli): define server connection boundary (#37133)

This commit is contained in:
Kit Langton 2026-07-15 11:48:38 -04:00 committed by GitHub
commit f5dd181443
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 250 additions and 121 deletions

View file

@ -118,6 +118,7 @@ const appBindingCommands = [
"provider.connect",
"opencode.status",
"server.pair",
"service.restart",
"opencode.debug",
"theme.switch",
"theme.switch_mode",
@ -138,8 +139,10 @@ const appBindingCommands = [
export type TuiInput = {
server: {
endpoint: Service.Endpoint
reconnect?: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<Service.Endpoint>
reload?: () => Promise<void>
service?: {
reconnect: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<Service.Endpoint>
restart: () => Promise<void>
}
}
args: Args
config: Config.Interface
@ -183,14 +186,15 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
Effect.catch(() => Effect.tryPromise(() => api.location.get()).pipe(Effect.map((response) => response.directory))),
)
const handoff = input.terminalHandoff ? yield* Effect.promise(input.terminalHandoff) : undefined
const reconnectEndpoint = input.server.reconnect
const reconnect = reconnectEndpoint
? async (onStatus: (status: Service.Status) => void, signal: AbortSignal) => {
const endpoint = await reconnectEndpoint(onStatus, signal)
const next = { baseUrl: endpoint.url, headers: Service.headers(endpoint) }
return {
api: OpenCode.make(next),
}
const managed = input.server.service
const service = managed
? {
reconnect: async (onStatus: (status: Service.Status) => void, signal: AbortSignal) => {
const endpoint = await managed.reconnect(onStatus, signal)
const next = { baseUrl: endpoint.url, headers: Service.headers(endpoint) }
return { api: OpenCode.make(next) }
},
restart: managed.restart,
}
: undefined
const exit = { epilogue: undefined as string | undefined, reason: undefined as unknown }
@ -324,7 +328,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
}
>
<PluginRuntimeProvider value={pluginRuntime}>
<ClientProvider api={api} reconnect={reconnect} reload={input.server.reload}>
<ClientProvider api={api} service={service}>
<PermissionProvider>
<DataProvider>
<LocationProvider>
@ -757,19 +761,21 @@ function App(props: { pair?: DialogPairCredentials }) {
},
category: "System",
},
...(client.reload
...(client.restart
? [
{
name: "server.reload",
title: "Reload server",
slash: { name: "reload" },
name: "service.restart",
title: "Restart service",
slash: { name: "restart" },
run: async () => {
const restart = client.restart
if (!restart) return
dialog.clear()
toast.show({ variant: "info", message: "Reloading server...", duration: 30000 })
// reload resolves once the replacement service is healthy; the
toast.show({ variant: "info", message: "Restarting service...", duration: 30000 })
// restart resolves once the replacement service is healthy; the
// event stream reattaches through the reconnect loop.
await client.reload!()
.then(() => toast.show({ variant: "success", message: "Server reloaded" }))
await restart()
.then(() => toast.show({ variant: "success", message: "Service restarted" }))
.catch(toast.error)
},
category: "System",

View file

@ -18,18 +18,18 @@ export type ClientConnectionEvent = {
}
}
type ManagedService = {
reconnect: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<{ api: OpenCodeClient }>
restart: () => Promise<void>
}
type ClientEventMap = { [Type in OpenCodeEvent["type"]]: Extract<OpenCodeEvent, { type: Type }> }
const connectTimeout = 2_000
const connectionHistoryLimit = 50
export const { use: useClient, provider: ClientProvider } = createSimpleContext({
name: "Client",
init: (props: {
api: OpenCodeClient
reconnect?: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<{ api: OpenCodeClient }>
// Stops and starts the managed service; present only in service mode.
reload?: () => Promise<void>
}) => {
init: (props: { api: OpenCodeClient; service?: ManagedService }) => {
const log = useLog({ component: "client" })
const abort = new AbortController()
const history: ClientConnectionEvent[] = []
@ -115,8 +115,8 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext(
// Re-resolve the transport before retrying: the server may have
// moved (service restarted on a new port) or need starting. Static
// transports (--server, standalone) resolve to the same address.
if (props.reconnect) {
const next = await props.reconnect(setService, controller.signal).catch((error) => {
if (props.service) {
const next = await props.service.reconnect(setService, controller.signal).catch((error) => {
if (!controller.signal.aborted)
log.info("server resolution failed", {
attempt,
@ -168,7 +168,7 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext(
},
},
},
reload: props.reload,
restart: props.service?.restart,
}
},
})