diff --git a/packages/cli/src/tui.ts b/packages/cli/src/tui.ts index 4722441b2c..ea75ec77b7 100644 --- a/packages/cli/src/tui.ts +++ b/packages/cli/src/tui.ts @@ -2,14 +2,14 @@ import { run } from "@opencode-ai/tui" import { TuiConfig } from "@opencode-ai/tui/config" import { Effect } from "effect" import { Global } from "@opencode-ai/core/global" +import { createOpencodeClient } from "@opencode-ai/sdk/v2" export function runTui(transport: { url: string; headers: RequestInit["headers"] }) { const config = TuiConfig.resolve({}, { terminalSuspend: false }) return run({ - ...transport, + sdk: createOpencodeClient({ baseUrl: transport.url, headers: transport.headers, fetch: gracefulFetch }), args: {}, config, - fetch: gracefulFetch, pluginHost: { async start() {}, async dispose() {}, diff --git a/packages/opencode/src/cli/cmd/attach.ts b/packages/opencode/src/cli/cmd/attach.ts index 278cee8a70..8af257a083 100644 --- a/packages/opencode/src/cli/cmd/attach.ts +++ b/packages/opencode/src/cli/cmd/attach.ts @@ -1,8 +1,10 @@ import { cmd } from "./cmd" import { UI } from "@/cli/ui" import { errorMessage } from "@opencode-ai/tui/util/error" -import { validateSession } from "../tui/validate-session" import { ServerAuth } from "@/server/auth" +import { createOpencodeClient } from "@opencode-ai/sdk/v2" +import { SessionID } from "@/session/schema" +import { Schema } from "effect" export const AttachCommand = cmd({ command: "attach ", @@ -61,15 +63,22 @@ export const AttachCommand = cmd({ } })() const headers = ServerAuth.headers({ password: args.password, username: args.username }) + const sdk = createOpencodeClient({ baseUrl: args.url, directory, headers }) const config = await TuiConfig.get() try { - await validateSession({ - url: args.url, - sessionID: args.session, - directory, - headers, - }) + if (args.session) { + const sessionID = (() => { + try { + return Schema.decodeUnknownSync(SessionID)(args.session) + } catch (error) { + throw new Error(`Invalid session ID: ${error instanceof Error ? error.message : "unknown error"}`, { + cause: error, + }) + } + })() + await sdk.session.get({ sessionID }, { throwOnError: true }) + } } catch (error) { UI.error(errorMessage(error)) process.exitCode = 1 @@ -77,11 +86,12 @@ export const AttachCommand = cmd({ } const { Effect } = await import("effect") - const { run } = await import("../tui/layer") + const { Global } = await import("@opencode-ai/core/global") + const { run } = await import("@opencode-ai/tui") const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime") await Effect.runPromise( run({ - url: args.url, + sdk, config, pluginHost: createLegacyTuiPluginHost(), args: { @@ -90,8 +100,7 @@ export const AttachCommand = cmd({ fork: args.fork, }, directory, - headers, - }), + }).pipe(Effect.provide(Global.defaultLayer)), ) }, }) diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index ed2e46a1a0..0ffc2491a5 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -1,7 +1,12 @@ import { cmd } from "@/cli/cmd/cmd" import { Rpc } from "@/util/rpc" import { type rpc } from "../tui/worker" +import { ServerAuth } from "@/server/auth" +import { createOpencodeClient } from "@opencode-ai/sdk/v2" +import { SessionID } from "@/session/schema" +import { Schema } from "effect" import path from "path" +import { randomBytes } from "node:crypto" import { fileURLToPath } from "url" import { UI } from "@/cli/ui" import * as Log from "@opencode-ai/core/util/log" @@ -9,8 +14,6 @@ import { errorMessage } from "@opencode-ai/tui/util/error" import { withTimeout } from "@/util/timeout" import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network" import { Filesystem } from "@/util/filesystem" -import type { GlobalEvent } from "@opencode-ai/sdk/v2" -import type { EventSource } from "@opencode-ai/tui/context/sdk" import { writeHeapSnapshot } from "v8" import { OPENCODE_PROCESS_ROLE, @@ -18,43 +21,12 @@ import { ensureRunID, sanitizedProcessEnv, } from "@opencode-ai/core/util/opencode-process" -import { validateSession } from "../tui/validate-session" import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32" declare global { const OPENCODE_WORKER_PATH: string } -type RpcClient = ReturnType> - -function createWorkerFetch(client: RpcClient): typeof fetch { - const fn = async (input: RequestInfo | URL, init?: RequestInit): Promise => { - const request = new Request(input, init) - const body = request.body ? await request.text() : undefined - const result = await client.call("fetch", { - url: request.url, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - body, - }) - return new Response(result.body, { - status: result.status, - headers: result.headers, - }) - } - return fn as typeof fetch -} - -function createEventSource(client: RpcClient): EventSource { - return { - subscribe: async (handler) => { - return client.on("global.event", (e) => { - handler(e) - }) - }, - } -} - async function target() { if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH const dist = new URL("./cli/tui/worker.js", import.meta.url) @@ -132,6 +104,15 @@ export const TuiThreadCommand = cmd({ return } const cwd = Filesystem.resolve(process.cwd()) + const network = resolveNetworkOptionsNoConfig(args) + const external = + process.argv.includes("--port") || + process.argv.includes("--hostname") || + process.argv.includes("--mdns") || + network.mdns || + network.port !== 0 || + network.hostname !== "127.0.0.1" + const password = external ? undefined : randomBytes(32).toString("base64url") const env = sanitizedProcessEnv({ [OPENCODE_PROCESS_ROLE]: "worker", [OPENCODE_RUN_ID]: ensureRunID(), @@ -183,34 +164,41 @@ export const TuiThreadCommand = cmd({ const prompt = await input(args.prompt) const config = await TuiConfig.get() - const network = resolveNetworkOptionsNoConfig(args) - const external = - process.argv.includes("--port") || - process.argv.includes("--hostname") || - process.argv.includes("--mdns") || - network.mdns || - network.port !== 0 || - network.hostname !== "127.0.0.1" - const transport = external ? { url: (await client.call("server", network)).url, - fetch: undefined, - events: undefined, + headers: ServerAuth.headers(), } : { - url: "http://opencode.internal", - fetch: createWorkerFetch(client), - events: createEventSource(client), + url: ( + await client.call("server", { + hostname: "127.0.0.1", + port: 0, + preferredPort: 0, + auth: { password }, + }) + ).url, + headers: ServerAuth.headers({ password }), } + const sdk = createOpencodeClient({ + baseUrl: transport.url, + directory: cwd, + headers: transport.headers, + }) try { - await validateSession({ - url: transport.url, - sessionID: args.session, - directory: cwd, - fetch: transport.fetch, - }) + if (args.session) { + const sessionID = (() => { + try { + return Schema.decodeUnknownSync(SessionID)(args.session) + } catch (error) { + throw new Error(`Invalid session ID: ${error instanceof Error ? error.message : "unknown error"}`, { + cause: error, + }) + } + })() + await sdk.session.get({ sessionID }, { throwOnError: true }) + } } catch (error) { UI.error(errorMessage(error)) process.exitCode = 1 @@ -223,11 +211,12 @@ export const TuiThreadCommand = cmd({ try { const { Effect } = await import("effect") - const { run } = await import("../tui/layer") + const { Global } = await import("@opencode-ai/core/global") + const { run } = await import("@opencode-ai/tui") const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime") await Effect.runPromise( run({ - url: transport.url, + sdk, async onSnapshot() { const tui = writeHeapSnapshot("tui.heapsnapshot") const server = await client.call("snapshot", undefined) @@ -236,8 +225,6 @@ export const TuiThreadCommand = cmd({ config, pluginHost: createLegacyTuiPluginHost(), directory: cwd, - fetch: transport.fetch, - events: transport.events, args: { continue: args.continue, sessionID: args.session, @@ -246,12 +233,11 @@ export const TuiThreadCommand = cmd({ prompt, fork: args.fork, }, - }), + }).pipe(Effect.provide(Global.defaultLayer)), ) } finally { await stop() } - process.exit(0) } finally { try { unguard?.() @@ -259,6 +245,6 @@ export const TuiThreadCommand = cmd({ Log.Default.warn("failed to restore terminal guard", { error: errorMessage(error) }) } } + process.exit(0) }, }) -// scratch diff --git a/packages/opencode/src/cli/tui/layer.ts b/packages/opencode/src/cli/tui/layer.ts deleted file mode 100644 index cc88498cb9..0000000000 --- a/packages/opencode/src/cli/tui/layer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { run as runTui, type TuiInput } from "@opencode-ai/tui" -import { Global } from "@opencode-ai/core/global" -import { Effect } from "effect" - -export function run(input: TuiInput) { - return runTui(input).pipe(Effect.provide(Global.defaultLayer)) -} diff --git a/packages/opencode/src/cli/tui/validate-session.ts b/packages/opencode/src/cli/tui/validate-session.ts deleted file mode 100644 index 31329a6533..0000000000 --- a/packages/opencode/src/cli/tui/validate-session.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createOpencodeClient } from "@opencode-ai/sdk/v2" -import { SessionID } from "@/session/schema" -import { Schema } from "effect" - -const decodeSessionID = Schema.decodeUnknownSync(SessionID) - -export async function validateSession(input: { - url: string - sessionID?: string - directory?: string - fetch?: typeof fetch - headers?: RequestInit["headers"] -}) { - if (!input.sessionID) return - - let sessionID: SessionID - try { - sessionID = decodeSessionID(input.sessionID) - } catch (error) { - throw new Error(`Invalid session ID: ${error instanceof Error ? error.message : "unknown error"}`, { cause: error }) - } - - await createOpencodeClient({ - baseUrl: input.url, - directory: input.directory, - fetch: input.fetch, - headers: input.headers, - }).session.get({ sessionID }, { throwOnError: true }) -} diff --git a/packages/opencode/src/cli/tui/worker.ts b/packages/opencode/src/cli/tui/worker.ts index 31ead18cf8..4f1d27ef28 100644 --- a/packages/opencode/src/cli/tui/worker.ts +++ b/packages/opencode/src/cli/tui/worker.ts @@ -5,8 +5,6 @@ import { InstanceRuntime } from "@/project/instance-runtime" import { Rpc } from "@/util/rpc" import { upgrade } from "@/cli/upgrade" import { Config } from "@/config/config" -import { GlobalBus } from "@/bus/global" -import { ServerAuth } from "@/server/auth" import { writeHeapSnapshot } from "node:v8" import { Heap } from "@/cli/heap" import { AppRuntime } from "@/effect/app-runtime" @@ -39,38 +37,22 @@ process.on("uncaughtException", (e) => { }) }) -// Subscribe to global events and forward them via RPC -GlobalBus.on("event", (event) => { - Rpc.emit("global.event", event) -}) - let server: Awaited> | undefined export const rpc = { - async fetch(input: { url: string; method: string; headers: Record; body?: string }) { - const headers = { ...input.headers } - const auth = ServerAuth.header() - if (auth && !headers["authorization"] && !headers["Authorization"]) { - headers["Authorization"] = auth - } - const request = new Request(input.url, { - method: input.method, - headers, - body: input.body, - }) - const response = await Server.Default().app.fetch(request) - const body = await response.text() - return { - status: response.status, - headers: Object.fromEntries(response.headers.entries()), - body, - } - }, snapshot() { const result = writeHeapSnapshot("server.heapsnapshot") return result }, - async server(input: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) { + async server(input: { + port: number + hostname: string + mdns?: boolean + mdnsDomain?: string + cors?: string[] + preferredPort?: number + auth?: { username?: string; password?: string } + }) { if (server) await server.stop(true) server = await Server.listen(input) return { url: server.url.toString() } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 9a448b78d6..4df1370353 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -13,6 +13,7 @@ import { WebSocketTracker } from "./routes/instance/httpapi/websocket-tracker" import { PublicApi } from "./routes/instance/httpapi/public" import type { CorsOptions } from "./cors" import { lazy } from "@/util/lazy" +import { ServerAuth } from "./auth" // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false @@ -36,6 +37,8 @@ type ListenOptions = CorsOptions & { hostname: string mdns?: boolean mdnsDomain?: string + preferredPort?: number + auth?: ServerAuth.Credentials } type ListenerState = { scope: Scope.Scope @@ -109,19 +112,30 @@ function listenerLayer(opts: ListenOptions, port: number) { Layer.provideMerge(WebSocketTracker.layer), Layer.provideMerge(serverLayer({ port, hostname: opts.hostname })), // Install a fresh `ConfigProvider` per listener so `Config.string(...)` - // reads reflect the current `process.env`. Effect's default + // reads reflect the current `process.env` and listener-local credentials. Effect's default // `ConfigProvider` snapshots `process.env` on first read and caches the // result on a module-singleton Reference; without overriding it here, // every later `Server.listen()` keeps observing that initial snapshot. - Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv())), + Layer.provide( + ConfigProvider.layer( + opts.auth + ? ConfigProvider.fromUnknown({ + ...process.env, + OPENCODE_SERVER_PASSWORD: opts.auth.password, + OPENCODE_SERVER_USERNAME: opts.auth.username ?? "opencode", + }) + : ConfigProvider.fromEnv(), + ), + ), ) } function startWithPortFallback(opts: ListenOptions) { if (opts.port !== 0) return startListener(opts, opts.port) + if (opts.preferredPort === 0) return startListener(opts, 0) // Match the legacy listener port-resolution behavior: explicit `0` prefers // 4096 first, then any free port. - return startListener(opts, 4096).pipe(Effect.catch(() => startListener(opts, 0))) + return startListener(opts, opts.preferredPort ?? 4096).pipe(Effect.catch(() => startListener(opts, 0))) } function startListener(opts: ListenOptions, port: number) { diff --git a/packages/opencode/test/cli/tui/attach.test.ts b/packages/opencode/test/cli/tui/attach.test.ts index 3535063d0e..a2306b4ac4 100644 --- a/packages/opencode/test/cli/tui/attach.test.ts +++ b/packages/opencode/test/cli/tui/attach.test.ts @@ -4,7 +4,7 @@ describe("tui attach", () => { test("loads the TUI integration lazily", async () => { const source = await Bun.file(new URL("../../../src/cli/cmd/attach.ts", import.meta.url)).text() - expect(source).toContain('await import("../tui/layer")') + expect(source).toContain('await import("@opencode-ai/tui")') expect(source).toMatch(/await import\(["']@\/plugin\/tui\/runtime["']\)/) expect(source).not.toContain('import("./app")') }) diff --git a/packages/opencode/test/cli/tui/thread.test.ts b/packages/opencode/test/cli/tui/thread.test.ts index f79fd40da7..e8fa3663e2 100644 --- a/packages/opencode/test/cli/tui/thread.test.ts +++ b/packages/opencode/test/cli/tui/thread.test.ts @@ -8,7 +8,7 @@ describe("tui thread", () => { test("loads the TUI integration lazily", async () => { const source = await Bun.file(new URL("../../../src/cli/cmd/tui.ts", import.meta.url)).text() - expect(source).toContain('await import("../tui/layer")') + expect(source).toContain('await import("@opencode-ai/tui")') expect(source).toMatch(/await import\(["']@\/plugin\/tui\/runtime["']\)/) expect(source).not.toContain('import("./app")') }) diff --git a/packages/opencode/test/server/httpapi-listen.test.ts b/packages/opencode/test/server/httpapi-listen.test.ts index 39b7100462..de10152e23 100644 --- a/packages/opencode/test/server/httpapi-listen.test.ts +++ b/packages/opencode/test/server/httpapi-listen.test.ts @@ -3,6 +3,7 @@ import net from "node:net" import { Flag } from "@opencode-ai/core/flag/flag" import * as Log from "@opencode-ai/core/util/log" import { Server } from "../../src/server/server" +import { ServerAuth } from "../../src/server/auth" import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty" import { withTimeout } from "../../src/util/timeout" import { resetDatabase } from "../fixture/db" @@ -313,6 +314,38 @@ describe("HttpApi Server.listen", () => { } }) + test("preferred port 0 uses an OS-assigned port", async () => { + const listener = await Server.listen({ hostname: "127.0.0.1", port: 0, preferredPort: 0 }) + try { + expect(listener.port).toBeGreaterThan(0) + expect(listener.port).not.toBe(4096) + } finally { + await stop(listener, "timed out cleaning up OS-assigned listener") + } + }) + + test("accepts listener-local credentials without changing process auth", async () => { + const listener = await Server.listen({ + hostname: "127.0.0.1", + port: 0, + preferredPort: 0, + auth: { password: "local-secret" }, + }) + try { + expect((await fetch(new URL("/doc", listener.url))).status).toBe(401) + expect( + ( + await fetch(new URL("/doc", listener.url), { + headers: { authorization: ServerAuth.header({ password: "local-secret" }) ?? "" }, + }) + ).status, + ).toBe(200) + expect(process.env.OPENCODE_SERVER_PASSWORD).toBe(original.envPassword) + } finally { + await stop(listener, "timed out cleaning up listener-local auth listener") + } + }) + test("port 0 falls back when 4096 is taken", async () => { const blocker = await occupyPort(4096) if (!blocker) return diff --git a/packages/opencode/test/server/httpapi-sdk.test.ts b/packages/opencode/test/server/httpapi-sdk.test.ts index 786039bd11..79db9e1f60 100644 --- a/packages/opencode/test/server/httpapi-sdk.test.ts +++ b/packages/opencode/test/server/httpapi-sdk.test.ts @@ -9,7 +9,6 @@ import { FSUtil } from "@opencode-ai/core/fs-util" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Flag } from "@opencode-ai/core/flag/flag" import { createOpencodeClient } from "@opencode-ai/sdk/v2" -import { validateSession } from "../../src/cli/tui/validate-session" import { InstanceBootstrap } from "../../src/project/bootstrap-service" import { InstanceStore } from "../../src/project/instance-store" import { MessageID, PartID, SessionID } from "../../src/session/schema" @@ -470,14 +469,8 @@ describe("HttpApi SDK", () => { Effect.gen(function* () { const sessionID = "ses_206f84f18ffeZ6hhD7pFYAiW5T" const fetch = yield* serverFetch(serverPath) - const thrown = yield* captureThrown(() => - validateSession({ - url: "http://localhost", - directory, - sessionID, - fetch, - }), - ) + const sdk = createOpencodeClient({ baseUrl: "http://localhost", directory, fetch }) + const thrown = yield* captureThrown(() => sdk.session.get({ sessionID }, { throwOnError: true })) expect(errorMessage(thrown)).toBe(`Session not found: ${sessionID}`) return errorMessage(thrown) }), diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index ec1fe4d7d3..af2870ba8b 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -31,6 +31,7 @@ import { ProjectProvider, useProject } from "./context/project" import { EditorContextProvider } from "./context/editor" import { useEvent } from "./context/event" import { SDKProvider, useSDK } from "./context/sdk" +import type { OpencodeClient } from "@opencode-ai/sdk/v2" import { StartupLoading } from "./component/startup-loading" import { SyncProvider, useSync } from "./context/sync" import { SyncProviderV2 } from "./context/sync-v2" @@ -130,13 +131,11 @@ const appBindingCommands = [ ] as const export type TuiInput = { - url: string + sdk: OpencodeClient args: Args config: TuiConfig.Resolved onSnapshot?: () => Promise directory?: string - fetch?: typeof fetch - headers?: RequestInit["headers"] events?: EventSource pluginHost: TuiPluginHost } @@ -270,10 +269,8 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { diff --git a/packages/tui/src/context/sdk.tsx b/packages/tui/src/context/sdk.tsx index 93180c6e21..be8c2706b9 100644 --- a/packages/tui/src/context/sdk.tsx +++ b/packages/tui/src/context/sdk.tsx @@ -1,5 +1,4 @@ -import { createOpencodeClient } from "@opencode-ai/sdk/v2" -import type { GlobalEvent } from "@opencode-ai/sdk/v2" +import type { GlobalEvent, OpencodeClient } from "@opencode-ai/sdk/v2" import { Flag } from "@opencode-ai/core/flag/flag" import { createSimpleContext } from "./helper" import { batch, onCleanup, onMount } from "solid-js" @@ -11,26 +10,14 @@ export type EventSource = { export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ name: "SDK", init: (props: { - url: string + sdk: OpencodeClient directory?: string - fetch?: typeof fetch - headers?: RequestInit["headers"] events?: EventSource }) => { const abort = new AbortController() let sse: AbortController | undefined - function createSDK() { - return createOpencodeClient({ - baseUrl: props.url, - signal: abort.signal, - directory: props.directory, - fetch: props.fetch, - headers: props.headers, - }) - } - - let sdk = createSDK() + const sdk = props.sdk const handlers = new Set<(event: GlobalEvent) => void>() const emitter = { @@ -144,8 +131,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ }, directory: props.directory, event: emitter, - fetch: props.fetch ?? fetch, - url: props.url, } }, }) diff --git a/packages/tui/test/app-lifecycle.test.tsx b/packages/tui/test/app-lifecycle.test.tsx index 4c42debc8e..1e927865bf 100644 --- a/packages/tui/test/app-lifecycle.test.tsx +++ b/packages/tui/test/app-lifecycle.test.tsx @@ -3,7 +3,7 @@ import { createTestRenderer } from "@opentui/core/testing" import { Effect } from "effect" import { Global } from "@opencode-ai/core/global" import { createTuiResolvedConfig } from "./fixture/tui-runtime" -import { createEventSource, createFetch, directory } from "./fixture/tui-sdk" +import { createClient, createEventSource, createFetch, directory } from "./fixture/tui-sdk" test("SIGHUP clears title and disposes scoped resources once", async () => { const setup = await createTestRenderer({ width: 80, height: 24, useThread: false }) @@ -28,10 +28,9 @@ test("SIGHUP clears title and disposes scoped resources once", async () => { const { run } = await import("../src/app") const task = Effect.runPromise( run({ - url: "http://test", + sdk: createClient(calls.fetch), directory, config: createTuiResolvedConfig({ plugin_enabled: {} }), - fetch: calls.fetch, events: events.source, args: {}, pluginHost: { diff --git a/packages/tui/test/cli/cmd/tui/sync-fixture.tsx b/packages/tui/test/cli/cmd/tui/sync-fixture.tsx index a212f5be1d..98d8ed81c4 100644 --- a/packages/tui/test/cli/cmd/tui/sync-fixture.tsx +++ b/packages/tui/test/cli/cmd/tui/sync-fixture.tsx @@ -6,7 +6,7 @@ import { KVProvider, useKV } from "../../../../src/context/kv" import { ProjectProvider, useProject } from "../../../../src/context/project" import { SDKProvider } from "../../../../src/context/sdk" import { SyncProvider, useSync } from "../../../../src/context/sync" -import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk" +import { createClient, createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk" import { TestTuiContexts } from "../../../fixture/tui-environment" export { createEventSource, createFetch, directory, eventSource, json, worktree } from "../../../fixture/tui-sdk" @@ -46,7 +46,7 @@ export async function mount(override?: FetchHandler, state?: string) { - + diff --git a/packages/tui/test/cli/tui/sync-v2.test.tsx b/packages/tui/test/cli/tui/sync-v2.test.tsx index 68de5fdf81..a9540850f1 100644 --- a/packages/tui/test/cli/tui/sync-v2.test.tsx +++ b/packages/tui/test/cli/tui/sync-v2.test.tsx @@ -6,7 +6,7 @@ import { onMount } from "solid-js" import { ProjectProvider } from "../../../src/context/project" import { SDKProvider } from "../../../src/context/sdk" import { SyncProviderV2, useSyncV2 } from "../../../src/context/sync-v2" -import { createEventSource, createFetch, directory, json } from "../../fixture/tui-sdk" +import { createClient, createEventSource, createFetch, directory, json } from "../../fixture/tui-sdk" import { TestTuiContexts } from "../../fixture/tui-environment" async function wait(fn: () => boolean, timeout = 2000) { @@ -44,7 +44,7 @@ test("sync v2 settles pending tools when a live failure arrives", async () => { const app = await testRender(() => ( - + @@ -173,7 +173,7 @@ test("sync v2 renders admitted prompts only after promotion", async () => { const app = await testRender(() => ( - + @@ -237,7 +237,7 @@ test("sync v2 renders a promoted prompt when admission was missed", async () => const app = await testRender(() => ( - + @@ -285,7 +285,7 @@ test("sync v2 projects live context updates with their message ID", async () => const app = await testRender(() => ( - + @@ -340,7 +340,7 @@ test("sync v2 preserves live events while snapshot hydration is in flight", asyn const app = await testRender(() => ( - + @@ -390,7 +390,7 @@ test("sync v2 replaces stale cached rows while preserving in-flight live rows", const app = await testRender(() => ( - + @@ -459,7 +459,7 @@ test("sync v2 preserves snapshot order and metadata for in-flight updates", asyn const app = await testRender(() => ( - + diff --git a/packages/tui/test/cli/tui/use-event.test.tsx b/packages/tui/test/cli/tui/use-event.test.tsx index 8b29a7f5b8..66e02688d2 100644 --- a/packages/tui/test/cli/tui/use-event.test.tsx +++ b/packages/tui/test/cli/tui/use-event.test.tsx @@ -6,7 +6,7 @@ import { onMount } from "solid-js" import { ProjectProvider, useProject } from "../../../src/context/project" import { SDKProvider } from "../../../src/context/sdk" import { useEvent } from "../../../src/context/event" -import { createEventSource, createFetch, directory } from "../../fixture/tui-sdk" +import { createClient, createEventSource, createFetch, directory } from "../../fixture/tui-sdk" import { TestTuiContexts } from "../../fixture/tui-environment" const projectID = "proj_test" @@ -61,7 +61,7 @@ async function mount() { const app = await testRender(() => ( - + { diff --git a/packages/tui/test/fixture/tui-sdk.ts b/packages/tui/test/fixture/tui-sdk.ts index f7e5bbe3fe..e8b34d6424 100644 --- a/packages/tui/test/fixture/tui-sdk.ts +++ b/packages/tui/test/fixture/tui-sdk.ts @@ -1,9 +1,13 @@ -import type { GlobalEvent } from "@opencode-ai/sdk/v2" +import { createOpencodeClient, type GlobalEvent } from "@opencode-ai/sdk/v2" import type { EventSource } from "../../src/context/sdk" export const worktree = "/tmp/opencode" export const directory = `${worktree}/packages/tui` +export function createClient(fetch: typeof globalThis.fetch) { + return createOpencodeClient({ baseUrl: "http://test", directory, fetch }) +} + export function json(data: unknown, init?: ResponseInit) { return new Response(JSON.stringify(data), { ...init,