feat(tui): reload command restarts the service

This commit is contained in:
Dax Raad 2026-07-03 14:01:00 -04:00
commit 2ff19171dc
4 changed files with 51 additions and 3 deletions

View file

@ -63,6 +63,24 @@ export default Runtime.handler(Commands, (input) =>
}).pipe(Effect.provide(NodeFileSystem.layer)),
)
: () => Promise.resolve(transport)
yield* runTui(transport, { continue: input.continue, sessionID: Option.getOrUndefined(input.session) }, discover)
// Restart the managed service in place; start() resolves once the
// replacement is healthy and the reconnect loop reattaches on its own.
// Only meaningful in service mode: --server is not ours to restart and a
// standalone child cannot be respawned.
const reload = serviceOptions
? () =>
Effect.runPromise(
Effect.gen(function* () {
yield* Service.stop(serviceOptions)
yield* Service.start(serviceOptions)
}).pipe(Effect.provide(NodeFileSystem.layer)),
)
: undefined
yield* runTui(
transport,
{ continue: input.continue, sessionID: Option.getOrUndefined(input.session) },
discover,
reload,
)
}),
)

View file

@ -9,7 +9,12 @@ import type { Service } from "@opencode-ai/client/effect"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import type { Args } from "@opencode-ai/tui/context/args"
export function runTui(transport: Service.Transport, args: Args, discover?: () => Promise<Service.Transport>) {
export function runTui(
transport: Service.Transport,
args: Args,
discover?: () => Promise<Service.Transport>,
reload?: () => Promise<void>,
) {
const config = TuiConfig.resolve({}, { terminalSuspend: false })
let disposeSlots: (() => void) | undefined
return Effect.gen(function* () {
@ -33,6 +38,7 @@ export function runTui(transport: Service.Transport, args: Args, discover?: () =
}
}
: undefined,
reload,
args,
config,
pluginHost: {

View file

@ -139,6 +139,7 @@ export type TuiInput = {
client: OpencodeClient
api: OpenCodeClient
discover?: () => Promise<{ client: OpencodeClient; api: OpenCodeClient }>
reload?: () => Promise<void>
args: Args
config: TuiConfig.Resolved
onSnapshot?: () => Promise<string[]>
@ -301,7 +302,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
>
<TuiConfigProvider config={input.config}>
<PluginRuntimeProvider value={pluginRuntime}>
<SDKProvider client={input.client} api={input.api} discover={input.discover}>
<SDKProvider client={input.client} api={input.api} discover={input.discover} reload={input.reload}>
<PermissionProvider>
<ProjectProvider>
<SyncProvider>
@ -800,6 +801,26 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
},
category: "System",
},
...(sdk.reload
? [
{
name: "server.reload",
title: "Reload server",
slashName: "reload",
run: async () => {
dialog.clear()
toast.show({ variant: "info", message: "Reloading server...", duration: 30000 })
// reload resolves once the replacement service is healthy; the
// event stream reattaches through the reconnect loop.
await sdk
.reload!()
.then(() => toast.show({ variant: "success", message: "Server reloaded" }))
.catch(toast.error)
},
category: "System",
},
]
: []),
{
name: "theme.switch",
title: "Switch theme",

View file

@ -16,6 +16,8 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
client: OpencodeClient
api: OpenCodeClient
discover?: () => Promise<{ client: OpencodeClient; api: OpenCodeClient }>
// Stops and starts the managed service; present only in service mode.
reload?: () => Promise<void>
}) => {
const abort = new AbortController()
let client = props.client
@ -138,6 +140,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
return connection.connectedOnce
},
},
reload: props.reload,
}
},
})