mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-29 14:41:48 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20f6635ca0 | |||
| a7f335aec0 |
@@ -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() {},
|
||||
|
||||
@@ -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 <url>",
|
||||
@@ -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)),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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<typeof Rpc.client<typeof rpc>>
|
||||
|
||||
function createWorkerFetch(client: RpcClient): typeof fetch {
|
||||
const fn = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
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<GlobalEvent>("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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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<ReturnType<typeof Server.listen>> | undefined
|
||||
|
||||
export const rpc = {
|
||||
async fetch(input: { url: string; method: string; headers: Record<string, string>; 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() }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")')
|
||||
})
|
||||
|
||||
@@ -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")')
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
|
||||
@@ -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<string[]>
|
||||
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) {
|
||||
<TuiConfigProvider config={input.config}>
|
||||
<PluginRuntimeProvider value={pluginRuntime}>
|
||||
<SDKProvider
|
||||
url={input.url}
|
||||
sdk={input.sdk}
|
||||
directory={input.directory}
|
||||
fetch={input.fetch}
|
||||
headers={input.headers}
|
||||
events={input.events}
|
||||
>
|
||||
<ProjectProvider>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) {
|
||||
<TestTuiContexts paths={state ? { state } : undefined}>
|
||||
<ArgsProvider>
|
||||
<KVProvider>
|
||||
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
|
||||
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||
<ProjectProvider>
|
||||
<SyncProvider>
|
||||
<Probe />
|
||||
|
||||
@@ -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(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
<Probe />
|
||||
@@ -173,7 +173,7 @@ test("sync v2 renders admitted prompts only after promotion", async () => {
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
<Probe />
|
||||
@@ -237,7 +237,7 @@ test("sync v2 renders a promoted prompt when admission was missed", async () =>
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
<Probe />
|
||||
@@ -285,7 +285,7 @@ test("sync v2 projects live context updates with their message ID", async () =>
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
<Probe />
|
||||
@@ -340,7 +340,7 @@ test("sync v2 preserves live events while snapshot hydration is in flight", asyn
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
<Probe />
|
||||
@@ -390,7 +390,7 @@ test("sync v2 replaces stale cached rows while preserving in-flight live rows",
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
<Probe />
|
||||
@@ -459,7 +459,7 @@ test("sync v2 preserves snapshot order and metadata for in-flight updates", asyn
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
<Probe />
|
||||
|
||||
@@ -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(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<SDKProvider sdk={createClient(calls.fetch)} directory={directory} events={events.source}>
|
||||
<ProjectProvider>
|
||||
<Probe
|
||||
onReady={async (ctx) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user