From 94d7de6e318c9da9676bfb596e7085768a956a5d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Jul 2026 10:06:50 -0400 Subject: [PATCH 1/5] refactor(cli): preserve server lifecycle effects --- packages/cli/src/commands/handlers/default.ts | 12 +++- packages/cli/src/services/server.ts | 25 ++++----- packages/cli/test/server.test.ts | 56 +++++++++++++++++++ packages/tui/src/app.tsx | 22 +++++--- packages/tui/src/context/client.tsx | 4 +- 5 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 packages/cli/test/server.test.ts diff --git a/packages/cli/src/commands/handlers/default.ts b/packages/cli/src/commands/handlers/default.ts index 8143c26352cb..0f648c6a43bd 100644 --- a/packages/cli/src/commands/handlers/default.ts +++ b/packages/cli/src/commands/handlers/default.ts @@ -4,7 +4,7 @@ import { run } from "@opencode-ai/tui" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" import { Config } from "../../config" -import { Effect, Option } from "effect" +import { Effect, FileSystem, Option } from "effect" import { Server } from "../../services/server" import { Updater } from "../../services/updater" import { UpdatePreflight } from "../../services/update-preflight" @@ -37,11 +37,17 @@ export default Runtime.handler(Commands, (input) => preflight.loading() const config = yield* Config.Service const npm = yield* Npm.Service - const context = yield* Effect.context() + const context = yield* Effect.context() const runFork = Effect.runForkWith(context) const runPromise = Effect.runPromiseWith(context) + const reconnect = server.reconnect + const restart = server.restart yield* run({ - server, + server: { + endpoint: server.endpoint, + reconnect: reconnect ? (onStatus, signal) => runPromise(reconnect(onStatus), { signal }) : undefined, + restart: restart ? () => runPromise(restart) : undefined, + }, args: { continue: input.continue, sessionID: Option.getOrUndefined(input.session) }, config: { path: config.path, diff --git a/packages/cli/src/services/server.ts b/packages/cli/src/services/server.ts index b1cdce4cbbf8..2dda4825b3d6 100644 --- a/packages/cli/src/services/server.ts +++ b/packages/cli/src/services/server.ts @@ -1,4 +1,3 @@ -import { NodeFileSystem } from "@effect/platform-node" import { Service } from "@opencode-ai/client/effect" import { ClientError, isUnauthorizedError, OpenCode } from "@opencode-ai/client/promise" import { InstallationVersion } from "@opencode-ai/core/installation/version" @@ -16,8 +15,12 @@ export type Args = { export type Resolved = { readonly endpoint: Service.Endpoint - readonly reconnect?: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise - readonly reload?: () => Promise + readonly reconnect?: (onStatus: (status: Service.Status) => void) => ReturnType + readonly restart?: Effect.Effect< + void, + Effect.Error> | Effect.Error>, + Effect.Services> | Effect.Services> + > } export const resolve = Effect.fn("cli.server.resolve")(function* (args: Args) { @@ -49,17 +52,11 @@ export const resolve = Effect.fn("cli.server.resolve")(function* (args: Args) { const reconnectOptions = { ...options, version: undefined } return { endpoint, - reconnect: (onStatus, signal) => - Effect.runPromise(Service.start({ ...reconnectOptions, onStatus }).pipe(Effect.provide(NodeFileSystem.layer)), { - signal, - }), - reload: () => - Effect.runPromise( - Effect.gen(function* () { - yield* Service.stop(options, { targetVersion: options.version }) - yield* Service.start(options) - }).pipe(Effect.provide(NodeFileSystem.layer)), - ), + reconnect: (onStatus) => Service.start({ ...reconnectOptions, onStatus }), + restart: Effect.gen(function* () { + yield* Service.stop(options, { targetVersion: options.version }) + yield* Service.start(options) + }), } satisfies Resolved }) diff --git a/packages/cli/test/server.test.ts b/packages/cli/test/server.test.ts new file mode 100644 index 000000000000..f695c16d969f --- /dev/null +++ b/packages/cli/test/server.test.ts @@ -0,0 +1,56 @@ +import { NodeFileSystem } from "@effect/platform-node" +import { Global } from "@opencode-ai/core/global" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { expect, test } from "bun:test" +import { Effect } from "effect" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { Server } from "../src/services/server" +import { ServiceConfig } from "../src/services/service-config" + +test("managed resolution keeps lifecycle operations Effect-native", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-server-resolution-")) + const id = "server-resolution-test" + const server = Bun.serve({ + port: 0, + fetch() { + return Response.json({ + healthy: true, + version: InstallationVersion, + pid: process.pid, + instanceID: id, + status: { type: "ready" }, + }) + }, + }) + const registration = path.join(root, "state", ServiceConfig.filename()) + + try { + await fs.mkdir(path.dirname(registration), { recursive: true }) + await fs.writeFile( + registration, + JSON.stringify({ + id, + version: InstallationVersion, + url: server.url.toString(), + pid: process.pid, + }), + ) + const layer = Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") }) + const resolved = await Effect.runPromise( + Server.resolve({}).pipe(Effect.provide(layer), Effect.provide(NodeFileSystem.layer), Effect.scoped), + ) + + expect(resolved.endpoint.url).toBe(server.url.toString()) + expect(resolved.reconnect).toBeFunction() + expect(Effect.isEffect(resolved.restart)).toBe(true) + if (!resolved.reconnect) throw new Error("Expected managed reconnect") + expect(await Effect.runPromise(resolved.reconnect(() => {}).pipe(Effect.provide(NodeFileSystem.layer)))).toEqual( + resolved.endpoint, + ) + } finally { + server.stop(true) + await fs.rm(root, { recursive: true, force: true }) + } +}) diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 452b1242bf79..db3725bc23ad 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -139,7 +139,7 @@ export type TuiInput = { server: { endpoint: Service.Endpoint reconnect?: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise - reload?: () => Promise + restart?: () => Promise } args: Args config: Config.Interface @@ -324,7 +324,11 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { } > - + @@ -757,19 +761,21 @@ function App(props: { pair?: DialogPairCredentials }) { }, category: "System", }, - ...(client.reload + ...(client.restart ? [ { name: "server.reload", - title: "Reload server", + title: "Restart server", slash: { name: "reload" }, 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 server...", 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: "Server restarted" })) .catch(toast.error) }, category: "System", diff --git a/packages/tui/src/context/client.tsx b/packages/tui/src/context/client.tsx index 6994f6a6f754..063a5c99b104 100644 --- a/packages/tui/src/context/client.tsx +++ b/packages/tui/src/context/client.tsx @@ -28,7 +28,7 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext( 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 + restart?: () => Promise }) => { const log = useLog({ component: "client" }) const abort = new AbortController() @@ -168,7 +168,7 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext( }, }, }, - reload: props.reload, + restart: props.restart, } }, }) From 513091290daee4bc610c7fbfd8c344d7c2149571 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Jul 2026 11:01:53 -0400 Subject: [PATCH 2/5] refactor(cli): define server connection boundary --- docs/design/service-lifecycle.md | 26 +++++++++++++++ packages/cli/src/commands/handlers/api.ts | 10 ++---- packages/cli/src/commands/handlers/default.ts | 15 +++++---- packages/cli/src/commands/handlers/mini.ts | 4 +-- packages/cli/src/commands/handlers/run.ts | 4 +-- packages/cli/src/mini/mini.ts | 9 ++--- packages/cli/src/mini/run.ts | 7 ++-- .../{server.ts => server-connection.ts} | 33 ++++++++++--------- ...rver.test.ts => server-connection.test.ts} | 31 +++++++++-------- packages/docs/troubleshooting.mdx | 16 ++++----- packages/tui/src/app.tsx | 21 +++++++----- packages/tui/src/context/client.tsx | 2 +- .../content/docs/(docs)/troubleshooting.mdx | 16 ++++----- 13 files changed, 111 insertions(+), 83 deletions(-) rename packages/cli/src/services/{server.ts => server-connection.ts} (78%) rename packages/cli/test/{server.test.ts => server-connection.test.ts} (50%) diff --git a/docs/design/service-lifecycle.md b/docs/design/service-lifecycle.md index d4506521a9a0..a516455fd4ae 100644 --- a/docs/design/service-lifecycle.md +++ b/docs/design/service-lifecycle.md @@ -30,6 +30,32 @@ This proposal does not introduce a supervisor process, warm candidate server, protocol negotiation, idle background restart, or general execution-recovery framework. +## Architecture at a Glance + +```mermaid +flowchart LR + CLIENT[client Effect Service] --> CONNECTION[CLI ServerConnection] + CONFIG[CLI ServiceConfig] --> CONNECTION + CONFIG --> DAEMON[CLI server-process daemon] + CONNECTION -->|Effects| SEAM[CLI runPromiseWith seam] + SEAM -->|Promises| TUI[TUI and Solid reconnect loop] + CLIENT -->|start or stop| DAEMON + TUI -->|HTTP and events| HTTP[server HTTP transport] + DAEMON --> HTTP + HTTP --> CORE[core application behavior] +``` + +| Owner | Responsibility | +| ------------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| `packages/client/src/effect/service.ts` | Effect-native discovery, start, and stop lifecycle operations | +| `packages/cli/src/services/service-config.ts` | CLI registration path, installed version, and daemon command | +| `packages/cli/src/services/server-connection.ts` | Resolve an endpoint and, only for the shared service, grouped reconnect and restart Effects | +| `packages/cli/src/server-process.ts` | Daemon election, registration, and server process boot | +| `packages/server/src/process.ts` | HTTP lifecycle shell and application transport | +| `packages/core` | Application behavior behind the transport | +| CLI default handler | Convert lifecycle Effects with the outer `FileSystem` context and pass grouped Promise capabilities | +| `packages/tui` Solid client context | Own event-stream reconnect, endpoint replacement, status, and user-triggered restart UI | + ## Implementation Status | Area | State | diff --git a/packages/cli/src/commands/handlers/api.ts b/packages/cli/src/commands/handlers/api.ts index 49e367d48e58..781af277d78b 100644 --- a/packages/cli/src/commands/handlers/api.ts +++ b/packages/cli/src/commands/handlers/api.ts @@ -3,7 +3,7 @@ import { Effect, Option } from "effect" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" import { Service } from "@opencode-ai/client/effect" -import { Server } from "../../services/server" +import { ServerConnection } from "../../services/server-connection" const methods = new Set(["delete", "get", "head", "options", "patch", "post", "put"]) @@ -18,7 +18,7 @@ type OpenApi = { export default Runtime.handler( Commands.commands.api, Effect.fn("cli.api")(function* (input) { - const server = yield* Server.resolve({ + const server = yield* ServerConnection.resolve({ server: Option.getOrUndefined(input.server), standalone: input.standalone, mismatch: "ignore", @@ -62,11 +62,7 @@ export function rawRequest(input: readonly string[]) { return { method: input[0].toUpperCase(), path: input[1] } } -function resolveRequest( - endpoint: Service.Endpoint, - input: readonly string[], - params: Record, -) { +function resolveRequest(endpoint: Service.Endpoint, input: readonly string[], params: Record) { const raw = rawRequest(input) if (raw) return Effect.succeed(raw) if (input.length !== 1) return Effect.fail(new Error("Expected an operation name or an HTTP method and path")) diff --git a/packages/cli/src/commands/handlers/default.ts b/packages/cli/src/commands/handlers/default.ts index 0f648c6a43bd..18fd139724ca 100644 --- a/packages/cli/src/commands/handlers/default.ts +++ b/packages/cli/src/commands/handlers/default.ts @@ -5,7 +5,7 @@ import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" import { Config } from "../../config" import { Effect, FileSystem, Option } from "effect" -import { Server } from "../../services/server" +import { ServerConnection } from "../../services/server-connection" import { Updater } from "../../services/updater" import { UpdatePreflight } from "../../services/update-preflight" import { Npm } from "@opencode-ai/core/npm" @@ -18,7 +18,7 @@ export default Runtime.handler(Commands, (input) => yield* updater.check().pipe(Effect.forkScoped) const preflight = UpdatePreflight.make() yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close())) - const server = yield* Server.resolve({ + const server = yield* ServerConnection.resolve({ server: Option.getOrUndefined(input.server), standalone: input.standalone, onStart: (reason, existing) => { @@ -40,13 +40,16 @@ export default Runtime.handler(Commands, (input) => const context = yield* Effect.context() const runFork = Effect.runForkWith(context) const runPromise = Effect.runPromiseWith(context) - const reconnect = server.reconnect - const restart = server.restart + const service = server.service yield* run({ server: { endpoint: server.endpoint, - reconnect: reconnect ? (onStatus, signal) => runPromise(reconnect(onStatus), { signal }) : undefined, - restart: restart ? () => runPromise(restart) : undefined, + service: service + ? { + reconnect: (onStatus, signal) => runPromise(service.reconnect(onStatus), { signal }), + restart: () => runPromise(service.restart()), + } + : undefined, }, args: { continue: input.continue, sessionID: Option.getOrUndefined(input.session) }, config: { diff --git a/packages/cli/src/commands/handlers/mini.ts b/packages/cli/src/commands/handlers/mini.ts index 7919b2bc8e44..e44f7e0b2df5 100644 --- a/packages/cli/src/commands/handlers/mini.ts +++ b/packages/cli/src/commands/handlers/mini.ts @@ -1,14 +1,14 @@ import { Effect, Option } from "effect" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" -import { Server } from "../../services/server" +import { ServerConnection } from "../../services/server-connection" export default Runtime.handler(Commands.commands.mini, (input) => Effect.gen(function* () { const { runMini, validateMiniTerminal } = yield* Effect.promise(() => import("../../mini")) yield* Effect.promise(async () => validateMiniTerminal()) const serverURL = Option.getOrUndefined(input.server) - const server = yield* Server.resolve({ server: serverURL, standalone: input.standalone }) + const server = yield* ServerConnection.resolve({ server: serverURL, standalone: input.standalone }) yield* Effect.promise(() => runMini({ server, diff --git a/packages/cli/src/commands/handlers/run.ts b/packages/cli/src/commands/handlers/run.ts index e8abf0cb1e8b..50fbaa4e2c44 100644 --- a/packages/cli/src/commands/handlers/run.ts +++ b/packages/cli/src/commands/handlers/run.ts @@ -1,13 +1,13 @@ import { Effect, Option } from "effect" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" -import { Server } from "../../services/server" +import { ServerConnection } from "../../services/server-connection" export default Runtime.handler(Commands.commands.run, (input) => Effect.gen(function* () { const { runNonInteractive } = yield* Effect.promise(() => import("../../mini")) const separator = process.argv.indexOf("--", 2) - const server = yield* Server.resolve({ + const server = yield* ServerConnection.resolve({ server: Option.getOrUndefined(input.server), standalone: input.standalone, }) diff --git a/packages/cli/src/mini/mini.ts b/packages/cli/src/mini/mini.ts index 521995b45cbb..ccbb900ba04e 100644 --- a/packages/cli/src/mini/mini.ts +++ b/packages/cli/src/mini/mini.ts @@ -1,12 +1,12 @@ import { Service } from "@opencode-ai/client/effect" import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" -import { Server } from "../services/server" +import { ServerConnection } from "../services/server-connection" import { waitForCatalogReady } from "./catalog.shared" import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin" import type { RunInput, RunTuiConfig } from "./types" export type MiniCommandInput = { - server: Server.Resolved + server: ServerConnection.Resolved continue?: boolean session?: string fork?: boolean @@ -38,10 +38,7 @@ export async function runMini(input: MiniCommandInput) { return agentTask } const resolveSession = async () => { - const [agent, selected] = await Promise.all([ - resolveAgent(), - selectSession(sdk, directory, input), - ]) + const [agent, selected] = await Promise.all([resolveAgent(), selectSession(sdk, directory, input)]) const readyModel = model ?? (selected?.model ? { providerID: selected.model.providerID, modelID: selected.model.id } : undefined) if (readyModel) await waitForCatalogReady({ sdk, directory, model: readyModel }) diff --git a/packages/cli/src/mini/run.ts b/packages/cli/src/mini/run.ts index 2fc849bce00f..ad95f52f843f 100644 --- a/packages/cli/src/mini/run.ts +++ b/packages/cli/src/mini/run.ts @@ -4,7 +4,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util" import { Model } from "@opencode-ai/schema/model" import { open } from "node:fs/promises" import path from "node:path" -import { Server } from "../services/server" +import { ServerConnection } from "../services/server-connection" import { loadRunAgents, waitForCatalogReady } from "./catalog.shared" import { runNonInteractivePrompt } from "./noninteractive" import { toolInlineInfo } from "./tool" @@ -12,7 +12,7 @@ import type { MiniToolPart } from "./types" import { UI } from "./ui" export type RunCommandInput = { - server: Server.Resolved + server: ServerConnection.Resolved message: string[] continue?: boolean session?: string @@ -73,8 +73,7 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Ser .then((result) => (result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined)) : undefined const model = pickRunModel(explicitModel, variant, sessionModel, defaultModel) - if (variant && !model) - return reportError(input, "Cannot select a variant before selecting a model", session?.id) + if (variant && !model) return reportError(input, "Cannot select a variant before selecting a model", session?.id) if (model) { await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model }) const available = await client.model.list({ location: { directory: cwd, workspace } }) diff --git a/packages/cli/src/services/server.ts b/packages/cli/src/services/server-connection.ts similarity index 78% rename from packages/cli/src/services/server.ts rename to packages/cli/src/services/server-connection.ts index 2dda4825b3d6..9a4bbcc39cb9 100644 --- a/packages/cli/src/services/server.ts +++ b/packages/cli/src/services/server-connection.ts @@ -15,15 +15,10 @@ export type Args = { export type Resolved = { readonly endpoint: Service.Endpoint - readonly reconnect?: (onStatus: (status: Service.Status) => void) => ReturnType - readonly restart?: Effect.Effect< - void, - Effect.Error> | Effect.Error>, - Effect.Services> | Effect.Services> - > + readonly service?: ReturnType } -export const resolve = Effect.fn("cli.server.resolve")(function* (args: Args) { +export const resolve = Effect.fn("cli.server-connection.resolve")(function* (args: Args) { if (args.server !== undefined && args.standalone) return yield* Effect.fail(new Error("--server and --standalone cannot be combined")) if (args.server !== undefined) { @@ -48,18 +43,24 @@ export const resolve = Effect.fn("cli.server.resolve")(function* (args: Args) { } const options = yield* ServiceConfig.options() - const endpoint = yield* resolveManaged({ ...options, onStart: args.onStart }, args.mismatch ?? "replace") - const reconnectOptions = { ...options, version: undefined } return { - endpoint, - reconnect: (onStatus) => Service.start({ ...reconnectOptions, onStatus }), - restart: Effect.gen(function* () { - yield* Service.stop(options, { targetVersion: options.version }) - yield* Service.start(options) - }), + endpoint: yield* resolveManaged({ ...options, onStart: args.onStart }, args.mismatch ?? "replace"), + service: managedService(options), } satisfies Resolved }) +function managedService(options: Service.StartOptions) { + const reconnectOptions = { ...options, version: undefined } + return { + reconnect: (onStatus: (status: Service.Status) => void) => Service.start({ ...reconnectOptions, onStatus }), + restart: () => + Effect.gen(function* () { + yield* Service.stop(options, { targetVersion: options.version }) + yield* Service.start(options) + }), + } +} + const resolveManaged = Effect.fnUntraced(function* ( options: Service.StartOptions, mismatch: NonNullable, @@ -89,4 +90,4 @@ function connectError(endpoint: Service.Endpoint, cause: unknown) { return new Error(`Server at ${endpoint.url} did not provide a compatible V2 health response`, { cause }) } -export * as Server from "./server" +export * as ServerConnection from "./server-connection" diff --git a/packages/cli/test/server.test.ts b/packages/cli/test/server-connection.test.ts similarity index 50% rename from packages/cli/test/server.test.ts rename to packages/cli/test/server-connection.test.ts index f695c16d969f..3dd68e2bc9dd 100644 --- a/packages/cli/test/server.test.ts +++ b/packages/cli/test/server-connection.test.ts @@ -2,14 +2,14 @@ import { NodeFileSystem } from "@effect/platform-node" import { Global } from "@opencode-ai/core/global" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { expect, test } from "bun:test" -import { Effect } from "effect" +import { Effect, FileSystem, Scope } from "effect" import fs from "node:fs/promises" import os from "node:os" import path from "node:path" -import { Server } from "../src/services/server" +import { ServerConnection } from "../src/services/server-connection" import { ServiceConfig } from "../src/services/service-config" -test("managed resolution keeps lifecycle operations Effect-native", async () => { +test("resolution groups Effect-native lifecycle operations only for the managed service", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-server-resolution-")) const id = "server-resolution-test" const server = Bun.serve({ @@ -25,6 +25,9 @@ test("managed resolution keeps lifecycle operations Effect-native", async () => }, }) const registration = path.join(root, "state", ServiceConfig.filename()) + const layer = Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") }) + const runPromise = (effect: Effect.Effect) => + Effect.runPromise(effect.pipe(Effect.provide(layer), Effect.provide(NodeFileSystem.layer), Effect.scoped)) try { await fs.mkdir(path.dirname(registration), { recursive: true }) @@ -37,20 +40,20 @@ test("managed resolution keeps lifecycle operations Effect-native", async () => pid: process.pid, }), ) - const layer = Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") }) - const resolved = await Effect.runPromise( - Server.resolve({}).pipe(Effect.provide(layer), Effect.provide(NodeFileSystem.layer), Effect.scoped), - ) + const resolved = await runPromise(ServerConnection.resolve({})) expect(resolved.endpoint.url).toBe(server.url.toString()) - expect(resolved.reconnect).toBeFunction() - expect(Effect.isEffect(resolved.restart)).toBe(true) - if (!resolved.reconnect) throw new Error("Expected managed reconnect") - expect(await Effect.runPromise(resolved.reconnect(() => {}).pipe(Effect.provide(NodeFileSystem.layer)))).toEqual( - resolved.endpoint, - ) + expect(resolved.service).toBeDefined() + if (!resolved.service) throw new Error("Expected managed service capabilities") + expect(Effect.isEffect(resolved.service.reconnect(() => {}))).toBe(true) + expect(Effect.isEffect(resolved.service.restart())).toBe(true) + expect(await runPromise(resolved.service.reconnect(() => {}))).toEqual(resolved.endpoint) + + const explicit = await runPromise(ServerConnection.resolve({ server: server.url.toString() })) + expect(explicit.endpoint.url).toBe(server.url.toString()) + expect(explicit.service).toBeUndefined() } finally { - server.stop(true) + await server.stop(true) await fs.rm(root, { recursive: true, force: true }) } }) diff --git a/packages/docs/troubleshooting.mdx b/packages/docs/troubleshooting.mdx index 57856c9d71c6..f0fe58cee922 100644 --- a/packages/docs/troubleshooting.mdx +++ b/packages/docs/troubleshooting.mdx @@ -4,8 +4,8 @@ description: "Diagnose OpenCode startup, server, and session issues." --- - You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read the - steps below, inspect its service and logs, and help identify the issue. + You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read + the steps below, inspect its service and logs, and help identify the issue. OpenCode runs as two processes: the TUI is a client, while a background server owns sessions, plugins, permissions, and @@ -31,10 +31,10 @@ If the service is stuck or unhealthy, restart it: opencode2 service restart ``` -From inside the TUI, run `/reload` to restart the managed service and reconnect: +From inside the TUI, run `/restart` to restart the managed service and reconnect: ```text -/reload +/restart ``` You can also stop and start it explicitly: @@ -45,8 +45,8 @@ opencode2 service start ``` - OpenCode normally discovers or starts the shared background service automatically. The service commands are only needed - when diagnosing its lifecycle. + OpenCode normally discovers or starts the shared background service automatically. The service commands are only + needed when diagnosing its lifecycle. ## Run an isolated session @@ -125,8 +125,8 @@ The database normally lives at: `OPENCODE_DB` can override the database location. - Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the daemon, - and make a backup before inspecting persistent data with external tools. + Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the + daemon, and make a backup before inspecting persistent data with external tools. ## Explicit servers diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index db3725bc23ad..6c155b733623 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -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 - restart?: () => Promise + service?: { + reconnect: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise + restart: () => Promise + } } args: Args config: Config.Interface @@ -183,7 +186,7 @@ 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 reconnectEndpoint = input.server.service?.reconnect const reconnect = reconnectEndpoint ? async (onStatus: (status: Service.Status) => void, signal: AbortSignal) => { const endpoint = await reconnectEndpoint(onStatus, signal) @@ -327,7 +330,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { @@ -764,18 +767,18 @@ function App(props: { pair?: DialogPairCredentials }) { ...(client.restart ? [ { - name: "server.reload", - title: "Restart 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: "Restarting server...", duration: 30000 }) + 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 restart() - .then(() => toast.show({ variant: "success", message: "Server restarted" })) + .then(() => toast.show({ variant: "success", message: "Service restarted" })) .catch(toast.error) }, category: "System", diff --git a/packages/tui/src/context/client.tsx b/packages/tui/src/context/client.tsx index 063a5c99b104..c3566a04180f 100644 --- a/packages/tui/src/context/client.tsx +++ b/packages/tui/src/context/client.tsx @@ -27,7 +27,7 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext( 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. + // Stops and replaces the managed service; present only in service mode. restart?: () => Promise }) => { const log = useLog({ component: "client" }) diff --git a/packages/www/content/docs/(docs)/troubleshooting.mdx b/packages/www/content/docs/(docs)/troubleshooting.mdx index dd3b4362fc1c..9696c1fdf061 100644 --- a/packages/www/content/docs/(docs)/troubleshooting.mdx +++ b/packages/www/content/docs/(docs)/troubleshooting.mdx @@ -4,8 +4,8 @@ description: "Diagnose OpenCode startup, server, and session issues." --- - You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read the - steps below, inspect its service and logs, and help identify the issue. + You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read + the steps below, inspect its service and logs, and help identify the issue. OpenCode runs as two processes: the TUI is a client, while a background server owns sessions, plugins, permissions, and @@ -31,10 +31,10 @@ If the service is stuck or unhealthy, restart it: opencode2 service restart ``` -From inside the TUI, run `/reload` to restart the managed service and reconnect: +From inside the TUI, run `/restart` to restart the managed service and reconnect: ```text -/reload +/restart ``` You can also stop and start it explicitly: @@ -45,8 +45,8 @@ opencode2 service start ``` - OpenCode normally discovers or starts the shared background service automatically. The service commands are only needed - when diagnosing its lifecycle. + OpenCode normally discovers or starts the shared background service automatically. The service commands are only + needed when diagnosing its lifecycle. ## Run an isolated session @@ -125,8 +125,8 @@ The database normally lives at: `OPENCODE_DB` can override the database location. - Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the daemon, - and make a backup before inspecting persistent data with external tools. + Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the + daemon, and make a backup before inspecting persistent data with external tools. ## Explicit servers From 68804936c9ad0550dcf60690a58cd5b55a896109 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Jul 2026 11:05:31 -0400 Subject: [PATCH 3/5] docs: render service lifecycle diagrams --- docs/design/service-lifecycle.md | 120 ++++++++++++++++++++----------- 1 file changed, 78 insertions(+), 42 deletions(-) diff --git a/docs/design/service-lifecycle.md b/docs/design/service-lifecycle.md index a516455fd4ae..e32a6cb441f3 100644 --- a/docs/design/service-lifecycle.md +++ b/docs/design/service-lifecycle.md @@ -32,17 +32,35 @@ framework. ## Architecture at a Glance -```mermaid -flowchart LR - CLIENT[client Effect Service] --> CONNECTION[CLI ServerConnection] - CONFIG[CLI ServiceConfig] --> CONNECTION - CONFIG --> DAEMON[CLI server-process daemon] - CONNECTION -->|Effects| SEAM[CLI runPromiseWith seam] - SEAM -->|Promises| TUI[TUI and Solid reconnect loop] - CLIENT -->|start or stop| DAEMON - TUI -->|HTTP and events| HTTP[server HTTP transport] - DAEMON --> HTTP - HTTP --> CORE[core application behavior] +```text + ╭───────────────────╮ + │ CLI ServiceConfig │ + ╰─────────┬─────────╯ + │ + ▼ + ╭──────────────────────╮ + │ CLI ServerConnection │ + ╰───────────┬──────────╯ + ╭──────────────────╰───────────────────╮ + ▼ ▼ +╭──────────────────────────╮ ╭─────────────────────────╮ +│ Client Service lifecycle │ │ CLI runPromiseWith seam │ +╰─────────────┬────────────╯ ╰─────────────┬───────────╯ + ╰─────╮ │ + ▼ ▼ + ╭────────────────────────────╮ ╭─────────────╮ + │ Background service process │ │ TUI / Solid │ + ╰──────────────┬─────────────╯ ╰──────┬──────╯ + │ │ + ╰────────────◀────────────────────╯ + ╭───────────────────────╮ + │ Server HTTP transport │ + ╰───────────┬───────────╯ + │ + ▼ + ╭──────────────────╮ + │ Core application │ + ╰──────────────────╯ ``` | Owner | Responsibility | @@ -190,19 +208,22 @@ This design gives each concept one authority. ## System Model -```mermaid -flowchart LR - TUI[Fresh or existing TUI] - REG[Registration file] - LOCK[Process-held OS service lock] - SHELL[Lifecycle shell] - APP[OpenCode application] - - TUI -->|discover| REG - TUI -->|observe| SHELL - TUI -->|normal requests| APP - SHELL --> APP - LOCK -->|authorizes one owner| SHELL +```text +╭───────────────────────╮ ╭──────────────────────────────╮ +│ Fresh or existing TUI │ │ Process-held OS service lock │ +╰───────────┬───────────╯ ╰───────────────┬──────────────╯ + ╰─────┬ normal requests observe ───────────────────────╮ │ + │ discover │ ├──╯ authorizes one owner + ▼ │ ▼ + ╭───────────────────╮ │ ╭─────────────────╮ + │ Registration file │ │ │ Lifecycle shell │ + ╰───────────────────╯ │ ╰────────┬────────╯ + │ │ + ├────────────────────────╯ + ▼ + ╭──────────────────────╮ + │ OpenCode application │ + ╰──────────────────────╯ ``` The lifecycle shell and application run in the same process. The distinction is @@ -337,24 +358,39 @@ Windows, where Bun FFI is not available on every shipped architecture. It lives alongside the existing utility in `packages/core/src/util`. This primitive is the foundation of the design, so the delivery sequence spikes it first. -```mermaid -flowchart TD - A[Contender process starts] - B{Acquire service lock?} - C[Exit successfully] - D[Bind lifecycle shell] - E[Write registration] - F[Report starting] - G[Initialize application] - H[Report ready] - I[Serve until shutdown] - - A --> B - B -->|No| C - B -->|Yes| D - D --> E --> F --> G - G -->|Success| H --> I - G -->|Failure| J[Report failed and stay bound] +```text +Contender Lock Lifecycle Application + │ │ │ │ + ├─ try acquire ───▶ │ │ + │ │ │ │ + ╭─ alt: lock held ────────────────────────────────────────────────╮ + │ │ │ │ │ │ + │ ◀─ busy ──────────┤ │ │ │ + │ │ │ │ │ │ + │ ├─────────╮ │ │ │ │ + │ │ exit │ │ │ │ │ + │ ◀─────────╯ │ │ │ │ + │ │ │ │ │ │ + ├─ else: lock acquired ───────────────────────────────────────────┤ + │ │ │ │ │ │ + │ ◀─ owner ─────────┤ │ │ │ + │ │ │ │ │ │ + │ ├─ bind, register, starting ────────▶ │ │ + │ │ │ │ │ │ + │ ├─ initialize ──────────────────────────────────────────────▶ │ + │ │ │ │ │ │ + │╭─ alt: boot succeeds ──────────────────────────────────────────╮│ + ││ │ │ │ │ ││ + ││ │ │ ◀─ ready ───────────────┤ ││ + ││ │ │ │ │ ││ + │├─ else: boot fails ────────────────────────────────────────────┤│ + ││ │ │ │ │ ││ + ││ │ │ ◀─ failed, stay bound ──┤ ││ + ││ │ │ │ │ ││ + │╰───────────────────────────────────────────────────────────────╯│ + │ │ │ │ │ │ + ╰─────────────────────────────────────────────────────────────────╯ + │ │ │ │ ``` Lock acquisition by a contender is nonblocking or tightly bounded. A loser From 266154707fe47298b319c2d168c9f4bc0df5bb64 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Jul 2026 11:17:53 -0400 Subject: [PATCH 4/5] refactor(cli): narrow service runtime adapter --- packages/cli/src/commands/handlers/default.ts | 8 ++++--- packages/cli/src/commands/handlers/mini.ts | 2 +- packages/cli/src/commands/handlers/run.ts | 2 +- packages/cli/src/mini/mini.ts | 7 +++--- packages/cli/src/mini/run.ts | 5 ++-- packages/tui/src/app.tsx | 23 ++++++++----------- packages/tui/src/context/client.tsx | 18 +++++++-------- packages/tui/test/cli/tui/use-event.test.tsx | 3 ++- 8 files changed, 33 insertions(+), 35 deletions(-) diff --git a/packages/cli/src/commands/handlers/default.ts b/packages/cli/src/commands/handlers/default.ts index 18fd139724ca..3f187369b575 100644 --- a/packages/cli/src/commands/handlers/default.ts +++ b/packages/cli/src/commands/handlers/default.ts @@ -4,7 +4,7 @@ import { run } from "@opencode-ai/tui" import { Commands } from "../commands" import { Runtime } from "../../framework/runtime" import { Config } from "../../config" -import { Effect, FileSystem, Option } from "effect" +import { Context, Effect, FileSystem, Option } from "effect" import { ServerConnection } from "../../services/server-connection" import { Updater } from "../../services/updater" import { UpdatePreflight } from "../../services/update-preflight" @@ -37,6 +37,8 @@ export default Runtime.handler(Commands, (input) => preflight.loading() const config = yield* Config.Service const npm = yield* Npm.Service + const fileSystem = yield* FileSystem.FileSystem + const runServicePromise = Effect.runPromiseWith(Context.make(FileSystem.FileSystem, fileSystem)) const context = yield* Effect.context() const runFork = Effect.runForkWith(context) const runPromise = Effect.runPromiseWith(context) @@ -46,8 +48,8 @@ export default Runtime.handler(Commands, (input) => endpoint: server.endpoint, service: service ? { - reconnect: (onStatus, signal) => runPromise(service.reconnect(onStatus), { signal }), - restart: () => runPromise(service.restart()), + reconnect: (onStatus, signal) => runServicePromise(service.reconnect(onStatus), { signal }), + restart: () => runServicePromise(service.restart()), } : undefined, }, diff --git a/packages/cli/src/commands/handlers/mini.ts b/packages/cli/src/commands/handlers/mini.ts index e44f7e0b2df5..0bc506dc3992 100644 --- a/packages/cli/src/commands/handlers/mini.ts +++ b/packages/cli/src/commands/handlers/mini.ts @@ -11,7 +11,7 @@ export default Runtime.handler(Commands.commands.mini, (input) => const server = yield* ServerConnection.resolve({ server: serverURL, standalone: input.standalone }) yield* Effect.promise(() => runMini({ - server, + endpoint: server.endpoint, continue: input.continue, session: Option.getOrUndefined(input.session), fork: input.fork, diff --git a/packages/cli/src/commands/handlers/run.ts b/packages/cli/src/commands/handlers/run.ts index 50fbaa4e2c44..bdde4a0907b9 100644 --- a/packages/cli/src/commands/handlers/run.ts +++ b/packages/cli/src/commands/handlers/run.ts @@ -13,7 +13,7 @@ export default Runtime.handler(Commands.commands.run, (input) => }) yield* Effect.promise(() => runNonInteractive({ - server, + endpoint: server.endpoint, message: [...input.message, ...(separator === -1 ? [] : process.argv.slice(separator + 1))], continue: input.continue, session: Option.getOrUndefined(input.session), diff --git a/packages/cli/src/mini/mini.ts b/packages/cli/src/mini/mini.ts index ccbb900ba04e..dc6aed4df8cd 100644 --- a/packages/cli/src/mini/mini.ts +++ b/packages/cli/src/mini/mini.ts @@ -1,12 +1,11 @@ import { Service } from "@opencode-ai/client/effect" import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" -import { ServerConnection } from "../services/server-connection" import { waitForCatalogReady } from "./catalog.shared" import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin" import type { RunInput, RunTuiConfig } from "./types" export type MiniCommandInput = { - server: ServerConnection.Resolved + endpoint: Service.Endpoint continue?: boolean session?: string fork?: boolean @@ -28,8 +27,8 @@ export async function runMini(input: MiniCommandInput) { try { const sdk = OpenCode.make({ - baseUrl: input.server.endpoint.url, - headers: Service.headers(input.server.endpoint), + baseUrl: input.endpoint.url, + headers: Service.headers(input.endpoint), }) const model = parseModel(input.model) let agentTask: Promise | undefined diff --git a/packages/cli/src/mini/run.ts b/packages/cli/src/mini/run.ts index ad95f52f843f..f8dff81d7cc8 100644 --- a/packages/cli/src/mini/run.ts +++ b/packages/cli/src/mini/run.ts @@ -4,7 +4,6 @@ import { FSUtil } from "@opencode-ai/core/fs-util" import { Model } from "@opencode-ai/schema/model" import { open } from "node:fs/promises" import path from "node:path" -import { ServerConnection } from "../services/server-connection" import { loadRunAgents, waitForCatalogReady } from "./catalog.shared" import { runNonInteractivePrompt } from "./noninteractive" import { toolInlineInfo } from "./tool" @@ -12,7 +11,7 @@ import type { MiniToolPart } from "./types" import { UI } from "./ui" export type RunCommandInput = { - server: ServerConnection.Resolved + endpoint: Service.Endpoint message: string[] continue?: boolean session?: string @@ -52,7 +51,7 @@ async function run(input: RunCommandInput) { if (!message?.trim()) fail("You must provide a message") const files = await Promise.all(input.file.map((file) => prepareFile(file, root))) const prepared = { directory, message, files } - return execute(input, prepared, input.server.endpoint) + return execute(input, prepared, input.endpoint) } async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Service.Endpoint) { diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 6c155b733623..4e45e66312fc 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -186,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.service?.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 } @@ -327,11 +328,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { } > - + diff --git a/packages/tui/src/context/client.tsx b/packages/tui/src/context/client.tsx index c3566a04180f..1c9d7e8d248e 100644 --- a/packages/tui/src/context/client.tsx +++ b/packages/tui/src/context/client.tsx @@ -18,18 +18,18 @@ export type ClientConnectionEvent = { } } +type ManagedService = { + reconnect: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<{ api: OpenCodeClient }> + restart: () => Promise +} + type ClientEventMap = { [Type in OpenCodeEvent["type"]]: Extract } 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 replaces the managed service; present only in service mode. - restart?: () => Promise - }) => { + 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( }, }, }, - restart: props.restart, + restart: props.service?.restart, } }, }) diff --git a/packages/tui/test/cli/tui/use-event.test.tsx b/packages/tui/test/cli/tui/use-event.test.tsx index 42949ce5d09b..c14f0e1e3382 100644 --- a/packages/tui/test/cli/tui/use-event.test.tsx +++ b/packages/tui/test/cli/tui/use-event.test.tsx @@ -65,10 +65,11 @@ async function mount( const ready = new Promise((resolve) => { done = resolve }) + const service = reconnect ? { reconnect, restart: () => Promise.resolve() } : undefined const app = await testRender(() => ( - + { client = ctx.client From 0596a8bac4b24cfdb603ff534f00abbbd272b1af Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Jul 2026 11:27:30 -0400 Subject: [PATCH 5/5] fix(cli): preserve mini server input --- packages/cli/src/commands/handlers/mini.ts | 2 +- packages/cli/src/commands/handlers/run.ts | 2 +- packages/cli/src/mini/mini.ts | 7 ++++--- packages/cli/src/mini/run.ts | 5 +++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/handlers/mini.ts b/packages/cli/src/commands/handlers/mini.ts index 0bc506dc3992..e44f7e0b2df5 100644 --- a/packages/cli/src/commands/handlers/mini.ts +++ b/packages/cli/src/commands/handlers/mini.ts @@ -11,7 +11,7 @@ export default Runtime.handler(Commands.commands.mini, (input) => const server = yield* ServerConnection.resolve({ server: serverURL, standalone: input.standalone }) yield* Effect.promise(() => runMini({ - endpoint: server.endpoint, + server, continue: input.continue, session: Option.getOrUndefined(input.session), fork: input.fork, diff --git a/packages/cli/src/commands/handlers/run.ts b/packages/cli/src/commands/handlers/run.ts index bdde4a0907b9..50fbaa4e2c44 100644 --- a/packages/cli/src/commands/handlers/run.ts +++ b/packages/cli/src/commands/handlers/run.ts @@ -13,7 +13,7 @@ export default Runtime.handler(Commands.commands.run, (input) => }) yield* Effect.promise(() => runNonInteractive({ - endpoint: server.endpoint, + server, message: [...input.message, ...(separator === -1 ? [] : process.argv.slice(separator + 1))], continue: input.continue, session: Option.getOrUndefined(input.session), diff --git a/packages/cli/src/mini/mini.ts b/packages/cli/src/mini/mini.ts index dc6aed4df8cd..ccbb900ba04e 100644 --- a/packages/cli/src/mini/mini.ts +++ b/packages/cli/src/mini/mini.ts @@ -1,11 +1,12 @@ import { Service } from "@opencode-ai/client/effect" import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" +import { ServerConnection } from "../services/server-connection" import { waitForCatalogReady } from "./catalog.shared" import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin" import type { RunInput, RunTuiConfig } from "./types" export type MiniCommandInput = { - endpoint: Service.Endpoint + server: ServerConnection.Resolved continue?: boolean session?: string fork?: boolean @@ -27,8 +28,8 @@ export async function runMini(input: MiniCommandInput) { try { const sdk = OpenCode.make({ - baseUrl: input.endpoint.url, - headers: Service.headers(input.endpoint), + baseUrl: input.server.endpoint.url, + headers: Service.headers(input.server.endpoint), }) const model = parseModel(input.model) let agentTask: Promise | undefined diff --git a/packages/cli/src/mini/run.ts b/packages/cli/src/mini/run.ts index f8dff81d7cc8..ad95f52f843f 100644 --- a/packages/cli/src/mini/run.ts +++ b/packages/cli/src/mini/run.ts @@ -4,6 +4,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util" import { Model } from "@opencode-ai/schema/model" import { open } from "node:fs/promises" import path from "node:path" +import { ServerConnection } from "../services/server-connection" import { loadRunAgents, waitForCatalogReady } from "./catalog.shared" import { runNonInteractivePrompt } from "./noninteractive" import { toolInlineInfo } from "./tool" @@ -11,7 +12,7 @@ import type { MiniToolPart } from "./types" import { UI } from "./ui" export type RunCommandInput = { - endpoint: Service.Endpoint + server: ServerConnection.Resolved message: string[] continue?: boolean session?: string @@ -51,7 +52,7 @@ async function run(input: RunCommandInput) { if (!message?.trim()) fail("You must provide a message") const files = await Promise.all(input.file.map((file) => prepareFile(file, root))) const prepared = { directory, message, files } - return execute(input, prepared, input.endpoint) + return execute(input, prepared, input.server.endpoint) } async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Service.Endpoint) {