mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-27 21:51:49 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc70651f62 |
@@ -32,9 +32,6 @@ permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
env:
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'next') || '' }}
|
||||
|
||||
jobs:
|
||||
version:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- v2
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -77,7 +76,6 @@ jobs:
|
||||
|
||||
e2e:
|
||||
name: e2e (${{ matrix.settings.name }})
|
||||
if: github.ref_name != 'v2' && github.head_ref != 'v2'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.14",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/cli --conditions=browser src/index.ts",
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev:desktop": "bun --cwd packages/desktop dev",
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
|
||||
@@ -25,7 +25,7 @@ for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" }
|
||||
}
|
||||
console.log("binaries", binaries)
|
||||
const version = Object.values(binaries)[0]
|
||||
const name = pkg.name
|
||||
const name = "opencode-ai"
|
||||
|
||||
await $`mkdir -p ./dist/${name}/bin`
|
||||
await $`cp ./bin/opencode2.cjs ./dist/${name}/bin/opencode2`
|
||||
|
||||
@@ -11,14 +11,6 @@ export default Runtime.handler(Commands, (input) =>
|
||||
const daemon = yield* Daemon.Service
|
||||
const transport = yield* (input.standalone ? Standalone.transport() : daemon.transport())
|
||||
const { runTui } = yield* Effect.promise(() => import("../../tui"))
|
||||
yield* runTui(
|
||||
transport,
|
||||
input.standalone
|
||||
? undefined
|
||||
: async () => {
|
||||
await Effect.runPromise(daemon.stop())
|
||||
return Effect.runPromise(daemon.transport())
|
||||
},
|
||||
)
|
||||
yield* runTui(transport)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -32,27 +32,12 @@ export default Runtime.handler(
|
||||
if (input.register) yield* daemon.register(address)
|
||||
const url = HttpServer.formatAddress(address)
|
||||
console.log(input.stdio ? JSON.stringify({ url }) : `server listening on ${url}`)
|
||||
return yield* (input.stdio ? waitForStdinClose() : Effect.never)
|
||||
return yield* Effect.never
|
||||
}).pipe(Effect.annotateLogs({ role: "server" })),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
function waitForStdinClose() {
|
||||
return Effect.callback<void>((resume) => {
|
||||
const close = () => resume(Effect.void)
|
||||
process.stdin.once("end", close)
|
||||
process.stdin.once("close", close)
|
||||
process.stdin.resume()
|
||||
if (process.stdin.readableEnded || process.stdin.destroyed) close()
|
||||
return Effect.sync(() => {
|
||||
process.stdin.off("end", close)
|
||||
process.stdin.off("close", close)
|
||||
process.stdin.pause()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function listen(hostname: string, port: Option.Option<number>, password: string) {
|
||||
if (Option.isSome(port)) return bind(hostname, port.value, password)
|
||||
const next = (port: number): ReturnType<typeof bind> =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { ServerAuth } from "@opencode-ai/server/auth"
|
||||
import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
|
||||
@@ -41,7 +41,7 @@ export const layer = Layer.effect(
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const directory = Global.Path.state
|
||||
const file = path.join(directory, InstallationChannel === "local" ? "server-local.json" : "server.json")
|
||||
const file = path.join(directory, "server.json")
|
||||
const configFile = path.join(Global.Path.config, "service.json")
|
||||
const legacyPasswordFile = path.join(directory, "password")
|
||||
const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
|
||||
|
||||
@@ -16,12 +16,9 @@ function command(password: string) {
|
||||
cwd: process.cwd(),
|
||||
env: { OPENCODE_SERVER_PASSWORD: password },
|
||||
extendEnv: true,
|
||||
// The server treats EOF on this pipe as the end of its ownership lease.
|
||||
// The OS closes it even when the TUI is killed before Effect finalizers run.
|
||||
stdin: "pipe",
|
||||
stdin: "ignore",
|
||||
stderr: "ignore",
|
||||
killSignal: "SIGTERM",
|
||||
forceKillAfter: "3 seconds",
|
||||
killSignal: "SIGKILL",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,7 +30,7 @@ export const transport = Effect.fn("cli.standalone.transport")(
|
||||
const output = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString)
|
||||
if (!output) return yield* Effect.fail(new Error("Standalone server exited before reporting readiness"))
|
||||
const ready = yield* Effect.tryPromise(() => decodeReady(output))
|
||||
return { url: ready.url, headers: ServerAuth.headers({ password }), pid: proc.pid }
|
||||
return { url: ready.url, headers: ServerAuth.headers({ password }) }
|
||||
},
|
||||
Effect.provide(CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
|
||||
@@ -5,9 +5,7 @@ import { Global } from "@opencode-ai/core/global"
|
||||
import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
type Transport = { url: string; headers: RequestInit["headers"] }
|
||||
|
||||
export function runTui(transport: Transport, reload?: () => Promise<Transport>) {
|
||||
export function runTui(transport: { url: string; headers: RequestInit["headers"] }) {
|
||||
const config = TuiConfig.resolve({}, { terminalSuspend: false })
|
||||
let disposeSlots: (() => void) | undefined
|
||||
return Effect.gen(function* () {
|
||||
@@ -25,12 +23,6 @@ export function runTui(transport: Transport, reload?: () => Promise<Transport>)
|
||||
)
|
||||
return yield* run({
|
||||
client: createOpencodeClient({ ...options, directory }),
|
||||
reload: reload
|
||||
? async () => {
|
||||
const next = await reload()
|
||||
return createOpencodeClient({ baseUrl: next.url, headers: next.headers, directory })
|
||||
}
|
||||
: undefined,
|
||||
args: {},
|
||||
config,
|
||||
pluginHost: {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Effect } from "effect"
|
||||
import path from "node:path"
|
||||
import { Standalone } from "../../src/services/standalone"
|
||||
|
||||
process.argv[1] = path.join(import.meta.dir, "../../src/index.ts")
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const transport = yield* Standalone.transport()
|
||||
console.log(`${transport.pid} ${transport.url}`)
|
||||
return yield* Effect.never
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -1,65 +0,0 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import path from "node:path"
|
||||
|
||||
test("standalone server exits when its owner is killed", async () => {
|
||||
const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "fixture/standalone-owner.ts")], {
|
||||
cwd: path.join(import.meta.dir, ".."),
|
||||
env: process.env,
|
||||
stdin: "ignore",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const line = await Promise.race([readLine(owner.stdout), Bun.sleep(10_000).then(() => undefined)])
|
||||
const [rawPID, url] = line?.split(" ") ?? []
|
||||
const pid = Number(rawPID)
|
||||
|
||||
try {
|
||||
expect(pid).toBeGreaterThan(0)
|
||||
expect(url).toStartWith("http://127.0.0.1:")
|
||||
expect(running(pid)).toBe(true)
|
||||
|
||||
owner.kill("SIGKILL")
|
||||
await owner.exited
|
||||
|
||||
expect(await waitForExit(pid)).toBe(true)
|
||||
} finally {
|
||||
owner.kill("SIGKILL")
|
||||
if (running(pid)) process.kill(pid, "SIGKILL")
|
||||
}
|
||||
})
|
||||
|
||||
async function readLine(stream: ReadableStream<Uint8Array>) {
|
||||
const reader = stream.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const chunks: string[] = []
|
||||
while (true) {
|
||||
const result = await reader.read()
|
||||
if (result.done) break
|
||||
chunks.push(decoder.decode(result.value, { stream: true }))
|
||||
const output = chunks.join("")
|
||||
const newline = output.indexOf("\n")
|
||||
if (newline !== -1) {
|
||||
reader.releaseLock()
|
||||
return output.slice(0, newline)
|
||||
}
|
||||
}
|
||||
reader.releaseLock()
|
||||
return chunks.join("") + decoder.decode()
|
||||
}
|
||||
|
||||
async function waitForExit(pid: number, attempts = 100): Promise<boolean> {
|
||||
if (!running(pid)) return true
|
||||
if (attempts === 0) return false
|
||||
await Bun.sleep(50)
|
||||
return waitForExit(pid, attempts - 1)
|
||||
}
|
||||
|
||||
function running(pid: number) {
|
||||
if (!Number.isSafeInteger(pid) || pid <= 0) return false
|
||||
try {
|
||||
process.kill(pid, 0)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ Private generation target for clients derived directly from OpenCode's authorita
|
||||
- `@opencode-ai/client`: zero-Effect Promise client using `fetch`.
|
||||
- `@opencode-ai/client/effect`: rich Effect network client using an environment-provided `HttpClient`.
|
||||
|
||||
The generated surface includes every group from Server's concrete API. The build compiler reads `@opencode-ai/server/api`; the generated Effect runtime imports a client-local projection built from Protocol, with a generation-equivalence test preventing transport drift. Run `bun run generate` after changing the contract and `bun run check:generated` to detect committed-output drift.
|
||||
The generated surface starts with the Session group from Server's concrete API. The build compiler reads `@opencode-ai/server/api`; the generated Effect runtime imports a client-local projection built from Protocol, with a generation-equivalence test preventing transport drift. Run `bun run generate` after changing the contract and `bun run check:generated` to detect committed-output drift.
|
||||
|
||||
The Effect entrypoint uses canonical decoded values such as `Session.ID`, `Location.Ref`, and `Prompt`. These datatypes come from the lightweight `@opencode-ai/schema` package and are re-exported so callers depend only on the client surface. Protocol owns endpoint construction and middleware placement; Server supplies the concrete middleware keys used by the build-time API.
|
||||
|
||||
|
||||
@@ -2,17 +2,19 @@ import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen"
|
||||
import { Api } from "@opencode-ai/server/api"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApi } from "effect/unstable/httpapi"
|
||||
import { fileURLToPath } from "url"
|
||||
import { endpointNames, groupNames } from "../src/contract"
|
||||
|
||||
const contract = compile(Api, { groupNames, endpointNames })
|
||||
const contract = compile(HttpApi.make("opencode-client").add(Api.groups["server.session"]), {
|
||||
groupNames: { "server.session": "sessions" },
|
||||
})
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.all(
|
||||
[
|
||||
write(emitPromise(contract), fileURLToPath(new URL("../src/generated", import.meta.url))),
|
||||
write(
|
||||
emitEffectImported(contract, { module: "../contract", api: "Api" }),
|
||||
emitEffectImported(contract, { module: "../contract", group: "SessionGroup" }),
|
||||
fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -11,41 +11,9 @@ class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocatio
|
||||
{ error: [InvalidRequestError, SessionNotFoundError] },
|
||||
) {}
|
||||
|
||||
export const Api = makeDefaultApi({
|
||||
const Api = makeDefaultApi({
|
||||
locationMiddleware: LocationMiddleware,
|
||||
sessionLocationMiddleware: SessionLocationMiddleware,
|
||||
})
|
||||
|
||||
export const groupNames = {
|
||||
"server.health": "health",
|
||||
"server.location": "location",
|
||||
"server.agent": "agents",
|
||||
"server.session": "sessions",
|
||||
"server.message": "messages",
|
||||
"server.model": "models",
|
||||
"server.provider": "providers",
|
||||
"server.integration": "integrations",
|
||||
"server.credential": "credentials",
|
||||
"server.permission": "permissions",
|
||||
"server.fs": "files",
|
||||
"server.command": "commands",
|
||||
"server.skill": "skills",
|
||||
"server.event": "events",
|
||||
"server.pty": "ptys",
|
||||
"server.question": "questions",
|
||||
"server.reference": "references",
|
||||
"server.projectCopy": "projectCopies",
|
||||
} as const
|
||||
|
||||
export const endpointNames = {
|
||||
"session.messages": "list",
|
||||
"integration.connect.key": "connectKey",
|
||||
"integration.connect.oauth": "connectOauth",
|
||||
"integration.attempt.status": "attemptStatus",
|
||||
"integration.attempt.complete": "attemptComplete",
|
||||
"integration.attempt.cancel": "attemptCancel",
|
||||
"permission.request.list": "listRequests",
|
||||
"permission.saved.list": "listSaved",
|
||||
"permission.saved.remove": "removeSaved",
|
||||
"question.request.list": "listRequests",
|
||||
} as const
|
||||
export const SessionGroup = Api.groups["server.session"]
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import { Effect, Stream, Schema } from "effect"
|
||||
import { Sse } from "effect/unstable/encoding"
|
||||
import { HttpClientError } from "effect/unstable/http"
|
||||
import { HttpApiClient } from "effect/unstable/httpapi"
|
||||
import { Api } from "../contract"
|
||||
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi"
|
||||
import { SessionGroup } from "../contract"
|
||||
import { ClientError } from "./client-error"
|
||||
|
||||
const Api = HttpApi.make("generated").add(SessionGroup)
|
||||
|
||||
type RawClient = HttpApiClient.ForApi<typeof Api>
|
||||
|
||||
const mapClientError = <E>(error: E) =>
|
||||
@@ -13,37 +15,18 @@ const mapClientError = <E>(error: E) =>
|
||||
? new ClientError({ cause: error })
|
||||
: error
|
||||
|
||||
const Endpoint0_0 = (raw: RawClient["server.health"]) => () =>
|
||||
raw["health.get"]({}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw) })
|
||||
|
||||
type Endpoint1_0Request = Parameters<RawClient["server.location"]["location.get"]>[0]
|
||||
type Endpoint1_0Input = { readonly location?: Endpoint1_0Request["query"]["location"] }
|
||||
const Endpoint1_0 = (raw: RawClient["server.location"]) => (input?: Endpoint1_0Input) =>
|
||||
raw["location.get"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup1 = (raw: RawClient["server.location"]) => ({ get: Endpoint1_0(raw) })
|
||||
|
||||
type Endpoint2_0Request = Parameters<RawClient["server.agent"]["agent.list"]>[0]
|
||||
type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] }
|
||||
const Endpoint2_0 = (raw: RawClient["server.agent"]) => (input?: Endpoint2_0Input) =>
|
||||
raw["agent.list"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup2 = (raw: RawClient["server.agent"]) => ({ list: Endpoint2_0(raw) })
|
||||
|
||||
type Endpoint3_0Request = Parameters<RawClient["server.session"]["session.list"]>[0]
|
||||
type Endpoint3_0Input = {
|
||||
readonly workspace?: Endpoint3_0Request["query"]["workspace"]
|
||||
readonly limit?: Endpoint3_0Request["query"]["limit"]
|
||||
readonly order?: Endpoint3_0Request["query"]["order"]
|
||||
readonly search?: Endpoint3_0Request["query"]["search"]
|
||||
readonly directory?: Endpoint3_0Request["query"]["directory"]
|
||||
readonly project?: Endpoint3_0Request["query"]["project"]
|
||||
readonly subpath?: Endpoint3_0Request["query"]["subpath"]
|
||||
readonly cursor?: Endpoint3_0Request["query"]["cursor"]
|
||||
type Endpoint0_0Request = Parameters<RawClient["server.session"]["session.list"]>[0]
|
||||
type Endpoint0_0Input = {
|
||||
readonly workspace?: Endpoint0_0Request["query"]["workspace"]
|
||||
readonly limit?: Endpoint0_0Request["query"]["limit"]
|
||||
readonly order?: Endpoint0_0Request["query"]["order"]
|
||||
readonly search?: Endpoint0_0Request["query"]["search"]
|
||||
readonly directory?: Endpoint0_0Request["query"]["directory"]
|
||||
readonly project?: Endpoint0_0Request["query"]["project"]
|
||||
readonly subpath?: Endpoint0_0Request["query"]["subpath"]
|
||||
readonly cursor?: Endpoint0_0Request["query"]["cursor"]
|
||||
}
|
||||
const Endpoint3_0 = (raw: RawClient["server.session"]) => (input?: Endpoint3_0Input) =>
|
||||
const Endpoint0_0 = (raw: RawClient["server.session"]) => (input?: Endpoint0_0Input) =>
|
||||
raw["session.list"]({
|
||||
query: {
|
||||
workspace: input?.workspace,
|
||||
@@ -57,14 +40,14 @@ const Endpoint3_0 = (raw: RawClient["server.session"]) => (input?: Endpoint3_0In
|
||||
},
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint3_1Request = Parameters<RawClient["server.session"]["session.create"]>[0]
|
||||
type Endpoint3_1Input = {
|
||||
readonly id?: Endpoint3_1Request["payload"]["id"]
|
||||
readonly agent?: Endpoint3_1Request["payload"]["agent"]
|
||||
readonly model?: Endpoint3_1Request["payload"]["model"]
|
||||
readonly location?: Endpoint3_1Request["payload"]["location"]
|
||||
type Endpoint0_1Request = Parameters<RawClient["server.session"]["session.create"]>[0]
|
||||
type Endpoint0_1Input = {
|
||||
readonly id?: Endpoint0_1Request["payload"]["id"]
|
||||
readonly agent?: Endpoint0_1Request["payload"]["agent"]
|
||||
readonly model?: Endpoint0_1Request["payload"]["model"]
|
||||
readonly location?: Endpoint0_1Request["payload"]["location"]
|
||||
}
|
||||
const Endpoint3_1 = (raw: RawClient["server.session"]) => (input?: Endpoint3_1Input) =>
|
||||
const Endpoint0_1 = (raw: RawClient["server.session"]) => (input?: Endpoint0_1Input) =>
|
||||
raw["session.create"]({
|
||||
payload: { id: input?.id, agent: input?.agent, model: input?.model, location: input?.location },
|
||||
}).pipe(
|
||||
@@ -72,49 +55,49 @@ const Endpoint3_1 = (raw: RawClient["server.session"]) => (input?: Endpoint3_1In
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
const Endpoint3_2 = (raw: RawClient["server.session"]) => () =>
|
||||
const Endpoint0_2 = (raw: RawClient["server.session"]) => () =>
|
||||
raw["session.active"]({}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint3_3Request = Parameters<RawClient["server.session"]["session.get"]>[0]
|
||||
type Endpoint3_3Input = { readonly sessionID: Endpoint3_3Request["params"]["sessionID"] }
|
||||
const Endpoint3_3 = (raw: RawClient["server.session"]) => (input: Endpoint3_3Input) =>
|
||||
type Endpoint0_3Request = Parameters<RawClient["server.session"]["session.get"]>[0]
|
||||
type Endpoint0_3Input = { readonly sessionID: Endpoint0_3Request["params"]["sessionID"] }
|
||||
const Endpoint0_3 = (raw: RawClient["server.session"]) => (input: Endpoint0_3Input) =>
|
||||
raw["session.get"]({ params: { sessionID: input.sessionID } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint3_4Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
|
||||
type Endpoint3_4Input = {
|
||||
readonly sessionID: Endpoint3_4Request["params"]["sessionID"]
|
||||
readonly agent: Endpoint3_4Request["payload"]["agent"]
|
||||
type Endpoint0_4Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
|
||||
type Endpoint0_4Input = {
|
||||
readonly sessionID: Endpoint0_4Request["params"]["sessionID"]
|
||||
readonly agent: Endpoint0_4Request["payload"]["agent"]
|
||||
}
|
||||
const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) =>
|
||||
const Endpoint0_4 = (raw: RawClient["server.session"]) => (input: Endpoint0_4Input) =>
|
||||
raw["session.switchAgent"]({ params: { sessionID: input.sessionID }, payload: { agent: input.agent } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint3_5Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
|
||||
type Endpoint3_5Input = {
|
||||
readonly sessionID: Endpoint3_5Request["params"]["sessionID"]
|
||||
readonly model: Endpoint3_5Request["payload"]["model"]
|
||||
type Endpoint0_5Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
|
||||
type Endpoint0_5Input = {
|
||||
readonly sessionID: Endpoint0_5Request["params"]["sessionID"]
|
||||
readonly model: Endpoint0_5Request["payload"]["model"]
|
||||
}
|
||||
const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) =>
|
||||
const Endpoint0_5 = (raw: RawClient["server.session"]) => (input: Endpoint0_5Input) =>
|
||||
raw["session.switchModel"]({ params: { sessionID: input.sessionID }, payload: { model: input.model } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint3_6Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||
type Endpoint3_6Input = {
|
||||
readonly sessionID: Endpoint3_6Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint3_6Request["payload"]["id"]
|
||||
readonly prompt: Endpoint3_6Request["payload"]["prompt"]
|
||||
readonly delivery?: Endpoint3_6Request["payload"]["delivery"]
|
||||
readonly resume?: Endpoint3_6Request["payload"]["resume"]
|
||||
type Endpoint0_6Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||
type Endpoint0_6Input = {
|
||||
readonly sessionID: Endpoint0_6Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint0_6Request["payload"]["id"]
|
||||
readonly prompt: Endpoint0_6Request["payload"]["prompt"]
|
||||
readonly delivery?: Endpoint0_6Request["payload"]["delivery"]
|
||||
readonly resume?: Endpoint0_6Request["payload"]["resume"]
|
||||
}
|
||||
const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Input) =>
|
||||
const Endpoint0_6 = (raw: RawClient["server.session"]) => (input: Endpoint0_6Input) =>
|
||||
raw["session.prompt"]({
|
||||
params: { sessionID: input.sessionID },
|
||||
payload: { id: input.id, prompt: input.prompt, delivery: input.delivery, resume: input.resume },
|
||||
@@ -123,23 +106,23 @@ const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Inp
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint3_7Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||
type Endpoint3_7Input = { readonly sessionID: Endpoint3_7Request["params"]["sessionID"] }
|
||||
const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) =>
|
||||
type Endpoint0_7Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||
type Endpoint0_7Input = { readonly sessionID: Endpoint0_7Request["params"]["sessionID"] }
|
||||
const Endpoint0_7 = (raw: RawClient["server.session"]) => (input: Endpoint0_7Input) =>
|
||||
raw["session.compact"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint3_8Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||
type Endpoint3_8Input = { readonly sessionID: Endpoint3_8Request["params"]["sessionID"] }
|
||||
const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) =>
|
||||
type Endpoint0_8Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||
type Endpoint0_8Input = { readonly sessionID: Endpoint0_8Request["params"]["sessionID"] }
|
||||
const Endpoint0_8 = (raw: RawClient["server.session"]) => (input: Endpoint0_8Input) =>
|
||||
raw["session.wait"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint3_9Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||
type Endpoint3_9Input = {
|
||||
readonly sessionID: Endpoint3_9Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint3_9Request["payload"]["messageID"]
|
||||
readonly files?: Endpoint3_9Request["payload"]["files"]
|
||||
type Endpoint0_9Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||
type Endpoint0_9Input = {
|
||||
readonly sessionID: Endpoint0_9Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint0_9Request["payload"]["messageID"]
|
||||
readonly files?: Endpoint0_9Request["payload"]["files"]
|
||||
}
|
||||
const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) =>
|
||||
const Endpoint0_9 = (raw: RawClient["server.session"]) => (input: Endpoint0_9Input) =>
|
||||
raw["session.revert.stage"]({
|
||||
params: { sessionID: input.sessionID },
|
||||
payload: { messageID: input.messageID, files: input.files },
|
||||
@@ -148,30 +131,30 @@ const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Inp
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint3_10Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||
type Endpoint3_10Input = { readonly sessionID: Endpoint3_10Request["params"]["sessionID"] }
|
||||
const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) =>
|
||||
type Endpoint0_10Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||
type Endpoint0_10Input = { readonly sessionID: Endpoint0_10Request["params"]["sessionID"] }
|
||||
const Endpoint0_10 = (raw: RawClient["server.session"]) => (input: Endpoint0_10Input) =>
|
||||
raw["session.revert.clear"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint3_11Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||
type Endpoint3_11Input = { readonly sessionID: Endpoint3_11Request["params"]["sessionID"] }
|
||||
const Endpoint3_11 = (raw: RawClient["server.session"]) => (input: Endpoint3_11Input) =>
|
||||
type Endpoint0_11Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||
type Endpoint0_11Input = { readonly sessionID: Endpoint0_11Request["params"]["sessionID"] }
|
||||
const Endpoint0_11 = (raw: RawClient["server.session"]) => (input: Endpoint0_11Input) =>
|
||||
raw["session.revert.commit"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint3_12Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||
type Endpoint3_12Input = { readonly sessionID: Endpoint3_12Request["params"]["sessionID"] }
|
||||
const Endpoint3_12 = (raw: RawClient["server.session"]) => (input: Endpoint3_12Input) =>
|
||||
type Endpoint0_12Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||
type Endpoint0_12Input = { readonly sessionID: Endpoint0_12Request["params"]["sessionID"] }
|
||||
const Endpoint0_12 = (raw: RawClient["server.session"]) => (input: Endpoint0_12Input) =>
|
||||
raw["session.context"]({ params: { sessionID: input.sessionID } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint3_13Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
||||
type Endpoint3_13Input = {
|
||||
readonly sessionID: Endpoint3_13Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint3_13Request["query"]["after"]
|
||||
type Endpoint0_13Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
||||
type Endpoint0_13Input = {
|
||||
readonly sessionID: Endpoint0_13Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint0_13Request["query"]["after"]
|
||||
}
|
||||
const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) =>
|
||||
const Endpoint0_13 = (raw: RawClient["server.session"]) => (input: Endpoint0_13Input) =>
|
||||
Stream.unwrap(
|
||||
raw["session.events"]({ params: { sessionID: input.sessionID }, query: { after: input.after } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
@@ -179,525 +162,42 @@ const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13I
|
||||
),
|
||||
)
|
||||
|
||||
type Endpoint3_14Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint3_14Input = { readonly sessionID: Endpoint3_14Request["params"]["sessionID"] }
|
||||
const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) =>
|
||||
type Endpoint0_14Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint0_14Input = { readonly sessionID: Endpoint0_14Request["params"]["sessionID"] }
|
||||
const Endpoint0_14 = (raw: RawClient["server.session"]) => (input: Endpoint0_14Input) =>
|
||||
raw["session.interrupt"]({ params: { sessionID: input.sessionID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint3_15Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
type Endpoint3_15Input = {
|
||||
readonly sessionID: Endpoint3_15Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint3_15Request["params"]["messageID"]
|
||||
type Endpoint0_15Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
type Endpoint0_15Input = {
|
||||
readonly sessionID: Endpoint0_15Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint0_15Request["params"]["messageID"]
|
||||
}
|
||||
const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) =>
|
||||
const Endpoint0_15 = (raw: RawClient["server.session"]) => (input: Endpoint0_15Input) =>
|
||||
raw["session.message"]({ params: { sessionID: input.sessionID, messageID: input.messageID } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
const adaptGroup3 = (raw: RawClient["server.session"]) => ({
|
||||
list: Endpoint3_0(raw),
|
||||
create: Endpoint3_1(raw),
|
||||
active: Endpoint3_2(raw),
|
||||
get: Endpoint3_3(raw),
|
||||
switchAgent: Endpoint3_4(raw),
|
||||
switchModel: Endpoint3_5(raw),
|
||||
prompt: Endpoint3_6(raw),
|
||||
compact: Endpoint3_7(raw),
|
||||
wait: Endpoint3_8(raw),
|
||||
stage: Endpoint3_9(raw),
|
||||
clear: Endpoint3_10(raw),
|
||||
commit: Endpoint3_11(raw),
|
||||
context: Endpoint3_12(raw),
|
||||
events: Endpoint3_13(raw),
|
||||
interrupt: Endpoint3_14(raw),
|
||||
message: Endpoint3_15(raw),
|
||||
const adaptGroup0 = (raw: RawClient["server.session"]) => ({
|
||||
list: Endpoint0_0(raw),
|
||||
create: Endpoint0_1(raw),
|
||||
active: Endpoint0_2(raw),
|
||||
get: Endpoint0_3(raw),
|
||||
switchAgent: Endpoint0_4(raw),
|
||||
switchModel: Endpoint0_5(raw),
|
||||
prompt: Endpoint0_6(raw),
|
||||
compact: Endpoint0_7(raw),
|
||||
wait: Endpoint0_8(raw),
|
||||
stage: Endpoint0_9(raw),
|
||||
clear: Endpoint0_10(raw),
|
||||
commit: Endpoint0_11(raw),
|
||||
context: Endpoint0_12(raw),
|
||||
events: Endpoint0_13(raw),
|
||||
interrupt: Endpoint0_14(raw),
|
||||
message: Endpoint0_15(raw),
|
||||
})
|
||||
|
||||
type Endpoint4_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
||||
type Endpoint4_0Input = {
|
||||
readonly sessionID: Endpoint4_0Request["params"]["sessionID"]
|
||||
readonly limit?: Endpoint4_0Request["query"]["limit"]
|
||||
readonly order?: Endpoint4_0Request["query"]["order"]
|
||||
readonly cursor?: Endpoint4_0Request["query"]["cursor"]
|
||||
}
|
||||
const Endpoint4_0 = (raw: RawClient["server.message"]) => (input: Endpoint4_0Input) =>
|
||||
raw["session.messages"]({
|
||||
params: { sessionID: input.sessionID },
|
||||
query: { limit: input.limit, order: input.order, cursor: input.cursor },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup4 = (raw: RawClient["server.message"]) => ({ list: Endpoint4_0(raw) })
|
||||
|
||||
type Endpoint5_0Request = Parameters<RawClient["server.model"]["model.list"]>[0]
|
||||
type Endpoint5_0Input = { readonly location?: Endpoint5_0Request["query"]["location"] }
|
||||
const Endpoint5_0 = (raw: RawClient["server.model"]) => (input?: Endpoint5_0Input) =>
|
||||
raw["model.list"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup5 = (raw: RawClient["server.model"]) => ({ list: Endpoint5_0(raw) })
|
||||
|
||||
type Endpoint6_0Request = Parameters<RawClient["server.provider"]["provider.list"]>[0]
|
||||
type Endpoint6_0Input = { readonly location?: Endpoint6_0Request["query"]["location"] }
|
||||
const Endpoint6_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint6_0Input) =>
|
||||
raw["provider.list"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint6_1Request = Parameters<RawClient["server.provider"]["provider.get"]>[0]
|
||||
type Endpoint6_1Input = {
|
||||
readonly providerID: Endpoint6_1Request["params"]["providerID"]
|
||||
readonly location?: Endpoint6_1Request["query"]["location"]
|
||||
}
|
||||
const Endpoint6_1 = (raw: RawClient["server.provider"]) => (input: Endpoint6_1Input) =>
|
||||
raw["provider.get"]({ params: { providerID: input.providerID }, query: { location: input.location } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
const adaptGroup6 = (raw: RawClient["server.provider"]) => ({ list: Endpoint6_0(raw), get: Endpoint6_1(raw) })
|
||||
|
||||
type Endpoint7_0Request = Parameters<RawClient["server.integration"]["integration.list"]>[0]
|
||||
type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] }
|
||||
const Endpoint7_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint7_0Input) =>
|
||||
raw["integration.list"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint7_1Request = Parameters<RawClient["server.integration"]["integration.get"]>[0]
|
||||
type Endpoint7_1Input = {
|
||||
readonly integrationID: Endpoint7_1Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint7_1Request["query"]["location"]
|
||||
}
|
||||
const Endpoint7_1 = (raw: RawClient["server.integration"]) => (input: Endpoint7_1Input) =>
|
||||
raw["integration.get"]({ params: { integrationID: input.integrationID }, query: { location: input.location } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint7_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
|
||||
type Endpoint7_2Input = {
|
||||
readonly integrationID: Endpoint7_2Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint7_2Request["query"]["location"]
|
||||
readonly key: Endpoint7_2Request["payload"]["key"]
|
||||
readonly label?: Endpoint7_2Request["payload"]["label"]
|
||||
}
|
||||
const Endpoint7_2 = (raw: RawClient["server.integration"]) => (input: Endpoint7_2Input) =>
|
||||
raw["integration.connect.key"]({
|
||||
params: { integrationID: input.integrationID },
|
||||
query: { location: input.location },
|
||||
payload: { key: input.key, label: input.label },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint7_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
|
||||
type Endpoint7_3Input = {
|
||||
readonly integrationID: Endpoint7_3Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint7_3Request["query"]["location"]
|
||||
readonly methodID: Endpoint7_3Request["payload"]["methodID"]
|
||||
readonly inputs: Endpoint7_3Request["payload"]["inputs"]
|
||||
readonly label?: Endpoint7_3Request["payload"]["label"]
|
||||
}
|
||||
const Endpoint7_3 = (raw: RawClient["server.integration"]) => (input: Endpoint7_3Input) =>
|
||||
raw["integration.connect.oauth"]({
|
||||
params: { integrationID: input.integrationID },
|
||||
query: { location: input.location },
|
||||
payload: { methodID: input.methodID, inputs: input.inputs, label: input.label },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint7_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
|
||||
type Endpoint7_4Input = {
|
||||
readonly attemptID: Endpoint7_4Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint7_4Request["query"]["location"]
|
||||
}
|
||||
const Endpoint7_4 = (raw: RawClient["server.integration"]) => (input: Endpoint7_4Input) =>
|
||||
raw["integration.attempt.status"]({
|
||||
params: { attemptID: input.attemptID },
|
||||
query: { location: input.location },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint7_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
|
||||
type Endpoint7_5Input = {
|
||||
readonly attemptID: Endpoint7_5Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint7_5Request["query"]["location"]
|
||||
readonly code?: Endpoint7_5Request["payload"]["code"]
|
||||
}
|
||||
const Endpoint7_5 = (raw: RawClient["server.integration"]) => (input: Endpoint7_5Input) =>
|
||||
raw["integration.attempt.complete"]({
|
||||
params: { attemptID: input.attemptID },
|
||||
query: { location: input.location },
|
||||
payload: { code: input.code },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint7_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
|
||||
type Endpoint7_6Input = {
|
||||
readonly attemptID: Endpoint7_6Request["params"]["attemptID"]
|
||||
readonly location?: Endpoint7_6Request["query"]["location"]
|
||||
}
|
||||
const Endpoint7_6 = (raw: RawClient["server.integration"]) => (input: Endpoint7_6Input) =>
|
||||
raw["integration.attempt.cancel"]({
|
||||
params: { attemptID: input.attemptID },
|
||||
query: { location: input.location },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup7 = (raw: RawClient["server.integration"]) => ({
|
||||
list: Endpoint7_0(raw),
|
||||
get: Endpoint7_1(raw),
|
||||
connectKey: Endpoint7_2(raw),
|
||||
connectOauth: Endpoint7_3(raw),
|
||||
attemptStatus: Endpoint7_4(raw),
|
||||
attemptComplete: Endpoint7_5(raw),
|
||||
attemptCancel: Endpoint7_6(raw),
|
||||
})
|
||||
|
||||
type Endpoint8_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||
type Endpoint8_0Input = {
|
||||
readonly credentialID: Endpoint8_0Request["params"]["credentialID"]
|
||||
readonly location?: Endpoint8_0Request["query"]["location"]
|
||||
readonly label: Endpoint8_0Request["payload"]["label"]
|
||||
}
|
||||
const Endpoint8_0 = (raw: RawClient["server.credential"]) => (input: Endpoint8_0Input) =>
|
||||
raw["credential.update"]({
|
||||
params: { credentialID: input.credentialID },
|
||||
query: { location: input.location },
|
||||
payload: { label: input.label },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint8_1Request = Parameters<RawClient["server.credential"]["credential.remove"]>[0]
|
||||
type Endpoint8_1Input = {
|
||||
readonly credentialID: Endpoint8_1Request["params"]["credentialID"]
|
||||
readonly location?: Endpoint8_1Request["query"]["location"]
|
||||
}
|
||||
const Endpoint8_1 = (raw: RawClient["server.credential"]) => (input: Endpoint8_1Input) =>
|
||||
raw["credential.remove"]({ params: { credentialID: input.credentialID }, query: { location: input.location } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
const adaptGroup8 = (raw: RawClient["server.credential"]) => ({ update: Endpoint8_0(raw), remove: Endpoint8_1(raw) })
|
||||
|
||||
type Endpoint9_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
|
||||
type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] }
|
||||
const Endpoint9_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_0Input) =>
|
||||
raw["permission.request.list"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint9_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
|
||||
type Endpoint9_1Input = { readonly projectID?: Endpoint9_1Request["query"]["projectID"] }
|
||||
const Endpoint9_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_1Input) =>
|
||||
raw["permission.saved.list"]({ query: { projectID: input?.projectID } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint9_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
|
||||
type Endpoint9_2Input = { readonly id: Endpoint9_2Request["params"]["id"] }
|
||||
const Endpoint9_2 = (raw: RawClient["server.permission"]) => (input: Endpoint9_2Input) =>
|
||||
raw["permission.saved.remove"]({ params: { id: input.id } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint9_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
|
||||
type Endpoint9_3Input = {
|
||||
readonly sessionID: Endpoint9_3Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint9_3Request["payload"]["id"]
|
||||
readonly action: Endpoint9_3Request["payload"]["action"]
|
||||
readonly resources: Endpoint9_3Request["payload"]["resources"]
|
||||
readonly save?: Endpoint9_3Request["payload"]["save"]
|
||||
readonly metadata?: Endpoint9_3Request["payload"]["metadata"]
|
||||
readonly source?: Endpoint9_3Request["payload"]["source"]
|
||||
readonly agent?: Endpoint9_3Request["payload"]["agent"]
|
||||
}
|
||||
const Endpoint9_3 = (raw: RawClient["server.permission"]) => (input: Endpoint9_3Input) =>
|
||||
raw["session.permission.create"]({
|
||||
params: { sessionID: input.sessionID },
|
||||
payload: {
|
||||
id: input.id,
|
||||
action: input.action,
|
||||
resources: input.resources,
|
||||
save: input.save,
|
||||
metadata: input.metadata,
|
||||
source: input.source,
|
||||
agent: input.agent,
|
||||
},
|
||||
}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint9_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
|
||||
type Endpoint9_4Input = { readonly sessionID: Endpoint9_4Request["params"]["sessionID"] }
|
||||
const Endpoint9_4 = (raw: RawClient["server.permission"]) => (input: Endpoint9_4Input) =>
|
||||
raw["session.permission.list"]({ params: { sessionID: input.sessionID } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint9_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
|
||||
type Endpoint9_5Input = {
|
||||
readonly sessionID: Endpoint9_5Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint9_5Request["params"]["requestID"]
|
||||
}
|
||||
const Endpoint9_5 = (raw: RawClient["server.permission"]) => (input: Endpoint9_5Input) =>
|
||||
raw["session.permission.get"]({ params: { sessionID: input.sessionID, requestID: input.requestID } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint9_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
|
||||
type Endpoint9_6Input = {
|
||||
readonly sessionID: Endpoint9_6Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint9_6Request["params"]["requestID"]
|
||||
readonly reply: Endpoint9_6Request["payload"]["reply"]
|
||||
readonly message?: Endpoint9_6Request["payload"]["message"]
|
||||
}
|
||||
const Endpoint9_6 = (raw: RawClient["server.permission"]) => (input: Endpoint9_6Input) =>
|
||||
raw["session.permission.reply"]({
|
||||
params: { sessionID: input.sessionID, requestID: input.requestID },
|
||||
payload: { reply: input.reply, message: input.message },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup9 = (raw: RawClient["server.permission"]) => ({
|
||||
listRequests: Endpoint9_0(raw),
|
||||
listSaved: Endpoint9_1(raw),
|
||||
removeSaved: Endpoint9_2(raw),
|
||||
create: Endpoint9_3(raw),
|
||||
list: Endpoint9_4(raw),
|
||||
get: Endpoint9_5(raw),
|
||||
reply: Endpoint9_6(raw),
|
||||
})
|
||||
|
||||
type Endpoint10_0Request = Parameters<RawClient["server.fs"]["fs.read"]>[0]
|
||||
type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] }
|
||||
const Endpoint10_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint10_0Input) =>
|
||||
raw["fs.read"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint10_1Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
|
||||
type Endpoint10_1Input = {
|
||||
readonly location?: Endpoint10_1Request["query"]["location"]
|
||||
readonly path?: Endpoint10_1Request["query"]["path"]
|
||||
}
|
||||
const Endpoint10_1 = (raw: RawClient["server.fs"]) => (input?: Endpoint10_1Input) =>
|
||||
raw["fs.list"]({ query: { location: input?.location, path: input?.path } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint10_2Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
|
||||
type Endpoint10_2Input = {
|
||||
readonly location?: Endpoint10_2Request["query"]["location"]
|
||||
readonly query: Endpoint10_2Request["query"]["query"]
|
||||
readonly type?: Endpoint10_2Request["query"]["type"]
|
||||
readonly limit?: Endpoint10_2Request["query"]["limit"]
|
||||
}
|
||||
const Endpoint10_2 = (raw: RawClient["server.fs"]) => (input: Endpoint10_2Input) =>
|
||||
raw["fs.find"]({
|
||||
query: { location: input.location, query: input.query, type: input.type, limit: input.limit },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup10 = (raw: RawClient["server.fs"]) => ({
|
||||
read: Endpoint10_0(raw),
|
||||
list: Endpoint10_1(raw),
|
||||
find: Endpoint10_2(raw),
|
||||
})
|
||||
|
||||
type Endpoint11_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
|
||||
type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] }
|
||||
const Endpoint11_0 = (raw: RawClient["server.command"]) => (input?: Endpoint11_0Input) =>
|
||||
raw["command.list"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup11 = (raw: RawClient["server.command"]) => ({ list: Endpoint11_0(raw) })
|
||||
|
||||
type Endpoint12_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
|
||||
type Endpoint12_0Input = { readonly location?: Endpoint12_0Request["query"]["location"] }
|
||||
const Endpoint12_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint12_0Input) =>
|
||||
raw["skill.list"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup12 = (raw: RawClient["server.skill"]) => ({ list: Endpoint12_0(raw) })
|
||||
|
||||
const Endpoint13_0 = (raw: RawClient["server.event"]) => () =>
|
||||
raw["event.subscribe"]({}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup13 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint13_0(raw) })
|
||||
|
||||
type Endpoint14_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
|
||||
type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
|
||||
const Endpoint14_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_0Input) =>
|
||||
raw["pty.list"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint14_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
|
||||
type Endpoint14_1Input = {
|
||||
readonly location?: Endpoint14_1Request["query"]["location"]
|
||||
readonly command?: Endpoint14_1Request["payload"]["command"]
|
||||
readonly args?: Endpoint14_1Request["payload"]["args"]
|
||||
readonly cwd?: Endpoint14_1Request["payload"]["cwd"]
|
||||
readonly title?: Endpoint14_1Request["payload"]["title"]
|
||||
readonly env?: Endpoint14_1Request["payload"]["env"]
|
||||
}
|
||||
const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Input) =>
|
||||
raw["pty.create"]({
|
||||
query: { location: input?.location },
|
||||
payload: { command: input?.command, args: input?.args, cwd: input?.cwd, title: input?.title, env: input?.env },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint14_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
|
||||
type Endpoint14_2Input = {
|
||||
readonly ptyID: Endpoint14_2Request["params"]["ptyID"]
|
||||
readonly location?: Endpoint14_2Request["query"]["location"]
|
||||
}
|
||||
const Endpoint14_2 = (raw: RawClient["server.pty"]) => (input: Endpoint14_2Input) =>
|
||||
raw["pty.get"]({ params: { ptyID: input.ptyID }, query: { location: input.location } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint14_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
|
||||
type Endpoint14_3Input = {
|
||||
readonly ptyID: Endpoint14_3Request["params"]["ptyID"]
|
||||
readonly location?: Endpoint14_3Request["query"]["location"]
|
||||
readonly title?: Endpoint14_3Request["payload"]["title"]
|
||||
readonly size?: Endpoint14_3Request["payload"]["size"]
|
||||
}
|
||||
const Endpoint14_3 = (raw: RawClient["server.pty"]) => (input: Endpoint14_3Input) =>
|
||||
raw["pty.update"]({
|
||||
params: { ptyID: input.ptyID },
|
||||
query: { location: input.location },
|
||||
payload: { title: input.title, size: input.size },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint14_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
|
||||
type Endpoint14_4Input = {
|
||||
readonly ptyID: Endpoint14_4Request["params"]["ptyID"]
|
||||
readonly location?: Endpoint14_4Request["query"]["location"]
|
||||
}
|
||||
const Endpoint14_4 = (raw: RawClient["server.pty"]) => (input: Endpoint14_4Input) =>
|
||||
raw["pty.remove"]({ params: { ptyID: input.ptyID }, query: { location: input.location } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint14_5Request = Parameters<RawClient["server.pty"]["pty.connectToken"]>[0]
|
||||
type Endpoint14_5Input = {
|
||||
readonly ptyID: Endpoint14_5Request["params"]["ptyID"]
|
||||
readonly location?: Endpoint14_5Request["query"]["location"]
|
||||
}
|
||||
const Endpoint14_5 = (raw: RawClient["server.pty"]) => (input: Endpoint14_5Input) =>
|
||||
raw["pty.connectToken"]({ params: { ptyID: input.ptyID }, query: { location: input.location } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint14_6Request = Parameters<RawClient["server.pty"]["pty.connect"]>[0]
|
||||
type Endpoint14_6Input = { readonly ptyID: Endpoint14_6Request["params"]["ptyID"] }
|
||||
const Endpoint14_6 = (raw: RawClient["server.pty"]) => (input: Endpoint14_6Input) =>
|
||||
raw["pty.connect"]({ params: { ptyID: input.ptyID } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup14 = (raw: RawClient["server.pty"]) => ({
|
||||
list: Endpoint14_0(raw),
|
||||
create: Endpoint14_1(raw),
|
||||
get: Endpoint14_2(raw),
|
||||
update: Endpoint14_3(raw),
|
||||
remove: Endpoint14_4(raw),
|
||||
connectToken: Endpoint14_5(raw),
|
||||
connect: Endpoint14_6(raw),
|
||||
})
|
||||
|
||||
type Endpoint15_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
||||
type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] }
|
||||
const Endpoint15_0 = (raw: RawClient["server.question"]) => (input?: Endpoint15_0Input) =>
|
||||
raw["question.request.list"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint15_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
|
||||
type Endpoint15_1Input = { readonly sessionID: Endpoint15_1Request["params"]["sessionID"] }
|
||||
const Endpoint15_1 = (raw: RawClient["server.question"]) => (input: Endpoint15_1Input) =>
|
||||
raw["session.question.list"]({ params: { sessionID: input.sessionID } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint15_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
|
||||
type Endpoint15_2Input = {
|
||||
readonly sessionID: Endpoint15_2Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint15_2Request["params"]["requestID"]
|
||||
readonly answers: Endpoint15_2Request["payload"]["answers"]
|
||||
}
|
||||
const Endpoint15_2 = (raw: RawClient["server.question"]) => (input: Endpoint15_2Input) =>
|
||||
raw["session.question.reply"]({
|
||||
params: { sessionID: input.sessionID, requestID: input.requestID },
|
||||
payload: { answers: input.answers },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint15_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
|
||||
type Endpoint15_3Input = {
|
||||
readonly sessionID: Endpoint15_3Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint15_3Request["params"]["requestID"]
|
||||
}
|
||||
const Endpoint15_3 = (raw: RawClient["server.question"]) => (input: Endpoint15_3Input) =>
|
||||
raw["session.question.reject"]({ params: { sessionID: input.sessionID, requestID: input.requestID } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
const adaptGroup15 = (raw: RawClient["server.question"]) => ({
|
||||
listRequests: Endpoint15_0(raw),
|
||||
list: Endpoint15_1(raw),
|
||||
reply: Endpoint15_2(raw),
|
||||
reject: Endpoint15_3(raw),
|
||||
})
|
||||
|
||||
type Endpoint16_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
|
||||
type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
|
||||
const Endpoint16_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint16_0Input) =>
|
||||
raw["reference.list"]({ query: { location: input?.location } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup16 = (raw: RawClient["server.reference"]) => ({ list: Endpoint16_0(raw) })
|
||||
|
||||
type Endpoint17_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
|
||||
type Endpoint17_0Input = {
|
||||
readonly projectID: Endpoint17_0Request["params"]["projectID"]
|
||||
readonly location?: Endpoint17_0Request["query"]["location"]
|
||||
readonly strategy: Endpoint17_0Request["payload"]["strategy"]
|
||||
readonly directory: Endpoint17_0Request["payload"]["directory"]
|
||||
readonly name?: Endpoint17_0Request["payload"]["name"]
|
||||
}
|
||||
const Endpoint17_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_0Input) =>
|
||||
raw["projectCopy.create"]({
|
||||
params: { projectID: input.projectID },
|
||||
query: { location: input.location },
|
||||
payload: { strategy: input.strategy, directory: input.directory, name: input.name },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint17_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
|
||||
type Endpoint17_1Input = {
|
||||
readonly projectID: Endpoint17_1Request["params"]["projectID"]
|
||||
readonly location?: Endpoint17_1Request["query"]["location"]
|
||||
readonly directory: Endpoint17_1Request["payload"]["directory"]
|
||||
readonly force: Endpoint17_1Request["payload"]["force"]
|
||||
}
|
||||
const Endpoint17_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_1Input) =>
|
||||
raw["projectCopy.remove"]({
|
||||
params: { projectID: input.projectID },
|
||||
query: { location: input.location },
|
||||
payload: { directory: input.directory, force: input.force },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint17_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
|
||||
type Endpoint17_2Input = {
|
||||
readonly projectID: Endpoint17_2Request["params"]["projectID"]
|
||||
readonly location?: Endpoint17_2Request["query"]["location"]
|
||||
}
|
||||
const Endpoint17_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_2Input) =>
|
||||
raw["projectCopy.refresh"]({ params: { projectID: input.projectID }, query: { location: input.location } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
const adaptGroup17 = (raw: RawClient["server.projectCopy"]) => ({
|
||||
create: Endpoint17_0(raw),
|
||||
remove: Endpoint17_1(raw),
|
||||
refresh: Endpoint17_2(raw),
|
||||
})
|
||||
|
||||
const adaptClient = (raw: RawClient) => ({
|
||||
health: adaptGroup0(raw["server.health"]),
|
||||
location: adaptGroup1(raw["server.location"]),
|
||||
agents: adaptGroup2(raw["server.agent"]),
|
||||
sessions: adaptGroup3(raw["server.session"]),
|
||||
messages: adaptGroup4(raw["server.message"]),
|
||||
models: adaptGroup5(raw["server.model"]),
|
||||
providers: adaptGroup6(raw["server.provider"]),
|
||||
integrations: adaptGroup7(raw["server.integration"]),
|
||||
credentials: adaptGroup8(raw["server.credential"]),
|
||||
permissions: adaptGroup9(raw["server.permission"]),
|
||||
files: adaptGroup10(raw["server.fs"]),
|
||||
commands: adaptGroup11(raw["server.command"]),
|
||||
skills: adaptGroup12(raw["server.skill"]),
|
||||
events: adaptGroup13(raw["server.event"]),
|
||||
ptys: adaptGroup14(raw["server.pty"]),
|
||||
questions: adaptGroup15(raw["server.question"]),
|
||||
references: adaptGroup16(raw["server.reference"]),
|
||||
projectCopies: adaptGroup17(raw["server.projectCopy"]),
|
||||
})
|
||||
const adaptClient = (raw: RawClient) => ({ sessions: adaptGroup0(raw["server.session"]) })
|
||||
|
||||
export const make = (options?: { readonly baseUrl?: URL | string }) =>
|
||||
HttpApiClient.make(Api, options).pipe(Effect.map(adaptClient))
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import type {
|
||||
HealthGetOutput,
|
||||
LocationGetInput,
|
||||
LocationGetOutput,
|
||||
AgentsListInput,
|
||||
AgentsListOutput,
|
||||
SessionsListInput,
|
||||
SessionsListOutput,
|
||||
SessionsCreateInput,
|
||||
@@ -35,87 +30,6 @@ import type {
|
||||
SessionsInterruptOutput,
|
||||
SessionsMessageInput,
|
||||
SessionsMessageOutput,
|
||||
MessagesListInput,
|
||||
MessagesListOutput,
|
||||
ModelsListInput,
|
||||
ModelsListOutput,
|
||||
ProvidersListInput,
|
||||
ProvidersListOutput,
|
||||
ProvidersGetInput,
|
||||
ProvidersGetOutput,
|
||||
IntegrationsListInput,
|
||||
IntegrationsListOutput,
|
||||
IntegrationsGetInput,
|
||||
IntegrationsGetOutput,
|
||||
IntegrationsConnectKeyInput,
|
||||
IntegrationsConnectKeyOutput,
|
||||
IntegrationsConnectOauthInput,
|
||||
IntegrationsConnectOauthOutput,
|
||||
IntegrationsAttemptStatusInput,
|
||||
IntegrationsAttemptStatusOutput,
|
||||
IntegrationsAttemptCompleteInput,
|
||||
IntegrationsAttemptCompleteOutput,
|
||||
IntegrationsAttemptCancelInput,
|
||||
IntegrationsAttemptCancelOutput,
|
||||
CredentialsUpdateInput,
|
||||
CredentialsUpdateOutput,
|
||||
CredentialsRemoveInput,
|
||||
CredentialsRemoveOutput,
|
||||
PermissionsListRequestsInput,
|
||||
PermissionsListRequestsOutput,
|
||||
PermissionsListSavedInput,
|
||||
PermissionsListSavedOutput,
|
||||
PermissionsRemoveSavedInput,
|
||||
PermissionsRemoveSavedOutput,
|
||||
PermissionsCreateInput,
|
||||
PermissionsCreateOutput,
|
||||
PermissionsListInput,
|
||||
PermissionsListOutput,
|
||||
PermissionsGetInput,
|
||||
PermissionsGetOutput,
|
||||
PermissionsReplyInput,
|
||||
PermissionsReplyOutput,
|
||||
FilesReadInput,
|
||||
FilesReadOutput,
|
||||
FilesListInput,
|
||||
FilesListOutput,
|
||||
FilesFindInput,
|
||||
FilesFindOutput,
|
||||
CommandsListInput,
|
||||
CommandsListOutput,
|
||||
SkillsListInput,
|
||||
SkillsListOutput,
|
||||
EventsSubscribeOutput,
|
||||
PtysListInput,
|
||||
PtysListOutput,
|
||||
PtysCreateInput,
|
||||
PtysCreateOutput,
|
||||
PtysGetInput,
|
||||
PtysGetOutput,
|
||||
PtysUpdateInput,
|
||||
PtysUpdateOutput,
|
||||
PtysRemoveInput,
|
||||
PtysRemoveOutput,
|
||||
PtysConnectTokenInput,
|
||||
PtysConnectTokenOutput,
|
||||
PtysConnectInput,
|
||||
PtysConnectOutput,
|
||||
QuestionsListRequestsInput,
|
||||
QuestionsListRequestsOutput,
|
||||
QuestionsListInput,
|
||||
QuestionsListOutput,
|
||||
QuestionsReplyInput,
|
||||
QuestionsReplyOutput,
|
||||
QuestionsRejectInput,
|
||||
QuestionsRejectOutput,
|
||||
ReferencesListInput,
|
||||
ReferencesListOutput,
|
||||
ProjectCopiesCreateInput,
|
||||
ProjectCopiesCreateOutput,
|
||||
ProjectCopiesRemoveInput,
|
||||
ProjectCopiesRemoveOutput,
|
||||
ProjectCopiesRefreshInput,
|
||||
ProjectCopiesRefreshOutput,
|
||||
} from "./types"
|
||||
import { ClientError } from "./client-error"
|
||||
|
||||
@@ -139,7 +53,6 @@ interface RequestDescriptor {
|
||||
readonly successStatus: number
|
||||
readonly declaredStatuses: ReadonlyArray<number>
|
||||
readonly empty: boolean
|
||||
readonly binary: boolean
|
||||
}
|
||||
|
||||
export function make(options: ClientOptions) {
|
||||
@@ -191,13 +104,6 @@ export function make(options: ClientOptions) {
|
||||
} catch {}
|
||||
return undefined as A
|
||||
}
|
||||
if (descriptor.binary) {
|
||||
try {
|
||||
return new Uint8Array(await response.arrayBuffer()) as A
|
||||
} catch (cause) {
|
||||
throw new ClientError("Transport", { cause })
|
||||
}
|
||||
}
|
||||
return (await json(response)) as A
|
||||
}
|
||||
|
||||
@@ -259,50 +165,6 @@ export function make(options: ClientOptions) {
|
||||
})
|
||||
|
||||
return {
|
||||
health: {
|
||||
get: (requestOptions?: RequestOptions) =>
|
||||
request<HealthGetOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/health`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
location: {
|
||||
get: (input?: LocationGetInput, requestOptions?: RequestOptions) =>
|
||||
request<LocationGetOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/location`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
agents: {
|
||||
list: (input?: AgentsListInput, requestOptions?: RequestOptions) =>
|
||||
request<AgentsListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/agent`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
sessions: {
|
||||
list: (input?: SessionsListInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionsListOutput>(
|
||||
@@ -322,7 +184,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
@@ -335,7 +196,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
@@ -347,7 +207,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
@@ -359,7 +218,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
@@ -372,7 +230,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
@@ -385,7 +242,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
@@ -398,7 +254,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 200,
|
||||
declaredStatuses: [409, 404, 400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
@@ -410,7 +265,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 503, 400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
@@ -422,7 +276,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 503, 400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
@@ -435,7 +288,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 500, 400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
@@ -447,7 +299,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 500, 400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
@@ -459,7 +310,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
@@ -471,7 +321,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 500, 400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
@@ -484,7 +333,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
@@ -496,7 +344,6 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
@@ -508,581 +355,10 @@ export function make(options: ClientOptions) {
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
},
|
||||
messages: {
|
||||
list: (input: MessagesListInput, requestOptions?: RequestOptions) =>
|
||||
request<MessagesListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/message`,
|
||||
query: { limit: input.limit, order: input.order, cursor: input.cursor },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 404, 500, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
models: {
|
||||
list: (input?: ModelsListInput, requestOptions?: RequestOptions) =>
|
||||
request<ModelsListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/model`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [503, 401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
providers: {
|
||||
list: (input?: ProvidersListInput, requestOptions?: RequestOptions) =>
|
||||
request<ProvidersListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/provider`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [503, 401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
get: (input: ProvidersGetInput, requestOptions?: RequestOptions) =>
|
||||
request<ProvidersGetOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/provider/${encodeURIComponent(input.providerID)}`,
|
||||
query: { location: input.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 503, 401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
integrations: {
|
||||
list: (input?: IntegrationsListInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationsListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/integration`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
get: (input: IntegrationsGetInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationsGetOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}`,
|
||||
query: { location: input.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
connectKey: (input: IntegrationsConnectKeyInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationsConnectKeyOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
|
||||
query: { location: input.location },
|
||||
body: { key: input.key, label: input.label },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
connectOauth: (input: IntegrationsConnectOauthInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationsConnectOauthOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
|
||||
query: { location: input.location },
|
||||
body: { methodID: input.methodID, inputs: input.inputs, label: input.label },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
attemptStatus: (input: IntegrationsAttemptStatusInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationsAttemptStatusOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
attemptComplete: (input: IntegrationsAttemptCompleteInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationsAttemptCompleteOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
|
||||
query: { location: input.location },
|
||||
body: { code: input.code },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
attemptCancel: (input: IntegrationsAttemptCancelInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationsAttemptCancelOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input.location },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
credentials: {
|
||||
update: (input: CredentialsUpdateInput, requestOptions?: RequestOptions) =>
|
||||
request<CredentialsUpdateOutput>(
|
||||
{
|
||||
method: "PATCH",
|
||||
path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
|
||||
query: { location: input.location },
|
||||
body: { label: input.label },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
remove: (input: CredentialsRemoveInput, requestOptions?: RequestOptions) =>
|
||||
request<CredentialsRemoveOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
|
||||
query: { location: input.location },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
permissions: {
|
||||
listRequests: (input?: PermissionsListRequestsInput, requestOptions?: RequestOptions) =>
|
||||
request<PermissionsListRequestsOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/permission/request`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
listSaved: (input?: PermissionsListSavedInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: PermissionsListSavedOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/permission/saved`,
|
||||
query: { projectID: input?.projectID },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
removeSaved: (input: PermissionsRemoveSavedInput, requestOptions?: RequestOptions) =>
|
||||
request<PermissionsRemoveSavedOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
create: (input: PermissionsCreateInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: PermissionsCreateOutput }>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`,
|
||||
body: {
|
||||
id: input.id,
|
||||
action: input.action,
|
||||
resources: input.resources,
|
||||
save: input.save,
|
||||
metadata: input.metadata,
|
||||
source: input.source,
|
||||
agent: input.agent,
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
list: (input: PermissionsListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: PermissionsListOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
get: (input: PermissionsGetInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: PermissionsGetOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
reply: (input: PermissionsReplyInput, requestOptions?: RequestOptions) =>
|
||||
request<PermissionsReplyOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}/reply`,
|
||||
body: { reply: input.reply, message: input.message },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
files: {
|
||||
read: (input?: FilesReadInput, requestOptions?: RequestOptions) =>
|
||||
request<FilesReadOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/fs/read/*`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
list: (input?: FilesListInput, requestOptions?: RequestOptions) =>
|
||||
request<FilesListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/fs/list`,
|
||||
query: { location: input?.location, path: input?.path },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
find: (input: FilesFindInput, requestOptions?: RequestOptions) =>
|
||||
request<FilesFindOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/fs/find`,
|
||||
query: { location: input.location, query: input.query, type: input.type, limit: input.limit },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
commands: {
|
||||
list: (input?: CommandsListInput, requestOptions?: RequestOptions) =>
|
||||
request<CommandsListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/command`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
skills: {
|
||||
list: (input?: SkillsListInput, requestOptions?: RequestOptions) =>
|
||||
request<SkillsListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/skill`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
events: {
|
||||
subscribe: (requestOptions?: RequestOptions) =>
|
||||
request<EventsSubscribeOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/event`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
ptys: {
|
||||
list: (input?: PtysListInput, requestOptions?: RequestOptions) =>
|
||||
request<PtysListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/pty`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
create: (input?: PtysCreateInput, requestOptions?: RequestOptions) =>
|
||||
request<PtysCreateOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/pty`,
|
||||
query: { location: input?.location },
|
||||
body: { command: input?.command, args: input?.args, cwd: input?.cwd, title: input?.title, env: input?.env },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
get: (input: PtysGetInput, requestOptions?: RequestOptions) =>
|
||||
request<PtysGetOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
|
||||
query: { location: input.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
update: (input: PtysUpdateInput, requestOptions?: RequestOptions) =>
|
||||
request<PtysUpdateOutput>(
|
||||
{
|
||||
method: "PUT",
|
||||
path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
|
||||
query: { location: input.location },
|
||||
body: { title: input.title, size: input.size },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
remove: (input: PtysRemoveInput, requestOptions?: RequestOptions) =>
|
||||
request<PtysRemoveOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
|
||||
query: { location: input.location },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 401, 400],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
connectToken: (input: PtysConnectTokenInput, requestOptions?: RequestOptions) =>
|
||||
request<PtysConnectTokenOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/pty/${encodeURIComponent(input.ptyID)}/connect-token`,
|
||||
query: { location: input.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [403, 404, 401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
connect: (input: PtysConnectInput, requestOptions?: RequestOptions) =>
|
||||
request<PtysConnectOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/pty/${encodeURIComponent(input.ptyID)}/connect`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [403, 404, 401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
questions: {
|
||||
listRequests: (input?: QuestionsListRequestsInput, requestOptions?: RequestOptions) =>
|
||||
request<QuestionsListRequestsOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/question/request`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
list: (input: QuestionsListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: QuestionsListOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/question`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
reply: (input: QuestionsReplyInput, requestOptions?: RequestOptions) =>
|
||||
request<QuestionsReplyOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reply`,
|
||||
body: { answers: input.answers },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
reject: (input: QuestionsRejectInput, requestOptions?: RequestOptions) =>
|
||||
request<QuestionsRejectOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reject`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
references: {
|
||||
list: (input?: ReferencesListInput, requestOptions?: RequestOptions) =>
|
||||
request<ReferencesListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/reference`,
|
||||
query: { location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
projectCopies: {
|
||||
create: (input: ProjectCopiesCreateInput, requestOptions?: RequestOptions) =>
|
||||
request<ProjectCopiesCreateOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`,
|
||||
query: { location: input.location },
|
||||
body: { strategy: input.strategy, directory: input.directory, name: input.name },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: false,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
remove: (input: ProjectCopiesRemoveInput, requestOptions?: RequestOptions) =>
|
||||
request<ProjectCopiesRemoveOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`,
|
||||
query: { location: input.location },
|
||||
body: { directory: input.directory, force: input.force },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
refresh: (input: ProjectCopiesRefreshInput, requestOptions?: RequestOptions) =>
|
||||
request<ProjectCopiesRefreshOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy/refresh`,
|
||||
query: { location: input.location },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
binary: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,8 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { Workspace } from "@opencode-ai/schema/workspace"
|
||||
import { Api } from "@opencode-ai/server/api"
|
||||
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
|
||||
import { Api as ClientApi, endpointNames, groupNames } from "../src/contract"
|
||||
import { HttpApi } from "effect/unstable/httpapi"
|
||||
import { SessionGroup } from "../src/contract"
|
||||
|
||||
test("Core and Server reuse the authoritative Schema and Protocol values", () => {
|
||||
expect(AgentV2.ID).toBe(Agent.ID)
|
||||
@@ -30,16 +31,17 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
|
||||
expect(CoreSessionMessage.Message).toBe(SessionMessage.Message)
|
||||
expect(CorePrompt).toBe(Prompt)
|
||||
expect(Api.groups["server.session"].identifier).toBe("server.session")
|
||||
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
|
||||
expect(SessionGroup.identifier).toBe(Api.groups["server.session"].identifier)
|
||||
expect(Session.ID.create()).toStartWith("ses_")
|
||||
expect(Project.ID.global).toBe("global")
|
||||
expect(Provider.ID.anthropic).toBe("anthropic")
|
||||
expect(Workspace.ID.create()).toStartWith("wrk_")
|
||||
})
|
||||
|
||||
test("client and Server contracts generate identically", () => {
|
||||
const server = compile(Api, { groupNames, endpointNames })
|
||||
const client = compile(ClientApi, { groupNames, endpointNames })
|
||||
test("client and Server Session contracts generate identically", () => {
|
||||
const options = { groupNames: { "server.session": "sessions" } }
|
||||
const server = compile(HttpApi.make("server").add(Api.groups["server.session"]), options)
|
||||
const client = compile(HttpApi.make("client").add(SessionGroup), options)
|
||||
|
||||
expect(emitPromise(client)).toEqual(emitPromise(server))
|
||||
})
|
||||
|
||||
@@ -1,50 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { isUnauthorizedError, OpenCode } from "../src"
|
||||
|
||||
test("exposes every authoritative API group", () => {
|
||||
const client = OpenCode.make({ baseUrl: "http://localhost:3000" })
|
||||
|
||||
expect(Object.keys(client)).toEqual([
|
||||
"health",
|
||||
"location",
|
||||
"agents",
|
||||
"sessions",
|
||||
"messages",
|
||||
"models",
|
||||
"providers",
|
||||
"integrations",
|
||||
"credentials",
|
||||
"permissions",
|
||||
"files",
|
||||
"commands",
|
||||
"skills",
|
||||
"events",
|
||||
"ptys",
|
||||
"questions",
|
||||
"references",
|
||||
"projectCopies",
|
||||
])
|
||||
expect(Object.keys(client.messages)).toEqual(["list"])
|
||||
expect(Object.keys(client.integrations)).toEqual([
|
||||
"list",
|
||||
"get",
|
||||
"connectKey",
|
||||
"connectOauth",
|
||||
"attemptStatus",
|
||||
"attemptComplete",
|
||||
"attemptCancel",
|
||||
])
|
||||
expect(Object.keys(client.permissions)).toEqual([
|
||||
"listRequests",
|
||||
"listSaved",
|
||||
"removeSaved",
|
||||
"create",
|
||||
"list",
|
||||
"get",
|
||||
"reply",
|
||||
])
|
||||
})
|
||||
|
||||
test("sessions.get returns the wire projection", async () => {
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * as Integration from "./integration"
|
||||
|
||||
import {
|
||||
Cache,
|
||||
Cause,
|
||||
Clock,
|
||||
Context,
|
||||
@@ -310,6 +311,25 @@ export const locationLayer = Layer.effect(
|
||||
const authorize = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||
effect.pipe(Effect.mapError((cause) => new AuthorizationError({ cause })))
|
||||
|
||||
const refreshes = yield* Cache.make<Credential.ID, Credential.Value | undefined, AuthorizationError>({
|
||||
capacity: Number.POSITIVE_INFINITY,
|
||||
timeToLive: Duration.zero,
|
||||
lookup: Effect.fnUntraced(function* (credentialID) {
|
||||
const credential = yield* credentials.get(credentialID)
|
||||
if (!credential || credential.value.type === "key") return credential?.value
|
||||
const implementation = state
|
||||
.get()
|
||||
.integrations.get(credential.integrationID)
|
||||
?.implementations.get(credential.value.methodID)
|
||||
if (!implementation?.refresh) return credential.value
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
if (credential.value.expires > now + Duration.toMillis(Duration.minutes(5))) return credential.value
|
||||
const value = yield* authorize(implementation.refresh(credential.value))
|
||||
yield* credentials.update(credentialID, { value })
|
||||
return value
|
||||
}),
|
||||
})
|
||||
|
||||
const close = (attemptScope: Scope.Closeable) =>
|
||||
Scope.close(attemptScope, Exit.void).pipe(Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
|
||||
|
||||
@@ -396,9 +416,7 @@ export const locationLayer = Layer.effect(
|
||||
if (!implementation?.refresh) return credential.value
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
if (credential.value.expires > now + Duration.toMillis(Duration.minutes(5))) return credential.value
|
||||
const value = yield* authorize(implementation.refresh(credential.value))
|
||||
yield* credentials.update(credential.id, { value })
|
||||
return value
|
||||
return yield* Cache.get(refreshes, credential.id)
|
||||
}),
|
||||
key: Effect.fn("Integration.connection.key")(function* (input) {
|
||||
const method = state
|
||||
|
||||
@@ -23,7 +23,6 @@ import { Npm } from "../npm"
|
||||
import { PluginV2 } from "../plugin"
|
||||
import { Reference } from "../reference"
|
||||
import { SkillV2 } from "../skill"
|
||||
import { State } from "../state"
|
||||
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
|
||||
import { AgentPlugin } from "./agent"
|
||||
import { CommandPlugin } from "./command"
|
||||
@@ -103,22 +102,20 @@ export const locationLayer = Layer.effectDiscard(
|
||||
return plugin.add(PluginV2.ID.make(loaded.id), loaded.effect)
|
||||
}
|
||||
|
||||
yield* State.batch(
|
||||
Effect.gen(function* () {
|
||||
yield* add(ConfigReferencePlugin.Plugin)
|
||||
yield* add(AgentPlugin.Plugin)
|
||||
yield* add(CommandPlugin.Plugin)
|
||||
yield* add(SkillPlugin.Plugin)
|
||||
yield* add(ModelsDevPlugin)
|
||||
yield* add(ConfigAgentPlugin.Plugin)
|
||||
yield* add(ConfigCommandPlugin.Plugin)
|
||||
yield* add(ConfigSkillPlugin.Plugin)
|
||||
for (const item of ProviderPlugins) yield* add(item)
|
||||
yield* add(ConfigExternalPlugin.Plugin)
|
||||
yield* add(ConfigProviderPlugin.Plugin)
|
||||
yield* add(VariantPlugin.Plugin)
|
||||
}),
|
||||
).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
|
||||
yield* Effect.gen(function* () {
|
||||
yield* add(ConfigReferencePlugin.Plugin)
|
||||
yield* add(AgentPlugin.Plugin)
|
||||
yield* add(CommandPlugin.Plugin)
|
||||
yield* add(SkillPlugin.Plugin)
|
||||
yield* add(ModelsDevPlugin)
|
||||
yield* add(ConfigAgentPlugin.Plugin)
|
||||
yield* add(ConfigCommandPlugin.Plugin)
|
||||
yield* add(ConfigSkillPlugin.Plugin)
|
||||
for (const item of ProviderPlugins) yield* add(item)
|
||||
yield* add(ConfigExternalPlugin.Plugin)
|
||||
yield* add(ConfigProviderPlugin.Plugin)
|
||||
yield* add(VariantPlugin.Plugin)
|
||||
}).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
|
||||
}),
|
||||
).pipe(
|
||||
Layer.provideMerge(PluginV2.locationLayer),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Duration, Effect, Schema, Semaphore, Stream } from "effect"
|
||||
import { Duration, Effect, Schema, Stream } from "effect"
|
||||
import type { Scope } from "effect"
|
||||
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
@@ -79,7 +79,6 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const events = yield* EventV2.Service
|
||||
const http = yield* HttpClient.HttpClient
|
||||
const loading = Semaphore.makeUnsafe(1)
|
||||
let connected = false
|
||||
let providers: typeof ConfigV1.Info.Type.provider | undefined
|
||||
|
||||
@@ -106,7 +105,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
||||
draft.method.update({ integrationID: "opencode", method: { type: "key", label: "API key (service account)" } })
|
||||
})
|
||||
|
||||
connected = (yield* ctx.integration.connection.active("opencode")) !== undefined
|
||||
yield* load()
|
||||
yield* ctx.catalog.transform((catalog) => {
|
||||
for (const [providerID, item] of Object.entries(providers ?? {})) {
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
@@ -177,13 +176,11 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
||||
}
|
||||
})
|
||||
|
||||
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
|
||||
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
|
||||
Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")),
|
||||
Stream.runForEach(refresh),
|
||||
Stream.runForEach(() => load().pipe(Effect.andThen(ctx.catalog.reload()))),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* refresh().pipe(Effect.forkScoped)
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()(
|
||||
}
|
||||
|
||||
export interface RunOptions {
|
||||
readonly combineOutput?: boolean
|
||||
readonly maxOutputBytes?: number
|
||||
readonly maxErrorBytes?: number
|
||||
readonly signal?: AbortSignal
|
||||
@@ -38,10 +37,8 @@ export interface RunStreamOptions {
|
||||
export interface RunResult {
|
||||
readonly command: string
|
||||
readonly exitCode: number
|
||||
readonly output?: Buffer
|
||||
readonly stdout: Buffer
|
||||
readonly stderr: Buffer
|
||||
readonly outputTruncated?: boolean
|
||||
readonly stdoutTruncated: boolean
|
||||
readonly stderrTruncated: boolean
|
||||
}
|
||||
@@ -146,22 +143,6 @@ export const layer = Layer.effect(
|
||||
const collect = Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const handle = yield* spawner.spawn(command)
|
||||
if (options?.combineOutput) {
|
||||
const [output, exitCode] = yield* Effect.all(
|
||||
[collectStream(handle.all, options.maxOutputBytes), handle.exitCode],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
return {
|
||||
command: description,
|
||||
exitCode,
|
||||
output: output.buffer,
|
||||
stdout: Buffer.alloc(0),
|
||||
stderr: Buffer.alloc(0),
|
||||
outputTruncated: output.truncated,
|
||||
stdoutTruncated: false,
|
||||
stderrTruncated: false,
|
||||
} satisfies RunResult
|
||||
}
|
||||
const [stdout, stderr, exitCode] = yield* Effect.all(
|
||||
[
|
||||
collectStream(handle.stdout, options?.maxOutputBytes),
|
||||
|
||||
@@ -333,8 +333,7 @@ export const layer = Layer.effect(
|
||||
if (stream._tag === "Success" && !publisher.hasProviderError())
|
||||
yield* withPublication(publisher.failUnsettledTools("Provider did not return a tool result", true))
|
||||
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
|
||||
if (settled._tag === "Failure" && Cause.hasInterrupts(settled.cause))
|
||||
return yield* Effect.failCause(settled.cause)
|
||||
if (settled._tag === "Failure") return yield* Effect.failCause(settled.cause)
|
||||
return { needsContinuation: !publisher.hasProviderError() && needsContinuation, step: currentStep }
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -30,15 +30,16 @@ export const Input = Schema.Struct({
|
||||
}),
|
||||
})
|
||||
|
||||
const StructuredOutput = Schema.Struct({
|
||||
exit: Schema.Number.pipe(Schema.optional),
|
||||
truncated: Schema.Boolean,
|
||||
timeout: Schema.Boolean.pipe(Schema.optional),
|
||||
})
|
||||
|
||||
const Output = Schema.Struct({
|
||||
...StructuredOutput.fields,
|
||||
command: Schema.String,
|
||||
cwd: Schema.String,
|
||||
exitCode: Schema.Number.pipe(Schema.optional),
|
||||
/** Bounded compact equivalent of stdout/stderr: stderr is labeled when present. */
|
||||
output: Schema.String,
|
||||
truncated: Schema.Boolean,
|
||||
stdoutTruncated: Schema.Boolean.pipe(Schema.optional),
|
||||
stderrTruncated: Schema.Boolean.pipe(Schema.optional),
|
||||
timedOut: Schema.Boolean.pipe(Schema.optional),
|
||||
warnings: Schema.Array(Schema.String).pipe(Schema.optional),
|
||||
})
|
||||
|
||||
@@ -46,12 +47,24 @@ type Output = typeof Output.Type
|
||||
|
||||
const defaultShell = () => (process.platform === "win32" ? (process.env.COMSPEC ?? "cmd.exe") : "/bin/sh")
|
||||
|
||||
const compactOutput = (stdout: string, stderr: string) => {
|
||||
const output = stdout && stderr ? `${stdout}\n\nstderr:\n${stderr}` : stderr ? `stderr:\n${stderr}` : stdout
|
||||
return output || "(no output)"
|
||||
}
|
||||
|
||||
const captureNotice = (stdoutTruncated: boolean, stderrTruncated: boolean) => {
|
||||
if (stdoutTruncated && stderrTruncated) return "[stdout and stderr capture truncated at the in-memory safety limit]"
|
||||
if (stdoutTruncated) return "[stdout capture truncated at the in-memory safety limit]"
|
||||
if (stderrTruncated) return "[stderr capture truncated at the in-memory safety limit]"
|
||||
return undefined
|
||||
}
|
||||
|
||||
const modelOutput = (output: Output) => {
|
||||
const warnings = output.warnings?.length
|
||||
? `\n\nWarnings:\n${output.warnings.map((warning) => `- ${warning}`).join("\n")}`
|
||||
: ""
|
||||
if (output.timeout) return `${warnings.trimStart()}${warnings ? "\n\n" : ""}Command timed out before completion.`
|
||||
return `${warnings.trimStart()}${warnings ? "\n\n" : ""}Command exited with code ${output.exit}.`
|
||||
if (output.timedOut) return `${output.output}${warnings}\n\nCommand timed out before completion.`
|
||||
return `${output.output}${warnings}\n\nCommand exited with code ${output.exitCode}.`
|
||||
}
|
||||
|
||||
const isTimeout = (error: AppProcess.AppProcessError) =>
|
||||
@@ -103,16 +116,7 @@ export const layer = Layer.effectDiscard(
|
||||
description: `Execute one shell command string with the host user's filesystem, process, and network authority. The active Location is the default working directory. Relative workdir values resolve from that Location. External workdir values require external_directory approval; best-effort command-argument path warnings are advisory only. Timeout values are milliseconds (default: ${DEFAULT_TIMEOUT_MS}; maximum: ${MAX_TIMEOUT_MS}). Uses the configured shell when set; otherwise uses /bin/sh on POSIX and COMSPEC or cmd.exe on Windows.`,
|
||||
input: Input,
|
||||
output: Output,
|
||||
structured: StructuredOutput,
|
||||
toStructuredOutput: ({ output }) => ({
|
||||
truncated: output.truncated,
|
||||
...(output.exit === undefined ? {} : { exit: output.exit }),
|
||||
...(output.timeout === undefined ? {} : { timeout: output.timeout }),
|
||||
}),
|
||||
toModelOutput: ({ output }) => [
|
||||
{ type: "text", text: output.output },
|
||||
{ type: "text", text: modelOutput(output) },
|
||||
],
|
||||
toModelOutput: ({ output }) => [{ type: "text", text: modelOutput(output) }],
|
||||
execute: (input, context) =>
|
||||
Effect.gen(function* () {
|
||||
const source = {
|
||||
@@ -159,9 +163,9 @@ export const layer = Layer.effectDiscard(
|
||||
const timeout = input.timeout ?? DEFAULT_TIMEOUT_MS
|
||||
const result = yield* appProcess
|
||||
.run(command, {
|
||||
combineOutput: true,
|
||||
timeout: Duration.millis(timeout),
|
||||
maxOutputBytes: MAX_CAPTURE_BYTES,
|
||||
maxErrorBytes: MAX_CAPTURE_BYTES,
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchTag("AppProcessError", (error) =>
|
||||
@@ -170,22 +174,26 @@ export const layer = Layer.effectDiscard(
|
||||
)
|
||||
if (!result) {
|
||||
return {
|
||||
command: input.command,
|
||||
cwd: target.canonical,
|
||||
output: `Command exceeded timeout of ${timeout} ms. Retry with a larger timeout if the command is expected to take longer.`,
|
||||
truncated: false,
|
||||
timeout: true,
|
||||
timedOut: true,
|
||||
...(warnings.length ? { warnings } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
const output = result.output?.toString("utf8") || "(no output)"
|
||||
const notice = result.outputTruncated
|
||||
? "[output capture truncated at the in-memory safety limit]"
|
||||
: undefined
|
||||
const compact = compactOutput(result.stdout.toString("utf8"), result.stderr.toString("utf8"))
|
||||
const notice = captureNotice(result.stdoutTruncated, result.stderrTruncated)
|
||||
return {
|
||||
exit: result.exitCode,
|
||||
output: notice ? `${output}\n\n${notice}` : output,
|
||||
truncated: result.outputTruncated === true,
|
||||
command: input.command,
|
||||
cwd: target.canonical,
|
||||
exitCode: result.exitCode,
|
||||
output: notice ? `${compact}\n\n${notice}` : compact,
|
||||
truncated: result.stdoutTruncated || result.stderrTruncated,
|
||||
...(warnings.length ? { warnings } : {}),
|
||||
...(result.stdoutTruncated ? { stdoutTruncated: true } : {}),
|
||||
...(result.stderrTruncated ? { stderrTruncated: true } : {}),
|
||||
}
|
||||
}).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to execute command: ${input.command}` }))),
|
||||
}),
|
||||
|
||||
@@ -37,19 +37,10 @@ export type Content =
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
| { readonly type: "file"; readonly data: string; readonly mime: string; readonly name?: string }
|
||||
|
||||
type Config<
|
||||
Input extends SchemaType<any>,
|
||||
Output extends SchemaType<any>,
|
||||
Structured extends SchemaType<any> = Output,
|
||||
> = {
|
||||
type Config<Input extends SchemaType<any>, Output extends SchemaType<any>> = {
|
||||
readonly description: string
|
||||
readonly input: Input
|
||||
readonly output: Output
|
||||
readonly structured?: Structured
|
||||
readonly toStructuredOutput?: (input: {
|
||||
readonly input: Schema.Schema.Type<Input>
|
||||
readonly output: Output["Encoded"]
|
||||
}) => Schema.Schema.Type<Structured>
|
||||
readonly execute: (
|
||||
input: Schema.Schema.Type<Input>,
|
||||
context: Context,
|
||||
@@ -68,12 +59,10 @@ type Runtime = {
|
||||
|
||||
const runtimes = new WeakMap<AnyTool, Runtime>()
|
||||
|
||||
export function make<
|
||||
Input extends SchemaType<any>,
|
||||
Output extends SchemaType<any>,
|
||||
Structured extends SchemaType<any> = Output,
|
||||
>(config: Config<Input, Output, Structured>): Definition<Input, Structured> {
|
||||
const tool = Object.freeze({}) as Definition<Input, Structured>
|
||||
export function make<Input extends SchemaType<any>, Output extends SchemaType<any>>(
|
||||
config: Config<Input, Output>,
|
||||
): Definition<Input, Output> {
|
||||
const tool = Object.freeze({}) as Definition<Input, Output>
|
||||
const definitions = new Map<string, ToolDefinition>()
|
||||
runtimes.set(tool, {
|
||||
definition: (name) => {
|
||||
@@ -83,7 +72,7 @@ export function make<
|
||||
name,
|
||||
description: config.description,
|
||||
inputSchema: toJsonSchema(config.input),
|
||||
outputSchema: toJsonSchema(config.structured ?? config.output),
|
||||
outputSchema: toJsonSchema(config.output),
|
||||
})
|
||||
definitions.set(name, definition)
|
||||
return definition
|
||||
@@ -95,13 +84,6 @@ export function make<
|
||||
config.execute(input, context).pipe(
|
||||
Effect.flatMap((output) =>
|
||||
Schema.encodeEffect(config.output)(output).pipe(
|
||||
Effect.flatMap((output) => {
|
||||
if (!config.structured || !config.toStructuredOutput)
|
||||
return Effect.succeed({ output, structured: output })
|
||||
return Schema.encodeEffect(config.structured)(config.toStructuredOutput({ input, output })).pipe(
|
||||
Effect.map((structured) => ({ output, structured })),
|
||||
)
|
||||
}),
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new ToolFailure({
|
||||
@@ -110,8 +92,8 @@ export function make<
|
||||
),
|
||||
),
|
||||
),
|
||||
Effect.map(({ output, structured }) => ({
|
||||
structured,
|
||||
Effect.map((output) => ({
|
||||
structured: output,
|
||||
content:
|
||||
config.toModelOutput?.({ input, output }).map((part) =>
|
||||
part.type === "text"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Duration, Effect, Exit, Fiber, Layer, Scope, Stream } from "effect"
|
||||
import { Deferred, Duration, Effect, Exit, Fiber, Layer, Scope, Stream } from "effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
@@ -346,4 +346,116 @@ describe("Integration", () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it.effect("shares concurrent OAuth credential refreshes", () =>
|
||||
Effect.gen(function* () {
|
||||
const integrations = yield* Integration.Service
|
||||
const credentials = yield* Credential.Service
|
||||
const integrationID = Integration.ID.make("openai")
|
||||
const methodID = Integration.MethodID.make("chatgpt")
|
||||
const started = yield* Deferred.make<void>()
|
||||
const release = yield* Deferred.make<void>()
|
||||
let refreshes = 0
|
||||
const value = Credential.OAuth.make({
|
||||
type: "oauth",
|
||||
methodID,
|
||||
access: "refreshed",
|
||||
refresh: "refresh-2",
|
||||
expires: Duration.toMillis(Duration.hours(1)),
|
||||
})
|
||||
yield* integrations.transform((editor) =>
|
||||
editor.method.update({
|
||||
integrationID,
|
||||
method: { id: methodID, type: "oauth", label: "ChatGPT" },
|
||||
authorize: () => Effect.die("unexpected authorization"),
|
||||
refresh: () =>
|
||||
Effect.sync(() => refreshes++).pipe(
|
||||
Effect.andThen(Deferred.succeed(started, undefined)),
|
||||
Effect.andThen(Deferred.await(release)),
|
||||
Effect.as(value),
|
||||
),
|
||||
}),
|
||||
)
|
||||
const credential = yield* credentials.create({
|
||||
integrationID,
|
||||
value: Credential.OAuth.make({
|
||||
type: "oauth",
|
||||
methodID,
|
||||
access: "expired",
|
||||
refresh: "refresh-1",
|
||||
expires: 0,
|
||||
}),
|
||||
})
|
||||
const connection = { type: "credential" as const, id: credential.id, label: credential.label }
|
||||
|
||||
const first = yield* integrations.connection.resolve(connection).pipe(Effect.forkChild)
|
||||
yield* Deferred.await(started)
|
||||
const second = yield* integrations.connection.resolve(connection).pipe(Effect.forkChild)
|
||||
yield* Effect.yieldNow
|
||||
expect(refreshes).toBe(1)
|
||||
|
||||
yield* Deferred.succeed(release, undefined)
|
||||
expect(yield* Effect.all([Fiber.join(first), Fiber.join(second)], { concurrency: "unbounded" })).toEqual([
|
||||
value,
|
||||
value,
|
||||
])
|
||||
expect(refreshes).toBe(1)
|
||||
expect((yield* credentials.get(credential.id))?.value).toEqual(value)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("shares concurrent refresh failures and retries later", () =>
|
||||
Effect.gen(function* () {
|
||||
const integrations = yield* Integration.Service
|
||||
const credentials = yield* Credential.Service
|
||||
const integrationID = Integration.ID.make("openai")
|
||||
const methodID = Integration.MethodID.make("chatgpt")
|
||||
const started = yield* Deferred.make<void>()
|
||||
const release = yield* Deferred.make<void>()
|
||||
const failure = new Error("refresh failed")
|
||||
let refreshes = 0
|
||||
yield* integrations.transform((editor) =>
|
||||
editor.method.update({
|
||||
integrationID,
|
||||
method: { id: methodID, type: "oauth", label: "ChatGPT" },
|
||||
authorize: () => Effect.die("unexpected authorization"),
|
||||
refresh: () =>
|
||||
Effect.sync(() => refreshes++).pipe(
|
||||
Effect.andThen(Deferred.succeed(started, undefined)),
|
||||
Effect.andThen(Deferred.await(release)),
|
||||
Effect.andThen(Effect.fail(failure)),
|
||||
),
|
||||
}),
|
||||
)
|
||||
const credential = yield* credentials.create({
|
||||
integrationID,
|
||||
value: Credential.OAuth.make({
|
||||
type: "oauth",
|
||||
methodID,
|
||||
access: "expired",
|
||||
refresh: "refresh",
|
||||
expires: 0,
|
||||
}),
|
||||
})
|
||||
const connection = { type: "credential" as const, id: credential.id, label: credential.label }
|
||||
|
||||
const first = yield* integrations.connection.resolve(connection).pipe(Effect.flip, Effect.forkChild)
|
||||
yield* Deferred.await(started)
|
||||
const second = yield* integrations.connection.resolve(connection).pipe(Effect.flip, Effect.forkChild)
|
||||
yield* Effect.yieldNow
|
||||
expect(refreshes).toBe(1)
|
||||
|
||||
yield* Deferred.succeed(release, undefined)
|
||||
const results = yield* Effect.all([Fiber.join(first), Fiber.join(second)], { concurrency: "unbounded" })
|
||||
expect(results).toEqual([
|
||||
new Integration.AuthorizationError({ cause: failure }),
|
||||
new Integration.AuthorizationError({ cause: failure }),
|
||||
])
|
||||
|
||||
expect(yield* integrations.connection.resolve(connection).pipe(Effect.flip)).toEqual(
|
||||
new Integration.AuthorizationError({ cause: failure }),
|
||||
)
|
||||
expect(refreshes).toBe(2)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -25,20 +25,6 @@ function required<T>(value: T | undefined): T {
|
||||
return value
|
||||
}
|
||||
|
||||
function eventually<A>(
|
||||
effect: Effect.Effect<A>,
|
||||
predicate: (value: A) => boolean,
|
||||
remaining = 1000,
|
||||
): Effect.Effect<A, Error> {
|
||||
return Effect.gen(function* () {
|
||||
const value = yield* effect
|
||||
if (predicate(value)) return value
|
||||
if (remaining === 0) return yield* Effect.fail(new Error("Timed out waiting for value"))
|
||||
yield* Effect.promise(() => Bun.sleep(1))
|
||||
return yield* eventually(effect, predicate, remaining - 1)
|
||||
})
|
||||
}
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
@@ -81,14 +67,11 @@ describe("OpencodePlugin", () => {
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const authorization: Array<string | null> = []
|
||||
const gate = Promise.withResolvers<void>()
|
||||
return {
|
||||
authorization,
|
||||
release: gate.resolve,
|
||||
server: Bun.serve({
|
||||
port: 0,
|
||||
fetch: async (request) => {
|
||||
await gate.promise
|
||||
fetch: (request) => {
|
||||
authorization.push(request.headers.get("authorization"))
|
||||
const origin = new URL(request.url).origin
|
||||
return Response.json({
|
||||
@@ -127,7 +110,7 @@ describe("OpencodePlugin", () => {
|
||||
}),
|
||||
}
|
||||
}),
|
||||
({ authorization, release, server }) =>
|
||||
({ authorization, server }) =>
|
||||
Effect.gen(function* () {
|
||||
const credentials = yield* Credential.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
@@ -145,15 +128,8 @@ describe("OpencodePlugin", () => {
|
||||
})
|
||||
|
||||
yield* addPlugin()
|
||||
expect(authorization).toEqual([])
|
||||
release()
|
||||
|
||||
const provider = required(
|
||||
yield* eventually(
|
||||
catalog.provider.get(ProviderV2.ID.make("remote")),
|
||||
(item) => item?.integrationID === Integration.ID.make("opencode"),
|
||||
),
|
||||
)
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("remote")))
|
||||
expect(provider).toMatchObject({
|
||||
name: "Remote",
|
||||
integrationID: "opencode",
|
||||
|
||||
@@ -39,22 +39,6 @@ describe("AppProcess", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect(
|
||||
"captures stdout and stderr in emission order",
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* AppProcess.Service
|
||||
const script = [
|
||||
'process.stdout.write("out 1\\n")',
|
||||
'setTimeout(() => process.stderr.write("err 1\\n"), 10)',
|
||||
'setTimeout(() => process.stdout.write("out 2\\n"), 20)',
|
||||
].join(";")
|
||||
const result = yield* svc.run(cmd("-e", script), { combineOutput: true })
|
||||
expect(result.output?.toString("utf8")).toBe("out 1\nerr 1\nout 2\n")
|
||||
expect(result.stdout.toString("utf8")).toBe("")
|
||||
expect(result.stderr.toString("utf8")).toBe("")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect(
|
||||
"non-zero exit returns RunResult; caller can require success",
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -2565,7 +2565,7 @@ describe("SessionRunnerLLM", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("returns unexpected local tool defects to the model and continues", () =>
|
||||
it.effect("propagates unexpected local tool defects operationally", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
const session = yield* SessionV2.Service
|
||||
@@ -2579,20 +2579,11 @@ describe("SessionRunnerLLM", () => {
|
||||
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||
LLMEvent.finish({ reason: "tool-calls" }),
|
||||
],
|
||||
[
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.textStart({ id: "text-after-defect" }),
|
||||
LLMEvent.textDelta({ id: "text-after-defect", text: "Recovered" }),
|
||||
LLMEvent.textEnd({ id: "text-after-defect" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
],
|
||||
]
|
||||
|
||||
yield* session.resume(sessionID)
|
||||
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe("unexpected tool defect")
|
||||
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "assistant", "tool"])
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(yield* session.context(sessionID)).toMatchObject([
|
||||
{ type: "user", text: "Call defect" },
|
||||
{
|
||||
@@ -2608,7 +2599,6 @@ describe("SessionRunnerLLM", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: "assistant", finish: "stop", content: [{ type: "text", text: "Recovered" }] },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -31,10 +31,8 @@ let denyAction: string | undefined
|
||||
let result: AppProcess.RunResult = {
|
||||
command: "mock",
|
||||
exitCode: 0,
|
||||
output: Buffer.from("hello\n"),
|
||||
stdout: Buffer.from("hello\n"),
|
||||
stderr: Buffer.alloc(0),
|
||||
outputTruncated: false,
|
||||
stdoutTruncated: false,
|
||||
stderrTruncated: false,
|
||||
}
|
||||
@@ -85,10 +83,8 @@ const reset = () => {
|
||||
result = {
|
||||
command: "mock",
|
||||
exitCode: 0,
|
||||
output: Buffer.from("hello\n"),
|
||||
stdout: Buffer.from("hello\n"),
|
||||
stderr: Buffer.alloc(0),
|
||||
outputTruncated: false,
|
||||
stdoutTruncated: false,
|
||||
stderrTruncated: false,
|
||||
}
|
||||
@@ -139,33 +135,24 @@ describe("BashTool", () => {
|
||||
expect(definitions.map((tool) => tool.name)).toEqual(["bash"])
|
||||
expect(definitions[0]?.inputSchema).not.toHaveProperty("properties.background")
|
||||
expect(definitions[0]?.inputSchema).not.toHaveProperty("properties.description")
|
||||
expect(definitions[0]?.outputSchema).not.toHaveProperty("properties.output")
|
||||
expect(definitions[0]?.outputSchema).not.toHaveProperty("properties.command")
|
||||
expect(definitions[0]?.outputSchema).not.toHaveProperty("properties.cwd")
|
||||
expect(yield* toolDefinitions(registry, [{ action: "bash", resource: "*", effect: "deny" }])).toEqual([])
|
||||
expect(yield* settleTool(registry, call({ command: "pwd" }))).toEqual({
|
||||
result: {
|
||||
type: "content",
|
||||
value: [
|
||||
{ type: "text", text: "hello\n" },
|
||||
{ type: "text", text: "Command exited with code 0." },
|
||||
],
|
||||
},
|
||||
result: { type: "text", value: "hello\n\n\nCommand exited with code 0." },
|
||||
output: {
|
||||
structured: {
|
||||
exit: 0,
|
||||
command: "pwd",
|
||||
cwd: realpathSync(tmp.path),
|
||||
exitCode: 0,
|
||||
output: "hello\n",
|
||||
truncated: false,
|
||||
},
|
||||
content: [
|
||||
{ type: "text", text: "hello\n" },
|
||||
{ type: "text", text: "Command exited with code 0." },
|
||||
],
|
||||
content: [{ type: "text", text: "hello\n\n\nCommand exited with code 0." }],
|
||||
},
|
||||
})
|
||||
expect(runs).toMatchObject([{ command: "pwd", cwd: realpathSync(tmp.path) }])
|
||||
expect(runs[0]?.options).toMatchObject({
|
||||
combineOutput: true,
|
||||
maxOutputBytes: BashTool.MAX_CAPTURE_BYTES,
|
||||
maxErrorBytes: BashTool.MAX_CAPTURE_BYTES,
|
||||
})
|
||||
expect(assertions).toMatchObject([{ sessionID, action: "bash", resources: ["pwd"], save: ["pwd"] }])
|
||||
}),
|
||||
@@ -235,17 +222,13 @@ describe("BashTool", () => {
|
||||
).pipe(
|
||||
Effect.andThen((settled) =>
|
||||
Effect.sync(() => {
|
||||
expect(settled.result).toEqual({
|
||||
type: "content",
|
||||
value: [
|
||||
{ type: "text", text: "core-bash" },
|
||||
{ type: "text", text: "Command exited with code 0." },
|
||||
],
|
||||
})
|
||||
expect(settled.result).toEqual({ type: "text", value: "core-bash\n\nCommand exited with code 0." })
|
||||
expect(settled.output?.structured).toMatchObject({
|
||||
exit: 0,
|
||||
command: "printf core-bash",
|
||||
cwd: realpathSync(tmp.path),
|
||||
exitCode: 0,
|
||||
output: "core-bash",
|
||||
})
|
||||
expect(settled.output?.structured).not.toHaveProperty("output")
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -320,13 +303,11 @@ describe("BashTool", () => {
|
||||
expect(assertions.map((item) => item.action)).toEqual(["bash"])
|
||||
expect(runs).toHaveLength(1)
|
||||
expect(settled.output?.structured).toMatchObject({
|
||||
truncated: false,
|
||||
})
|
||||
expect(settled.output?.structured).not.toHaveProperty("warnings")
|
||||
expect(settled.output?.content[1]).toMatchObject({
|
||||
type: "text",
|
||||
text: expect.stringContaining("Warnings:"),
|
||||
warnings: [
|
||||
`Command argument references external directory ${path.join(realpathSync(outside.path), "*").replaceAll("\\", "/")}. Bash runs with host-user filesystem, process, and network authority; this scan is advisory only.`,
|
||||
],
|
||||
})
|
||||
expect(settled.result).toMatchObject({ type: "text", value: expect.stringContaining("Warnings:") })
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -343,19 +324,21 @@ describe("BashTool", () => {
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
result = { ...result, exitCode: 7, output: Buffer.from("HEAD full output TAIL") }
|
||||
result = { ...result, exitCode: 7, stdout: Buffer.from("HEAD full output TAIL") }
|
||||
return withTool(tmp.path, (registry) => settleTool(registry, call({ command: "false" }, "call-overflow"))).pipe(
|
||||
Effect.andThen((settled) =>
|
||||
Effect.sync(() => {
|
||||
expect(settled.output?.content[1]).toMatchObject({
|
||||
expect(settled.result).toMatchObject({
|
||||
type: "text",
|
||||
text: expect.stringContaining("Command exited with code 7"),
|
||||
value: expect.stringContaining("Command exited with code 7"),
|
||||
})
|
||||
expect(settled.output?.structured).toMatchObject({
|
||||
exit: 7,
|
||||
command: "false",
|
||||
cwd: realpathSync(tmp.path),
|
||||
exitCode: 7,
|
||||
output: "HEAD full output TAIL",
|
||||
truncated: false,
|
||||
})
|
||||
expect(settled.output?.content[0]).toEqual({ type: "text", text: "HEAD full output TAIL" })
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -369,14 +352,14 @@ describe("BashTool", () => {
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
result = { ...result, outputTruncated: true }
|
||||
result = { ...result, stdoutTruncated: true }
|
||||
return withTool(tmp.path, (registry) => settleTool(registry, call({ command: "verbose" }))).pipe(
|
||||
Effect.andThen((settled) =>
|
||||
Effect.sync(() => {
|
||||
expect(settled.output?.structured).toMatchObject({ truncated: true })
|
||||
expect(settled.output?.content[0]).toMatchObject({
|
||||
expect(settled.output?.structured).toMatchObject({ truncated: true, stdoutTruncated: true })
|
||||
expect(settled.result).toMatchObject({
|
||||
type: "text",
|
||||
text: expect.stringContaining("output capture truncated"),
|
||||
value: expect.stringContaining("stdout capture truncated"),
|
||||
})
|
||||
expect(settled.output?.structured).not.toHaveProperty("resource")
|
||||
}),
|
||||
@@ -396,12 +379,13 @@ describe("BashTool", () => {
|
||||
return withTool(tmp.path, (registry) => settleTool(registry, call({ command: "sleep 60", timeout: 10 }))).pipe(
|
||||
Effect.andThen((settled) =>
|
||||
Effect.sync(() => {
|
||||
expect(settled.output?.content[1]).toMatchObject({
|
||||
expect(settled.result).toMatchObject({
|
||||
type: "text",
|
||||
text: expect.stringContaining("Command timed out"),
|
||||
value: expect.stringContaining("Command timed out"),
|
||||
})
|
||||
expect(settled.output?.structured).toMatchObject({
|
||||
timeout: true,
|
||||
command: "sleep 60",
|
||||
timedOut: true,
|
||||
truncated: false,
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -75,10 +75,7 @@ const manifestName = ".httpapi-codegen.json"
|
||||
|
||||
export function compile<Id extends string, Groups extends HttpApiGroup.Any>(
|
||||
api: HttpApi.HttpApi<Id, Groups>,
|
||||
options?: {
|
||||
readonly groupNames?: Readonly<Record<string, string>>
|
||||
readonly endpointNames?: Readonly<Record<string, string>>
|
||||
},
|
||||
options?: { readonly groupNames?: Readonly<Record<string, string>> },
|
||||
): Contract {
|
||||
const endpoints: Array<Endpoint> = []
|
||||
const portable = new Map<SchemaAST.AST, boolean>()
|
||||
@@ -153,7 +150,7 @@ export function compile<Id extends string, Groups extends HttpApiGroup.Any>(
|
||||
effectPortable,
|
||||
operation: {
|
||||
group: groupName,
|
||||
name: options?.endpointNames?.[endpoint.name] ?? clientEndpointName(endpoint.name),
|
||||
name: clientEndpointName(endpoint.name),
|
||||
input: inputs.map(({ name, source }) => ({ name, source })),
|
||||
inputMode: inputs.length === 0 ? "none" : inputs.every((field) => field.optional) ? "optional" : "required",
|
||||
success: isStreamSchema(success.schema)
|
||||
@@ -248,13 +245,7 @@ export function emitPromise(contract: Contract): Output {
|
||||
},
|
||||
{
|
||||
path: "client.ts",
|
||||
content: renderPromiseClient(groups)
|
||||
.replace("readonly empty: boolean\n}", "readonly empty: boolean\n readonly binary: boolean\n}")
|
||||
.replace(
|
||||
"return await json(response) as A",
|
||||
'if (descriptor.binary) {\n try {\n return new Uint8Array(await response.arrayBuffer()) as A\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n }\n return await json(response) as A',
|
||||
)
|
||||
.replace("let next: ReadableStreamReadResult<Uint8Array>", "let next"),
|
||||
content: renderPromiseClient(groups).replace("let next: ReadableStreamReadResult<Uint8Array>", "let next"),
|
||||
},
|
||||
{
|
||||
path: "index.ts",
|
||||
@@ -286,13 +277,13 @@ function assertPromiseEndpoint(endpoint: Endpoint) {
|
||||
}
|
||||
} else if (
|
||||
!HttpApiSchema.isNoContent(success.ast) &&
|
||||
!["Json", "Uint8Array"].includes(resolveHttpApiEncoding(success.ast)?._tag ?? "Json")
|
||||
(resolveHttpApiEncoding(success.ast)?._tag ?? "Json") !== "Json"
|
||||
) {
|
||||
throw new GenerationError({ reason: `Unsupported Promise success encoding: ${name}` })
|
||||
}
|
||||
for (const error of endpoint.errors) {
|
||||
if (declaredErrorFields(error) === undefined) {
|
||||
throw new GenerationError({ reason: `Promise error must have a literal discriminator: ${name}` })
|
||||
if (taggedErrorFields(error) === undefined) {
|
||||
throw new GenerationError({ reason: `Promise error must be tagged: ${name}` })
|
||||
}
|
||||
if ((resolveHttpApiEncoding(error.ast)?._tag ?? "Json") !== "Json") {
|
||||
throw new GenerationError({ reason: `Unsupported Promise error encoding: ${name}` })
|
||||
@@ -431,7 +422,7 @@ function renderPromiseTypes(groups: ReadonlyArray<Group>) {
|
||||
groups.flatMap((group) =>
|
||||
group.endpoints.flatMap((endpoint) =>
|
||||
endpoint.errors.flatMap((schema) => {
|
||||
const tagged = declaredErrorFields(schema)
|
||||
const tagged = taggedErrorFields(schema)
|
||||
return tagged === undefined ? [] : [[tagged.tag, tagged] as const]
|
||||
}),
|
||||
),
|
||||
@@ -441,7 +432,7 @@ function renderPromiseTypes(groups: ReadonlyArray<Group>) {
|
||||
const fields = error.fields
|
||||
.map(([name, schema, optional]) => `readonly ${JSON.stringify(name)}${optional ? "?" : ""}: ${typeOf(schema)}`)
|
||||
.join("; ")
|
||||
return `export type ${error.identifier} = { readonly ${JSON.stringify(error.key)}: ${JSON.stringify(error.tag)}; ${fields} }\nexport const is${error.identifier} = (value: unknown): value is ${error.identifier} => typeof value === "object" && value !== null && ${JSON.stringify(error.key)} in value && value[${JSON.stringify(error.key)}] === ${JSON.stringify(error.tag)}`
|
||||
return `export type ${error.identifier} = { readonly _tag: ${JSON.stringify(error.tag)}; ${fields} }\nexport const is${error.identifier} = (value: unknown): value is ${error.identifier} => typeof value === "object" && value !== null && "_tag" in value && value._tag === ${JSON.stringify(error.tag)}`
|
||||
})
|
||||
const operations = groups
|
||||
.flatMap((group) =>
|
||||
@@ -514,7 +505,7 @@ function renderPromiseClient(groups: ReadonlyArray<Group>) {
|
||||
endpoint.errors.map((schema) => resolveHttpApiStatus(schema.ast)).filter((status) => status !== undefined),
|
||||
),
|
||||
]
|
||||
const descriptor = `{ method: ${JSON.stringify(endpoint.endpoint.method)}, path: ${path}${parts.length === 0 ? "" : `, ${parts.join(", ")}`}, successStatus: ${resolveHttpApiStatus(endpoint.successes[0].ast) ?? 200}, declaredStatuses: [${declaredStatuses.join(", ")}], empty: ${endpoint.operation.success === "void"}, binary: ${resolveHttpApiEncoding(endpoint.successes[0].ast)?._tag === "Uint8Array"} }`
|
||||
const descriptor = `{ method: ${JSON.stringify(endpoint.endpoint.method)}, path: ${path}${parts.length === 0 ? "" : `, ${parts.join(", ")}`}, successStatus: ${resolveHttpApiStatus(endpoint.successes[0].ast) ?? 200}, declaredStatuses: [${declaredStatuses.join(", ")}], empty: ${endpoint.operation.success === "void"} }`
|
||||
if (endpoint.operation.success === "stream") {
|
||||
const success = endpoint.successes[0]
|
||||
if (!isStreamSchema(success) || success._tag !== "StreamSse" || success.sseMode !== "data") {
|
||||
@@ -565,12 +556,9 @@ function structuralType(schema: Schema.Top) {
|
||||
)
|
||||
const expand = (type: string, seen = new Set<string>()): string => {
|
||||
for (const [reference, value] of references) {
|
||||
const pattern = `(?<![A-Za-z0-9_$.'"])${reference.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$.'"])`
|
||||
if (!new RegExp(pattern).test(type)) continue
|
||||
if (seen.has(reference)) {
|
||||
throw new GenerationError({ reason: `Recursive Promise types are not implemented: ${reference}` })
|
||||
}
|
||||
type = type.replace(new RegExp(pattern, "g"), `(${expand(value, new Set([...seen, reference]))})`)
|
||||
if (!type.includes(reference)) continue
|
||||
if (seen.has(reference)) throw new GenerationError({ reason: "Recursive Promise types are not implemented" })
|
||||
type = type.replaceAll(reference, `(${expand(value, new Set([...seen, reference]))})`)
|
||||
}
|
||||
return type
|
||||
}
|
||||
@@ -933,26 +921,18 @@ function serializable(value: unknown): boolean {
|
||||
}
|
||||
|
||||
function taggedErrorFields(schema: Schema.Top) {
|
||||
const fields = declaredErrorFields(schema)
|
||||
return fields?.key === "_tag" ? fields : undefined
|
||||
}
|
||||
|
||||
function declaredErrorFields(schema: Schema.Top) {
|
||||
if (!SchemaAST.isDeclaration(schema.ast) || schema.ast.annotations?.["~effect/Schema/Class"] === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const fields = schema.ast.typeParameters[0]
|
||||
if (!SchemaAST.isObjects(fields) || fields.indexSignatures.length > 0) return undefined
|
||||
const key = fields.propertySignatures.find((field) => field.name === "_tag" || field.name === "name")?.name
|
||||
if (key !== "_tag" && key !== "name") return undefined
|
||||
const tag = fields.propertySignatures.find((field) => field.name === key)?.type
|
||||
const tag = fields.propertySignatures.find((field) => field.name === "_tag")?.type
|
||||
if (tag === undefined || !SchemaAST.isLiteral(tag) || typeof tag.literal !== "string") return undefined
|
||||
return {
|
||||
key,
|
||||
tag: tag.literal,
|
||||
identifier: SchemaAST.resolveIdentifier(schema.ast) ?? tag.literal,
|
||||
fields: fields.propertySignatures.flatMap((field) =>
|
||||
field.name === key || typeof field.name !== "string"
|
||||
field.name === "_tag" || typeof field.name !== "string"
|
||||
? []
|
||||
: [[field.name, Schema.make(field.type), SchemaAST.isOptional(field.type)] as const],
|
||||
),
|
||||
|
||||
@@ -151,19 +151,6 @@ describe("HttpApiCodegen.generate", () => {
|
||||
expect(effect).toContain('raw["session.get"]')
|
||||
})
|
||||
|
||||
test("supports explicit public endpoint names", () => {
|
||||
const source = HttpApi.make("test").add(
|
||||
HttpApiGroup.make("server.permission")
|
||||
.add(HttpApiEndpoint.get("permission.request.list", "/request", { success: Schema.String }))
|
||||
.add(HttpApiEndpoint.get("session.permission.list", "/session", { success: Schema.String })),
|
||||
)
|
||||
const contract = compileContract(source, {
|
||||
endpointNames: { "permission.request.list": "listRequests" },
|
||||
})
|
||||
|
||||
expect(contract.groups[0]?.endpoints.map((endpoint) => endpoint.operation.name)).toEqual(["listRequests", "list"])
|
||||
})
|
||||
|
||||
test("preserves optional keys in Promise error types", () => {
|
||||
class OptionalError extends Schema.TaggedErrorClass<OptionalError>()(
|
||||
"OptionalError",
|
||||
@@ -179,22 +166,6 @@ describe("HttpApiCodegen.generate", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("supports name-discriminated Promise errors", () => {
|
||||
class NamedError extends Schema.ErrorClass<NamedError>("NamedError")(
|
||||
{ name: Schema.Literal("NamedError"), message: Schema.String },
|
||||
{ httpApiStatus: 400 },
|
||||
) {}
|
||||
const output = emitPromise(
|
||||
compileContract(
|
||||
api(HttpApiEndpoint.get("get", "/session", { success: Schema.NumberFromString, error: NamedError })),
|
||||
),
|
||||
)
|
||||
const types = output.files.find((file) => file.path === "types.ts")?.content
|
||||
|
||||
expect(types).toContain('readonly "name": "NamedError"')
|
||||
expect(types).toContain('"name" in value && value["name"] === "NamedError"')
|
||||
})
|
||||
|
||||
test("erases brands from Promise wire types", () => {
|
||||
const output = emitPromise(
|
||||
compileContract(
|
||||
@@ -229,26 +200,6 @@ describe("HttpApiCodegen.generate", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("expands Promise references only at identifier boundaries", () => {
|
||||
const Session = Schema.Struct({ name: Schema.Literal("Session"), id: Schema.String }).annotate({
|
||||
identifier: "Session",
|
||||
})
|
||||
const SessionID = Schema.String.annotate({ identifier: "SessionID" })
|
||||
const output = emitPromise(
|
||||
compileContract(
|
||||
api(
|
||||
HttpApiEndpoint.get("get", "/session", {
|
||||
success: Schema.Struct({ session: Session, sessionID: SessionID }),
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(output.files.find((file) => file.path === "types.ts")?.content).toContain(
|
||||
'readonly "session": ({ readonly "name": "Session", readonly "id": string })',
|
||||
)
|
||||
})
|
||||
|
||||
test("emits Effect Json schemas as standalone Promise types", () => {
|
||||
const output = emitPromise(
|
||||
compileContract(
|
||||
@@ -309,32 +260,6 @@ describe("HttpApiCodegen.generate", () => {
|
||||
).toThrow("Unsupported Promise stream: session.events")
|
||||
})
|
||||
|
||||
test("executes an emitted binary Promise response", async () => {
|
||||
const output = emitPromise(
|
||||
compileContract(
|
||||
api(
|
||||
HttpApiEndpoint.get("read", "/file", {
|
||||
success: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-httpapi-codegen-"))
|
||||
|
||||
try {
|
||||
await Promise.all(output.files.map((file) => Bun.write(join(directory, file.path), file.content)))
|
||||
const generated = await import(`${join(directory, "index.ts")}?t=${crypto.randomUUID()}`)
|
||||
const client = generated.OpenCode.make({
|
||||
baseUrl: "https://example.com",
|
||||
fetch: async () => new Response(new Uint8Array([1, 2, 3])),
|
||||
})
|
||||
|
||||
expect(await client.session.read()).toEqual(new Uint8Array([1, 2, 3]))
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("executes an emitted Promise GET through fetch", async () => {
|
||||
const output = emitPromise(
|
||||
compileContract(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
AgentPart,
|
||||
OpencodeClient,
|
||||
V2Event,
|
||||
Event,
|
||||
FilePart,
|
||||
LspStatus,
|
||||
McpStatus,
|
||||
@@ -517,10 +517,7 @@ export type TuiSlots = {
|
||||
}
|
||||
|
||||
export type TuiEventBus = {
|
||||
on: <Type extends V2Event["type"]>(
|
||||
type: Type,
|
||||
handler: (event: Extract<V2Event, { type: Type }>) => void,
|
||||
) => () => void
|
||||
on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, { type: Type }>) => void) => () => void
|
||||
}
|
||||
|
||||
export type TuiDispose = () => void | Promise<void>
|
||||
|
||||
@@ -969,11 +969,7 @@ export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean; onS
|
||||
<div data-component="context-tool-group-trigger">
|
||||
<span
|
||||
data-slot="context-tool-group-title"
|
||||
class="min-w-0 flex items-center gap-2 text-14-medium"
|
||||
classList={{
|
||||
"text-text-strong": pending(),
|
||||
"text-text-weak": !pending(),
|
||||
}}
|
||||
class="min-w-0 flex items-center gap-2 text-14-medium text-text-strong"
|
||||
>
|
||||
<span data-slot="context-tool-group-label" class="shrink-0">
|
||||
<ToolStatusTitle
|
||||
@@ -985,11 +981,7 @@ export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean; onS
|
||||
</span>
|
||||
<span
|
||||
data-slot="context-tool-group-summary"
|
||||
class="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap font-normal"
|
||||
classList={{
|
||||
"text-text-base": pending(),
|
||||
"text-text-weak": !pending(),
|
||||
}}
|
||||
class="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap font-normal text-text-base"
|
||||
>
|
||||
<AnimatedCountList
|
||||
items={[
|
||||
|
||||
+12
-41
@@ -135,7 +135,6 @@ const appBindingCommands = [
|
||||
|
||||
export type TuiInput = {
|
||||
client: OpencodeClient
|
||||
reload?: () => Promise<OpencodeClient>
|
||||
args: Args
|
||||
config: TuiConfig.Resolved
|
||||
onSnapshot?: () => Promise<string[]>
|
||||
@@ -290,7 +289,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
>
|
||||
<TuiConfigProvider config={input.config}>
|
||||
<PluginRuntimeProvider value={pluginRuntime}>
|
||||
<SDKProvider client={input.client} reload={input.reload}>
|
||||
<SDKProvider client={input.client}>
|
||||
<ProjectProvider>
|
||||
<SyncProvider>
|
||||
<DataProvider>
|
||||
@@ -361,7 +360,6 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||
const keymap = useOpencodeKeymap()
|
||||
const event = useEvent()
|
||||
const sdk = useSDK()
|
||||
const reload = sdk.reload
|
||||
const toast = useToast()
|
||||
const themeState = useTheme()
|
||||
const { theme, mode, setMode, locked, lock, unlock } = themeState
|
||||
@@ -746,33 +744,6 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
...(reload
|
||||
? [
|
||||
{
|
||||
name: "server.reload",
|
||||
title: "Reload server",
|
||||
slashName: "reload",
|
||||
slashAliases: ["restart"],
|
||||
run: async () => {
|
||||
dialog.clear()
|
||||
toast.show({
|
||||
variant: "info",
|
||||
message: "Reloading server...",
|
||||
duration: 30000,
|
||||
})
|
||||
await reload()
|
||||
.then(() =>
|
||||
toast.show({
|
||||
variant: "success",
|
||||
message: "Server reloaded",
|
||||
}),
|
||||
)
|
||||
.catch(toast.error)
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: "theme.switch",
|
||||
title: "Switch theme",
|
||||
@@ -969,16 +940,16 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||
|
||||
event.on("tui.command.execute", (evt, { workspace }) => {
|
||||
if (workspace !== project.workspace.current()) return
|
||||
keymap.dispatchCommand(evt.data.command)
|
||||
keymap.dispatchCommand(evt.properties.command)
|
||||
})
|
||||
|
||||
event.on("tui.toast.show", (evt, { workspace }) => {
|
||||
if (workspace !== project.workspace.current()) return
|
||||
toast.show({
|
||||
title: evt.data.title,
|
||||
message: evt.data.message,
|
||||
variant: evt.data.variant,
|
||||
duration: evt.data.duration,
|
||||
title: evt.properties.title,
|
||||
message: evt.properties.message,
|
||||
variant: evt.properties.variant,
|
||||
duration: evt.properties.duration,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -986,12 +957,12 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||
if (workspace !== project.workspace.current()) return
|
||||
route.navigate({
|
||||
type: "session",
|
||||
sessionID: evt.data.sessionID,
|
||||
sessionID: evt.properties.sessionID,
|
||||
})
|
||||
})
|
||||
|
||||
event.on("session.deleted", (evt) => {
|
||||
if (route.data.type === "session" && route.data.sessionID === evt.data.info.id) {
|
||||
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
|
||||
route.navigate({ type: "home" })
|
||||
toast.show({
|
||||
variant: "info",
|
||||
@@ -1002,7 +973,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||
|
||||
event.on("session.error", (evt, { workspace }) => {
|
||||
if (workspace !== project.workspace.current()) return
|
||||
const error = evt.data.error
|
||||
const error = evt.properties.error
|
||||
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
|
||||
const message = errorMessage(error)
|
||||
|
||||
@@ -1015,7 +986,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||
|
||||
event.on("installation.update-available", async (evt) => {
|
||||
console.log("installation.update-available", evt)
|
||||
const version = evt.data.version
|
||||
const version = evt.properties.version
|
||||
|
||||
const skipped = kv.get("skipped_version")
|
||||
if (skipped && !isVersionGreater(version, skipped)) return
|
||||
@@ -1114,8 +1085,8 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||
<Show when={!startup.skipInitialLoading}>
|
||||
<StartupLoading ready={ready} />
|
||||
</Show>
|
||||
<Show when={sdk.connection.status() === "reconnecting"}>
|
||||
<Reconnecting attempt={sdk.connection.attempt()} error={sdk.connection.error()} />
|
||||
<Show when={data.connection.status() === "reconnecting"}>
|
||||
<Reconnecting attempt={data.connection.attempt()} error={data.connection.error()} />
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
|
||||
@@ -158,12 +158,6 @@ export function Prompt(props: PromptProps) {
|
||||
const dialog = useDialog()
|
||||
const toast = useToast()
|
||||
const status = createMemo(() => data.session.status(props.sessionID ?? ""))
|
||||
const activeSubagents = createMemo(() =>
|
||||
data.session
|
||||
.list()
|
||||
.filter((session) => session.parentID === props.sessionID && data.session.status(session.id) === "running")
|
||||
.length,
|
||||
)
|
||||
const history = usePromptHistory()
|
||||
const stash = usePromptStash()
|
||||
const keymap = useOpencodeKeymap()
|
||||
@@ -241,7 +235,7 @@ export function Prompt(props: PromptProps) {
|
||||
event.on("tui.prompt.append", (evt, { workspace }) => {
|
||||
if (workspace !== project.workspace.current()) return
|
||||
if (!input || input.isDestroyed) return
|
||||
input.insertText(evt.data.text)
|
||||
input.insertText(evt.properties.text)
|
||||
setTimeout(() => {
|
||||
// setTimeout is a workaround and needs to be addressed properly
|
||||
if (!input || input.isDestroyed) return
|
||||
@@ -407,7 +401,7 @@ export function Prompt(props: PromptProps) {
|
||||
}, 5000)
|
||||
|
||||
if (store.interrupt >= 2) {
|
||||
void sdk.client.v2.session.interrupt({
|
||||
void sdk.client.session.abort({
|
||||
sessionID: props.sessionID,
|
||||
})
|
||||
setStore("interrupt", 0)
|
||||
@@ -1540,13 +1534,6 @@ export function Prompt(props: PromptProps) {
|
||||
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={activeSubagents()}>
|
||||
{(count) => (
|
||||
<Spinner color={theme.textMuted}>
|
||||
{count()} active subagent{count() === 1 ? "" : "s"}
|
||||
</Spinner>
|
||||
)}
|
||||
</Show>
|
||||
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
||||
esc{" "}
|
||||
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
||||
|
||||
@@ -21,10 +21,15 @@ import type {
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useSDK } from "./sdk"
|
||||
import { createSignal, onCleanup } from "solid-js"
|
||||
import { createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
|
||||
export type DataConnectionStatus = "connecting" | "connected" | "reconnecting"
|
||||
export type DataSessionStatus = "idle" | "running"
|
||||
|
||||
export type DataEvent = V2Event
|
||||
type DataEventMap = { [T in DataEvent["type"]]: Extract<DataEvent, { type: T }> }
|
||||
|
||||
type LocationData = {
|
||||
agent?: AgentV2Info[]
|
||||
command?: CommandV2Info[]
|
||||
@@ -36,6 +41,11 @@ type LocationData = {
|
||||
}
|
||||
|
||||
type Data = {
|
||||
connection: {
|
||||
status: DataConnectionStatus
|
||||
attempt: number
|
||||
error?: string
|
||||
}
|
||||
session: {
|
||||
info: Record<string, SessionV2Info>
|
||||
status: Record<string, DataSessionStatus>
|
||||
@@ -61,6 +71,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
name: "Data",
|
||||
init: () => {
|
||||
const [store, setStore] = createStore<Data>({
|
||||
connection: {
|
||||
status: "connecting",
|
||||
attempt: 0,
|
||||
},
|
||||
session: {
|
||||
info: {},
|
||||
status: {},
|
||||
@@ -75,6 +89,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
})
|
||||
|
||||
const sdk = useSDK()
|
||||
const events = createGlobalEmitter<DataEventMap>()
|
||||
const [defaultLocation, setDefaultLocation] = createSignal<LocationRef>({
|
||||
directory: process.cwd(),
|
||||
})
|
||||
@@ -136,9 +151,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
|
||||
function handleEvent(event: V2Event) {
|
||||
switch (event.type) {
|
||||
case "session.created":
|
||||
void result.session.refresh(event.data.sessionID)
|
||||
break
|
||||
case "catalog.updated":
|
||||
void Promise.all([
|
||||
result.location.model.refresh(event.location),
|
||||
@@ -465,20 +477,21 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
])
|
||||
break
|
||||
}
|
||||
events.emit(event.type, event)
|
||||
}
|
||||
|
||||
const result = {
|
||||
on: sdk.event.on,
|
||||
listen: sdk.event.listen,
|
||||
on: events.on,
|
||||
listen: events.listen,
|
||||
connection: {
|
||||
status() {
|
||||
return sdk.connection.status()
|
||||
return store.connection.status
|
||||
},
|
||||
attempt() {
|
||||
return sdk.connection.attempt()
|
||||
return store.connection.attempt
|
||||
},
|
||||
error() {
|
||||
return sdk.connection.error()
|
||||
return store.connection.error
|
||||
},
|
||||
},
|
||||
session: {
|
||||
@@ -674,13 +687,52 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
console.error("Failed to refresh default location data", failure.reason)
|
||||
}
|
||||
|
||||
onCleanup(
|
||||
sdk.event.listen(({ details }) => {
|
||||
handleEvent(details)
|
||||
if (details.type === "server.connected") void bootstrap()
|
||||
}),
|
||||
)
|
||||
onMount(() => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort())
|
||||
void (async () => {
|
||||
while (!controller.signal.aborted) {
|
||||
const error = await (async () => {
|
||||
const events = await sdk.client.v2.event.subscribe({
|
||||
signal: controller.signal,
|
||||
sseMaxRetryAttempts: 0,
|
||||
throwOnError: true,
|
||||
})
|
||||
const stream = events.stream[Symbol.asyncIterator]()
|
||||
const first = await stream.next()
|
||||
if (first.done) return new Error("Event stream disconnected")
|
||||
setStore("connection", { status: "connected", attempt: 0, error: undefined })
|
||||
handleEvent(first.value)
|
||||
await bootstrap()
|
||||
while (!controller.signal.aborted) {
|
||||
const event = await stream.next()
|
||||
if (event.done) return new Error("Event stream disconnected")
|
||||
handleEvent(event.value)
|
||||
}
|
||||
})().catch((error) => error)
|
||||
if (controller.signal.aborted) return
|
||||
setStore("connection", {
|
||||
status: "reconnecting",
|
||||
attempt: store.connection.status === "connected" ? 1 : store.connection.attempt + 1,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
await wait(250, controller.signal)
|
||||
}
|
||||
})()
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
})
|
||||
|
||||
function wait(delay: number, signal: AbortSignal) {
|
||||
return new Promise<void>((resolve) => {
|
||||
const timer = setTimeout(done, delay)
|
||||
signal.addEventListener("abort", done, { once: true })
|
||||
function done() {
|
||||
clearTimeout(timer)
|
||||
signal.removeEventListener("abort", done)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import type { V2Event } from "@opencode-ai/sdk/v2"
|
||||
import type { Event } from "@opencode-ai/sdk/v2"
|
||||
import { useSDK } from "./sdk"
|
||||
|
||||
type EventMetadata = {
|
||||
directory: string | undefined
|
||||
directory: string
|
||||
workspace: string | undefined
|
||||
}
|
||||
|
||||
export function useEvent() {
|
||||
const sdk = useSDK()
|
||||
|
||||
function subscribe(handler: (event: V2Event, metadata: EventMetadata) => void) {
|
||||
return sdk.event.listen(({ details }) => {
|
||||
if (details.type === "server.connected") return
|
||||
handler(details, { directory: details.location?.directory, workspace: details.location?.workspaceID })
|
||||
function subscribe(handler: (event: Event, metadata: EventMetadata) => void) {
|
||||
return sdk.event.on("event", (event) => {
|
||||
if (event.payload.type === "sync") {
|
||||
return
|
||||
}
|
||||
|
||||
handler(event.payload, { directory: event.directory, workspace: event.workspace })
|
||||
})
|
||||
}
|
||||
|
||||
function on<T extends V2Event["type"]>(
|
||||
function on<T extends Event["type"]>(
|
||||
type: T,
|
||||
handler: (event: Extract<V2Event, { type: T }>, metadata: EventMetadata) => void,
|
||||
handler: (event: Extract<Event, { type: T }>, metadata: EventMetadata) => void,
|
||||
) {
|
||||
return sdk.event.on(type, (event) => {
|
||||
handler(event, { directory: event.location?.directory, workspace: event.location?.workspaceID })
|
||||
return subscribe((event: Event, metadata: EventMetadata) => {
|
||||
if (event.type !== type) return
|
||||
handler(event as Extract<Event, { type: T }>, metadata)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -474,7 +474,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
|
||||
event.on("session.deleted", (evt) => {
|
||||
prune(evt.data.info.id)
|
||||
prune(evt.properties.info.id)
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { batch, onCleanup } from "solid-js"
|
||||
import { batch } from "solid-js"
|
||||
import type { Path, Workspace } from "@opencode-ai/sdk/v2"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
@@ -67,11 +67,11 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex
|
||||
})
|
||||
}
|
||||
|
||||
onCleanup(
|
||||
sdk.event.on("workspace.status", (event) => {
|
||||
setStore("workspace", "status", event.data.workspaceID, event.data.status)
|
||||
}),
|
||||
)
|
||||
sdk.event.on("event", (event) => {
|
||||
if (event.payload.type === "workspace.status") {
|
||||
setStore("workspace", "status", event.payload.properties.workspaceID, event.payload.properties.status)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
data: store,
|
||||
|
||||
@@ -1,145 +1,89 @@
|
||||
import type { OpencodeClient, V2Event } from "@opencode-ai/sdk/v2"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { GlobalEvent, OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
||||
export type SDKConnectionStatus = "connecting" | "connected" | "reconnecting"
|
||||
|
||||
type SDKEventMap = { [Type in V2Event["type"]]: Extract<V2Event, { type: Type }> }
|
||||
const connectTimeout = 2_000
|
||||
import { batch, onCleanup, onMount } from "solid-js"
|
||||
|
||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
name: "SDK",
|
||||
init: (props: { client: OpencodeClient; reload?: () => Promise<OpencodeClient> }) => {
|
||||
init: (props: { client: OpencodeClient }) => {
|
||||
const abort = new AbortController()
|
||||
let client = props.client
|
||||
const events = createGlobalEmitter<SDKEventMap>()
|
||||
const [connection, setConnection] = createStore<{
|
||||
status: SDKConnectionStatus
|
||||
attempt: number
|
||||
error?: string
|
||||
}>({
|
||||
status: "connecting",
|
||||
attempt: 0,
|
||||
})
|
||||
let stream: AbortController | undefined
|
||||
let pending: Promise<void> | undefined
|
||||
|
||||
function start() {
|
||||
stream?.abort()
|
||||
const controller = new AbortController()
|
||||
const current = client
|
||||
let connected!: () => void
|
||||
const ready = new Promise<void>((resolve) => {
|
||||
connected = resolve
|
||||
})
|
||||
stream = controller
|
||||
void (async () => {
|
||||
let attempt = 0
|
||||
while (!abort.signal.aborted && !controller.signal.aborted) {
|
||||
const connection = new AbortController()
|
||||
const cancel = () => connection.abort(controller.signal.reason)
|
||||
const timeout = setTimeout(() => connection.abort(new Error("Timed out connecting to server")), connectTimeout)
|
||||
controller.signal.addEventListener("abort", cancel, { once: true })
|
||||
const error = await (async () => {
|
||||
const response = await current.v2.event.subscribe({
|
||||
signal: connection.signal,
|
||||
sseMaxRetryAttempts: 0,
|
||||
throwOnError: true,
|
||||
})
|
||||
const iterator = response.stream[Symbol.asyncIterator]()
|
||||
const first = await iterator.next()
|
||||
if (abort.signal.aborted || controller.signal.aborted) return
|
||||
if (first.done)
|
||||
return connection.signal.reason instanceof Error
|
||||
? connection.signal.reason
|
||||
: new Error("Event stream disconnected")
|
||||
clearTimeout(timeout)
|
||||
attempt = 0
|
||||
setConnection({ status: "connected", attempt: 0, error: undefined })
|
||||
events.emit(first.value.type, first.value)
|
||||
connected()
|
||||
while (!abort.signal.aborted && !controller.signal.aborted) {
|
||||
const event = await iterator.next()
|
||||
if (abort.signal.aborted || controller.signal.aborted) return
|
||||
if (event.done) return new Error("Event stream disconnected")
|
||||
events.emit(event.value.type, event.value)
|
||||
}
|
||||
})()
|
||||
.catch((error) => error)
|
||||
.finally(() => {
|
||||
clearTimeout(timeout)
|
||||
controller.signal.removeEventListener("abort", cancel)
|
||||
})
|
||||
if (abort.signal.aborted || controller.signal.aborted) return
|
||||
attempt += 1
|
||||
setConnection({
|
||||
status: "reconnecting",
|
||||
attempt,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
await wait(250, controller.signal)
|
||||
const handlers = new Set<(event: GlobalEvent) => void>()
|
||||
const emitter = {
|
||||
emit(_type: "event", event: GlobalEvent) {
|
||||
for (const handler of handlers) handler(event)
|
||||
},
|
||||
on(_type: "event", handler: (event: GlobalEvent) => void) {
|
||||
handlers.add(handler)
|
||||
return () => {
|
||||
handlers.delete(handler)
|
||||
}
|
||||
})()
|
||||
return ready
|
||||
},
|
||||
}
|
||||
|
||||
const reload = props.reload
|
||||
? () => {
|
||||
if (pending) return pending
|
||||
pending = Promise.resolve()
|
||||
.then(props.reload)
|
||||
.then(async (next) => {
|
||||
client = next
|
||||
if (!abort.signal.aborted) await start()
|
||||
})
|
||||
.finally(() => {
|
||||
pending = undefined
|
||||
})
|
||||
return pending
|
||||
}
|
||||
: undefined
|
||||
let queue: GlobalEvent[] = []
|
||||
let timer: Timer | undefined
|
||||
let last = 0
|
||||
const retryDelay = 1000
|
||||
const maxRetryDelay = 30000
|
||||
|
||||
const flush = () => {
|
||||
if (queue.length === 0) return
|
||||
const events = queue
|
||||
queue = []
|
||||
timer = undefined
|
||||
last = Date.now()
|
||||
batch(() => {
|
||||
for (const event of events) emitter.emit("event", event)
|
||||
})
|
||||
}
|
||||
|
||||
const handleEvent = (event: GlobalEvent) => {
|
||||
queue.push(event)
|
||||
const elapsed = Date.now() - last
|
||||
if (timer) return
|
||||
if (elapsed < 16) {
|
||||
timer = setTimeout(flush, 16)
|
||||
return
|
||||
}
|
||||
flush()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
void (async () => {
|
||||
let attempt = 0
|
||||
while (!abort.signal.aborted) {
|
||||
const events = await props.client.global.event({
|
||||
signal: abort.signal,
|
||||
sseMaxRetryAttempts: 0,
|
||||
})
|
||||
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) await props.client.sync.start().catch(() => {})
|
||||
|
||||
for await (const event of events.stream) {
|
||||
if (abort.signal.aborted) break
|
||||
handleEvent(event)
|
||||
}
|
||||
|
||||
if (timer) clearTimeout(timer)
|
||||
if (queue.length > 0) flush()
|
||||
attempt += 1
|
||||
if (abort.signal.aborted) break
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, Math.min(retryDelay * 2 ** (attempt - 1), maxRetryDelay)),
|
||||
)
|
||||
}
|
||||
})().catch(() => {})
|
||||
})
|
||||
|
||||
onMount(() => void start())
|
||||
onCleanup(() => {
|
||||
abort.abort()
|
||||
stream?.abort()
|
||||
events.clear()
|
||||
if (timer) clearTimeout(timer)
|
||||
handlers.clear()
|
||||
})
|
||||
|
||||
return {
|
||||
get client() {
|
||||
return client
|
||||
},
|
||||
event: {
|
||||
on: events.on,
|
||||
listen: events.listen,
|
||||
},
|
||||
connection: {
|
||||
status() {
|
||||
return connection.status
|
||||
},
|
||||
attempt() {
|
||||
return connection.attempt
|
||||
},
|
||||
error() {
|
||||
return connection.error
|
||||
},
|
||||
},
|
||||
reload,
|
||||
client: props.client,
|
||||
event: emitter,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function wait(delay: number, signal: AbortSignal) {
|
||||
return new Promise<void>((resolve) => {
|
||||
const timer = setTimeout(done, delay)
|
||||
signal.addEventListener("abort", done, { once: true })
|
||||
function done() {
|
||||
clearTimeout(timer)
|
||||
signal.removeEventListener("abort", done)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { V2Event } from "@opencode-ai/sdk/v2"
|
||||
import type { Event } from "@opencode-ai/sdk/v2"
|
||||
import type { TuiAttentionSoundName, TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||
import type { BuiltinTuiPlugin } from "../builtins"
|
||||
|
||||
const id = "internal:notifications"
|
||||
|
||||
type SessionError = Extract<V2Event, { type: "session.error" }>["data"]["error"]
|
||||
type SessionError = Extract<Event, { type: "session.error" }>["properties"]["error"]
|
||||
|
||||
function notify(api: TuiPluginApi, sessionID: string | undefined, message: string, sound: TuiAttentionSoundName) {
|
||||
const session = sessionID ? api.state.session.get(sessionID) : undefined
|
||||
@@ -33,27 +33,27 @@ const tui: TuiPlugin = async (api) => {
|
||||
const permissions = new Set<string>()
|
||||
|
||||
api.event.on("question.asked", (event) => {
|
||||
if (questions.has(event.data.id)) return
|
||||
questions.add(event.data.id)
|
||||
notify(api, event.data.sessionID, "Question needs input", "question")
|
||||
if (questions.has(event.properties.id)) return
|
||||
questions.add(event.properties.id)
|
||||
notify(api, event.properties.sessionID, "Question needs input", "question")
|
||||
})
|
||||
|
||||
api.event.on("question.replied", (event) => {
|
||||
questions.delete(event.data.requestID)
|
||||
questions.delete(event.properties.requestID)
|
||||
})
|
||||
|
||||
api.event.on("question.rejected", (event) => {
|
||||
questions.delete(event.data.requestID)
|
||||
questions.delete(event.properties.requestID)
|
||||
})
|
||||
|
||||
api.event.on("permission.asked", (event) => {
|
||||
if (permissions.has(event.data.id)) return
|
||||
permissions.add(event.data.id)
|
||||
notify(api, event.data.sessionID, "Permission needs input", "permission")
|
||||
if (permissions.has(event.properties.id)) return
|
||||
permissions.add(event.properties.id)
|
||||
notify(api, event.properties.sessionID, "Permission needs input", "permission")
|
||||
})
|
||||
|
||||
api.event.on("permission.replied", (event) => {
|
||||
permissions.delete(event.data.requestID)
|
||||
permissions.delete(event.properties.requestID)
|
||||
})
|
||||
|
||||
const started = (sessionID: string) => {
|
||||
@@ -74,18 +74,18 @@ const tui: TuiPlugin = async (api) => {
|
||||
notify(api, sessionID, "Session done", session?.parentID ? "subagent_done" : "done")
|
||||
}
|
||||
|
||||
api.event.on("session.next.prompted", (event) => started(event.data.sessionID))
|
||||
api.event.on("session.next.shell.started", (event) => started(event.data.sessionID))
|
||||
api.event.on("session.next.step.started", (event) => started(event.data.sessionID))
|
||||
api.event.on("session.next.retried", (event) => started(event.data.sessionID))
|
||||
api.event.on("session.next.compaction.started", (event) => started(event.data.sessionID))
|
||||
api.event.on("session.next.shell.ended", (event) => ended(event.data.sessionID))
|
||||
api.event.on("session.next.prompted", (event) => started(event.properties.sessionID))
|
||||
api.event.on("session.next.shell.started", (event) => started(event.properties.sessionID))
|
||||
api.event.on("session.next.step.started", (event) => started(event.properties.sessionID))
|
||||
api.event.on("session.next.retried", (event) => started(event.properties.sessionID))
|
||||
api.event.on("session.next.compaction.started", (event) => started(event.properties.sessionID))
|
||||
api.event.on("session.next.shell.ended", (event) => ended(event.properties.sessionID))
|
||||
api.event.on("session.next.step.ended", (event) => {
|
||||
if (event.data.finish === "tool-calls") return
|
||||
ended(event.data.sessionID)
|
||||
if (event.properties.finish === "tool-calls") return
|
||||
ended(event.properties.sessionID)
|
||||
})
|
||||
api.event.on("session.next.step.failed", (event) => {
|
||||
const sessionID = event.data.sessionID
|
||||
const sessionID = event.properties.sessionID
|
||||
if (!active.has(sessionID)) return
|
||||
errored.add(sessionID)
|
||||
notify(api, sessionID, "Session error", "error")
|
||||
@@ -93,11 +93,11 @@ const tui: TuiPlugin = async (api) => {
|
||||
})
|
||||
|
||||
api.event.on("session.error", (event) => {
|
||||
const sessionID = event.data.sessionID
|
||||
const sessionID = event.properties.sessionID
|
||||
if (!sessionID) return
|
||||
if (!active.has(sessionID)) return
|
||||
errored.add(sessionID)
|
||||
notify(api, sessionID, sessionErrorMessage(event.data.error), "error")
|
||||
notify(api, sessionID, sessionErrorMessage(event.properties.error), "error")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1091,15 +1091,32 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
|
||||
props.message.time.completed ? props.message.time.completed - props.message.time.created : 0,
|
||||
)
|
||||
return (
|
||||
<box paddingLeft={3}>
|
||||
<text>
|
||||
<span style={{ fg: local.agent.color(props.message.agent) }}>{Locale.titlecase(props.message.agent)}</span>
|
||||
<span style={{ fg: theme.textMuted }}> · {model()}</span>
|
||||
<Show when={duration()}>
|
||||
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
|
||||
</Show>
|
||||
</text>
|
||||
</box>
|
||||
<>
|
||||
<Show when={props.message.error}>
|
||||
<box
|
||||
border={["left"]}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.error}
|
||||
>
|
||||
<text fg={theme.textMuted}>{errorMessage(props.message.error)}</text>
|
||||
</box>
|
||||
</Show>
|
||||
<box paddingLeft={3} marginTop={props.message.error ? 1 : 0}>
|
||||
<text>
|
||||
<span style={{ fg: props.message.error ? theme.textMuted : local.agent.color(props.message.agent) }}>
|
||||
{Locale.titlecase(props.message.agent)}
|
||||
</span>
|
||||
<span style={{ fg: theme.textMuted }}> · {model()}</span>
|
||||
<Show when={duration()}>
|
||||
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
|
||||
</Show>
|
||||
</text>
|
||||
</box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1851,19 +1868,13 @@ function Shell(props: ToolProps) {
|
||||
const color = createMemo(() => (permission() ? theme.warning : theme.text))
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
const command = createMemo(() => stringValue(props.input.command))
|
||||
const output = createMemo(() => {
|
||||
if (props.part.state.status === "pending") return ""
|
||||
const content = props.part.state.content[0]
|
||||
return stripAnsi(content?.type === "text" ? content.text.trim() : "")
|
||||
})
|
||||
const output = createMemo(() => stripAnsi(stringValue(props.metadata.output)?.trim() ?? ""))
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const maxLines = 10
|
||||
const maxChars = createMemo(() => maxLines * Math.max(20, ctx.width - 6))
|
||||
const input = createMemo(() => (command() ? `${isRunning() ? "" : "$ "}${command()}` : ""))
|
||||
const content = createMemo(() => [input(), output()].filter(Boolean).join("\n\n"))
|
||||
const collapsed = createMemo(() => collapseToolOutput(content(), maxLines, maxChars()))
|
||||
const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars()))
|
||||
const limited = createMemo(() => {
|
||||
if (expanded() || !collapsed().overflow) return content()
|
||||
if (expanded() || !collapsed().overflow) return output()
|
||||
return collapsed().output
|
||||
})
|
||||
|
||||
@@ -1871,21 +1882,13 @@ function Shell(props: ToolProps) {
|
||||
<BlockTool part={props.part} onClick={collapsed().overflow ? () => setExpanded((prev) => !prev) : undefined}>
|
||||
<box gap={1}>
|
||||
<Show when={command()} fallback={<Spinner color={color()}>Writing command...</Spinner>}>
|
||||
<Show
|
||||
when={isRunning()}
|
||||
fallback={
|
||||
<text>
|
||||
<span style={{ fg: theme.text }}>{limited().slice(0, input().length)}</span>
|
||||
<span style={{ fg: theme.textMuted }}>{limited().slice(input().length)}</span>
|
||||
</text>
|
||||
}
|
||||
>
|
||||
<Spinner color={color()}>
|
||||
<span style={{ fg: theme.text }}>{limited().slice(0, input().length)}</span>
|
||||
<span style={{ fg: theme.textMuted }}>{limited().slice(input().length)}</span>
|
||||
</Spinner>
|
||||
<Show when={isRunning()} fallback={<text fg={permission() ? theme.warning : theme.textMuted}>$ {command()}</text>}>
|
||||
<Spinner color={color()}>{command()}</Spinner>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={output()}>
|
||||
<text fg={theme.text}>{limited()}</text>
|
||||
</Show>
|
||||
<Show when={collapsed().overflow}>
|
||||
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
||||
</Show>
|
||||
|
||||
@@ -145,7 +145,6 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
||||
export function reduceSessionRows(messages: SessionMessage[]) {
|
||||
return messages.reduce<SessionRow[]>((rows, message) => {
|
||||
if (message.type !== "assistant") {
|
||||
completePrevious(rows)
|
||||
rows.push({ type: "message", messageID: message.id })
|
||||
return rows
|
||||
}
|
||||
@@ -153,10 +152,8 @@ export function reduceSessionRows(messages: SessionMessage[]) {
|
||||
if ((part.type === "text" || part.type === "reasoning") && !part.text.trim()) return
|
||||
append(rows, { messageID: message.id, partID: part.id }, part)
|
||||
})
|
||||
if ((message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error) {
|
||||
completePrevious(rows)
|
||||
if ((message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error)
|
||||
rows.push({ type: "assistant-footer", messageID: message.id })
|
||||
}
|
||||
return rows
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ export function collapseToolOutput(output: string, maxLines: number, maxChars: n
|
||||
return { output, overflow: false }
|
||||
}
|
||||
|
||||
const visible = lines.slice(0, maxLines)
|
||||
if (lines.length > maxLines && visible.length > 0) visible[visible.length - 1] += "…"
|
||||
const preview = visible.join("\n")
|
||||
const preview = lines.slice(0, maxLines).join("\n")
|
||||
if (Array.from(preview).length > maxChars) {
|
||||
return {
|
||||
output:
|
||||
@@ -17,5 +15,5 @@ export function collapseToolOutput(output: string, maxLines: number, maxChars: n
|
||||
}
|
||||
}
|
||||
|
||||
return { output: preview, overflow: true }
|
||||
return { output: [...lines.slice(0, maxLines), "…"].join("\n"), overflow: true }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import Notifications from "../../../../src/feature-plugins/system/notifications"
|
||||
import type { PermissionRequest, QuestionRequest, Session, V2Event } from "@opencode-ai/sdk/v2"
|
||||
import type { Event, PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2"
|
||||
import type { TuiAttentionNotifyInput } from "@opencode-ai/plugin/tui"
|
||||
import { createTuiPluginApi } from "../../../fixture/tui-plugin"
|
||||
|
||||
async function setup() {
|
||||
const notifications: TuiAttentionNotifyInput[] = []
|
||||
const handlers = new Map<V2Event["type"], ((event: V2Event) => void)[]>()
|
||||
const handlers = new Map<Event["type"], ((event: Event) => void)[]>()
|
||||
const session = (id: string, title: string, parentID?: string): Session => ({
|
||||
id,
|
||||
title,
|
||||
@@ -33,12 +33,9 @@ async function setup() {
|
||||
},
|
||||
},
|
||||
event: {
|
||||
on: <Type extends V2Event["type"]>(
|
||||
type: Type,
|
||||
handler: (event: Extract<V2Event, { type: Type }>) => void,
|
||||
) => {
|
||||
on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, { type: Type }>) => void) => {
|
||||
const list = handlers.get(type) ?? []
|
||||
const wrapped = handler as (event: V2Event) => void
|
||||
const wrapped = handler as (event: Event) => void
|
||||
list.push(wrapped)
|
||||
handlers.set(type, list)
|
||||
return () => {
|
||||
@@ -61,7 +58,7 @@ async function setup() {
|
||||
|
||||
return {
|
||||
notifications,
|
||||
emit(event: V2Event) {
|
||||
emit(event: Event) {
|
||||
for (const handler of handlers.get(event.type) ?? []) handler(event)
|
||||
},
|
||||
}
|
||||
@@ -86,11 +83,11 @@ function permission(id: string, sessionID = "session"): PermissionRequest {
|
||||
}
|
||||
}
|
||||
|
||||
function stepStarted(id: string, sessionID = "session"): V2Event {
|
||||
function stepStarted(id: string, sessionID = "session"): Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.started",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
@@ -100,11 +97,11 @@ function stepStarted(id: string, sessionID = "session"): V2Event {
|
||||
}
|
||||
}
|
||||
|
||||
function stepEnded(id: string, sessionID = "session", finish = "stop"): V2Event {
|
||||
function stepEnded(id: string, sessionID = "session", finish = "stop"): Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.ended",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
@@ -115,11 +112,11 @@ function stepEnded(id: string, sessionID = "session", finish = "stop"): V2Event
|
||||
}
|
||||
}
|
||||
|
||||
function stepFailed(id: string, sessionID = "session"): V2Event {
|
||||
function stepFailed(id: string, sessionID = "session"): Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.failed",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
@@ -146,8 +143,8 @@ describe("internal notifications TUI plugin", () => {
|
||||
test("notifies for question and permission requests with blurred notifications and always-on sounds", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({ id: "event-1", type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-2", type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1") })
|
||||
harness.emit({ id: "event-2", type: "permission.asked", properties: permission("permission-1") })
|
||||
|
||||
expect(harness.notifications).toEqual([questionNotification, permissionNotification])
|
||||
})
|
||||
@@ -155,23 +152,23 @@ describe("internal notifications TUI plugin", () => {
|
||||
test("dedupes pending questions and permissions until they are resolved", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({ id: "event-1", type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-2", type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1") })
|
||||
harness.emit({ id: "event-2", type: "question.asked", properties: question("question-1") })
|
||||
harness.emit({
|
||||
id: "event-3",
|
||||
type: "question.replied",
|
||||
data: { sessionID: "session", requestID: "question-1", answers: [] },
|
||||
properties: { sessionID: "session", requestID: "question-1", answers: [] },
|
||||
})
|
||||
harness.emit({ id: "event-4", type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-4", type: "question.asked", properties: question("question-1") })
|
||||
|
||||
harness.emit({ id: "event-5", type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({ id: "event-6", type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({ id: "event-5", type: "permission.asked", properties: permission("permission-1") })
|
||||
harness.emit({ id: "event-6", type: "permission.asked", properties: permission("permission-1") })
|
||||
harness.emit({
|
||||
id: "event-7",
|
||||
type: "permission.replied",
|
||||
data: { sessionID: "session", requestID: "permission-1", reply: "once" },
|
||||
properties: { sessionID: "session", requestID: "permission-1", reply: "once" },
|
||||
})
|
||||
harness.emit({ id: "event-8", type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({ id: "event-8", type: "permission.asked", properties: permission("permission-1") })
|
||||
|
||||
expect(harness.notifications).toEqual([
|
||||
questionNotification,
|
||||
@@ -201,7 +198,7 @@ describe("internal notifications TUI plugin", () => {
|
||||
test("uses sound-only notifications and subagent_done sound for subagent sessions", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({ id: "event-1", type: "question.asked", data: question("question-1", "subagent") })
|
||||
harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1", "subagent") })
|
||||
harness.emit(stepStarted("event-2", "subagent"))
|
||||
harness.emit(stepEnded("event-3", "subagent"))
|
||||
|
||||
@@ -245,13 +242,13 @@ describe("internal notifications TUI plugin", () => {
|
||||
harness.emit({
|
||||
id: "event-2",
|
||||
type: "session.error",
|
||||
data: { sessionID: "abort", error: { name: "MessageAbortedError", data: { message: "Aborted" } } },
|
||||
properties: { sessionID: "abort", error: { name: "MessageAbortedError", data: { message: "Aborted" } } },
|
||||
})
|
||||
harness.emit(stepStarted("event-3", "timeout"))
|
||||
harness.emit({
|
||||
id: "event-4",
|
||||
type: "session.error",
|
||||
data: { sessionID: "timeout", error: { name: "UnknownError", data: { message: "SSE read timed out" } } },
|
||||
properties: { sessionID: "timeout", error: { name: "UnknownError", data: { message: "SSE read timed out" } } },
|
||||
})
|
||||
|
||||
expect(harness.notifications).toEqual([
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { collapseToolOutput } from "../../../src/util/collapse-tool-output"
|
||||
|
||||
test("limits command input and output to the same line budget", () => {
|
||||
const command = Array.from({ length: 8 }, (_, index) => `command ${index + 1}`).join("\n")
|
||||
const output = Array.from({ length: 4 }, (_, index) => `output ${index + 1}`).join("\n")
|
||||
const collapsed = collapseToolOutput(`$ ${command}\n\n${output}`, 10, 1_000)
|
||||
|
||||
expect(collapsed.overflow).toBe(true)
|
||||
expect(collapsed.output.split("\n")).toHaveLength(10)
|
||||
expect(collapsed.output).toContain("$ command 1")
|
||||
expect(collapsed.output).toContain("command 8\n\noutput 1…")
|
||||
expect(collapsed.output).not.toContain("output 2")
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { expect, test } from "bun:test"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import type { V2Event } from "@opencode-ai/sdk/v2"
|
||||
import type { Event, GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||
import { onMount } from "solid-js"
|
||||
import { ProjectProvider } from "../../../src/context/project"
|
||||
import { SDKProvider } from "../../../src/context/sdk"
|
||||
@@ -17,8 +17,12 @@ async function wait(fn: () => boolean, timeout = 2000) {
|
||||
}
|
||||
}
|
||||
|
||||
function emitEvent(events: ReturnType<typeof createEventStream>, event: V2Event) {
|
||||
events.emit({ ...event, location: { directory } })
|
||||
function global(payload: Event): GlobalEvent {
|
||||
return { directory, project: "proj_test", payload }
|
||||
}
|
||||
|
||||
function emitEvent(events: ReturnType<typeof createEventStream>, payload: Event) {
|
||||
events.emit(global(payload))
|
||||
}
|
||||
|
||||
test("refreshes resources into reactive getters", async () => {
|
||||
@@ -201,7 +205,7 @@ test("tracks session status from active sessions and execution events", async ()
|
||||
emitEvent(events, {
|
||||
id: "evt_step_started",
|
||||
type: "session.next.step.started",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID: "session-live",
|
||||
assistantMessageID: "message-live",
|
||||
timestamp: 1,
|
||||
@@ -214,7 +218,7 @@ test("tracks session status from active sessions and execution events", async ()
|
||||
emitEvent(events, {
|
||||
id: "evt_step_ended",
|
||||
type: "session.next.step.ended",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID: "session-live",
|
||||
assistantMessageID: "message-live",
|
||||
timestamp: 2,
|
||||
@@ -287,7 +291,7 @@ test("refreshes integrations after integration updates", async () => {
|
||||
expect(data.location.integration.list()).toEqual([])
|
||||
const before = { ...requests }
|
||||
|
||||
emitEvent(events, { id: "evt_integration", type: "integration.updated", data: {} })
|
||||
emitEvent(events, { id: "evt_integration", type: "integration.updated", properties: {} })
|
||||
await wait(() => data.location.integration.list()?.length === 1)
|
||||
await wait(() => requests.model > before.model && requests.provider > before.provider)
|
||||
expect(data.location.integration.list()?.[0]).toMatchObject({ id: "openai", name: "OpenAI" })
|
||||
@@ -325,7 +329,7 @@ test("refreshes effective catalog data after catalog updates", async () => {
|
||||
try {
|
||||
await wait(() => requests.model > 0 && requests.provider > 0)
|
||||
const before = { ...requests }
|
||||
emitEvent(events, { id: "evt_catalog", type: "catalog.updated", data: {} })
|
||||
emitEvent(events, { id: "evt_catalog", type: "catalog.updated", properties: {} })
|
||||
await wait(() => requests.model > before.model && requests.provider > before.provider)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
@@ -370,7 +374,7 @@ test("refreshes references after updates", async () => {
|
||||
try {
|
||||
await mounted
|
||||
await wait(() => requests === 1)
|
||||
emitEvent(events, { id: "evt_reference_1", type: "reference.updated", data: {} })
|
||||
emitEvent(events, { id: "evt_reference_1", type: "reference.updated", properties: {} })
|
||||
await wait(() => data.location.reference.list()?.length === 1)
|
||||
expect(data.location.reference.list()?.[0]?.name).toBe("docs")
|
||||
} finally {
|
||||
@@ -405,7 +409,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_permission_asked_1",
|
||||
type: "permission.v2.asked",
|
||||
data: {
|
||||
properties: {
|
||||
id: "per_1",
|
||||
sessionID: "ses_1",
|
||||
action: "bash",
|
||||
@@ -415,7 +419,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_permission_asked_2",
|
||||
type: "permission.v2.asked",
|
||||
data: {
|
||||
properties: {
|
||||
id: "per_2",
|
||||
sessionID: "ses_1",
|
||||
action: "read",
|
||||
@@ -427,7 +431,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_permission_replied_1",
|
||||
type: "permission.v2.replied",
|
||||
data: { sessionID: "ses_1", requestID: "per_1", reply: "once" },
|
||||
properties: { sessionID: "ses_1", requestID: "per_1", reply: "once" },
|
||||
})
|
||||
await wait(() => data.session.permission.list("ses_1")?.length === 1)
|
||||
expect(data.session.permission.list("ses_1")?.[0]?.id).toBe("per_2")
|
||||
@@ -435,7 +439,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_permission_replied_2",
|
||||
type: "permission.v2.replied",
|
||||
data: { sessionID: "ses_1", requestID: "per_2", reply: "reject" },
|
||||
properties: { sessionID: "ses_1", requestID: "per_2", reply: "reject" },
|
||||
})
|
||||
await wait(() => data.session.permission.list("ses_1")?.length === 0)
|
||||
} finally {
|
||||
@@ -470,7 +474,7 @@ test("adds and dismisses question requests from live events", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_question_asked_1",
|
||||
type: "question.v2.asked",
|
||||
data: {
|
||||
properties: {
|
||||
id: "que_1",
|
||||
sessionID: "ses_1",
|
||||
questions: [{ question: "Which option?", header: "Option", options: [], multiple: false }],
|
||||
@@ -479,7 +483,7 @@ test("adds and dismisses question requests from live events", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_question_asked_2",
|
||||
type: "question.v2.asked",
|
||||
data: {
|
||||
properties: {
|
||||
id: "que_2",
|
||||
sessionID: "ses_1",
|
||||
questions: [{ question: "Which environment?", header: "Environment", options: [], multiple: false }],
|
||||
@@ -490,7 +494,7 @@ test("adds and dismisses question requests from live events", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_question_replied_1",
|
||||
type: "question.v2.replied",
|
||||
data: { sessionID: "ses_1", requestID: "que_1", answers: [["First"]] },
|
||||
properties: { sessionID: "ses_1", requestID: "que_1", answers: [["First"]] },
|
||||
})
|
||||
await wait(() => data.session.question.list("ses_1")?.length === 1)
|
||||
expect(data.session.question.list("ses_1")?.[0]?.id).toBe("que_2")
|
||||
@@ -498,7 +502,7 @@ test("adds and dismisses question requests from live events", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_question_rejected_2",
|
||||
type: "question.v2.rejected",
|
||||
data: { sessionID: "ses_1", requestID: "que_2" },
|
||||
properties: { sessionID: "ses_1", requestID: "que_2" },
|
||||
})
|
||||
await wait(() => data.session.question.list("ses_1")?.length === 0)
|
||||
} finally {
|
||||
@@ -538,12 +542,12 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_agent_1",
|
||||
type: "session.next.agent.switched",
|
||||
data: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" },
|
||||
properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" },
|
||||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_model_1",
|
||||
type: "session.next.model.switched",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_model_1",
|
||||
timestamp: 0,
|
||||
@@ -553,7 +557,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_step_started_1",
|
||||
type: "session.next.step.started",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
timestamp: 1,
|
||||
@@ -564,7 +568,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_input_1",
|
||||
type: "session.next.tool.input.started",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
timestamp: 2,
|
||||
@@ -575,7 +579,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_called_1",
|
||||
type: "session.next.tool.called",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 2,
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
@@ -588,7 +592,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_failed_1",
|
||||
type: "session.next.tool.failed",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 3,
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
@@ -669,7 +673,7 @@ test("renders admitted prompts only after they become model-visible", async () =
|
||||
emitEvent(events, {
|
||||
id: "evt_admitted_1",
|
||||
type: "session.next.prompt.admitted",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_user_1",
|
||||
timestamp: 0,
|
||||
@@ -682,7 +686,7 @@ test("renders admitted prompts only after they become model-visible", async () =
|
||||
emitEvent(events, {
|
||||
id: "evt_prompted_1",
|
||||
type: "session.next.prompted",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_user_1",
|
||||
timestamp: 0,
|
||||
@@ -740,7 +744,7 @@ test("projects live context updates with their message ID", async () => {
|
||||
emitEvent(events, {
|
||||
id: "evt_context_1",
|
||||
type: "session.next.context.updated",
|
||||
data: {
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_context_1",
|
||||
timestamp: 1,
|
||||
|
||||
@@ -89,39 +89,6 @@ test("groups across empty assistant reasoning parts", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("completes exploration groups when another row follows", () => {
|
||||
const finished = assistant("assistant-2", [
|
||||
{ type: "tool", id: "grep-1", name: "grep", state: pending(), time: { created: 3 } },
|
||||
])
|
||||
finished.finish = "stop"
|
||||
const messages: SessionMessage[] = [
|
||||
assistant("assistant-1", [
|
||||
{ type: "tool", id: "read-1", name: "read", state: pending(), time: { created: 1 } },
|
||||
]),
|
||||
{ type: "user", id: "user-1", text: "Continue", time: { created: 2 } },
|
||||
finished,
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "read-1" }],
|
||||
},
|
||||
{ type: "message", messageID: "user-1" },
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-2", partID: "grep-1" }],
|
||||
},
|
||||
{ type: "assistant-footer", messageID: "assistant-2" },
|
||||
])
|
||||
})
|
||||
|
||||
function assistant(id: string, content: SessionMessageAssistant["content"]): SessionMessageAssistant {
|
||||
return {
|
||||
type: "assistant",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import type { OpencodeClient, V2Event } from "@opencode-ai/sdk/v2"
|
||||
import type { Event, GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||
import { onMount } from "solid-js"
|
||||
import { ProjectProvider, useProject } from "../../../src/context/project"
|
||||
import { SDKProvider, useSDK } from "../../../src/context/sdk"
|
||||
import { SDKProvider } from "../../../src/context/sdk"
|
||||
import { useEvent } from "../../../src/context/event"
|
||||
import { createClient, createEventStream, createFetch, directory } from "../../fixture/tui-sdk"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
@@ -19,40 +19,41 @@ async function wait(fn: () => boolean, timeout = 2000) {
|
||||
}
|
||||
}
|
||||
|
||||
function event(payload: V2Event, input: { directory: string; project?: string; workspace?: string }): V2Event {
|
||||
function event(payload: Event, input: { directory: string; project?: string; workspace?: string }): GlobalEvent {
|
||||
return {
|
||||
...payload,
|
||||
location: { directory: input.directory, workspaceID: input.workspace },
|
||||
directory: input.directory,
|
||||
project: input.project,
|
||||
workspace: input.workspace,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
function vcs(branch: string): V2Event {
|
||||
function vcs(branch: string): Event {
|
||||
return {
|
||||
id: `evt_vcs_${branch}`,
|
||||
type: "vcs.branch.updated",
|
||||
data: {
|
||||
properties: {
|
||||
branch,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function update(version: string): V2Event {
|
||||
function update(version: string): Event {
|
||||
return {
|
||||
id: `evt_update_${version}`,
|
||||
type: "installation.update-available",
|
||||
data: {
|
||||
properties: {
|
||||
version,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function mount(reload?: () => Promise<OpencodeClient>) {
|
||||
async function mount() {
|
||||
const events = createEventStream()
|
||||
const calls = createFetch(undefined, events)
|
||||
const seen: V2Event[] = []
|
||||
const seen: Event[] = []
|
||||
const workspaces: Array<string | undefined> = []
|
||||
let project!: ReturnType<typeof useProject>
|
||||
let sdk!: ReturnType<typeof useSDK>
|
||||
let done!: () => void
|
||||
const ready = new Promise<void>((resolve) => {
|
||||
done = resolve
|
||||
@@ -60,12 +61,11 @@ async function mount(reload?: () => Promise<OpencodeClient>) {
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider client={createClient(calls.fetch)} reload={reload}>
|
||||
<SDKProvider client={createClient(calls.fetch)}>
|
||||
<ProjectProvider>
|
||||
<Probe
|
||||
onReady={async (ctx) => {
|
||||
project = ctx.project
|
||||
sdk = ctx.sdk
|
||||
await project.sync()
|
||||
done()
|
||||
}}
|
||||
@@ -78,16 +78,15 @@ async function mount(reload?: () => Promise<OpencodeClient>) {
|
||||
))
|
||||
|
||||
await ready
|
||||
return { app, emit: events.emit, project, sdk, seen, workspaces }
|
||||
return { app, emit: events.emit, project, seen, workspaces }
|
||||
}
|
||||
|
||||
function Probe(props: {
|
||||
seen: V2Event[]
|
||||
seen: Event[]
|
||||
workspaces: Array<string | undefined>
|
||||
onReady: (ctx: { project: ReturnType<typeof useProject>; sdk: ReturnType<typeof useSDK> }) => void
|
||||
onReady: (ctx: { project: ReturnType<typeof useProject> }) => void
|
||||
}) {
|
||||
const project = useProject()
|
||||
const sdk = useSDK()
|
||||
const event = useEvent()
|
||||
|
||||
onMount(() => {
|
||||
@@ -95,7 +94,7 @@ function Probe(props: {
|
||||
props.seen.push(evt)
|
||||
props.workspaces.push(workspace)
|
||||
})
|
||||
props.onReady({ project, sdk })
|
||||
props.onReady({ project })
|
||||
})
|
||||
|
||||
return <box />
|
||||
@@ -110,7 +109,7 @@ describe("useEvent", () => {
|
||||
|
||||
await wait(() => seen.length === 1)
|
||||
|
||||
expect(seen).toEqual([event(vcs("main"), { directory: "/tmp/other", workspace: "ws_a" })])
|
||||
expect(seen).toEqual([vcs("main")])
|
||||
expect(workspaces).toEqual(["ws_a"])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
@@ -126,7 +125,7 @@ describe("useEvent", () => {
|
||||
|
||||
await wait(() => seen.length === 1)
|
||||
|
||||
expect(seen).toEqual([event(vcs("ws"), { directory: "/tmp/other", workspace: "ws_b" })])
|
||||
expect(seen).toEqual([vcs("ws")])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
@@ -141,54 +140,7 @@ describe("useEvent", () => {
|
||||
|
||||
await wait(() => seen.length === 1)
|
||||
|
||||
expect(seen).toEqual([event(update("1.2.3"), { directory: "global" })])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("reloads the host and reconnects the event stream", async () => {
|
||||
let calls = 0
|
||||
const events = createEventStream()
|
||||
const replacement = createClient(createFetch(undefined, events).fetch)
|
||||
const { app, sdk, seen } = await mount(async () => {
|
||||
calls += 1
|
||||
return replacement
|
||||
})
|
||||
|
||||
try {
|
||||
await wait(() => sdk.connection.status() === "connected")
|
||||
await sdk.reload?.()
|
||||
await wait(() => sdk.connection.status() === "connected")
|
||||
events.emit(event(vcs("reloaded"), { directory: "/tmp/reloaded" }))
|
||||
await wait(() => seen.some((item) => item.type === "vcs.branch.updated" && item.data.branch === "reloaded"))
|
||||
|
||||
expect(calls).toBe(1)
|
||||
expect(sdk.client).toBe(replacement)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("keeps the current event stream alive while the host reload is pending", async () => {
|
||||
let complete!: (client: OpencodeClient) => void
|
||||
const pending = new Promise<OpencodeClient>((resolve) => {
|
||||
complete = resolve
|
||||
})
|
||||
const replacementEvents = createEventStream()
|
||||
const replacement = createClient(createFetch(undefined, replacementEvents).fetch)
|
||||
const { app, emit, sdk, seen } = await mount(() => pending)
|
||||
|
||||
try {
|
||||
await wait(() => sdk.connection.status() === "connected")
|
||||
const reload = sdk.reload?.()
|
||||
emit(event(vcs("during-reload"), { directory: "/tmp/reload" }))
|
||||
await wait(() => seen.some((item) => item.type === "vcs.branch.updated" && item.data.branch === "during-reload"))
|
||||
|
||||
expect(sdk.connection.status()).toBe("connected")
|
||||
complete(replacement)
|
||||
await reload
|
||||
expect(sdk.client).toBe(replacement)
|
||||
expect(seen).toEqual([update("1.2.3")])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import type { V2Event } from "@opencode-ai/sdk/v2"
|
||||
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export const worktree = "/tmp/opencode"
|
||||
export const directory = `${worktree}/packages/tui`
|
||||
@@ -13,8 +13,12 @@ export function json(data: unknown, init?: ResponseInit) {
|
||||
|
||||
export function createEventStream() {
|
||||
const encoder = new TextEncoder()
|
||||
const global = new Set<ReadableStreamDefaultController<Uint8Array>>()
|
||||
const v2 = new Set<ReadableStreamDefaultController<Uint8Array>>()
|
||||
const pending: Uint8Array[] = []
|
||||
const pending = {
|
||||
global: [] as Uint8Array[],
|
||||
v2: [] as Uint8Array[],
|
||||
}
|
||||
const response = (
|
||||
controllers: Set<ReadableStreamDefaultController<Uint8Array>>,
|
||||
queued: Uint8Array[],
|
||||
@@ -50,14 +54,24 @@ export function createEventStream() {
|
||||
}
|
||||
|
||||
return {
|
||||
emit(event: V2Event) {
|
||||
send(v2, pending, event)
|
||||
emit(event: GlobalEvent) {
|
||||
send(global, pending.global, event)
|
||||
if (!("properties" in event.payload)) return
|
||||
send(v2, pending.v2, {
|
||||
...event.payload,
|
||||
location: { directory: event.directory, workspaceID: event.workspace },
|
||||
data: event.payload.properties,
|
||||
})
|
||||
},
|
||||
global() {
|
||||
return response(global, pending.global)
|
||||
},
|
||||
v2() {
|
||||
return response(v2, pending, { id: "evt_connected", type: "server.connected", data: {} })
|
||||
return response(v2, pending.v2, { id: "evt_connected", type: "server.connected", data: {} })
|
||||
},
|
||||
disconnect() {
|
||||
for (const controller of v2) controller.close()
|
||||
for (const controller of [...global, ...v2]) controller.close()
|
||||
global.clear()
|
||||
v2.clear()
|
||||
},
|
||||
}
|
||||
@@ -72,6 +86,7 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
|
||||
if (url.pathname === "/session") session.push(url)
|
||||
const overridden = await override?.(url)
|
||||
if (overridden) return overridden
|
||||
if (url.pathname === "/global/event" && events) return events.global()
|
||||
if (url.pathname === "/api/event" && events) return events.v2()
|
||||
|
||||
if (
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": []
|
||||
},
|
||||
"@opencode-ai/tui#test": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": []
|
||||
},
|
||||
"@opencode-ai/app#test": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": []
|
||||
|
||||
Reference in New Issue
Block a user