From 709af586129ef659488a466ab2e3551b8b75d312 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:17:11 -0500 Subject: [PATCH 01/34] fix(core): stop after declined permissions (#35356) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/core/src/permission.ts | 19 +-- packages/core/src/session/runner/llm.ts | 13 +- packages/core/test/permission.test.ts | 24 +++- packages/core/test/session-runner.test.ts | 142 ++++++++++++++++++++ packages/core/test/tool-apply-patch.test.ts | 2 +- packages/core/test/tool-bash.test.ts | 2 +- packages/core/test/tool-edit.test.ts | 2 +- packages/core/test/tool-question.test.ts | 2 +- packages/core/test/tool-read.test.ts | 2 +- packages/core/test/tool-skill.test.ts | 2 +- packages/core/test/tool-todowrite.test.ts | 2 +- packages/core/test/tool-write.test.ts | 2 +- 12 files changed, 190 insertions(+), 24 deletions(-) diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index 95219e15148e..3f28632a034d 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -57,13 +57,13 @@ export type AskResult = typeof AskResult.Type export const Event = Permission.Event -export class RejectedError extends Schema.TaggedErrorClass()("PermissionV2.RejectedError", {}) {} +export class DeclinedError extends Schema.TaggedErrorClass()("PermissionV2.DeclinedError", {}) {} export class CorrectedError extends Schema.TaggedErrorClass()("PermissionV2.CorrectedError", { feedback: Schema.String, }) {} -export class DeniedError extends Schema.TaggedErrorClass()("PermissionV2.DeniedError", { +export class BlockedError extends Schema.TaggedErrorClass()("PermissionV2.BlockedError", { rules: Permission.Ruleset, }) {} @@ -71,7 +71,7 @@ export class NotFoundError extends Schema.TaggedErrorClass()("Per requestID: ID, }) {} -export type Error = DeniedError | RejectedError | CorrectedError +export type Error = BlockedError | CorrectedError export function evaluate(action: string, resource: string, ...rulesets: Permission.Ruleset[]): Permission.Rule { return ( @@ -103,7 +103,7 @@ export class Service extends Context.Service()("@opencode/v2 interface Pending { readonly request: Request readonly agent?: AgentV2.ID - readonly deferred: Deferred.Deferred + readonly deferred: Deferred.Deferred } const layer = Layer.effect( @@ -117,7 +117,7 @@ const layer = Layer.effect( const pending = new Map() yield* EffectRuntime.addFinalizer(() => - EffectRuntime.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), { + EffectRuntime.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new DeclinedError()), { discard: true, }).pipe( EffectRuntime.ensuring( @@ -176,7 +176,7 @@ const layer = Layer.effect( const create = (request: Request, agent?: AgentV2.ID) => EffectRuntime.uninterruptible( EffectRuntime.gen(function* () { - const deferred = yield* Deferred.make() + const deferred = yield* Deferred.make() const item = { request, agent, deferred } if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`) pending.set(request.id, item) @@ -199,13 +199,14 @@ const layer = Layer.effect( EffectRuntime.gen(function* () { const result = yield* evaluateInput(input) if (result.effect === "deny") { - return yield* new DeniedError({ + return yield* new BlockedError({ rules: relevant(input, result.rules), }) } if (result.effect === "allow") return const item = yield* create(request(input), input.agent) return yield* restore(Deferred.await(item.deferred)).pipe( + EffectRuntime.catchTag("PermissionV2.DeclinedError", (error) => EffectRuntime.die(error)), EffectRuntime.ensuring( EffectRuntime.sync(() => { pending.delete(item.request.id) @@ -230,7 +231,7 @@ const layer = Layer.effect( if (input.reply === "reject") { yield* Deferred.fail( existing.deferred, - input.message ? new CorrectedError({ feedback: input.message }) : new RejectedError(), + input.message ? new CorrectedError({ feedback: input.message }) : new DeclinedError(), ) pending.delete(input.requestID) for (const [id, item] of pending) { @@ -240,7 +241,7 @@ const layer = Layer.effect( requestID: item.request.id, reply: "reject", }) - yield* Deferred.fail(item.deferred, new RejectedError()) + yield* Deferred.fail(item.deferred, new DeclinedError()) pending.delete(id) } return diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 94c77d19e987..72c761e10d93 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -15,6 +15,7 @@ import { Database } from "../../database/database" import { EventV2 } from "../../event" import { Location } from "../../location" import { ModelV2 } from "../../model" +import { PermissionV2 } from "../../permission" import { ProviderV2 } from "../../provider" import { QuestionV2 } from "../../question" import { SystemContext } from "../../system-context/index" @@ -140,9 +141,13 @@ const layer = Layer.effect( const awaitToolFibers = (fibers: FiberSet.FiberSet) => Effect.raceFirst(FiberSet.join(fibers), FiberSet.awaitEmpty(fibers)) - // Match V1: dismissing a question halts the loop instead of becoming model-facing tool output. - const isQuestionRejected = (cause: Cause.Cause) => - cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect instanceof QuestionV2.RejectedError) + // Match V1: declining a user prompt halts the loop instead of becoming model-facing tool output. + const isUserDeclined = (cause: Cause.Cause) => + cause.reasons.some( + (reason) => + Cause.isDieReason(reason) && + (reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionV2.RejectedError), + ) type TurnTransition = // Automatic compaction completed; rebuild the request from compacted history. @@ -289,7 +294,7 @@ const layer = Layer.effect( } if (stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)) yield* FiberSet.clear(toolFibers) const settled = yield* restore(awaitToolFibers(toolFibers)).pipe(Effect.exit) - if (settled._tag === "Failure" && isQuestionRejected(settled.cause)) { + if (settled._tag === "Failure" && isUserDeclined(settled.cause)) { yield* FiberSet.clear(toolFibers) yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted")) return yield* Effect.interrupt diff --git a/packages/core/test/permission.test.ts b/packages/core/test/permission.test.ts index a556f1fce217..df87c1d0efd6 100644 --- a/packages/core/test/permission.test.ts +++ b/packages/core/test/permission.test.ts @@ -1,5 +1,5 @@ import { describe, expect } from "bun:test" -import { Deferred, Effect, Fiber, Layer } from "effect" +import { Cause, Deferred, Effect, Fiber, Layer } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -147,8 +147,8 @@ describe("PermissionV2", () => { const service = yield* PermissionV2.Service yield* service.assert(assertion()) yield* setRules([{ action: "read", resource: "*", effect: "deny" }]) - const denied = yield* service.assert(assertion()).pipe(Effect.flip) - expect(denied).toBeInstanceOf(PermissionV2.DeniedError) + const blocked = yield* service.assert(assertion()).pipe(Effect.flip) + expect(blocked).toBeInstanceOf(PermissionV2.BlockedError) expect(yield* service.list()).toEqual([]) }), ) @@ -265,6 +265,24 @@ describe("PermissionV2", () => { }), ) + it.effect("defects when an asked permission is declined", () => + Effect.gen(function* () { + yield* setup() + const { service, fiber, request } = yield* waitForRequest() + yield* service.reply({ requestID: request.id, reply: "reject" }) + const exit = yield* Fiber.await(fiber) + + expect(exit._tag).toBe("Failure") + if (exit._tag === "Failure") + expect( + exit.cause.reasons.some( + (reason) => Cause.isDieReason(reason) && reason.defect instanceof PermissionV2.DeclinedError, + ), + ).toBe(true) + expect(yield* service.list()).toEqual([]) + }), + ) + it.effect("stores and removes saved resources for a project", () => Effect.gen(function* () { yield* setup() diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 9cf313935be2..0515d55cf5be 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -2609,6 +2609,148 @@ describe("SessionRunnerLLM", () => { }), ) + it.effect("returns policy-blocked tools to the model and continues", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const registry = yield* ToolRegistry.Service + yield* registry.register({ + blocked: Tool.make({ + description: "Fail because policy blocked execution", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: () => + Effect.fail(new PermissionV2.BlockedError({ rules: [] })).pipe( + Effect.mapError(() => new Tool.Failure({ message: "Permission blocked" })), + ), + }), + }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call blocked" }), resume: false }) + + requests.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-blocked", name: "blocked", input: {} }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + ] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(2) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Call blocked" }, + { + type: "assistant", + content: [ + { type: "tool", id: "call-blocked", state: { status: "error", error: { message: "Permission blocked" } } }, + ], + }, + { type: "assistant", finish: "stop" }, + ]) + }), + ) + + it.effect("interrupts runner continuation when permission approval is declined", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const registry = yield* ToolRegistry.Service + yield* registry.register({ + declined: Tool.make({ + description: "Fail because the user declined approval", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: () => Effect.die(new PermissionV2.DeclinedError()), + }), + }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call declined" }), resume: false }) + + requests.length = 0 + response = [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-declined", name: "declined", input: {} }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ] + + const exit = yield* session.resume(sessionID).pipe(Effect.exit) + + expect(exit._tag).toBe("Failure") + if (exit._tag === "Failure") expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true) + expect(requests).toHaveLength(1) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Call declined" }, + { + type: "assistant", + content: [ + { + type: "tool", + id: "call-declined", + state: { status: "error", error: { message: "Tool execution interrupted" } }, + }, + ], + }, + ]) + }), + ) + + it.effect("returns permission corrections to the model and continues", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const registry = yield* ToolRegistry.Service + yield* registry.register({ + corrected: Tool.make({ + description: "Fail with user correction feedback", + input: Schema.Struct({}), + output: Schema.Struct({}), + execute: () => + Effect.fail(new PermissionV2.CorrectedError({ feedback: "Use another tool" })).pipe( + Effect.mapError(() => new Tool.Failure({ message: "Use another tool" })), + ), + }), + }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call corrected" }), resume: false }) + + requests.length = 0 + responses = [ + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.toolCall({ id: "call-corrected", name: "corrected", input: {} }), + LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }), + LLMEvent.finish({ reason: "tool-calls" }), + ], + [ + LLMEvent.stepStart({ index: 0 }), + LLMEvent.stepFinish({ index: 0, reason: "stop" }), + LLMEvent.finish({ reason: "stop" }), + ], + ] + + yield* session.resume(sessionID) + + expect(requests).toHaveLength(2) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Call corrected" }, + { + type: "assistant", + content: [ + { type: "tool", id: "call-corrected", state: { status: "error", error: { message: "Use another tool" } } }, + ], + }, + { type: "assistant", finish: "stop" }, + ]) + }), + ) + it.effect("interrupts runner continuation when a question is dismissed", () => Effect.gen(function* () { yield* setup diff --git a/packages/core/test/tool-apply-patch.test.ts b/packages/core/test/tool-apply-patch.test.ts index b986d4972a33..542a08d4a64b 100644 --- a/packages/core/test/tool-apply-patch.test.ts +++ b/packages/core/test/tool-apply-patch.test.ts @@ -40,7 +40,7 @@ const permission = Layer.succeed( }).pipe( Effect.andThen(input.action === "edit" ? Effect.suspend(afterEditApproval) : Effect.void), Effect.andThen( - input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void, + input.action === denyAction ? Effect.fail(new PermissionV2.BlockedError({ rules: [] })) : Effect.void, ), ), ask: () => Effect.die("unused"), diff --git a/packages/core/test/tool-bash.test.ts b/packages/core/test/tool-bash.test.ts index 3a969e9570e1..1e67abe2bad9 100644 --- a/packages/core/test/tool-bash.test.ts +++ b/packages/core/test/tool-bash.test.ts @@ -51,7 +51,7 @@ const permission = Layer.succeed( Effect.sync(() => assertions.push(input)).pipe( Effect.andThen(Effect.suspend(() => afterPermission(input))), Effect.andThen( - input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void, + input.action === denyAction ? Effect.fail(new PermissionV2.BlockedError({ rules: [] })) : Effect.void, ), ), ask: () => Effect.die("unused"), diff --git a/packages/core/test/tool-edit.test.ts b/packages/core/test/tool-edit.test.ts index 2c684523b799..a9e4cbae4952 100644 --- a/packages/core/test/tool-edit.test.ts +++ b/packages/core/test/tool-edit.test.ts @@ -33,7 +33,7 @@ const permission = Layer.succeed( assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen( - input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void, + input.action === denyAction ? Effect.fail(new PermissionV2.BlockedError({ rules: [] })) : Effect.void, ), ), ask: () => Effect.die("unused"), diff --git a/packages/core/test/tool-question.test.ts b/packages/core/test/tool-question.test.ts index 5c5c95da6166..d76e15388252 100644 --- a/packages/core/test/tool-question.test.ts +++ b/packages/core/test/tool-question.test.ts @@ -22,7 +22,7 @@ const permission = Layer.succeed( PermissionV2.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( - Effect.andThen(deny ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void), + Effect.andThen(deny ? Effect.fail(new PermissionV2.BlockedError({ rules: [] })) : Effect.void), ), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), diff --git a/packages/core/test/tool-read.test.ts b/packages/core/test/tool-read.test.ts index 3c94888d57f0..29b32bd03dcb 100644 --- a/packages/core/test/tool-read.test.ts +++ b/packages/core/test/tool-read.test.ts @@ -64,7 +64,7 @@ const permission = Layer.succeed( assert: (input) => Effect.sync(() => { assertions.push(input) - }).pipe(Effect.andThen(allow ? Effect.void : Effect.fail(new PermissionV2.DeniedError({ rules: [] })))), + }).pipe(Effect.andThen(allow ? Effect.void : Effect.fail(new PermissionV2.BlockedError({ rules: [] })))), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), get: () => Effect.die("unused"), diff --git a/packages/core/test/tool-skill.test.ts b/packages/core/test/tool-skill.test.ts index 4831acb79932..23c1b843b493 100644 --- a/packages/core/test/tool-skill.test.ts +++ b/packages/core/test/tool-skill.test.ts @@ -47,7 +47,7 @@ describe("SkillTool", () => { PermissionV2.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( - Effect.andThen(deny ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void), + Effect.andThen(deny ? Effect.fail(new PermissionV2.BlockedError({ rules: [] })) : Effect.void), ), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), diff --git a/packages/core/test/tool-todowrite.test.ts b/packages/core/test/tool-todowrite.test.ts index 170d1230a3f5..6139be0b6041 100644 --- a/packages/core/test/tool-todowrite.test.ts +++ b/packages/core/test/tool-todowrite.test.ts @@ -26,7 +26,7 @@ const permission = Layer.succeed( PermissionV2.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( - Effect.andThen(deny ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void), + Effect.andThen(deny ? Effect.fail(new PermissionV2.BlockedError({ rules: [] })) : Effect.void), ), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), diff --git a/packages/core/test/tool-write.test.ts b/packages/core/test/tool-write.test.ts index a45bf73ee440..76c924c89025 100644 --- a/packages/core/test/tool-write.test.ts +++ b/packages/core/test/tool-write.test.ts @@ -31,7 +31,7 @@ const permission = Layer.succeed( assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen( - input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void, + input.action === denyAction ? Effect.fail(new PermissionV2.BlockedError({ rules: [] })) : Effect.void, ), ), ask: () => Effect.die("unused"), From a8983bd2c7075e029bcebae74fdfa34834b906bc Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:17:30 -0500 Subject: [PATCH 02/34] feat(codemode): add OpenAPI tool adapter (#35192) --- packages/codemode/AGENTS.md | 8 + packages/codemode/README.md | 27 + packages/codemode/src/index.ts | 1 + packages/codemode/src/openapi/TODO.md | 19 + packages/codemode/src/openapi/index.ts | 130 + packages/codemode/src/openapi/runtime.ts | 324 + packages/codemode/src/openapi/spec.ts | 507 + packages/codemode/src/openapi/types.ts | 112 + packages/codemode/src/token.ts | 10 - packages/codemode/src/tool-runtime.ts | 80 +- packages/codemode/src/tool.ts | 72 +- .../test/fixtures/openapi-happy-path.json | 245 + .../test/fixtures/opencode-v2-openapi.json | 26962 ++++++++++++++++ packages/codemode/test/openapi.test.ts | 958 + packages/codemode/test/signature.test.ts | 67 +- packages/protocol/src/groups/pty.ts | 1 + 16 files changed, 29461 insertions(+), 62 deletions(-) create mode 100644 packages/codemode/src/openapi/TODO.md create mode 100644 packages/codemode/src/openapi/index.ts create mode 100644 packages/codemode/src/openapi/runtime.ts create mode 100644 packages/codemode/src/openapi/spec.ts create mode 100644 packages/codemode/src/openapi/types.ts delete mode 100644 packages/codemode/src/token.ts create mode 100644 packages/codemode/test/fixtures/openapi-happy-path.json create mode 100644 packages/codemode/test/fixtures/opencode-v2-openapi.json create mode 100644 packages/codemode/test/openapi.test.ts diff --git a/packages/codemode/AGENTS.md b/packages/codemode/AGENTS.md index 88dd9c81d911..df812c6d86f2 100644 --- a/packages/codemode/AGENTS.md +++ b/packages/codemode/AGENTS.md @@ -5,6 +5,14 @@ - Keep Code Mode unaware of host session, channel, and conversation models. The hosting application supplies trusted execution scope around it. - Tool schemas are the model-facing Interface. Keep arguments minimal and natural to the operation; never add unrelated IDs as ambient capability tokens. +## OpenAPI + +- Generate an operation only when its transport semantics are supported; otherwise return a precise `skipped` reason. +- Never guess parameter serialization or malformed security semantics. Unsupported serialization is skipped and malformed security fails closed. +- Render unresolved schema constructs as `unknown`, never as invented TypeScript names. +- Keep network reads bounded and map expected encoding, transport, and decoding failures to model-safe `ToolError` values. +- Test supported behavior directly; do not reproduce adapter algorithms in tests. + ## Future Design Notes - If a captured user-visible output channel returns (an earlier `output.text`/`output.file`/`output.image` API was removed from v1), keep `output` as its name, distinct from the program return value: `return` stays the structured result for the model, while `output.*` describes artifacts the host may render into a conversation or UI after execution. Keep this host-neutral and let applications decide how captured output is delivered. In v1, hosts collect media host-side (outside the sandbox) instead. diff --git a/packages/codemode/README.md b/packages/codemode/README.md index 3f1f641b3fdc..bd14a24fb08f 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -152,6 +152,33 @@ interface ExecuteFailure { `onToolCallEnd` receives `{ index, name, input, durationMs, outcome, message? }` when an admitted call settles. `outcome` is `"success"` or `"failure"`; `message` is the model-safe failure message and is present only on failure. Interrupted calls (for example when the execution timeout fires) do not produce an end event. Both hooks are Effect-returning and must not fail. +### OpenAPI tools + +`OpenAPI.fromSpec` turns an OpenAPI 3.x document into a tool subtree - one tool per operation. Dotted `operationId` values form namespaces such as `v2.session.get`. Missing IDs receive a flat method/path fallback such as `getUsersById`; names are sanitized and deduplicated. The host places the subtree under a key in its `tools` tree; that key is the model-visible namespace. + +```ts +import { CodeMode, OpenAPI } from "@opencode-ai/codemode" +import { Effect } from "effect" +import { FetchHttpClient } from "effect/unstable/http" + +const api = OpenAPI.fromSpec({ + spec: await Bun.file("openapi.json").json(), // parsed document (no YAML) + auth: { + resolve: ({ name, scopes, operation }) => + name === "BearerAuth" + ? Effect.succeed({ type: "bearer", token }) + : Effect.succeed(undefined), + }, +}) + +const runtime = CodeMode.make({ tools: { opencode: api.tools } }) +const result = await Effect.runPromise(runtime.execute(code).pipe(Effect.provide(FetchHttpClient.layer))) +``` + +`fromSpec` is synchronous and returns `{ tools, skipped }`. The initial adapter supports query `form`/`deepObject`, path/header `simple`, JSON request bodies, JSON responses, and text responses; unsupported parameter encodings, non-JSON request bodies, binary responses, and streaming operations land in `skipped` instead of producing broken tools. Operation and path servers take precedence over document servers unless `baseUrl` explicitly overrides all of them. Tool inputs flatten path, query, header, and closed object-body fields into one model-facing object while retaining their HTTP locations internally. Cross-location name collisions receive a location prefix such as `path_id` and `query_id`; composed, nullable, dictionary, conditionally-required, and non-object JSON bodies remain under `body`. Auth is never model-visible. Responses are limited to 50 MiB, and non-2xx responses become safe tool failures carrying the status and a size-capped body summary. Deferred capabilities are tracked in `src/openapi/TODO.md`. + +Supported bearer, basic, header, and query authentication follows OpenAPI `security` semantics and is resolved host-side via `auth.resolve` - credential storage, OAuth flows, and token refresh never enter the compiler. Cookie authentication alternatives are discarded; an operation is skipped when it has no supported alternative. See the option docstrings in `src/openapi/types.ts` for the full semantics. Generated tools require `HttpClient.HttpClient` (from `effect/unstable/http`) in the Effect environment - provide `FetchHttpClient.layer` or a custom/test client layer at execution. The supplied client owns redirect policy; credentialed hosts should reject redirects or strip credentials when the origin changes. + ## Discovery The agent-tool instructions use a budgeted catalog. Every tool namespace is always listed with its tool count regardless of budget, and as many complete tool signatures (each with a one-line description) as fit an estimated-token budget are inlined. Selection is round-robin across namespaces for fairness: in each round (namespaces alphabetical), every namespace still holding un-inlined tools attempts to place its next-cheapest signature line against the shared budget, and a namespace whose next line does not fit drops out while the others keep going - so every namespace gets some representation before any namespace gets everything. The instructions state exactly how comprehensive the list is, both overall (`COMPLETE list` vs `PARTIAL - N of M shown`) and per namespace (`(3 tools)`, `(3 tools, 1 shown)`, `(3 tools, none shown)`). diff --git a/packages/codemode/src/index.ts b/packages/codemode/src/index.ts index 96629f486e3c..1aea6644da27 100644 --- a/packages/codemode/src/index.ts +++ b/packages/codemode/src/index.ts @@ -1,5 +1,6 @@ export { ToolError, CodeMode, ExecuteInputSchema, ExecuteResultSchema, toolError } from "./codemode.js" export { Tool } from "./tool.js" +export * as OpenAPI from "./openapi/index.js" export type { Definition as ToolDefinition, JsonSchema, ToolSchema } from "./tool.js" export type { ToolCallEnded, ToolCallHooks } from "./tool-runtime.js" export type { diff --git a/packages/codemode/src/openapi/TODO.md b/packages/codemode/src/openapi/TODO.md new file mode 100644 index 000000000000..cbcfe81a68c8 --- /dev/null +++ b/packages/codemode/src/openapi/TODO.md @@ -0,0 +1,19 @@ +# OpenAPI Follow-ups + +The initial adapter intentionally skips operations it cannot execute correctly. Future work may add: + +- Cookie parameters, authentication, and cookie-header merging. +- Matrix, label, space-delimited, pipe-delimited, `allowReserved`, and parameter `content` serialization. +- External references and complete nested `$defs` support. +- Relative or templated server URLs and server variables. +- Base URLs containing query strings or fragments. +- Runtime response-schema validation and full content negotiation. +- Binary response values and explicit byte-oriented return types. +- Request/response projection for `readOnly` and `writeOnly` properties. +- SSE, WebSocket, and other streaming transports. +- Recovery of responses rejected by a status-filtering `HttpClient`. +- Configurable request and response size limits. +- Adapter-enforced redirect policy independent of the supplied `HttpClient`. +- Strict UTF-8 and empty-body validation for JSON responses. +- Compile-time rejection of parameter schemas with nested values unsupported by their serialization style; runtime rejects them before auth resolution. +- Complete malformed-security-scheme validation and broader auth-combination coverage. diff --git a/packages/codemode/src/openapi/index.ts b/packages/codemode/src/openapi/index.ts new file mode 100644 index 000000000000..4ceda5eef005 --- /dev/null +++ b/packages/codemode/src/openapi/index.ts @@ -0,0 +1,130 @@ +import { HttpClient } from "effect/unstable/http" +import { Tool, type Definition } from "../tool.js" +import { invoke } from "./runtime.js" +import { + componentDefinitions, + inputSchema, + isRecord, + methods, + nonEmptyString, + operationInput, + operationOutput, + operationPath, + operationSecurityRequirements, + securityRequirements, + securitySchemes, + specServerUrl, + validateBaseUrl, +} from "./spec.js" +import type { Operation, Options, Result, Skipped, Tools } from "./types.js" + +export type { + AuthResolver, + Credential, + Document, + Operation, + Options, + Result, + SecurityScheme, + Skipped, + Tools, +} from "./types.js" + +/** + * Builds a CodeMode tool subtree from an OpenAPI 3.x document, one tool per + * operation. Auth is resolved host-side via `auth.resolve` and never + * model-visible. Tools require `HttpClient.HttpClient`; unrepresentable + * operations land in `skipped`. + */ +export const fromSpec = (options: Options): Result => { + const document = options.spec + const schemes = securitySchemes(document) + const defaultSecurity = securityRequirements(document.security) + const definitions = componentDefinitions(document) + const paths = isRecord(document.paths) ? document.paths : {} + const used = new Set() + const namespaces = new Set() + const skipped: Array = [] + const tools = Object.create(null) as Tools + + for (const [path, pathValue] of Object.entries(paths)) { + if (!isRecord(pathValue)) continue + for (const [method, operationValue] of Object.entries(pathValue)) { + if (!methods.has(method) || !isRecord(operationValue)) continue + const segments = operationPath(method, path, operationValue, used, namespaces) + const operation: Operation = { + operationId: nonEmptyString(operationValue.operationId), + method: method.toUpperCase(), + path, + summary: nonEmptyString(operationValue.summary), + description: nonEmptyString(operationValue.description), + } + const output = operationOutput(document, operationValue, definitions) + if (!output.ok) { + skipped.push({ method: operation.method, path, reason: output.reason }) + continue + } + + const resolvedBaseUrl = (() => { + if (options.baseUrl !== undefined) return validateBaseUrl(options.baseUrl) + if (operationValue.servers !== undefined) return specServerUrl(operationValue) + if (pathValue.servers !== undefined) return specServerUrl(pathValue) + return specServerUrl(document) + })() + if (!resolvedBaseUrl.ok) { + skipped.push({ method: operation.method, path, reason: resolvedBaseUrl.reason }) + continue + } + const parsedInput = operationInput(document, pathValue, operationValue) + if (!parsedInput.ok) { + skipped.push({ method: operation.method, path, reason: parsedInput.reason }) + continue + } + const input = parsedInput.value + + const security = operationSecurityRequirements(operationValue.security, defaultSecurity, schemes) + if (!security.ok) { + skipped.push({ method: operation.method, path, reason: security.reason }) + continue + } + const plan = { + operation, + url: `${resolvedBaseUrl.value.replace(/\/+$/, "")}${path}`, + fields: input.fields, + body: input.body, + security: security.value, + schemes, + auth: options.auth, + headers: options.headers ?? {}, + } + used.add(segments.join(".")) + for (const index of segments.slice(0, -1).keys()) namespaces.add(segments.slice(0, index + 1).join(".")) + setTool( + tools, + segments, + Tool.make({ + description: operation.description ?? operation.summary ?? `${operation.method} ${path}`, + input: inputSchema(input.fields, definitions), + output: output.value, + run: (input) => invoke(plan, input), + }), + ) + } + } + + return { tools, skipped } +} + +const setTool = (tools: Tools, path: ReadonlyArray, definition: Definition): void => { + const [head, ...rest] = path + if (head === undefined) return + if (rest.length === 0) { + tools[head] = definition + return + } + const child = tools[head] + if (child === undefined || !isRecord(child) || child._tag === "CodeModeTool") { + tools[head] = Object.create(null) as Tools + } + setTool(tools[head] as Tools, rest, definition) +} diff --git a/packages/codemode/src/openapi/runtime.ts b/packages/codemode/src/openapi/runtime.ts new file mode 100644 index 000000000000..47312ae1623b --- /dev/null +++ b/packages/codemode/src/openapi/runtime.ts @@ -0,0 +1,324 @@ +import { Effect, Option, Schema, Stream } from "effect" +import { HttpClient, HttpClientRequest, HttpClientResponse, type HttpMethod } from "effect/unstable/http" +import { ToolError, toolError } from "../tool-error.js" +import { isRecord, own } from "./spec.js" +import type { AppliedAuth, Credential, Plan, SecurityScheme } from "./types.js" + +const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) +const maxErrorBodyChars = 1_024 +const maxResponseBodyBytes = 50 * 1024 * 1024 + +export const invoke = (plan: Plan, input: unknown): Effect.Effect => + Effect.gen(function* () { + const value = isRecord(input) ? input : {} + + let request = yield* buildRequest(plan, value) + + const auth = yield* resolveAuth(plan) + for (const [name, item] of Object.entries(auth.query)) { + request = HttpClientRequest.setUrlParam(request, name, item) + } + request = HttpClientRequest.setHeaders(request, auth.headers) + + const client = yield* HttpClient.HttpClient + const response = yield* client + .execute(request) + .pipe( + Effect.catch((cause) => + Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} failed: transport error`, cause)), + ), + ) + const text = yield* readResponseBody(response, plan) + const mediaType = response.headers["content-type"]?.split(";")[0]?.trim().toLowerCase() + const json = mediaType === "application/json" || mediaType?.endsWith("+json") === true + const decoded = text === "" ? Option.some(null) : json ? decodeJson(text) : Option.none() + const parsed = json ? Option.getOrElse(decoded, () => text) : text === "" ? null : text + if (response.status < 200 || response.status >= 300) { + const rendered = typeof parsed === "string" ? parsed : (JSON.stringify(parsed) ?? "") + const summary = + rendered === "" || rendered === "null" + ? "no response body" + : rendered.length > maxErrorBodyChars + ? `${rendered.slice(0, maxErrorBodyChars)}...` + : rendered + return yield* Effect.fail( + toolError(`${plan.operation.method} ${plan.operation.path} failed with HTTP ${response.status}: ${summary}`), + ) + } + if (json && Option.isNone(decoded)) { + return yield* Effect.fail( + toolError(`${plan.operation.method} ${plan.operation.path} returned malformed JSON.`), + ) + } + return parsed + }) + +const buildRequest = ( + plan: Plan, + input: Readonly>, +): Effect.Effect => + Effect.gen(function* () { + // Validate every model-controlled value before auth resolution, which may refresh tokens. + const url = buildUrl(plan, input) + if (url instanceof ToolError) return yield* Effect.fail(url) + const missing = plan.fields.find( + (field) => field.required && field.location !== "path" && own(input, field.inputName) === undefined, + ) + if (missing !== undefined) { + const label = missing.location === "body" ? "body field" : `${missing.location} parameter` + return yield* Effect.fail(toolError(`Missing required ${label} '${missing.inputName}'.`)) + } + + let request = HttpClientRequest.make(plan.operation.method as HttpMethod.HttpMethod)(url) + for (const field of plan.fields) { + if (field.location !== "query") continue + const item = own(input, field.inputName) + if (item === undefined) continue + const serialized = serializeQuery(request, field, item) + if (serialized instanceof ToolError) return yield* Effect.fail(serialized) + request = serialized + } + + // Host headers first, then declared header parameters. + request = HttpClientRequest.setHeaders(request, plan.headers) + for (const field of plan.fields) { + if (field.location !== "header") continue + const item = own(input, field.inputName) + if (item === undefined) continue + const serialized = serializeSimple(field, item, String) + if (serialized instanceof ToolError) return yield* Effect.fail(serialized) + request = HttpClientRequest.setHeader(request, field.name, serialized) + } + + const setBody = (value: unknown, mediaType: string) => + HttpClientRequest.bodyJson(request, value).pipe( + Effect.map((next) => HttpClientRequest.setHeader(next, "content-type", mediaType)), + Effect.mapError((cause) => + toolError(`Invalid JSON body for ${plan.operation.method} ${plan.operation.path}.`, cause), + ), + ) + if (plan.body?.mode === "value") { + const field = plan.fields.find((field) => field.location === "body") + const body = field === undefined ? undefined : own(input, field.inputName) + if (body !== undefined) request = yield* setBody(body, plan.body.mediaType) + } + if (plan.body?.mode === "object") { + const entries = plan.fields.flatMap((field) => { + if (field.location !== "body") return [] + const item = own(input, field.inputName) + return item === undefined ? [] : [[field.name, item] as const] + }) + if (plan.body.required || entries.length > 0) { + request = yield* setBody(Object.fromEntries(entries), plan.body.mediaType) + } + } + return request + }) + +const resolveAuth = (plan: Plan): Effect.Effect => + Effect.gen(function* () { + const none: AppliedAuth = { headers: {}, query: {} } + if (plan.security.length === 0) return none + + const unavailable: Array = [] + alternatives: for (const requirement of plan.security) { + const names = Object.keys(requirement) + if (names.length === 0) return none + const credentials: Array = [] + for (const name of names) { + const scheme = own(plan.schemes, name) + if (scheme === undefined || plan.auth === undefined) { + unavailable.push(name) + continue alternatives + } + const credential = yield* plan.auth.resolve({ + name, + definition: scheme, + scopes: requirement[name] ?? [], + operation: plan.operation, + }) + if (credential === undefined) { + unavailable.push(name) + continue alternatives + } + credentials.push([name, scheme, credential]) + } + const applied = applyCredentials(credentials) + return applied instanceof ToolError ? yield* Effect.fail(applied) : applied + } + + return yield* Effect.fail( + toolError( + `${plan.operation.method} ${plan.operation.path} requires authentication; no credential available for: ${[...new Set(unavailable)].join(", ")}.`, + ), + ) + }) + +const applyCredentials = ( + credentials: ReadonlyArray, +): AppliedAuth | ToolError => { + const headers = new Map() + const query = new Map() + const add = (carrier: "header" | "query", name: string, value: string): ToolError | undefined => { + const target = carrier === "header" ? headers : query + if (target.has(name)) return toolError(`Authentication resolves multiple credentials for ${carrier} '${name}'.`) + target.set(name, value) + } + for (const [name, definition, credential] of credentials) { + if (credential.type === "bearer") { + const duplicate = add("header", "authorization", `Bearer ${credential.token}`) + if (duplicate !== undefined) return duplicate + continue + } + if (credential.type === "basic") { + // Buffer instead of btoa: btoa throws on non-Latin-1 credentials. + const duplicate = add( + "header", + "authorization", + `Basic ${Buffer.from(`${credential.username}:${credential.password}`, "utf8").toString("base64")}`, + ) + if (duplicate !== undefined) return duplicate + continue + } + if (credential.type === "header") { + const duplicate = add("header", credential.name.toLowerCase(), credential.value) + if (duplicate !== undefined) return duplicate + continue + } + // apiKey: the carrier comes from the scheme declaration. + if (definition.type !== "apiKey") { + return toolError( + `Security scheme '${name}' is not an apiKey scheme; resolve a bearer, basic, or header credential for it.`, + ) + } + if (definition.in === "cookie") return toolError(`Cookie authentication '${name}' is not supported.`) + const parameter = definition.in === "header" ? definition.name.toLowerCase() : definition.name + const duplicate = add(definition.in, parameter, credential.value) + if (duplicate !== undefined) return duplicate + } + return { headers: Object.fromEntries(headers), query: Object.fromEntries(query) } +} + +const buildUrl = (plan: Plan, input: Readonly>): string | ToolError => { + let url = plan.url + for (const field of plan.fields) { + if (field.location !== "path") continue + const item = own(input, field.inputName) + if (item === undefined) { + return toolError(`Missing required path parameter '${field.inputName}'.`) + } + const fieldValue = serializeSimple(field, item, (value) => + encodeURIComponent(value).replace(/[!'()*]/g, (character) => + `%${character.charCodeAt(0).toString(16).toUpperCase()}`, + ), + ) + if (fieldValue instanceof ToolError) return fieldValue + // '.'/'..' survive encoding and URL normalization collapses them, letting a + // model-supplied value retarget the request to a different endpoint. + if (fieldValue === "" || fieldValue === "." || fieldValue === "..") { + return toolError(`Invalid path parameter '${field.inputName}'.`) + } + url = url.replaceAll(`{${field.name}}`, fieldValue) + } + const unresolved = url.match(/\{[^{}]+\}/) + if (unresolved !== null) return toolError(`Unresolved path parameter ${unresolved[0]}.`) + return url +} + +const serializeSimple = ( + field: Plan["fields"][number], + value: unknown, + encode: (value: string) => string, +): string | ToolError => { + const scalar = (item: unknown): string | ToolError => + item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean" + ? toolError(`Parameter '${field.inputName}' contains an unsupported nested value.`) + : encode(String(item)) + if (Array.isArray(value)) { + const items = value.map(scalar) + const invalid = items.find((item): item is ToolError => item instanceof ToolError) + return invalid ?? items.join(",") + } + if (!isRecord(value)) return scalar(value) + const entries = Object.entries(value).flatMap(([name, item]) => { + const rendered = scalar(item) + if (rendered instanceof ToolError) return [rendered] + return field.explode ? [`${encode(name)}=${rendered}`] : [encode(name), rendered] + }) + const invalid = entries.find((item): item is ToolError => item instanceof ToolError) + return invalid ?? entries.join(",") +} + +const serializeQuery = ( + request: HttpClientRequest.HttpClientRequest, + field: Plan["fields"][number], + value: unknown, +): HttpClientRequest.HttpClientRequest | ToolError => { + if (field.style === "deepObject") { + if (!isRecord(value)) return toolError(`Deep-object parameter '${field.inputName}' must be an object.`) + return Object.entries(value).reduce((current, [name, item]) => { + if (current instanceof ToolError) return current + if (item === undefined || (item !== null && typeof item === "object")) { + return toolError(`Deep-object parameter '${field.inputName}' contains an unsupported nested value.`) + } + return HttpClientRequest.appendUrlParam(current, `${field.name}[${name}]`, String(item)) + }, request) + } + if (Array.isArray(value)) { + const rendered = serializeSimple(field, value, String) + if (rendered instanceof ToolError) return rendered + if (!field.explode) return HttpClientRequest.appendUrlParam(request, field.name, rendered) + if (value.some((item) => item === undefined || (item !== null && typeof item === "object"))) { + return toolError(`Query parameter '${field.inputName}' contains an unsupported nested value.`) + } + return value.reduce( + (current, item) => HttpClientRequest.appendUrlParam(current, field.name, String(item)), + request, + ) + } + if (isRecord(value) && field.explode) { + return Object.entries(value).reduce((current, [name, item]) => { + if (current instanceof ToolError) return current + if (item === undefined || (item !== null && typeof item === "object")) { + return toolError(`Query parameter '${field.inputName}' contains an unsupported nested value.`) + } + return HttpClientRequest.appendUrlParam(current, name, String(item)) + }, request) + } + const rendered = serializeSimple(field, value, String) + return rendered instanceof ToolError ? rendered : HttpClientRequest.appendUrlParam(request, field.name, rendered) +} + +const readResponseBody = (response: HttpClientResponse.HttpClientResponse, plan: Plan): Effect.Effect => + Effect.gen(function* () { + const contentLength = response.headers["content-length"] + const parsedSize = contentLength === undefined ? undefined : Number.parseInt(contentLength, 10) + const declaredSize = parsedSize !== undefined && Number.isSafeInteger(parsedSize) && parsedSize >= 0 ? parsedSize : undefined + if (declaredSize !== undefined && declaredSize > maxResponseBodyBytes) { + return yield* Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} response exceeds 50 MiB.`)) + } + let body = Buffer.allocUnsafe(Math.min(maxResponseBodyBytes, declaredSize ?? 64 * 1024)) + let size = 0 + yield* Stream.runForEach(response.stream, (chunk) => { + if (size + chunk.byteLength > maxResponseBodyBytes) { + return Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} response exceeds 50 MiB.`)) + } + if (size + chunk.byteLength > body.byteLength) { + const grown = Buffer.allocUnsafe(Math.min(maxResponseBodyBytes, Math.max(size + chunk.byteLength, body.byteLength * 2))) + body.copy(grown, 0, 0, size) + body = grown + } + body.set(chunk, size) + size += chunk.byteLength + return Effect.void + }).pipe( + Effect.catch((cause) => { + if (cause instanceof ToolError) return Effect.fail(cause) + if (cause.reason._tag === "EmptyBodyError") return Effect.void + return Effect.fail( + toolError(`${plan.operation.method} ${plan.operation.path} failed while reading the response body.`, cause), + ) + }), + ) + return new TextDecoder().decode(body.subarray(0, size)) + }) diff --git a/packages/codemode/src/openapi/spec.ts b/packages/codemode/src/openapi/spec.ts new file mode 100644 index 000000000000..22cf1535a832 --- /dev/null +++ b/packages/codemode/src/openapi/spec.ts @@ -0,0 +1,507 @@ +import { fromSchemaOpenApi3_0, fromSchemaOpenApi3_1 } from "effect/JsonSchema" +import type { JsonSchema } from "../tool.js" +import { isBlockedMember } from "../tool-runtime.js" +import type { + Body, + Document, + InputField, + OperationInput, + Parsed, + SecurityRequirement, + SecurityScheme, +} from "./types.js" + +export const methods = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"]) +const parameterLocations = ["path", "query", "header"] as const +const ignoredHeaderParameters = new Set(["accept", "content-type", "authorization"]) + +export const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value) + +const asArray = (value: unknown): ReadonlyArray => (Array.isArray(value) ? value : []) + +export const nonEmptyString = (value: unknown): string | undefined => + typeof value === "string" && value !== "" ? value : undefined + +// Guards record lookups keyed by spec- or model-controlled names against +// prototype-inherited values (e.g. a parameter named `toString`). +export const own = (record: Readonly>, key: string): T | undefined => + Object.hasOwn(record, key) ? record[key] : undefined + +export const resolve = (document: Document, value: unknown): unknown => { + const next = (current: unknown, seen: ReadonlySet): unknown => { + if (!isRecord(current)) return current + const ref = nonEmptyString(current.$ref) + if (ref === undefined || !ref.startsWith("#/") || seen.has(ref)) return current + const target = ref + .slice(2) + .split("/") + .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~")) + .reduce((item, segment) => (isRecord(item) ? own(item, segment) : undefined), document) + return target === undefined ? current : next(target, new Set([...seen, ref])) + } + return next(value, new Set()) +} + +const projectSchema = (document: Document, value: unknown): JsonSchema => { + if (!isRecord(value)) return {} + const normalized = nonEmptyString(document.openapi)?.startsWith("3.0") + ? fromSchemaOpenApi3_0(value) + : fromSchemaOpenApi3_1(value) + return Object.keys(normalized.definitions).length === 0 + ? normalized.schema + : { ...normalized.schema, $defs: normalized.definitions } +} + +export const componentDefinitions = (document: Document): Readonly> => { + const components = isRecord(document.components) ? document.components : {} + const schemas = isRecord(components.schemas) ? components.schemas : {} + return Object.fromEntries(Object.entries(schemas).map(([name, value]) => [name, projectSchema(document, value)])) +} + +const withDefinitions = (schema: JsonSchema, definitions: Readonly>): JsonSchema => { + if (Object.keys(definitions).length === 0) return schema + const local = isRecord(schema.$defs) ? schema.$defs : {} + return { ...schema, $defs: { ...definitions, ...local } } +} + +const isJsonMediaType = (mediaType: string): boolean => { + const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? "" + return normalized === "application/json" || normalized.endsWith("+json") +} + +const isBinaryMediaType = (document: Document, mediaType: string, value: unknown): boolean => { + const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? "" + if (!isJsonMediaType(normalized) && !normalized.startsWith("text/")) return true + if (!isRecord(value)) return false + const schema = resolve(document, value.schema) + return isRecord(schema) && schema.format === "binary" +} + +const jsonContent = (content: Record): { readonly mediaType: string; readonly schema: unknown } | undefined => { + const entry = Object.entries(content).find(([mediaType]) => isJsonMediaType(mediaType)) + return entry !== undefined && isRecord(entry[1]) ? { mediaType: entry[0], schema: entry[1].schema } : undefined +} + +const isFlattenableObjectBody = ( + schema: unknown, + requestRequired: boolean, +): schema is Record & { readonly properties: Record } => + isRecord(schema) && + requestRequired && + schema.type === "object" && + isRecord(schema.properties) && + schema.additionalProperties === false && + schema.nullable !== true && + schema.allOf === undefined && + schema.anyOf === undefined && + schema.oneOf === undefined + +type PlannedField = Omit + +const operationParameters = ( + document: Document, + pathItem: Record, + operation: Record, +): Parsed> => { + // Operation-level parameters override path-level ones sharing (location, name). + const declared = new Map< + string, + { readonly name: string; readonly location: string; readonly parameter: Record } + >() + for (const raw of [...asArray(pathItem.parameters), ...asArray(operation.parameters)]) { + const resolved = resolve(document, raw) + if (!isRecord(resolved)) return { ok: false, reason: "parameter declaration is invalid or unresolved" } + const name = nonEmptyString(resolved.name) + const location = nonEmptyString(resolved.in) + if (name === undefined || location === undefined) + return { ok: false, reason: "parameter declaration is missing name or location" } + declared.set(`${location}:${name}`, { name, location, parameter: resolved }) + } + const unordered: Array = [] + for (const item of declared.values()) { + const name = item.name + const location = item.location + const resolved = item.parameter + if (location === "cookie") return { ok: false, reason: `cookie parameter '${name}' is not supported` } + if (location !== "path" && location !== "query" && location !== "header") { + return { ok: false, reason: `parameter '${name}' uses unsupported location '${location}'` } + } + if (location === "header" && ignoredHeaderParameters.has(name.toLowerCase())) continue + if (resolved.schema === undefined && resolved.content === undefined) { + return { ok: false, reason: `parameter '${name}' declares neither schema nor content` } + } + if (resolved.content !== undefined) + return { ok: false, reason: `parameter '${name}' uses unsupported content encoding` } + if (resolved.style !== undefined && nonEmptyString(resolved.style) === undefined) { + return { ok: false, reason: `parameter '${name}' has an invalid style` } + } + if (resolved.explode !== undefined && typeof resolved.explode !== "boolean") { + return { ok: false, reason: `parameter '${name}' has an invalid explode value` } + } + if (resolved.allowReserved !== undefined && typeof resolved.allowReserved !== "boolean") { + return { ok: false, reason: `parameter '${name}' has an invalid allowReserved value` } + } + if (resolved.allowReserved === true) + return { ok: false, reason: `parameter '${name}' uses unsupported allowReserved encoding` } + const declaredStyle = nonEmptyString(resolved.style) ?? (location === "query" ? "form" : "simple") + if (location === "query" && declaredStyle !== "form" && declaredStyle !== "deepObject") { + return { ok: false, reason: `query parameter '${name}' uses unsupported style '${declaredStyle}'` } + } + if (location !== "query" && declaredStyle !== "simple") { + return { ok: false, reason: `${location} parameter '${name}' uses unsupported style '${declaredStyle}'` } + } + const style = declaredStyle === "deepObject" ? "deepObject" : declaredStyle === "form" ? "form" : "simple" + const explode = typeof resolved.explode === "boolean" ? resolved.explode : style === "form" + if (style === "deepObject" && !explode) { + return { ok: false, reason: `query parameter '${name}' uses deepObject with explode=false` } + } + const base = projectSchema(document, resolved.schema) + const description = nonEmptyString(resolved.description) + unordered.push({ + name, + location, + required: resolved.required === true || location === "path", + style, + explode, + schema: { + ...base, + ...(base.description === undefined && description !== undefined ? { description } : {}), + }, + }) + } + return { + ok: true, + value: parameterLocations.flatMap((location) => unordered.filter((field) => field.location === location)), + } +} + +const operationBody = ( + document: Document, + operation: Record, +): Parsed<{ readonly fields: ReadonlyArray; readonly body: Body | undefined }> => { + const resolved = resolve(document, operation.requestBody) + if (!isRecord(resolved)) return { ok: true, value: { fields: [], body: undefined } } + const content = isRecord(resolved.content) ? resolved.content : {} + const selected = jsonContent(content) + if (selected === undefined) { + return { + ok: false, + reason: `request body has no JSON content (declared: ${Object.keys(content).join(", ") || "none"})`, + } + } + const schema = resolve(document, selected.schema) + const required = resolved.required === true + if (!isFlattenableObjectBody(schema, required)) { + return { + ok: true, + value: { + fields: [ + { + name: "body", + location: "body", + required, + schema: projectSchema(document, selected.schema), + style: undefined, + explode: undefined, + }, + ], + body: { required, mode: "value", mediaType: selected.mediaType }, + }, + } + } + const requiredProperties = new Set( + Array.isArray(schema.required) ? schema.required.filter((item): item is string => typeof item === "string") : [], + ) + return { + ok: true, + value: { + fields: Object.entries(schema.properties).map(([name, value]) => ({ + name, + location: "body" as const, + required: required && requiredProperties.has(name), + schema: projectSchema(document, value), + style: undefined, + explode: undefined, + })), + body: { required, mode: "object", mediaType: selected.mediaType }, + }, + } +} + +export const operationInput = ( + document: Document, + pathItem: Record, + operation: Record, +): Parsed => { + const parameters = operationParameters(document, pathItem, operation) + if (!parameters.ok) return parameters + const requestBody = operationBody(document, operation) + if (!requestBody.ok) return requestBody + const fields = [...parameters.value, ...requestBody.value.fields] + + const conflicts = new Set( + [...Map.groupBy(fields, (field) => field.name)] + .filter(([, matches]) => new Set(matches.map((field) => field.location)).size > 1) + .map(([name]) => name), + ) + const used = new Set() + return { + ok: true, + value: { + fields: fields.map((field) => { + const visibleName = isBlockedMember(field.name) ? `${field.name}_2` : field.name + const base = conflicts.has(field.name) ? `${field.location}_${visibleName}` : visibleName + const next = (index: number): string => { + const candidate = index === 1 ? base : `${base}_${index}` + return used.has(candidate) ? next(index + 1) : candidate + } + const inputName = next(1) + used.add(inputName) + return { ...field, inputName } + }), + body: requestBody.value.body, + }, + } +} + +export const inputSchema = ( + fields: ReadonlyArray, + definitions: Readonly>, +): JsonSchema => { + const required = fields.filter((field) => field.required).map((field) => field.inputName) + return withDefinitions( + { + type: "object", + properties: Object.fromEntries(fields.map((field) => [field.inputName, field.schema])), + ...(required.length === 0 ? {} : { required }), + }, + definitions, + ) +} + +const successfulResponses = ( + document: Document, + operation: Record, +): Parsed>> => { + if (!isRecord(operation.responses)) return { ok: true, value: [] } + const entries = Object.entries(operation.responses) + const selected = [ + ...entries.filter(([status]) => /^2\d\d$/.test(status)).sort(([a], [b]) => a.localeCompare(b)), + ...entries.filter(([status]) => status.toUpperCase() === "2XX"), + ] + const responses: Array> = [] + for (const [, value] of selected) { + const resolved = resolve(document, value) + if (!isRecord(resolved) || nonEmptyString(resolved.$ref) !== undefined) { + return { ok: false, reason: "successful response declaration is invalid or unresolved" } + } + responses.push(resolved) + } + return { ok: true, value: responses } +} + +export const operationOutput = ( + document: Document, + operation: Record, + definitions: Readonly>, +): Parsed => { + if (operation["x-websocket"] === true) return { ok: false, reason: "WebSocket operations are not supported" } + const responses = successfulResponses(document, operation) + if (!responses.ok) return responses + const streams = responses.value.some( + (response) => + isRecord(response.content) && + Object.keys(response.content).some( + (mediaType) => mediaType.split(";")[0]?.trim().toLowerCase() === "text/event-stream", + ), + ) + if (streams) return { ok: false, reason: "SSE operations are not supported" } + const binary = responses.value.some( + (response) => + isRecord(response.content) && + Object.entries(response.content).some(([mediaType, value]) => isBinaryMediaType(document, mediaType, value)), + ) + if (binary) return { ok: false, reason: "binary responses are not supported" } + + const outcomes: Array = [] + for (const response of responses.value) { + if (response.content !== undefined && !isRecord(response.content)) return { ok: true, value: undefined } + const content = isRecord(response.content) ? response.content : {} + if (Object.keys(content).length === 0) { + outcomes.push({ type: "null" }) + continue + } + for (const [mediaType, value] of Object.entries(content)) { + if (!isJsonMediaType(mediaType)) { + outcomes.push({ type: "string" }) + continue + } + if (!isRecord(value) || value.schema === undefined) return { ok: true, value: undefined } + outcomes.push(projectSchema(document, value.schema)) + } + } + if (outcomes.length === 0) return { ok: true, value: undefined } + return { + ok: true, + value: withDefinitions(outcomes.length === 1 ? outcomes[0] ?? {} : { anyOf: outcomes }, definitions), + } +} + +const sanitizeOperationSegment = (raw: string): string => { + const base = + raw + .replaceAll(/[^A-Za-z0-9_$]+/g, "_") + .replace(/^_+|_+$/g, "") + .replace(/^([0-9])/, "_$1") || "operation" + return isBlockedMember(base) ? `${base}_2` : base +} + +const fallbackOperationId = (method: string, path: string): string => + [ + method, + ...path + .split("/") + .filter((part) => part !== "") + .flatMap((part) => (part.startsWith("{") && part.endsWith("}") ? ["by", part.slice(1, -1)] : [part])) + .flatMap((part) => part.split(/[^A-Za-z0-9]+/).filter((word) => word !== "")), + ] + .map((word, index) => { + const lower = word.toLowerCase() + return index === 0 ? lower : `${lower.charAt(0).toUpperCase()}${lower.slice(1)}` + }) + .join("") + +export const operationPath = ( + method: string, + path: string, + operation: Record, + used: ReadonlySet, + namespaces: ReadonlySet, +): ReadonlyArray => { + const raw = nonEmptyString(operation.operationId) + const segments = (raw === undefined ? [fallbackOperationId(method, path)] : raw.split(".")).map(sanitizeOperationSegment) + if (isOperationPathAvailable(segments, used, namespaces)) return segments + const conflict = segments.slice(0, -1).findIndex((_, index) => used.has(segments.slice(0, index + 1).join("."))) + if (conflict >= 0 && conflict + 1 < segments.length) { + const collapsed = segments.flatMap((segment, index) => { + if (index === conflict) { + const next = segments[index + 1] ?? "" + return [`${segment}${next.charAt(0).toUpperCase()}${next.slice(1)}`] + } + return index === conflict + 1 ? [] : [segment] + }) + if (isOperationPathAvailable(collapsed, used, namespaces)) return collapsed + } + const fallback = segments.join("_") + const next = (index: number): string => { + const candidate = `${fallback}_${index}` + return isOperationPathAvailable([candidate], used, namespaces) ? candidate : next(index + 1) + } + return [next(2)] +} + +const isOperationPathAvailable = ( + segments: ReadonlyArray, + used: ReadonlySet, + namespaces: ReadonlySet, +): boolean => { + const key = segments.join(".") + if (used.has(key) || namespaces.has(key)) return false + return segments.slice(0, -1).every((_, index) => !used.has(segments.slice(0, index + 1).join("."))) +} + +export const specServerUrl = (source: Record): Parsed => { + const server = asArray(source.servers).find(isRecord) + const url = server === undefined ? undefined : nonEmptyString(server.url) + if (url === undefined) return { ok: false, reason: "spec declares no servers; pass baseUrl" } + if (/\{[^{}]+\}/.test(url)) { + return { ok: false, reason: `server URL '${url}' is not an absolute URL; pass baseUrl` } + } + return validateBaseUrl(url) +} + +export const validateBaseUrl = (value: string): Parsed => { + if (!/^https?:\/\//i.test(value)) return { ok: false, reason: `server URL '${value}' is not an absolute HTTP(S) URL` } + const url = URL.parse(value) + if (url === null || (url.protocol !== "http:" && url.protocol !== "https:")) { + return { ok: false, reason: `server URL '${value}' is not an absolute HTTP(S) URL` } + } + if (url.search !== "" || url.hash !== "") { + return { ok: false, reason: `server URL '${value}' contains an unsupported query string or fragment` } + } + return { ok: true, value } +} + +export const securityRequirements = (value: unknown): Parsed> => { + if (value === undefined) return { ok: true, value: [] } + if (!Array.isArray(value)) return { ok: false, reason: "security declaration is not an array" } + const requirements: Array = [] + for (const item of value) { + if (!isRecord(item)) return { ok: false, reason: "security requirement is not an object" } + const requirement = Object.create(null) as Record> + for (const [name, scopes] of Object.entries(item)) { + if (!Array.isArray(scopes)) return { ok: false, reason: "security requirement scopes are not string arrays" } + const parsed = scopes.filter((scope): scope is string => typeof scope === "string") + if (parsed.length !== scopes.length) { + return { ok: false, reason: "security requirement scopes are not string arrays" } + } + requirement[name] = parsed + } + requirements.push(requirement) + } + return { ok: true, value: requirements } +} + +export const operationSecurityRequirements = ( + value: unknown, + defaults: Parsed>, + schemes: Readonly>, +): Parsed> => { + const parsed = value === undefined ? defaults : securityRequirements(value) + if (!parsed.ok) return parsed + const supported = parsed.value.filter((requirement) => + Object.keys(requirement).every((name) => { + const scheme = own(schemes, name) + return scheme !== undefined && !(scheme.type === "apiKey" && scheme.in === "cookie") + }), + ) + if (parsed.value.length === 0 || supported.length > 0) return { ok: true, value: supported } + + const names = [...new Set(parsed.value.flatMap((requirement) => Object.keys(requirement)))] + const cookieScheme = names.find((name) => { + const definition = own(schemes, name) + return definition?.type === "apiKey" && definition.in === "cookie" + }) + return { + ok: false, + reason: + cookieScheme === undefined + ? `security requirement references missing or malformed scheme: ${names.join(", ")}` + : `cookie authentication '${cookieScheme}' is not supported`, + } +} + +export const securitySchemes = (document: Document): Readonly> => { + const components = isRecord(document.components) ? document.components : {} + const declared = isRecord(components.securitySchemes) ? components.securitySchemes : {} + return Object.fromEntries( + Object.entries(declared).flatMap(([name, value]) => { + const resolved = resolve(document, value) + if (!isRecord(resolved)) return [] + const type = nonEmptyString(resolved.type) + if (type === "apiKey") { + const carrier = nonEmptyString(resolved.in) + const parameter = nonEmptyString(resolved.name) + if (parameter === undefined || (carrier !== "header" && carrier !== "query" && carrier !== "cookie")) return [] + return [[name, { type, name: parameter, in: carrier }] as const] + } + if (type === "http") { + const scheme = nonEmptyString(resolved.scheme)?.toLowerCase() + return scheme === undefined ? [] : [[name, { type, scheme }] as const] + } + if (type === "oauth2" || type === "openIdConnect") return [[name, { type }] as const] + return [] + }), + ) +} diff --git a/packages/codemode/src/openapi/types.ts b/packages/codemode/src/openapi/types.ts new file mode 100644 index 000000000000..cab772e70148 --- /dev/null +++ b/packages/codemode/src/openapi/types.ts @@ -0,0 +1,112 @@ +import { Effect } from "effect" +import { HttpClient } from "effect/unstable/http" +import type { Definition, JsonSchema } from "../tool.js" + +/** A parsed OpenAPI 3.x document. YAML must be parsed by the host. */ +export type Document = Record + +/** The operation identity handed to auth resolution and errors. */ +export type Operation = { + readonly operationId: string | undefined + readonly method: string + readonly path: string + readonly summary: string | undefined + readonly description: string | undefined +} + +/** A resolved OpenAPI security scheme from `components.securitySchemes`. */ +export type SecurityScheme = + | { readonly type: "apiKey"; readonly name: string; readonly in: "header" | "query" | "cookie" } + | { readonly type: "http"; readonly scheme: string } + | { readonly type: "oauth2" } + | { readonly type: "openIdConnect" } + +/** + * Credential material returned by a host auth resolver. The carrier for `apiKey` + * comes from the scheme definition, not the credential. `header` is the escape + * hatch for nonstandard schemes. + */ +export type Credential = + | { readonly type: "bearer"; readonly token: string } + | { readonly type: "basic"; readonly username: string; readonly password: string } + | { readonly type: "apiKey"; readonly value: string } + | { readonly type: "header"; readonly name: string; readonly value: string } + +/** + * Resolves credential material for one named security scheme at call time. + * `undefined` means unavailable, try the next OR alternative; a failure aborts + * the call rather than falling through. + */ +export type AuthResolver = (context: { + readonly name: string + readonly definition: SecurityScheme + readonly scopes: ReadonlyArray + readonly operation: Operation +}) => Effect.Effect + +export type Options = { + readonly spec: Document + /** Overrides all document, path, and operation `servers`. Required when no applicable absolute server URL exists. */ + readonly baseUrl?: string | undefined + /** Host credential resolution, keyed by security scheme name. */ + readonly auth?: { readonly resolve: AuthResolver } | undefined + /** Static headers on every request. Not model-visible; declared header params may override them, auth always wins. */ + readonly headers?: Readonly> | undefined +} + +/** An operation that could not be represented as a tool, and why. */ +export type Skipped = { + readonly method: string + readonly path: string + readonly reason: string +} + +export type Tools = { [name: string]: Definition | Tools } + +export type Result = { + /** Tool subtree; the host places it under a key in its `tools` tree. */ + readonly tools: Tools + readonly skipped: ReadonlyArray +} + +export type Parsed = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly reason: string } + +export type InputLocation = "path" | "query" | "header" | "body" + +export type InputField = { + /** Model-visible field name after cross-location collision handling. */ + readonly inputName: string + /** Original parameter or body-property name used on the wire. */ + readonly name: string + readonly location: InputLocation + readonly required: boolean + readonly schema: JsonSchema + readonly style: "simple" | "form" | "deepObject" | undefined + readonly explode: boolean | undefined +} + +export type Body = { readonly required: boolean; readonly mode: "object" | "value"; readonly mediaType: string } + +export type OperationInput = { + readonly fields: ReadonlyArray + readonly body: Body | undefined +} + +/** One OR alternative: scheme name -> required scopes. Empty object = unauthenticated is acceptable. */ +export type SecurityRequirement = Readonly>> + +export type Plan = { + readonly operation: Operation + readonly url: string + readonly fields: ReadonlyArray + readonly body: Body | undefined + readonly security: ReadonlyArray + readonly schemes: Readonly> + readonly auth: { readonly resolve: AuthResolver } | undefined + readonly headers: Readonly> +} + +export type AppliedAuth = { + readonly headers: Readonly> + readonly query: Readonly> +} diff --git a/packages/codemode/src/token.ts b/packages/codemode/src/token.ts deleted file mode 100644 index 1e06bec1e48c..000000000000 --- a/packages/codemode/src/token.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Token estimation for budgeting model-facing text. Copied from - * `@opencode-ai/core/util/token` (chars / 4) so this package stays - * dependency-free; keep the two in sync if the heuristic ever changes. - */ -export * as Token from "./token.js" - -const CHARS_PER_TOKEN = 4 - -export const estimate = (input: string) => Math.max(0, Math.round(input.length / CHARS_PER_TOKEN)) diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index da9f749499a5..f19e7a9b4d55 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -10,27 +10,32 @@ import { outputTypeScript, type Definition, } from "./tool.js" -import { estimate } from "./token.js" import { SandboxDate, SandboxMap, SandboxPromise, SandboxRegExp, SandboxSet } from "./values.js" +const estimateTokens = (input: string) => Math.max(0, Math.round(input.length / 4)) + export type HostTool = (...args: Array) => Effect.Effect export type HostTools = { [name: string]: HostTool | Definition | HostTools } -export type Services = Tools extends (...args: Array) => Effect.Effect - ? R - : Tools extends { - readonly _tag: "CodeModeTool" - readonly run: (input: unknown) => Effect.Effect - } +export type Services = ServicesOf + +type ServicesOf> = Depth["length"] extends 8 + ? never + : Tools extends (...args: Array) => Effect.Effect ? R - : Tools extends object - ? string extends keyof Tools - ? never - : Services - : never + : Tools extends { + readonly _tag: "CodeModeTool" + readonly run: (input: unknown) => Effect.Effect + } + ? R + : Tools extends object + ? string extends keyof Tools + ? ServicesOf + : ServicesOf + : never /** Minimal audit record retained for each admitted tool call. */ export type ToolCall = { @@ -290,17 +295,16 @@ const definitions = ( return entries } -const describeDefinition = (path: string, definition: Definition): ToolDescription => ({ - path, - description: definition.description, - signature: `${toolExpression(path)}(input: ${inputTypeScript(definition)}): Promise<${outputTypeScript(definition)}>`, -}) - const visibleDefinitions = (tools: HostTools) => - definitions(tools).flatMap(({ path, definition }) => { - const description = describeDefinition(path, definition) - return [{ path, definition, description }] - }) + definitions(tools).map(({ path, definition }) => ({ + path, + definition, + description: { + path, + description: definition.description, + signature: `${toolExpression(path)}(input: ${inputTypeScript(definition)}): Promise<${outputTypeScript(definition)}>`, + }, + })) export const catalog = (tools: HostTools): ReadonlyArray => visibleDefinitions(tools).map(({ description }) => description) @@ -351,16 +355,10 @@ const termForms = (term: string): Array => { return forms } -const firstLine = (text: string) => text.split("\n", 1)[0]!.trim() - -/** One-line description used on inline catalog lines; the full text stays in search results. */ -const brief = (text: string, max = 120) => { - const line = firstLine(text) - return line.length > max ? line.slice(0, max - 1) + "..." : line -} - const catalogLine = (tool: ToolDescription) => { - const description = brief(tool.description) + // Inline catalog lines use only a compact first line; full text stays in search results. + const line = tool.description.split("\n", 1)[0]!.trim() + const description = line.length > 120 ? line.slice(0, 119) + "..." : line return description === "" ? ` - ${tool.signature}` : ` - ${tool.signature} // ${description}` } @@ -430,7 +428,7 @@ export const discoveryPlan = ( picked: new Set(), queue: [...group].sort( (left, right) => - estimate(catalogLine(left)) - estimate(catalogLine(right)) || left.path.localeCompare(right.path), + estimateTokens(catalogLine(left)) - estimateTokens(catalogLine(right)) || left.path.localeCompare(right.path), ), })) let used = 0 @@ -439,7 +437,7 @@ export const discoveryPlan = ( const stillActive: typeof active = [] for (const selection of active) { const tool = selection.queue[0]! - const cost = estimate(catalogLine(tool)) + const cost = estimateTokens(catalogLine(tool)) if (used + cost > maxInlineCatalogTokens) continue selection.queue.shift() selection.picked.add(tool) @@ -638,9 +636,6 @@ export type ToolRuntime = { readonly keys: (path: ReadonlyArray) => ReadonlyArray } -const failureMessage = (error: unknown): string => - error instanceof ToolError || error instanceof ToolRuntimeError ? error.message : "Tool execution failed" - export const make = ( tools: HostTools, /** Undefined means unlimited tool calls. */ @@ -659,9 +654,16 @@ export const make = ( const startedAt = Date.now() return effect.pipe( Effect.tap(() => onEnd({ ...call, durationMs: Date.now() - startedAt, outcome: "success" })), - Effect.tapError((error) => - onEnd({ ...call, durationMs: Date.now() - startedAt, outcome: "failure", message: failureMessage(error) }), - ), + Effect.tapError((error) => { + const message = + error instanceof ToolError || error instanceof ToolRuntimeError ? error.message : "Tool execution failed" + return onEnd({ + ...call, + durationMs: Date.now() - startedAt, + outcome: "failure", + message, + }) + }), ) } diff --git a/packages/codemode/src/tool.ts b/packages/codemode/src/tool.ts index 5a0d84ad52c5..e89f3d39d6c5 100644 --- a/packages/codemode/src/tool.ts +++ b/packages/codemode/src/tool.ts @@ -1,4 +1,4 @@ -import { Effect, Schema } from "effect" +import { Effect, JsonPointer, Schema } from "effect" /** * JSON Schema subset accepted for render-only tool schemas. @@ -13,6 +13,7 @@ export type JsonSchema = { readonly const?: unknown readonly anyOf?: ReadonlyArray readonly oneOf?: ReadonlyArray + readonly allOf?: ReadonlyArray readonly properties?: Readonly> readonly required?: ReadonlyArray readonly items?: JsonSchema @@ -76,6 +77,13 @@ const effectNumberSentinel = (schema: JsonSchema) => schema.enum.length === 1 && (schema.enum[0] === "NaN" || schema.enum[0] === "Infinity" || schema.enum[0] === "-Infinity") +const intersection = (members: ReadonlyArray): string => { + const concrete = members.filter((member) => member !== "unknown") + if (concrete.length === 0) return "unknown" + if (concrete.length === 1) return concrete[0] ?? "unknown" + return concrete.map((member) => (member.includes(" | ") ? `(${member})` : member)).join(" & ") +} + /** * Recursion ceiling for schema rendering. Object, array, and union recursion all increment * depth, so this bounds every recursion path - pathological or structurally cyclic schemas @@ -89,6 +97,30 @@ type RenderContext = { readonly pretty: boolean } +const hasUnresolvedRef = ( + schema: JsonSchema, + definitions: Readonly>, + seen: ReadonlySet = new Set(), + visited: ReadonlySet = new Set(), +): boolean => { + if (visited.has(schema)) return false + const nextVisited = new Set([...visited, schema]) + if (schema.$ref !== undefined) { + const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1] + const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment) + if (name === undefined || definitions[name] === undefined || seen.has(name)) return true + if (hasUnresolvedRef(definitions[name], definitions, new Set([...seen, name]), nextVisited)) return true + } + return [ + ...(schema.anyOf ?? []), + ...(schema.oneOf ?? []), + ...(schema.allOf ?? []), + ...Object.values(schema.properties ?? {}), + ...(schema.items === undefined ? [] : [schema.items]), + ...(typeof schema.additionalProperties === "object" ? [schema.additionalProperties] : []), + ].some((item) => hasUnresolvedRef(item, definitions, seen, nextVisited)) +} + /** * Schema constraints a TypeScript type cannot express natively but a model benefits from, * surfaced as JSDoc tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`). @@ -136,11 +168,18 @@ const renderSchema = ( seen: ReadonlySet = new Set(), ): string => { if (depth > MAX_RENDER_DEPTH) return "unknown" + const nested = + schema.definitions === undefined && schema.$defs === undefined + ? ctx + : { ...ctx, definitions: { ...ctx.definitions, ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) } } if (schema.$ref) { - const name = schema.$ref.split("/").pop() - if (!name || !ctx.definitions[name]) return name ?? "unknown" - if (seen.has(name)) return name // recursive type: reference by name rather than loop - return renderSchema(ctx.definitions[name], ctx, depth, new Set([...seen, name])) + const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1] + const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment) + if (!name || !nested.definitions[name] || seen.has(name)) return "unknown" + return intersection([ + renderSchema(nested.definitions[name], nested, depth, new Set([...seen, name])), + renderSchema({ ...schema, $ref: undefined }, nested, depth + 1, seen), + ]) } if (schema.const !== undefined) return renderLiteral(schema.const) if (schema.enum) return schema.enum.map(renderLiteral).join(" | ") @@ -166,24 +205,34 @@ const renderSchema = ( ) { return "{}" } - return alternatives.map((item) => renderSchema(item, ctx, depth + 1, seen)).join(" | ") + const members = alternatives.map((item) => renderSchema(item, nested, depth + 1, seen)) + if (members.some((member) => member === "unknown")) return "unknown" + return intersection([ + members.join(" | "), + renderSchema({ ...schema, anyOf: undefined, oneOf: undefined }, nested, depth + 1, seen), + ]) + } + if (schema.allOf) { + const members = schema.allOf.map((item) => renderSchema(item, nested, depth + 1, seen)) + if (schema.allOf.some((item) => hasUnresolvedRef(item, nested.definitions))) return "unknown" + return intersection([renderSchema({ ...schema, allOf: undefined }, nested, depth + 1, seen), ...members]) } if (Array.isArray(schema.type)) { - return schema.type.map((item) => renderSchema({ type: item }, ctx, depth + 1, seen)).join(" | ") + return schema.type.map((item) => renderSchema({ ...schema, type: item }, nested, depth + 1, seen)).join(" | ") } if (schema.type === "string") return "string" if (schema.type === "number" || schema.type === "integer") return "number" if (schema.type === "boolean") return "boolean" if (schema.type === "null") return "null" - if (schema.type === "array") return `Array<${renderSchema(schema.items ?? {}, ctx, depth + 1, seen)}>` + if (schema.type === "array") return `Array<${renderSchema(schema.items ?? {}, nested, depth + 1, seen)}>` if (schema.type === "object" || schema.properties) { const required = new Set(schema.required ?? []) const properties = Object.entries(schema.properties ?? {}) const additional = schema.additionalProperties const indexType = - additional && typeof additional === "object" ? renderSchema(additional, ctx, depth + 1, seen) : undefined + additional && typeof additional === "object" ? renderSchema(additional, nested, depth + 1, seen) : undefined const field = ([name, value]: readonly [string, JsonSchema]) => - `${renderKey(name)}${required.has(name) ? "" : "?"}: ${renderSchema(value, ctx, depth + 1, seen)}` + `${renderKey(name)}${required.has(name) ? "" : "?"}: ${renderSchema(value, nested, depth + 1, seen)}` if (!ctx.pretty) { const fields = properties.map(field) @@ -253,7 +302,8 @@ export const inputProperties = (definition: Definition): Array" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Switch the agent used by subsequent provider turns.", + "summary": "Switch session agent", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "type": "string" + } + }, + "required": [ + "agent" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/model": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.switchModel", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Switch the model used by subsequent provider turns.", + "summary": "Switch session model", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "model": { + "$ref": "#/components/schemas/Model.Ref" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/rename": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.rename", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Update the session title.", + "summary": "Rename session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/prompt": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.prompt", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionInput.Admitted" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + } + }, + "description": "Durably admit one session input and schedule agent-loop execution unless resume is false.", + "summary": "Send message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "prompt": { + "$ref": "#/components/schemas/PromptInput" + }, + "delivery": { + "anyOf": [ + { + "type": "string", + "enum": [ + "steer", + "queue" + ] + }, + { + "type": "null" + } + ] + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "prompt" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/command": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.command", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionInput.Admitted" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | CommandNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/CommandNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + }, + "500": { + "description": "CommandEvaluationError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandEvaluationError" + } + } + } + } + }, + "description": "Resolve a slash command into prompt input, admit it durably, and schedule execution unless resume is false.", + "summary": "Run command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "command": { + "type": "string" + }, + "arguments": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Ref" + }, + { + "type": "null" + } + ] + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "delivery": { + "anyOf": [ + { + "type": "string", + "enum": [ + "steer", + "queue" + ] + }, + { + "type": "null" + } + ] + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/skill": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.skill", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | SkillNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SkillNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Activate a skill for a session by appending a skill message and resuming execution.", + "summary": "Activate skill", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + { + "type": "null" + } + ] + }, + "skill": { + "type": "string" + }, + "resume": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "skill" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/synthetic": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.synthetic", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Append a synthetic message to a session and resume execution.", + "summary": "Add synthetic message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/compact": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.compact", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Compact a session conversation.", + "summary": "Compact session" + } + }, + "/api/session/{sessionID}/wait": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.wait", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Wait for a session agent loop to become idle.", + "summary": "Wait for session" + } + }, + "/api/session/{sessionID}/revert/stage": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.revert.stage", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Revert.State" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "MessageNotFoundError | SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "description": "Stage or move a reversible session boundary and optionally apply its file changes.", + "summary": "Stage session revert", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "files": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "messageID" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/revert/clear": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.revert.clear", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "summary": "Clear staged revert" + } + }, + "/api/session/{sessionID}/revert/commit": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.revert.commit", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "SessionBusyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionBusyError" + } + } + } + } + }, + "summary": "Commit staged revert" + } + }, + "/api/session/{sessionID}/context": { + "get": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.context", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "description": "Retrieve the active context messages for a session (all messages after the last compaction).", + "summary": "Get session context" + } + }, + "/api/session/{sessionID}/context-entry": { + "get": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.context.entry.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionContextEntry.Info" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "List API-managed context entries attached to the session's system context.", + "summary": "List context entries" + } + }, + "/api/session/{sessionID}/context-entry/{key}": { + "put": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.context.entry.put", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "key", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SessionContextEntry.Key" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Attach or replace one durable context entry. The value is rendered into the session's system context; changes announce as updates at the next turn boundary.", + "summary": "Put context entry", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "value": {} + }, + "required": [ + "value" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.context.entry.remove", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "key", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SessionContextEntry.Key" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Remove one context entry; the removal is announced to the model at the next turn boundary.", + "summary": "Remove context entry" + } + }, + "/api/session/{sessionID}/log": { + "get": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.log", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "after", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "follow", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "text/event-stream": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "event": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/SessionLogItemStream" + } + }, + "required": [ + "id", + "event", + "data" + ], + "additionalProperties": false + }, + "x-effect-stream": { + "encoding": "sse", + "causeSchema": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Fail" + ] + }, + "error": { + "not": {} + } + }, + "required": [ + "_tag", + "error" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Die" + ] + }, + "defect": {} + }, + "required": [ + "_tag", + "defect" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Interrupt" + ] + }, + "fiberId": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "fiberId" + ], + "additionalProperties": false + } + ] + } + }, + "errorSchema": { + "not": {} + }, + "failureEvent": "effect/httpapi/stream/failure" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Durable, ordered, gap-free read of public session events after an exclusive aggregate sequence. Emits a synced marker once replay reaches the captured watermark, then completes; with follow=true it continues with live events instead. The only event API that promises reliability: attach after a snapshot watermark to compose fetch and stream without a race window.", + "summary": "Read the session log" + } + }, + "/api/session/{sessionID}/interrupt": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.interrupt", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op.", + "summary": "Interrupt session execution" + } + }, + "/api/session/{sessionID}/background": { + "post": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.background", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Move active foreground backgroundable tools for this session into background observation. Idle requests are a no-op.", + "summary": "Background blocking session tools" + } + }, + "/api/session/{sessionID}/message/{messageID}": { + "get": { + "tags": [ + "sessions" + ], + "operationId": "v2.session.message", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "messageID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Session.Message" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | MessageNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve one projected message owned by the Session.", + "summary": "Get session message" + } + }, + "/api/session/{sessionID}/message": { + "get": { + "tags": [ + "messages" + ], + "operationId": "v2.session.messages", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "limit", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to return. When omitted, the endpoint returns its default page size." + }, + "required": false + }, + { + "name": "order", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + { + "type": "null" + } + ], + "description": "Message order for the first page. Use desc for newest first or asc for oldest first." + }, + "required": false + }, + { + "name": "cursor", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string", + "description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order." + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "SessionMessagesResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionMessagesResponse" + } + } + } + }, + "400": { + "description": "InvalidCursorError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidCursorError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "500": { + "description": "UnknownError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnknownError" + } + } + } + } + }, + "description": "Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", + "summary": "Get session messages" + } + }, + "/api/model": { + "get": { + "tags": [ + "models" + ], + "operationId": "v2.model.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelV2.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve available models ordered by release date.", + "summary": "List models" + } + }, + "/api/model/default": { + "get": { + "tags": [ + "models" + ], + "operationId": "v2.model.default", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelV2.Info" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve the model used when a session has no explicit model selection.", + "summary": "Get default model" + } + }, + "/api/generate": { + "post": { + "tags": [ + "generate" + ], + "operationId": "v2.generate.text", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "GenerateTextResponse", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenerateTextResponse" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Run one stateless model generation at the requested location and return the assistant text. Uses the location's default model when none is specified.", + "summary": "Generate text", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Ref" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "prompt" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/provider": { + "get": { + "tags": [ + "providers" + ], + "operationId": "v2.provider.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProviderV2.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve active AI providers so clients can show provider availability and configuration.", + "summary": "List providers" + } + }, + "/api/provider/{providerID}": { + "get": { + "tags": [ + "providers" + ], + "operationId": "v2.provider.get", + "parameters": [ + { + "name": "providerID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/ProviderV2.Info" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "ProviderNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderNotFoundError" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableError" + } + } + } + } + }, + "description": "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.", + "summary": "Get provider" + } + }, + "/api/integration": { + "get": { + "tags": [ + "integrations" + ], + "operationId": "v2.integration.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Integration.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve available integrations and their authentication methods.", + "summary": "List integrations" + } + }, + "/api/integration/{integrationID}": { + "get": { + "tags": [ + "integrations" + ], + "operationId": "v2.integration.get", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.Info" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve one integration and its authentication methods.", + "summary": "Get integration" + } + }, + "/api/integration/{integrationID}/connect/key": { + "post": { + "tags": [ + "integrations" + ], + "operationId": "v2.integration.connect.key", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Run a key authentication method and store the resulting credential.", + "summary": "Connect with key", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "key" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/{integrationID}/connect/oauth": { + "post": { + "tags": [ + "integrations" + ], + "operationId": "v2.integration.connect.oauth", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Integration.Attempt" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Start an OAuth attempt and return the authorization details.", + "summary": "Begin OAuth connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "methodID": { + "type": "string" + }, + "inputs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "methodID", + "inputs" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/integration/attempt/{attemptID}": { + "get": { + "tags": [ + "integrations" + ], + "operationId": "v2.integration.attempt.status", + "parameters": [ + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Integration.AttemptStatus" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Poll the current status of an OAuth attempt.", + "summary": "Get OAuth attempt status" + }, + "delete": { + "tags": [ + "integrations" + ], + "operationId": "v2.integration.attempt.cancel", + "parameters": [ + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Cancel an OAuth attempt and release its resources.", + "summary": "Cancel OAuth connection" + } + }, + "/api/integration/attempt/{attemptID}/complete": { + "post": { + "tags": [ + "integrations" + ], + "operationId": "v2.integration.attempt.complete", + "parameters": [ + { + "name": "attemptID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Complete a code-based OAuth attempt and store the resulting credential.", + "summary": "Complete OAuth connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/mcp": { + "get": { + "tags": [ + "mcp" + ], + "operationId": "v2.mcp.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mcp.Server" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve configured MCP servers and their connection status.", + "summary": "List MCP servers" + } + }, + "/api/credential/{credentialID}": { + "patch": { + "tags": [ + "server.credential" + ], + "operationId": "v2.credential.update", + "parameters": [ + { + "name": "credentialID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Update a stored credential label.", + "summary": "Update credential", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string" + } + }, + "required": [ + "label" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": [ + "server.credential" + ], + "operationId": "v2.credential.remove", + "parameters": [ + { + "name": "credentialID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Remove a stored integration credential.", + "summary": "Remove credential" + } + }, + "/api/project/current": { + "get": { + "tags": [ + "projects" + ], + "operationId": "v2.project.current", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Project.Current", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project.Current" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Resolve the project for the requested location.", + "summary": "Get current project" + } + }, + "/api/project/{projectID}/directories": { + "get": { + "tags": [ + "projects" + ], + "operationId": "v2.project.directories", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Project.Directories", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project.Directories" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List known local absolute directories for a project.", + "summary": "List project directories" + } + }, + "/api/form/request": { + "get": { + "tags": [ + "forms" + ], + "operationId": "v2.form.request.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.FormInfo" + }, + { + "$ref": "#/components/schemas/Form.UrlInfo" + } + ] + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve pending forms for a location.", + "summary": "List pending form requests" + } + }, + "/api/session/{sessionID}/form": { + "get": { + "tags": [ + "forms" + ], + "operationId": "v2.session.form.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.FormInfo" + }, + { + "$ref": "#/components/schemas/Form.UrlInfo" + } + ] + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending forms for a session.", + "summary": "List session forms" + }, + "post": { + "tags": [ + "forms" + ], + "operationId": "v2.session.form.create", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.FormInfo" + }, + { + "$ref": "#/components/schemas/Form.UrlInfo" + } + ] + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestError1" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + } + }, + "description": "Create a form for a session.", + "summary": "Create session form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.CreatePayload" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/form/{formID}": { + "get": { + "tags": [ + "forms" + ], + "operationId": "v2.session.form.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.FormInfo" + }, + { + "$ref": "#/components/schemas/Form.UrlInfo" + } + ] + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve a form for a session.", + "summary": "Get session form" + } + }, + "/api/session/{sessionID}/form/{formID}/state": { + "get": { + "tags": [ + "forms" + ], + "operationId": "v2.session.form.state", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.State" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve the current state for a form.", + "summary": "Get form state" + } + }, + "/api/session/{sessionID}/form/{formID}/reply": { + "post": { + "tags": [ + "forms" + ], + "operationId": "v2.session.form.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "FormInvalidAnswerError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormInvalidAnswerError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledError" + } + } + } + } + }, + "description": "Submit an answer to a pending form.", + "summary": "Reply to form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/form/{formID}/cancel": { + "post": { + "tags": [ + "forms" + ], + "operationId": "v2.session.form.cancel", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledError" + } + } + } + } + }, + "description": "Cancel a pending form.", + "summary": "Cancel form" + } + }, + "/api/permission/request": { + "get": { + "tags": [ + "permissions" + ], + "operationId": "v2.permission.request.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2.Request" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve pending permission requests for a location.", + "summary": "List pending permission requests" + } + }, + "/api/permission/saved": { + "get": { + "tags": [ + "permissions" + ], + "operationId": "v2.permission.saved.list", + "parameters": [ + { + "name": "projectID", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionSaved.Info" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve saved permissions, optionally filtered by project.", + "summary": "List saved permissions" + } + }, + "/api/permission/saved/{id}": { + "delete": { + "tags": [ + "permissions" + ], + "operationId": "v2.permission.saved.remove", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Remove a saved permission by ID.", + "summary": "Remove saved permission" + } + }, + "/api/session/{sessionID}/permission": { + "post": { + "tags": [ + "permissions" + ], + "operationId": "v2.session.permission.create", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "effect": { + "$ref": "#/components/schemas/PermissionV2.Effect" + } + }, + "required": [ + "id", + "effect" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Evaluate and, when approval is required, create a permission request for a session.", + "summary": "Create permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + { + "type": "null" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/PermissionV2.Source" + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "action", + "resources" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "get": { + "tags": [ + "permissions" + ], + "operationId": "v2.session.permission.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2.Request" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending permission requests owned by a session.", + "summary": "List session permission requests" + } + }, + "/api/session/{sessionID}/permission/{requestID}": { + "get": { + "tags": [ + "permissions" + ], + "operationId": "v2.session.permission.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/PermissionV2.Request" + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve a pending permission request owned by a session.", + "summary": "Get permission request" + } + }, + "/api/session/{sessionID}/permission/{requestID}/reply": { + "post": { + "tags": [ + "permissions" + ], + "operationId": "v2.session.permission.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Respond to a pending permission request owned by a session.", + "summary": "Reply to pending permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reply": { + "$ref": "#/components/schemas/PermissionV2.Reply" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "reply" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/fs/read/*": { + "get": { + "tags": [ + "filesystem" + ], + "operationId": "v2.fs.read", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Serve one file relative to the requested location.", + "summary": "Read file" + } + }, + "/api/fs/list": { + "get": { + "tags": [ + "filesystem" + ], + "operationId": "v2.fs.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + }, + { + "name": "path", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List direct children of one directory relative to the requested location.", + "summary": "List directory" + } + }, + "/api/fs/find": { + "get": { + "tags": [ + "filesystem" + ], + "operationId": "v2.fs.find", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "file", + "directory" + ] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystem.Entry" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Find recursively ranked filesystem entries relative to the requested location.", + "summary": "Find files" + } + }, + "/api/command": { + "get": { + "tags": [ + "commands" + ], + "operationId": "v2.command.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CommandV2.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve currently registered commands.", + "summary": "List commands" + } + }, + "/api/skill": { + "get": { + "tags": [ + "skills" + ], + "operationId": "v2.skill.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SkillV2.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve currently registered skills.", + "summary": "List skills" + } + }, + "/api/event": { + "get": { + "tags": [ + "events" + ], + "operationId": "v2.event.subscribe", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "text/event-stream": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "event": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/V2EventStream" + } + }, + "required": [ + "id", + "event", + "data" + ], + "additionalProperties": false + }, + "x-effect-stream": { + "encoding": "sse", + "causeSchema": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Fail" + ] + }, + "error": { + "not": {} + } + }, + "required": [ + "_tag", + "error" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Die" + ] + }, + "defect": {} + }, + "required": [ + "_tag", + "defect" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Interrupt" + ] + }, + "fiberId": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "fiberId" + ], + "additionalProperties": false + } + ] + } + }, + "errorSchema": { + "not": {} + }, + "failureEvent": "effect/httpapi/stream/failure" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed. Consumers that need reliability should combine the changes feed with durable session log reads.", + "summary": "Subscribe to events" + } + }, + "/api/event/changes": { + "get": { + "tags": [ + "events" + ], + "operationId": "v2.event.changes", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "text/event-stream": { + "schema": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "event": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/EventLog.ChangeStream" + } + }, + "required": [ + "id", + "event", + "data" + ], + "additionalProperties": false + }, + "x-effect-stream": { + "encoding": "sse", + "causeSchema": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Fail" + ] + }, + "error": { + "not": {} + } + }, + "required": [ + "_tag", + "error" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Die" + ] + }, + "defect": {} + }, + "required": [ + "_tag", + "defect" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "Interrupt" + ] + }, + "fiberId": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "fiberId" + ], + "additionalProperties": false + } + ] + } + }, + "errorSchema": { + "not": {} + }, + "failureEvent": "effect/httpapi/stream/failure" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Payload-free hint channel: after an event commits, a subscriber eventually receives a hint for that aggregate with seq at or beyond the event, or a sweep-required marker. Hints coalesce to the latest seq per aggregate under backpressure and the stream never fails from overflow. No consumer may derive correctness from receiving a hint; correctness always comes from durable log reads plus the consumer's own checkpoint. A sweep-required marker is emitted first on every (re)subscribe and whenever hint retention is exceeded: treat every aggregate as potentially dirty and recover via bounded sweep plus log reads.", + "summary": "Subscribe to change hints" + } + }, + "/api/pty": { + "get": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pty" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List PTY sessions for a location, including exited sessions retained until removal.", + "summary": "List PTY sessions" + }, + "post": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.create", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Create a pseudo-terminal session for a location.", + "summary": "Create PTY session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "type": "string" + }, + "title": { + "type": "string" + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/pty/{ptyID}": { + "get": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.get", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Get one PTY session, including its exit code once exited.", + "summary": "Get PTY session" + }, + "put": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.update", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Update the title or viewport size of one PTY session.", + "summary": "Update PTY session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "size": { + "type": "object", + "properties": { + "rows": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "cols": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "required": [ + "rows", + "cols" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.remove", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Terminate and remove one PTY session.", + "summary": "Remove PTY session" + } + }, + "/api/pty/{ptyID}/connect-token": { + "post": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.connectToken", + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/PtyTicket.ConnectToken" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Create a short-lived single-use ticket for opening a PTY WebSocket connection.", + "summary": "Create PTY WebSocket token" + } + }, + "/api/pty/{ptyID}/connect": { + "get": { + "tags": [ + "pty" + ], + "operationId": "v2.pty.connect", + "x-websocket": true, + "parameters": [ + { + "name": "ptyID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "required": true + }, + { + "in": "query", + "name": "location[directory]", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "location[workspace]", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "cursor", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "ticket", + "schema": { + "type": "string" + } + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "ForbiddenError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "PtyNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PtyNotFoundError" + } + } + } + } + }, + "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", + "summary": "Connect to PTY session" + } + }, + "/api/shell": { + "get": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Shell1" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List currently running shell commands for a location. Exited commands are not included.", + "summary": "List running shell commands" + }, + "post": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.create", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Shell1" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.", + "summary": "Run shell command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "timeout": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/api/shell/{id}": { + "get": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.get", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Shell1" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } + } + }, + "description": "Get one shell command, including its status and exit code once exited.", + "summary": "Get shell command" + }, + "delete": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.remove", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } + } + }, + "description": "Terminate and remove one shell command and its retained output.", + "summary": "Remove shell command" + } + }, + "/api/shell/{id}/output": { + "get": { + "tags": [ + "shell" + ], + "operationId": "v2.shell.output", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" + } + ] + }, + "required": false + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "object", + "properties": { + "output": { + "type": "string" + }, + "cursor": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "size": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "truncated": { + "type": "boolean" + } + }, + "required": [ + "output", + "cursor", + "size", + "truncated" + ], + "additionalProperties": false + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "ShellNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShellNotFoundError" + } + } + } + } + }, + "description": "Page through captured combined output by absolute byte cursor.", + "summary": "Read shell output" + } + }, + "/api/question/request": { + "get": { + "tags": [ + "session questions" + ], + "operationId": "v2.question.request.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Request" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Retrieve pending question requests for a location.", + "summary": "List pending question requests" + } + }, + "/api/session/{sessionID}/question": { + "get": { + "tags": [ + "session questions" + ], + "operationId": "v2.session.question.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Request" + } + } + }, + "required": [ + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending question requests owned by a session.", + "summary": "List session question requests" + } + }, + "/api/session/{sessionID}/question/{requestID}/reply": { + "post": { + "tags": [ + "session questions" + ], + "operationId": "v2.session.question.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Answer a pending question request owned by a session.", + "summary": "Reply to pending question request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionV2.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/question/{requestID}/reject": { + "post": { + "tags": [ + "session questions" + ], + "operationId": "v2.session.question.reject", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | QuestionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Reject a pending question request owned by a session.", + "summary": "Reject pending question request" + } + }, + "/api/reference": { + "get": { + "tags": [ + "reference" + ], + "operationId": "v2.reference.list", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference.Info" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List references available in the requested location.", + "summary": "List references" + } + }, + "/experimental/project/{projectID}/copy": { + "post": { + "tags": [ + "projectCopy" + ], + "operationId": "v2.projectCopy.create", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "ProjectCopy.Copy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectCopy.Copy" + } + } + } + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "strategy": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "strategy", + "directory" + ], + "additionalProperties": false + } + } + }, + "required": true + } + }, + "delete": { + "tags": [ + "projectCopy" + ], + "operationId": "v2.projectCopy.remove", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "force": { + "type": "boolean" + } + }, + "required": [ + "directory", + "force" + ], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, + "/experimental/project/{projectID}/copy/refresh": { + "post": { + "tags": [ + "projectCopy" + ], + "operationId": "v2.projectCopy.refresh", + "parameters": [ + { + "name": "projectID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "ProjectCopyError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectCopyError" + }, + { + "$ref": "#/components/schemas/InvalidRequestError" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + } + } + }, + "/api/vcs/status": { + "get": { + "tags": [ + "vcs" + ], + "operationId": "v2.vcs.status", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Vcs.FileStatus" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "List uncommitted working-copy changes relative to the requested location.", + "summary": "VCS status" + } + }, + "/api/vcs/diff": { + "get": { + "tags": [ + "vcs" + ], + "operationId": "v2.vcs.diff", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + }, + { + "name": "mode", + "in": "query", + "schema": { + "$ref": "#/components/schemas/Vcs.Mode" + }, + "required": true + }, + { + "name": "context", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnapshotFileDiff" + } + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location.", + "summary": "VCS diff" + } + } + }, + "components": { + "schemas": { + "UnauthorizedError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "UnauthorizedError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "InvalidRequestError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "InvalidRequestError" + ] + }, + "message": { + "type": "string" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "field": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "Location.Info": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspaceID": { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + }, + "project": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "directory": { + "type": "string" + } + }, + "required": [ + "id", + "directory" + ], + "additionalProperties": false + } + }, + "required": [ + "directory", + "project" + ], + "additionalProperties": false + }, + "Model.Ref": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "id", + "providerID" + ], + "additionalProperties": false + }, + "Provider.Settings": { + "type": "object" + }, + "Provider.Request": { + "type": "object", + "properties": { + "settings": { + "$ref": "#/components/schemas/Provider.Settings" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": [ + "settings", + "headers", + "body" + ], + "additionalProperties": false + }, + "Agent.Color": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^#[0-9a-fA-F]{6}$" + } + ] + }, + { + "type": "string", + "enum": [ + "primary", + "secondary", + "accent", + "success", + "warning", + "error", + "info" + ] + } + ] + }, + "PermissionV2.Effect": { + "type": "string", + "enum": [ + "allow", + "deny", + "ask" + ] + }, + "PermissionV2.Rule": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "effect": { + "$ref": "#/components/schemas/PermissionV2.Effect" + } + }, + "required": [ + "action", + "resource", + "effect" + ], + "additionalProperties": false + }, + "PermissionV2.Ruleset": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2.Rule" + } + }, + "AgentV2.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "request": { + "$ref": "#/components/schemas/Provider.Request" + }, + "system": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "hidden": { + "type": "boolean" + }, + "color": { + "$ref": "#/components/schemas/Agent.Color" + }, + "steps": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + "permissions": { + "$ref": "#/components/schemas/PermissionV2.Ruleset" + } + }, + "required": [ + "id", + "request", + "mode", + "hidden", + "permissions" + ], + "additionalProperties": false + }, + "Plugin.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "Location.Ref": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspaceID": { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + } + }, + "required": [ + "directory" + ], + "additionalProperties": false + }, + "File.Diff": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "added", + "modified", + "deleted" + ] + }, + "additions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "deletions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "patch": { + "type": "string" + } + }, + "required": [ + "path", + "status", + "additions", + "deletions", + "patch" + ], + "additionalProperties": false + }, + "Revert.State": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "partID": { + "type": "string" + }, + "snapshot": { + "type": "string" + }, + "diff": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/File.Diff" + } + } + }, + "required": [ + "messageID" + ], + "additionalProperties": false + }, + "SessionV2.Info": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "projectID": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "updated": { + "type": "number" + }, + "archived": { + "type": "number" + } + }, + "required": [ + "created", + "updated" + ], + "additionalProperties": false + }, + "title": { + "type": "string" + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "subpath": { + "type": "string" + }, + "revert": { + "$ref": "#/components/schemas/Revert.State" + } + }, + "required": [ + "id", + "projectID", + "cost", + "tokens", + "time", + "title", + "location" + ], + "additionalProperties": false + }, + "SessionWatermarks": { + "type": "object", + "patternProperties": { + "^ses": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "description": "Durable log seq each session's snapshot was computed at. Attach a live log read after the watermark to compose fetch and stream gap-free; apply a snapshot only where its watermark is at or beyond already-applied events. Sessions without durable events are absent." + }, + "SessionsResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionV2.Info" + } + }, + "watermarks": { + "$ref": "#/components/schemas/SessionWatermarks" + }, + "cursor": { + "type": "object", + "properties": { + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "data", + "watermarks", + "cursor" + ], + "additionalProperties": false + }, + "InvalidCursorError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "InvalidCursorError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "InvalidRequestError1": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "InvalidRequestError" + ] + }, + "message": { + "type": "string" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "field": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "SessionActive": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "running" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "SessionNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "SessionNotFoundError" + ] + }, + "sessionID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "sessionID", + "message" + ], + "additionalProperties": false + }, + "MessageNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "MessageNotFoundError" + ] + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "sessionID", + "messageID", + "message" + ], + "additionalProperties": false + }, + "Prompt.Source": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + }, + "text": { + "type": "string" + } + }, + "required": [ + "start", + "end", + "text" + ], + "additionalProperties": false + }, + "PromptInput.FileAttachment": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/Prompt.Source" + } + }, + "required": [ + "uri" + ], + "additionalProperties": false + }, + "Prompt.AgentAttachment": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/Prompt.Source" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "PromptInput": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PromptInput.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "Prompt.FileAttachment": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "mime": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/Prompt.Source" + } + }, + "required": [ + "uri", + "mime" + ], + "additionalProperties": false + }, + "Prompt": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + } + }, + "required": [ + "text" + ], + "additionalProperties": false + }, + "SessionInput.Admitted": { + "type": "object", + "properties": { + "admittedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "prompt": { + "$ref": "#/components/schemas/Prompt" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + }, + "timeCreated": { + "type": "number" + }, + "promotedSeq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "admittedSeq", + "id", + "sessionID", + "prompt", + "delivery", + "timeCreated" + ], + "additionalProperties": false + }, + "ConflictError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ConflictError" + ] + }, + "message": { + "type": "string" + }, + "resource": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "CommandNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "CommandNotFoundError" + ] + }, + "command": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "command", + "message" + ], + "additionalProperties": false + }, + "CommandEvaluationError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "CommandEvaluationError" + ] + }, + "command": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "command", + "message" + ], + "additionalProperties": false + }, + "SkillNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "SkillNotFoundError" + ] + }, + "skill": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "skill", + "message" + ], + "additionalProperties": false + }, + "SessionBusyError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "SessionBusyError" + ] + }, + "sessionID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "sessionID", + "message" + ], + "additionalProperties": false + }, + "ServiceUnavailableError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ServiceUnavailableError" + ] + }, + "message": { + "type": "string" + }, + "service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "UnknownError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "UnknownError" + ] + }, + "message": { + "type": "string" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "Session.Message.AgentSwitched": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "agent-switched" + ] + }, + "agent": { + "type": "string" + } + }, + "required": [ + "id", + "time", + "type", + "agent" + ], + "additionalProperties": false + }, + "Session.Message.ModelSwitched": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "model-switched" + ] + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + } + }, + "required": [ + "id", + "time", + "type", + "model" + ], + "additionalProperties": false + }, + "Session.Message.User": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "text": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.AgentAttachment" + } + }, + "type": { + "type": "string", + "enum": [ + "user" + ] + } + }, + "required": [ + "id", + "time", + "text", + "type" + ], + "additionalProperties": false + }, + "Session.Message.Synthetic": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "synthetic" + ] + } + }, + "required": [ + "id", + "time", + "sessionID", + "text", + "type" + ], + "additionalProperties": false + }, + "Session.Message.System": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "system" + ] + }, + "text": { + "type": "string" + } + }, + "required": [ + "id", + "time", + "type", + "text" + ], + "additionalProperties": false + }, + "Session.Message.Skill": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "skill" + ] + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "id", + "time", + "type", + "name", + "text" + ], + "additionalProperties": false + }, + "Session.Message.Shell": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "shell" + ] + }, + "callID": { + "type": "string" + }, + "command": { + "type": "string" + }, + "output": { + "type": "string" + } + }, + "required": [ + "id", + "time", + "type", + "callID", + "command", + "output" + ], + "additionalProperties": false + }, + "Session.Message.Assistant.Text": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "id": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "type", + "id", + "text" + ], + "additionalProperties": false + }, + "LLM.ProviderMetadata": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "Session.Message.Assistant.Reasoning": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "reasoning" + ] + }, + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "providerMetadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "id", + "text" + ], + "additionalProperties": false + }, + "Session.Message.ToolState.Pending": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending" + ] + }, + "input": { + "type": "string" + } + }, + "required": [ + "status", + "input" + ], + "additionalProperties": false + }, + "Tool.TextContent": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "text": { + "type": "string" + } + }, + "required": [ + "type", + "text" + ], + "additionalProperties": false + }, + "Tool.FileContent": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "uri": { + "type": "string" + }, + "mime": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "type", + "uri", + "mime" + ], + "additionalProperties": false + }, + "LLM.ToolContent": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tool.TextContent" + }, + { + "$ref": "#/components/schemas/Tool.FileContent" + } + ] + }, + "Session.Message.ToolState.Running": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running" + ] + }, + "input": { + "type": "object" + }, + "structured": { + "type": "object" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + } + }, + "required": [ + "status", + "input", + "structured", + "content" + ], + "additionalProperties": false + }, + "Session.Message.ToolState.Completed": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "completed" + ] + }, + "input": { + "type": "object" + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Prompt.FileAttachment" + } + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "outputPaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "structured": { + "type": "object" + }, + "result": {} + }, + "required": [ + "status", + "input", + "content", + "structured" + ], + "additionalProperties": false + }, + "Session.Error.Unknown": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "unknown" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "type", + "message" + ], + "additionalProperties": false + }, + "Session.Message.ToolState.Error": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "input": { + "type": "object" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "structured": { + "type": "object" + }, + "error": { + "$ref": "#/components/schemas/Session.Error.Unknown" + }, + "result": {} + }, + "required": [ + "status", + "input", + "content", + "structured", + "error" + ], + "additionalProperties": false + }, + "Session.Message.Assistant.Tool": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tool" + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "provider": { + "type": "object", + "properties": { + "executed": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata" + }, + "resultMetadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata" + } + }, + "required": [ + "executed" + ], + "additionalProperties": false + }, + "state": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.ToolState.Pending" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Running" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Completed" + }, + { + "$ref": "#/components/schemas/Session.Message.ToolState.Error" + } + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "ran": { + "type": "number" + }, + "completed": { + "type": "number" + }, + "pruned": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "id", + "name", + "state", + "time" + ], + "additionalProperties": false + }, + "Session.Message.Assistant": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "assistant" + ] + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "content": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.Assistant.Text" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Reasoning" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant.Tool" + } + ] + } + }, + "snapshot": { + "type": "object", + "properties": { + "start": { + "type": "string" + }, + "end": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "finish": { + "type": "string" + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + }, + "error": { + "$ref": "#/components/schemas/Session.Error.Unknown" + } + }, + "required": [ + "id", + "time", + "type", + "agent", + "model", + "content" + ], + "additionalProperties": false + }, + "Session.Message.Compaction": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "summary": { + "type": "string" + }, + "recent": { + "type": "string" + }, + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": [ + "created" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "reason", + "summary", + "recent", + "id", + "time" + ], + "additionalProperties": false + }, + "Session.Message": { + "anyOf": [ + { + "$ref": "#/components/schemas/Session.Message.AgentSwitched" + }, + { + "$ref": "#/components/schemas/Session.Message.ModelSwitched" + }, + { + "$ref": "#/components/schemas/Session.Message.User" + }, + { + "$ref": "#/components/schemas/Session.Message.Synthetic" + }, + { + "$ref": "#/components/schemas/Session.Message.System" + }, + { + "$ref": "#/components/schemas/Session.Message.Skill" + }, + { + "$ref": "#/components/schemas/Session.Message.Shell" + }, + { + "$ref": "#/components/schemas/Session.Message.Assistant" + }, + { + "$ref": "#/components/schemas/Session.Message.Compaction" + } + ] + }, + "SessionContextEntry.Key": { + "type": "string", + "allOf": [ + { + "pattern": "^[a-z0-9][a-z0-9._-]*$", + "description": "Context entry key (lowercase alphanumerics plus . _ -)" + } + ] + }, + "SessionContextEntry.Info": { + "type": "object", + "properties": { + "key": { + "$ref": "#/components/schemas/SessionContextEntry.Key" + }, + "value": {} + }, + "required": [ + "key", + "value" + ], + "additionalProperties": false + }, + "session.next.agent.switched": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.agent.switched" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "agent": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID", + "agent" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.model.switched": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.model.switched" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID", + "model" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.moved": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.moved" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "subdirectory": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "location" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.renamed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.renamed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "title" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.forked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.forked" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "timestamp", + "sessionID", + "parentID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.prompted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.prompted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "prompt": { + "$ref": "#/components/schemas/Prompt" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID", + "prompt", + "delivery" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.prompt.admitted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.prompt.admitted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "prompt": { + "$ref": "#/components/schemas/Prompt" + }, + "delivery": { + "type": "string", + "enum": [ + "steer", + "queue" + ] + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID", + "prompt", + "delivery" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.context.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.context.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "text": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.synthetic": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.synthetic" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.skill.activated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.skill.activated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "name": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID", + "name", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.shell.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.shell.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "command": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID", + "callID", + "command" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.shell.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.shell.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "callID": { + "type": "string" + }, + "output": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "callID", + "output" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.step.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.step.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "snapshot": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "agent", + "model" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.step.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.step.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "finish": { + "type": "string" + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + }, + "snapshot": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "finish", + "cost", + "tokens" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.step.failed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.step.failed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "error": { + "$ref": "#/components/schemas/Session.Error.Unknown" + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.text.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.text.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "textID": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "textID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.text.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.text.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "textID": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "textID", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.tool.input.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.tool.input.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "callID", + "name" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.tool.input.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.tool.input.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "callID", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "LLM.ProviderMetadata3": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "session.next.tool.called": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.tool.called" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "tool": { + "type": "string" + }, + "input": { + "type": "object" + }, + "provider": { + "type": "object", + "properties": { + "executed": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata3" + } + }, + "required": [ + "executed" + ], + "additionalProperties": false + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "callID", + "tool", + "input", + "provider" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.tool.progress": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.tool.progress" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "structured": { + "type": "object" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "callID", + "structured", + "content" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "LLM.ProviderMetadata4": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "session.next.tool.success": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.tool.success" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "structured": { + "type": "object" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LLM.ToolContent" + } + }, + "outputPaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "result": {}, + "provider": { + "type": "object", + "properties": { + "executed": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata4" + } + }, + "required": [ + "executed" + ], + "additionalProperties": false + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "callID", + "structured", + "content", + "provider" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "LLM.ProviderMetadata5": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "session.next.tool.failed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.tool.failed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "error": { + "$ref": "#/components/schemas/Session.Error.Unknown" + }, + "result": {}, + "provider": { + "type": "object", + "properties": { + "executed": { + "type": "boolean" + }, + "metadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata5" + } + }, + "required": [ + "executed" + ], + "additionalProperties": false + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "callID", + "error", + "provider" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "LLM.ProviderMetadata6": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "session.next.reasoning.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.reasoning.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "reasoningID": { + "type": "string" + }, + "providerMetadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata6" + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "reasoningID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "LLM.ProviderMetadata7": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "session.next.reasoning.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.reasoning.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "reasoningID": { + "type": "string" + }, + "text": { + "type": "string" + }, + "providerMetadata": { + "$ref": "#/components/schemas/LLM.ProviderMetadata7" + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "reasoningID", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.retry_error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "statusCode": { + "type": "number" + }, + "isRetryable": { + "type": "boolean" + }, + "responseHeaders": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "responseBody": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "message", + "isRetryable" + ], + "additionalProperties": false + }, + "session.next.retried": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.retried" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "attempt": { + "type": "number" + }, + "error": { + "$ref": "#/components/schemas/session.next.retry_error" + } + }, + "required": [ + "timestamp", + "sessionID", + "attempt", + "error" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.compaction.started": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.compaction.started" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID", + "reason" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.compaction.ended": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.compaction.ended" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "text": { + "type": "string" + }, + "recent": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID", + "reason", + "text", + "recent" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.revert.staged": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.revert.staged" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "revert": { + "$ref": "#/components/schemas/Revert.State" + } + }, + "required": [ + "timestamp", + "sessionID", + "revert" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.revert.cleared": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.revert.cleared" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": [ + "timestamp", + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.revert.committed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.revert.committed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "SessionDurableEvent": { + "oneOf": [ + { + "$ref": "#/components/schemas/session.next.agent.switched" + }, + { + "$ref": "#/components/schemas/session.next.model.switched" + }, + { + "$ref": "#/components/schemas/session.next.moved" + }, + { + "$ref": "#/components/schemas/session.next.renamed" + }, + { + "$ref": "#/components/schemas/session.next.forked" + }, + { + "$ref": "#/components/schemas/session.next.prompted" + }, + { + "$ref": "#/components/schemas/session.next.prompt.admitted" + }, + { + "$ref": "#/components/schemas/session.next.context.updated" + }, + { + "$ref": "#/components/schemas/session.next.synthetic" + }, + { + "$ref": "#/components/schemas/session.next.skill.activated" + }, + { + "$ref": "#/components/schemas/session.next.shell.started" + }, + { + "$ref": "#/components/schemas/session.next.shell.ended" + }, + { + "$ref": "#/components/schemas/session.next.step.started" + }, + { + "$ref": "#/components/schemas/session.next.step.ended" + }, + { + "$ref": "#/components/schemas/session.next.step.failed" + }, + { + "$ref": "#/components/schemas/session.next.text.started" + }, + { + "$ref": "#/components/schemas/session.next.text.ended" + }, + { + "$ref": "#/components/schemas/session.next.tool.input.started" + }, + { + "$ref": "#/components/schemas/session.next.tool.input.ended" + }, + { + "$ref": "#/components/schemas/session.next.tool.called" + }, + { + "$ref": "#/components/schemas/session.next.tool.progress" + }, + { + "$ref": "#/components/schemas/session.next.tool.success" + }, + { + "$ref": "#/components/schemas/session.next.tool.failed" + }, + { + "$ref": "#/components/schemas/session.next.reasoning.started" + }, + { + "$ref": "#/components/schemas/session.next.reasoning.ended" + }, + { + "$ref": "#/components/schemas/session.next.retried" + }, + { + "$ref": "#/components/schemas/session.next.compaction.started" + }, + { + "$ref": "#/components/schemas/session.next.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.next.revert.staged" + }, + { + "$ref": "#/components/schemas/session.next.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.next.revert.committed" + } + ] + }, + "EventLog.Synced": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "log.synced" + ] + }, + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "type", + "aggregateID" + ], + "additionalProperties": false, + "description": "Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq." + }, + "SessionLogItem": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionDurableEvent" + }, + { + "$ref": "#/components/schemas/EventLog.Synced" + } + ] + }, + "SessionLogItemStream": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/SessionLogItem" + }, + "contentMediaType": "application/json" + }, + "SessionMessagesResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session.Message" + } + }, + "watermark": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "cursor": { + "type": "object", + "properties": { + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "data", + "cursor" + ], + "additionalProperties": false + }, + "Model.Api": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "aisdk" + ] + }, + "package": { + "type": "string" + }, + "url": { + "type": "string" + }, + "settings": { + "type": "object" + } + }, + "required": [ + "id", + "type", + "package" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "native" + ] + }, + "url": { + "type": "string" + }, + "settings": { + "type": "object" + } + }, + "required": [ + "id", + "type", + "settings" + ], + "additionalProperties": false + } + ] + }, + "Model.Capabilities": { + "type": "object", + "properties": { + "tools": { + "type": "boolean" + }, + "input": { + "type": "array", + "items": { + "type": "string" + } + }, + "output": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "tools", + "input", + "output" + ], + "additionalProperties": false + }, + "Model.Cost": { + "type": "object", + "properties": { + "tier": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "context" + ] + }, + "size": { + "type": "integer" + } + }, + "required": [ + "type", + "size" + ], + "additionalProperties": false + }, + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "cache" + ], + "additionalProperties": false + }, + "ModelV2.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "family": { + "type": "string" + }, + "name": { + "type": "string" + }, + "api": { + "$ref": "#/components/schemas/Model.Api" + }, + "capabilities": { + "$ref": "#/components/schemas/Model.Capabilities" + }, + "request": { + "type": "object", + "properties": { + "settings": { + "$ref": "#/components/schemas/Provider.Settings" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "settings", + "headers", + "body" + ], + "additionalProperties": false + }, + "variants": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "settings": { + "$ref": "#/components/schemas/Provider.Settings" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "object" + } + }, + "required": [ + "id", + "settings", + "headers", + "body" + ], + "additionalProperties": false + } + }, + "time": { + "type": "object", + "properties": { + "released": { + "type": "number" + } + }, + "required": [ + "released" + ], + "additionalProperties": false + }, + "cost": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model.Cost" + } + }, + "status": { + "type": "string", + "enum": [ + "alpha", + "beta", + "deprecated", + "active" + ] + }, + "enabled": { + "type": "boolean" + }, + "limit": { + "type": "object", + "properties": { + "context": { + "type": "integer" + }, + "input": { + "type": "integer" + }, + "output": { + "type": "integer" + } + }, + "required": [ + "context", + "output" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "providerID", + "name", + "api", + "capabilities", + "request", + "variants", + "time", + "cost", + "status", + "enabled", + "limit" + ], + "additionalProperties": false + }, + "GenerateTextResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "data" + ], + "additionalProperties": false + }, + "Provider.AISDK": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "aisdk" + ] + }, + "package": { + "type": "string" + }, + "url": { + "type": "string" + }, + "settings": { + "type": "object" + } + }, + "required": [ + "type", + "package" + ], + "additionalProperties": false + }, + "Provider.Native": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "native" + ] + }, + "url": { + "type": "string" + }, + "settings": { + "type": "object" + } + }, + "required": [ + "type", + "settings" + ], + "additionalProperties": false + }, + "Provider.Api": { + "anyOf": [ + { + "$ref": "#/components/schemas/Provider.AISDK" + }, + { + "$ref": "#/components/schemas/Provider.Native" + } + ] + }, + "ProviderV2.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "integrationID": { + "type": "string" + }, + "name": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "api": { + "$ref": "#/components/schemas/Provider.Api" + }, + "request": { + "$ref": "#/components/schemas/Provider.Request" + } + }, + "required": [ + "id", + "name", + "api", + "request" + ], + "additionalProperties": false + }, + "ProviderNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ProviderNotFoundError" + ] + }, + "providerID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "providerID", + "message" + ], + "additionalProperties": false + }, + "Integration.When": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "neq" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "op", + "value" + ], + "additionalProperties": false + }, + "Integration.TextPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "when": { + "$ref": "#/components/schemas/Integration.When" + } + }, + "required": [ + "type", + "key", + "message" + ], + "additionalProperties": false + }, + "Integration.SelectPrompt": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "select" + ] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "hint": { + "type": "string" + } + }, + "required": [ + "label", + "value" + ], + "additionalProperties": false + } + }, + "when": { + "$ref": "#/components/schemas/Integration.When" + } + }, + "required": [ + "type", + "key", + "message", + "options" + ], + "additionalProperties": false + }, + "Integration.OAuthMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "oauth" + ] + }, + "label": { + "type": "string" + }, + "prompts": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.TextPrompt" + }, + { + "$ref": "#/components/schemas/Integration.SelectPrompt" + } + ] + } + } + }, + "required": [ + "id", + "type", + "label" + ], + "additionalProperties": false + }, + "Integration.KeyMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "key" + ] + }, + "label": { + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "Integration.EnvMethod": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "env" + ] + }, + "names": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "names" + ], + "additionalProperties": false + }, + "Integration.Method": { + "anyOf": [ + { + "$ref": "#/components/schemas/Integration.OAuthMethod" + }, + { + "$ref": "#/components/schemas/Integration.KeyMethod" + }, + { + "$ref": "#/components/schemas/Integration.EnvMethod" + } + ] + }, + "Connection.CredentialInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "credential" + ] + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "type", + "id", + "label" + ], + "additionalProperties": false + }, + "Connection.EnvInfo": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "env" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "type", + "name" + ], + "additionalProperties": false + }, + "Connection.Info": { + "anyOf": [ + { + "$ref": "#/components/schemas/Connection.CredentialInfo" + }, + { + "$ref": "#/components/schemas/Connection.EnvInfo" + } + ] + }, + "Integration.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Integration.Method" + } + }, + "connections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Connection.Info" + } + } + }, + "required": [ + "id", + "name", + "methods", + "connections" + ], + "additionalProperties": false + }, + "Integration.Attempt": { + "type": "object", + "properties": { + "attemptID": { + "type": "string" + }, + "url": { + "type": "string" + }, + "instructions": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "auto", + "code" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "attemptID", + "url", + "instructions", + "mode", + "time" + ], + "additionalProperties": false + }, + "Integration.AttemptStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "complete" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "message": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "message", + "time" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "expired" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "expires": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "created", + "expires" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "time" + ], + "additionalProperties": false + } + ] + }, + "Mcp.Status.Connected": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "connected" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.Disconnected": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "disconnected" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.Disabled": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "disabled" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.Failed": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "failed" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "error" + ], + "additionalProperties": false + }, + "Mcp.Status.NeedsAuth": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "needs_auth" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + "Mcp.Status.NeedsClientRegistration": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "needs_client_registration" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "error" + ], + "additionalProperties": false + }, + "Mcp.Server": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.Status.Connected" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Disconnected" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Disabled" + }, + { + "$ref": "#/components/schemas/Mcp.Status.Failed" + }, + { + "$ref": "#/components/schemas/Mcp.Status.NeedsAuth" + }, + { + "$ref": "#/components/schemas/Mcp.Status.NeedsClientRegistration" + } + ] + }, + "integrationID": { + "type": "string" + } + }, + "required": [ + "name", + "status" + ], + "additionalProperties": false + }, + "Project.Current": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "directory": { + "type": "string" + } + }, + "required": [ + "id", + "directory" + ], + "additionalProperties": false + }, + "Project.Directory": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "strategy": { + "type": "string" + } + }, + "required": [ + "directory" + ], + "additionalProperties": false + }, + "Project.Directories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Project.Directory" + } + }, + "Form.Metadata": { + "type": "object" + }, + "Form.When": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "neq" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + { + "type": "boolean" + } + ] + } + }, + "required": [ + "key", + "op", + "value" + ], + "additionalProperties": false + }, + "Form.Option": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "value", + "label" + ], + "additionalProperties": false + }, + "Form.StringField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": [ + "string" + ] + }, + "format": { + "type": "string", + "enum": [ + "email", + "uri", + "date", + "date-time" + ] + }, + "minLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "pattern": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "default": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "custom": { + "type": "boolean" + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.NumberField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": [ + "number" + ] + }, + "minimum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "maximum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "default": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.IntegerField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": [ + "integer" + ] + }, + "minimum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "maximum": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "default": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.BooleanField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": [ + "boolean" + ] + }, + "default": { + "type": "boolean" + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.MultiselectField": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When" + } + }, + "type": { + "type": "string", + "enum": [ + "multiselect" + ] + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "minItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "custom": { + "type": "boolean" + }, + "default": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "key", + "type", + "options" + ], + "additionalProperties": false + }, + "Form.FormInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" + }, + "mode": { + "type": "string", + "enum": [ + "form" + ] + }, + "fields": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField" + }, + { + "$ref": "#/components/schemas/Form.NumberField" + }, + { + "$ref": "#/components/schemas/Form.IntegerField" + }, + { + "$ref": "#/components/schemas/Form.BooleanField" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField" + } + ] + } + } + }, + "required": [ + "id", + "sessionID", + "mode", + "fields" + ], + "additionalProperties": false + }, + "Form.UrlInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" + }, + "mode": { + "type": "string", + "enum": [ + "url" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "sessionID", + "mode", + "url" + ], + "additionalProperties": false + }, + "Form.CreatePayload": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + { + "type": "null" + } + ] + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" + }, + "mode": { + "type": "string", + "enum": [ + "form", + "url" + ] + }, + "fields": { + "anyOf": [ + { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField" + }, + { + "$ref": "#/components/schemas/Form.NumberField" + }, + { + "$ref": "#/components/schemas/Form.IntegerField" + }, + { + "$ref": "#/components/schemas/Form.BooleanField" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField" + } + ] + } + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "mode" + ], + "additionalProperties": false + }, + "FormNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "FormNotFoundError" + ] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "id", + "message" + ], + "additionalProperties": false + }, + "Form.Value": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "Form.Answer": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Form.Value" + } + }, + "Form.State": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "answered" + ] + }, + "answer": { + "$ref": "#/components/schemas/Form.Answer" + } + }, + "required": [ + "status", + "answer" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "cancelled" + ] + } + }, + "required": [ + "status" + ], + "additionalProperties": false + } + ] + }, + "Form.Reply": { + "type": "object", + "properties": { + "answer": { + "$ref": "#/components/schemas/Form.Answer" + } + }, + "required": [ + "answer" + ], + "additionalProperties": false + }, + "FormAlreadySettledError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "FormAlreadySettledError" + ] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "id", + "message" + ], + "additionalProperties": false + }, + "FormInvalidAnswerError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "FormInvalidAnswerError" + ] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "id", + "message" + ], + "additionalProperties": false + }, + "PermissionV2.Source": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tool" + ] + }, + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": [ + "type", + "messageID", + "callID" + ], + "additionalProperties": false + } + ] + }, + "PermissionV2.Request": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/PermissionV2.Source" + } + }, + "required": [ + "id", + "sessionID", + "action", + "resources" + ], + "additionalProperties": false + }, + "PermissionSaved.Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "projectID": { + "type": "string" + }, + "action": { + "type": "string" + }, + "resource": { + "type": "string" + } + }, + "required": [ + "id", + "projectID", + "action", + "resource" + ], + "additionalProperties": false + }, + "PermissionNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "PermissionNotFoundError" + ] + }, + "requestID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "requestID", + "message" + ], + "additionalProperties": false + }, + "PermissionV2.Reply": { + "type": "string", + "enum": [ + "once", + "always", + "reject" + ] + }, + "FileSystem.Entry": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "file", + "directory" + ] + } + }, + "required": [ + "path", + "type" + ], + "additionalProperties": false + }, + "CommandV2.Info": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "template": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "$ref": "#/components/schemas/Model.Ref" + }, + "subtask": { + "type": "boolean" + } + }, + "required": [ + "name", + "template" + ], + "additionalProperties": false + }, + "SkillV2.Info": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "slash": { + "type": "boolean" + }, + "autoinvoke": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "required": [ + "name", + "location", + "content" + ], + "additionalProperties": false + }, + "models-dev.refreshed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "models-dev.refreshed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "integration.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "integration.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "integration.connection.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "integration.connection.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "integrationID": { + "type": "string" + } + }, + "required": [ + "integrationID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "catalog.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "catalog.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "agent.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "agent.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "SnapshotFileDiff": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "patch": { + "type": "string" + }, + "additions": { + "type": "number" + }, + "deletions": { + "type": "number" + }, + "status": { + "type": "string", + "enum": [ + "added", + "deleted", + "modified" + ] + } + }, + "required": [ + "additions", + "deletions" + ], + "additionalProperties": false + }, + "PermissionAction": { + "type": "string", + "enum": [ + "allow", + "deny", + "ask" + ] + }, + "PermissionRule": { + "type": "object", + "properties": { + "permission": { + "type": "string" + }, + "pattern": { + "type": "string" + }, + "action": { + "$ref": "#/components/schemas/PermissionAction" + } + }, + "required": [ + "permission", + "pattern", + "action" + ], + "additionalProperties": false + }, + "PermissionRuleset": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionRule" + } + }, + "Session": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "slug": { + "type": "string" + }, + "projectID": { + "type": "string" + }, + "workspaceID": { + "type": "string", + "allOf": [ + { + "pattern": "^wrk" + } + ] + }, + "directory": { + "type": "string" + }, + "path": { + "type": "string" + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "summary": { + "type": "object", + "properties": { + "additions": { + "type": "number" + }, + "deletions": { + "type": "number" + }, + "files": { + "type": "number" + }, + "diffs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnapshotFileDiff" + } + } + }, + "required": [ + "additions", + "deletions", + "files" + ], + "additionalProperties": false + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + }, + "share": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + "title": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "id", + "providerID" + ], + "additionalProperties": false + }, + "version": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "updated": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "compacting": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "archived": { + "type": "number" + } + }, + "required": [ + "created", + "updated" + ], + "additionalProperties": false + }, + "permission": { + "$ref": "#/components/schemas/PermissionRuleset" + }, + "revert": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "partID": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "snapshot": { + "type": "string" + }, + "diff": { + "type": "string" + } + }, + "required": [ + "messageID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "slug", + "projectID", + "directory", + "title", + "version", + "time" + ], + "additionalProperties": false + }, + "session.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.created" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Session" + } + }, + "required": [ + "sessionID", + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Session" + } + }, + "required": [ + "sessionID", + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.deleted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.deleted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Session" + } + }, + "required": [ + "sessionID", + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "JSONSchema": { + "type": "object" + }, + "OutputFormat": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "json_schema" + ] + }, + "schema": { + "$ref": "#/components/schemas/JSONSchema" + }, + "retryCount": { + "anyOf": [ + { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "schema" + ], + "additionalProperties": false + } + ] + }, + "UserMessage": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "role": { + "type": "string", + "enum": [ + "user" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "format": { + "anyOf": [ + { + "$ref": "#/components/schemas/OutputFormat" + }, + { + "type": "null" + } + ] + }, + "summary": { + "anyOf": [ + { + "type": "object", + "properties": { + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "diffs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnapshotFileDiff" + } + } + }, + "required": [ + "diffs" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + }, + "variant": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "providerID", + "modelID" + ], + "additionalProperties": false + }, + "system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "tools": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "role", + "time", + "agent", + "model" + ], + "additionalProperties": false + }, + "ProviderAuthError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "ProviderAuthError" + ] + }, + "data": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "providerID", + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "UnknownError1": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "UnknownError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "MessageOutputLengthError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "MessageOutputLengthError" + ] + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "MessageAbortedError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "MessageAbortedError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "StructuredOutputError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "StructuredOutputError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "retries": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "message", + "retries" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "ContextOverflowError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "ContextOverflowError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "responseBody": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "ContentFilterError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "ContentFilterError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "APIError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "APIError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "statusCode": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "isRetryable": { + "type": "boolean" + }, + "responseHeaders": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "responseBody": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message", + "isRetryable" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "AssistantMessage": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "role": { + "type": "string", + "enum": [ + "assistant" + ] + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "completed": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "created" + ], + "additionalProperties": false + }, + "error": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderAuthError" + }, + { + "$ref": "#/components/schemas/UnknownError1" + }, + { + "$ref": "#/components/schemas/MessageOutputLengthError" + }, + { + "$ref": "#/components/schemas/MessageAbortedError" + }, + { + "$ref": "#/components/schemas/StructuredOutputError" + }, + { + "$ref": "#/components/schemas/ContextOverflowError" + }, + { + "$ref": "#/components/schemas/ContentFilterError" + }, + { + "$ref": "#/components/schemas/APIError" + } + ] + }, + { + "type": "null" + } + ] + }, + "parentID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "modelID": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "path": { + "type": "object", + "properties": { + "cwd": { + "type": "string" + }, + "root": { + "type": "string" + } + }, + "required": [ + "cwd", + "root" + ], + "additionalProperties": false + }, + "summary": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "total": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + }, + "structured": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "variant": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "finish": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "role", + "time", + "parentID", + "modelID", + "providerID", + "mode", + "agent", + "path", + "cost", + "tokens" + ], + "additionalProperties": false + }, + "Message": { + "anyOf": [ + { + "$ref": "#/components/schemas/UserMessage" + }, + { + "$ref": "#/components/schemas/AssistantMessage" + } + ] + }, + "message.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "message.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Message" + } + }, + "required": [ + "sessionID", + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "message.removed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "message.removed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + } + }, + "required": [ + "sessionID", + "messageID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "TextPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "text": { + "type": "string" + }, + "synthetic": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "ignored": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "time": { + "anyOf": [ + { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "start" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "text" + ], + "additionalProperties": false + }, + "SubtaskPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "subtask" + ] + }, + "prompt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "anyOf": [ + { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + } + }, + "required": [ + "providerID", + "modelID" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "prompt", + "description", + "agent" + ], + "additionalProperties": false + }, + "ReasoningPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "reasoning" + ] + }, + "text": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "start" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "text", + "time" + ], + "additionalProperties": false + }, + "FilePartSourceText": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": [ + "value", + "start", + "end" + ], + "additionalProperties": false + }, + "FileSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "text", + "type", + "path" + ], + "additionalProperties": false + }, + "Range": { + "type": "object", + "properties": { + "start": { + "type": "object", + "properties": { + "line": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "character": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "line", + "character" + ], + "additionalProperties": false + }, + "end": { + "type": "object", + "properties": { + "line": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "character": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "line", + "character" + ], + "additionalProperties": false + } + }, + "required": [ + "start", + "end" + ], + "additionalProperties": false + }, + "SymbolSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "enum": [ + "symbol" + ] + }, + "path": { + "type": "string" + }, + "range": { + "$ref": "#/components/schemas/Range" + }, + "name": { + "type": "string" + }, + "kind": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "text", + "type", + "path", + "range", + "name", + "kind" + ], + "additionalProperties": false + }, + "ResourceSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "enum": [ + "resource" + ] + }, + "clientName": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": [ + "text", + "type", + "clientName", + "uri" + ], + "additionalProperties": false + }, + "FilePartSource": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileSource" + }, + { + "$ref": "#/components/schemas/SymbolSource" + }, + { + "$ref": "#/components/schemas/ResourceSource" + } + ] + }, + "FilePart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "mime": { + "type": "string" + }, + "filename": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "url": { + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/components/schemas/FilePartSource" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "mime", + "url" + ], + "additionalProperties": false + }, + "ToolStatePending": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending" + ] + }, + "input": { + "type": "object" + }, + "raw": { + "type": "string" + } + }, + "required": [ + "status", + "input", + "raw" + ], + "additionalProperties": false + }, + "ToolStateRunning": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running" + ] + }, + "input": { + "type": "object" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "start" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "input", + "time" + ], + "additionalProperties": false + }, + "ToolStateCompleted": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "completed" + ] + }, + "input": { + "type": "object" + }, + "output": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "compacted": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "start", + "end" + ], + "additionalProperties": false + }, + "attachments": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/FilePart" + } + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "status", + "input", + "output", + "title", + "metadata", + "time" + ], + "additionalProperties": false + }, + "ToolStateError": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "input": { + "type": "object" + }, + "error": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "start", + "end" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "input", + "error", + "time" + ], + "additionalProperties": false + }, + "ToolState": { + "anyOf": [ + { + "$ref": "#/components/schemas/ToolStatePending" + }, + { + "$ref": "#/components/schemas/ToolStateRunning" + }, + { + "$ref": "#/components/schemas/ToolStateCompleted" + }, + { + "$ref": "#/components/schemas/ToolStateError" + } + ] + }, + "ToolPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "tool" + ] + }, + "callID": { + "type": "string" + }, + "tool": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/ToolState" + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "callID", + "tool", + "state" + ], + "additionalProperties": false + }, + "StepStartPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "step-start" + ] + }, + "snapshot": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type" + ], + "additionalProperties": false + }, + "StepFinishPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "step-finish" + ] + }, + "reason": { + "type": "string" + }, + "snapshot": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "total": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": [ + "read", + "write" + ], + "additionalProperties": false + } + }, + "required": [ + "input", + "output", + "reasoning", + "cache" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "reason", + "cost", + "tokens" + ], + "additionalProperties": false + }, + "SnapshotPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "snapshot" + ] + }, + "snapshot": { + "type": "string" + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "snapshot" + ], + "additionalProperties": false + }, + "PatchPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "patch" + ] + }, + "hash": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "hash", + "files" + ], + "additionalProperties": false + }, + "AgentPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "agent" + ] + }, + "name": { + "type": "string" + }, + "source": { + "anyOf": [ + { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "start": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "end": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "value", + "start", + "end" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "name" + ], + "additionalProperties": false + }, + "RetryPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "retry" + ] + }, + "attempt": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "error": { + "$ref": "#/components/schemas/APIError" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "created" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "attempt", + "error", + "time" + ], + "additionalProperties": false + }, + "CompactionPart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "compaction" + ] + }, + "auto": { + "type": "boolean" + }, + "overflow": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "tail_start_id": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "messageID", + "type", + "auto" + ], + "additionalProperties": false + }, + "Part": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextPart" + }, + { + "$ref": "#/components/schemas/SubtaskPart" + }, + { + "$ref": "#/components/schemas/ReasoningPart" + }, + { + "$ref": "#/components/schemas/FilePart" + }, + { + "$ref": "#/components/schemas/ToolPart" + }, + { + "$ref": "#/components/schemas/StepStartPart" + }, + { + "$ref": "#/components/schemas/StepFinishPart" + }, + { + "$ref": "#/components/schemas/SnapshotPart" + }, + { + "$ref": "#/components/schemas/PatchPart" + }, + { + "$ref": "#/components/schemas/AgentPart" + }, + { + "$ref": "#/components/schemas/RetryPart" + }, + { + "$ref": "#/components/schemas/CompactionPart" + } + ] + }, + "message.part.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "message.part.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "part": { + "$ref": "#/components/schemas/Part" + }, + "time": { + "type": "number" + } + }, + "required": [ + "sessionID", + "part", + "time" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "message.part.removed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "message.part.removed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "partID": { + "type": "string", + "allOf": [ + { + "pattern": "^prt" + } + ] + } + }, + "required": [ + "sessionID", + "messageID", + "partID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.execution.settled": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.execution.settled" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "outcome": { + "type": "string", + "enum": [ + "success", + "failure", + "interrupted" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.Error.Unknown" + } + }, + "required": [ + "timestamp", + "sessionID", + "outcome" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.text.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.text.delta" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "textID": { + "type": "string" + }, + "delta": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "textID", + "delta" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.reasoning.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.reasoning.delta" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "reasoningID": { + "type": "string" + }, + "delta": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "reasoningID", + "delta" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.tool.input.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.tool.input.delta" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "assistantMessageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "callID": { + "type": "string" + }, + "delta": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "assistantMessageID", + "callID", + "delta" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.next.compaction.delta": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.next.compaction.delta" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "timestamp": { + "type": "number" + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] + }, + "text": { + "type": "string" + } + }, + "required": [ + "timestamp", + "sessionID", + "messageID", + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "file.edited": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "file.edited" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + }, + "required": [ + "file" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "reference.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "reference.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "permission.v2.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "permission.v2.asked" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "action": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "save": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "source": { + "$ref": "#/components/schemas/PermissionV2.Source" + } + }, + "required": [ + "id", + "sessionID", + "action", + "resources" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "permission.v2.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "permission.v2.replied" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "reply": { + "$ref": "#/components/schemas/PermissionV2.Reply" + } + }, + "required": [ + "sessionID", + "requestID", + "reply" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "plugin.added": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "plugin.added" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "project.directories.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "project.directories.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "projectID": { + "type": "string" + } + }, + "required": [ + "projectID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "command.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "command.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "skill.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "skill.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "file.watcher.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "file.watcher.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "event": { + "type": "string", + "enum": [ + "add", + "change", + "unlink" + ] + } + }, + "required": [ + "file", + "event" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "Pty": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "title": { + "type": "string" + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited" + ] + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exitCode": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "id", + "title", + "command", + "args", + "cwd", + "status", + "pid" + ], + "additionalProperties": false + }, + "pty.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "pty.created" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "pty.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "pty.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": [ + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "pty.exited": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "pty.exited" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + }, + "exitCode": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "id", + "exitCode" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "pty.deleted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "pty.deleted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^pty" + } + ] + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "Shell": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "shell": { + "type": "string" + }, + "file": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exit": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "started": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "completed": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + } + }, + "required": [ + "started" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "status", + "command", + "cwd", + "shell", + "file", + "metadata", + "time" + ], + "additionalProperties": false + }, + "shell.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "shell.created" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Shell" + } + }, + "required": [ + "info" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "shell.exited": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "shell.exited" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "exit": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + } + }, + "required": [ + "id", + "status" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "shell.deleted": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "shell.deleted" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "QuestionV2.Option": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Display text (1-5 words, concise)" + }, + "description": { + "type": "string", + "description": "Explanation of choice" + } + }, + "required": [ + "label", + "description" + ], + "additionalProperties": false + }, + "QuestionV2.Info": { + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "Complete question" + }, + "header": { + "type": "string", + "description": "Very short label (max 30 chars)" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Option" + }, + "description": "Available choices" + }, + "multiple": { + "type": "boolean" + }, + "custom": { + "type": "boolean" + } + }, + "required": [ + "question", + "header", + "options" + ], + "additionalProperties": false + }, + "QuestionV2.Tool": { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": [ + "messageID", + "callID" + ], + "additionalProperties": false + }, + "question.v2.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.v2.asked" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Info" + }, + "description": "Questions to ask" + }, + "tool": { + "$ref": "#/components/schemas/QuestionV2.Tool" + } + }, + "required": [ + "id", + "sessionID", + "questions" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "QuestionV2.Answer": { + "type": "array", + "items": { + "type": "string" + } + }, + "question.v2.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.v2.replied" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Answer" + } + } + }, + "required": [ + "sessionID", + "requestID", + "answers" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "question.v2.rejected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.v2.rejected" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + } + }, + "required": [ + "sessionID", + "requestID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "Form.Metadata1": { + "type": "object" + }, + "Form.When1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "neq" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "boolean" + } + ] + } + }, + "required": [ + "key", + "op", + "value" + ], + "additionalProperties": false + }, + "Form.StringField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": [ + "string" + ] + }, + "format": { + "type": "string", + "enum": [ + "email", + "uri", + "date", + "date-time" + ] + }, + "minLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxLength": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "pattern": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "default": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "custom": { + "type": "boolean" + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.NumberField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": [ + "number" + ] + }, + "minimum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "maximum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "default": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.IntegerField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": [ + "integer" + ] + }, + "minimum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "maximum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + "default": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.BooleanField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": [ + "boolean" + ] + }, + "default": { + "type": "boolean" + } + }, + "required": [ + "key", + "type" + ], + "additionalProperties": false + }, + "Form.MultiselectField1": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "when": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.When1" + } + }, + "type": { + "type": "string", + "enum": [ + "multiselect" + ] + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Option" + } + }, + "minItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "maxItems": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "custom": { + "type": "boolean" + }, + "default": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "key", + "type", + "options" + ], + "additionalProperties": false + }, + "Form.FormInfo1": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata1" + }, + "mode": { + "type": "string", + "enum": [ + "form" + ] + }, + "fields": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.StringField1" + }, + { + "$ref": "#/components/schemas/Form.NumberField1" + }, + { + "$ref": "#/components/schemas/Form.IntegerField1" + }, + { + "$ref": "#/components/schemas/Form.BooleanField1" + }, + { + "$ref": "#/components/schemas/Form.MultiselectField1" + } + ] + } + } + }, + "required": [ + "id", + "sessionID", + "mode", + "fields" + ], + "additionalProperties": false + }, + "Form.UrlInfo1": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata1" + }, + "mode": { + "type": "string", + "enum": [ + "url" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "id", + "sessionID", + "mode", + "url" + ], + "additionalProperties": false + }, + "form.created": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "form.created" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "form": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.FormInfo1" + }, + { + "$ref": "#/components/schemas/Form.UrlInfo1" + } + ] + } + }, + "required": [ + "form" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "Form.Value1": { + "anyOf": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "Form.Answer1": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Form.Value1" + } + }, + "form.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "form.replied" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + }, + "answer": { + "$ref": "#/components/schemas/Form.Answer1" + } + }, + "required": [ + "id", + "sessionID", + "answer" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "form.cancelled": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "form.cancelled" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^frm_" + } + ] + }, + "sessionID": { + "type": "string" + } + }, + "required": [ + "id", + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "Todo": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Brief description of the task" + }, + "status": { + "type": "string", + "description": "Current status of the task: pending, in_progress, completed, cancelled" + }, + "priority": { + "type": "string", + "description": "Priority level of the task: high, medium, low" + } + }, + "required": [ + "content", + "status", + "priority" + ], + "additionalProperties": false + }, + "todo.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "todo.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "todos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": [ + "sessionID", + "todos" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "SessionStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "idle" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "retry" + ] + }, + "attempt": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "message": { + "type": "string" + }, + "action": { + "type": "object", + "properties": { + "reason": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "label": { + "type": "string" + }, + "link": { + "type": "string" + } + }, + "required": [ + "reason", + "provider", + "title", + "message", + "label" + ], + "additionalProperties": false + }, + "next": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "type", + "attempt", + "message", + "next" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "busy" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + ] + }, + "session.status": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.status" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "status": { + "$ref": "#/components/schemas/SessionStatus" + } + }, + "required": [ + "sessionID", + "status" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.idle": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.idle" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "tui.prompt.append": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "tui.prompt.append" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "tui.command.execute": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "tui.command.execute" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "command": { + "anyOf": [ + { + "type": "string", + "enum": [ + "session.list", + "session.new", + "session.share", + "session.interrupt", + "session.background", + "session.compact", + "session.page.up", + "session.page.down", + "session.line.up", + "session.line.down", + "session.half.page.up", + "session.half.page.down", + "session.first", + "session.last", + "prompt.clear", + "prompt.submit", + "agent.cycle" + ] + }, + { + "type": "string" + } + ] + } + }, + "required": [ + "command" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "tui.toast.show": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "tui.toast.show" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "variant": { + "type": "string", + "enum": [ + "info", + "success", + "warning", + "error" + ] + }, + "duration": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message", + "variant" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "tui.session.select": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "tui.session.select" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses", + "description": "Session ID to navigate to" + } + ] + } + }, + "required": [ + "sessionID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "installation.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "installation.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": [ + "version" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "installation.update-available": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "installation.update-available" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": [ + "version" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "vcs.branch.updated": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "vcs.branch.updated" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "branch": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "mcp.status.changed": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "mcp.status.changed" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "server": { + "type": "string" + } + }, + "required": [ + "server" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "permission.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "permission.asked" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "permission": { + "type": "string" + }, + "patterns": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object" + }, + "always": { + "type": "array", + "items": { + "type": "string" + } + }, + "tool": { + "anyOf": [ + { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": [ + "messageID", + "callID" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "permission", + "patterns", + "metadata", + "always" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "permission.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "permission.replied" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^per" + } + ] + }, + "reply": { + "type": "string", + "enum": [ + "once", + "always", + "reject" + ] + } + }, + "required": [ + "sessionID", + "requestID", + "reply" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "QuestionOption": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Display text (1-5 words, concise)" + }, + "description": { + "type": "string", + "description": "Explanation of choice" + } + }, + "required": [ + "label", + "description" + ], + "additionalProperties": false + }, + "QuestionInfo": { + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "Complete question" + }, + "header": { + "type": "string", + "description": "Very short label (max 30 chars)" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionOption" + }, + "description": "Available choices" + }, + "multiple": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Allow selecting multiple choices" + }, + "custom": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Allow typing a custom answer (default: true)" + } + }, + "required": [ + "question", + "header", + "options" + ], + "additionalProperties": false + }, + "QuestionTool": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg" + } + ] + }, + "callID": { + "type": "string" + } + }, + "required": [ + "messageID", + "callID" + ], + "additionalProperties": false + }, + "question.asked": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.asked" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionInfo" + }, + "description": "Questions to ask" + }, + "tool": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionTool" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sessionID", + "questions" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "QuestionAnswer": { + "type": "array", + "items": { + "type": "string" + } + }, + "question.replied": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.replied" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionAnswer" + } + } + }, + "required": [ + "sessionID", + "requestID", + "answers" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "question.rejected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "question.rejected" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "requestID": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + } + }, + "required": [ + "sessionID", + "requestID" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "session.error": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "type": "object" + }, + "type": { + "type": "string", + "enum": [ + "session.error" + ] + }, + "durable": { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/Location.Ref" + }, + "data": { + "type": "object", + "properties": { + "sessionID": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderAuthError" + }, + { + "$ref": "#/components/schemas/UnknownError1" + }, + { + "$ref": "#/components/schemas/MessageOutputLengthError" + }, + { + "$ref": "#/components/schemas/MessageAbortedError" + }, + { + "$ref": "#/components/schemas/StructuredOutputError" + }, + { + "$ref": "#/components/schemas/ContextOverflowError" + }, + { + "$ref": "#/components/schemas/ContentFilterError" + }, + { + "$ref": "#/components/schemas/APIError" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "V2Event.server.connected": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^evt_" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "durable": { + "anyOf": [ + { + "type": "object", + "properties": { + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "version": { + "type": "integer", + "allOf": [ + { + "minimum": 1 + } + ] + } + }, + "required": [ + "aggregateID", + "seq", + "version" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/Location.Ref" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "server.connected" + ] + }, + "data": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "id", + "type", + "data" + ], + "additionalProperties": false + }, + "V2Event": { + "anyOf": [ + { + "$ref": "#/components/schemas/models-dev.refreshed" + }, + { + "$ref": "#/components/schemas/integration.updated" + }, + { + "$ref": "#/components/schemas/integration.connection.updated" + }, + { + "$ref": "#/components/schemas/catalog.updated" + }, + { + "$ref": "#/components/schemas/agent.updated" + }, + { + "$ref": "#/components/schemas/session.created" + }, + { + "$ref": "#/components/schemas/session.updated" + }, + { + "$ref": "#/components/schemas/session.deleted" + }, + { + "$ref": "#/components/schemas/message.updated" + }, + { + "$ref": "#/components/schemas/message.removed" + }, + { + "$ref": "#/components/schemas/message.part.updated" + }, + { + "$ref": "#/components/schemas/message.part.removed" + }, + { + "$ref": "#/components/schemas/session.next.agent.switched" + }, + { + "$ref": "#/components/schemas/session.next.model.switched" + }, + { + "$ref": "#/components/schemas/session.next.moved" + }, + { + "$ref": "#/components/schemas/session.next.renamed" + }, + { + "$ref": "#/components/schemas/session.next.forked" + }, + { + "$ref": "#/components/schemas/session.next.prompted" + }, + { + "$ref": "#/components/schemas/session.next.prompt.admitted" + }, + { + "$ref": "#/components/schemas/session.next.execution.settled" + }, + { + "$ref": "#/components/schemas/session.next.context.updated" + }, + { + "$ref": "#/components/schemas/session.next.synthetic" + }, + { + "$ref": "#/components/schemas/session.next.skill.activated" + }, + { + "$ref": "#/components/schemas/session.next.shell.started" + }, + { + "$ref": "#/components/schemas/session.next.shell.ended" + }, + { + "$ref": "#/components/schemas/session.next.step.started" + }, + { + "$ref": "#/components/schemas/session.next.step.ended" + }, + { + "$ref": "#/components/schemas/session.next.step.failed" + }, + { + "$ref": "#/components/schemas/session.next.text.started" + }, + { + "$ref": "#/components/schemas/session.next.text.delta" + }, + { + "$ref": "#/components/schemas/session.next.text.ended" + }, + { + "$ref": "#/components/schemas/session.next.reasoning.started" + }, + { + "$ref": "#/components/schemas/session.next.reasoning.delta" + }, + { + "$ref": "#/components/schemas/session.next.reasoning.ended" + }, + { + "$ref": "#/components/schemas/session.next.tool.input.started" + }, + { + "$ref": "#/components/schemas/session.next.tool.input.delta" + }, + { + "$ref": "#/components/schemas/session.next.tool.input.ended" + }, + { + "$ref": "#/components/schemas/session.next.tool.called" + }, + { + "$ref": "#/components/schemas/session.next.tool.progress" + }, + { + "$ref": "#/components/schemas/session.next.tool.success" + }, + { + "$ref": "#/components/schemas/session.next.tool.failed" + }, + { + "$ref": "#/components/schemas/session.next.retried" + }, + { + "$ref": "#/components/schemas/session.next.compaction.started" + }, + { + "$ref": "#/components/schemas/session.next.compaction.delta" + }, + { + "$ref": "#/components/schemas/session.next.compaction.ended" + }, + { + "$ref": "#/components/schemas/session.next.revert.staged" + }, + { + "$ref": "#/components/schemas/session.next.revert.cleared" + }, + { + "$ref": "#/components/schemas/session.next.revert.committed" + }, + { + "$ref": "#/components/schemas/file.edited" + }, + { + "$ref": "#/components/schemas/reference.updated" + }, + { + "$ref": "#/components/schemas/permission.v2.asked" + }, + { + "$ref": "#/components/schemas/permission.v2.replied" + }, + { + "$ref": "#/components/schemas/plugin.added" + }, + { + "$ref": "#/components/schemas/project.directories.updated" + }, + { + "$ref": "#/components/schemas/command.updated" + }, + { + "$ref": "#/components/schemas/skill.updated" + }, + { + "$ref": "#/components/schemas/file.watcher.updated" + }, + { + "$ref": "#/components/schemas/pty.created" + }, + { + "$ref": "#/components/schemas/pty.updated" + }, + { + "$ref": "#/components/schemas/pty.exited" + }, + { + "$ref": "#/components/schemas/pty.deleted" + }, + { + "$ref": "#/components/schemas/shell.created" + }, + { + "$ref": "#/components/schemas/shell.exited" + }, + { + "$ref": "#/components/schemas/shell.deleted" + }, + { + "$ref": "#/components/schemas/question.v2.asked" + }, + { + "$ref": "#/components/schemas/question.v2.replied" + }, + { + "$ref": "#/components/schemas/question.v2.rejected" + }, + { + "$ref": "#/components/schemas/form.created" + }, + { + "$ref": "#/components/schemas/form.replied" + }, + { + "$ref": "#/components/schemas/form.cancelled" + }, + { + "$ref": "#/components/schemas/todo.updated" + }, + { + "$ref": "#/components/schemas/session.status" + }, + { + "$ref": "#/components/schemas/session.idle" + }, + { + "$ref": "#/components/schemas/tui.prompt.append" + }, + { + "$ref": "#/components/schemas/tui.command.execute" + }, + { + "$ref": "#/components/schemas/tui.toast.show" + }, + { + "$ref": "#/components/schemas/tui.session.select" + }, + { + "$ref": "#/components/schemas/installation.updated" + }, + { + "$ref": "#/components/schemas/installation.update-available" + }, + { + "$ref": "#/components/schemas/vcs.branch.updated" + }, + { + "$ref": "#/components/schemas/mcp.status.changed" + }, + { + "$ref": "#/components/schemas/permission.asked" + }, + { + "$ref": "#/components/schemas/permission.replied" + }, + { + "$ref": "#/components/schemas/question.asked" + }, + { + "$ref": "#/components/schemas/question.replied" + }, + { + "$ref": "#/components/schemas/question.rejected" + }, + { + "$ref": "#/components/schemas/session.error" + }, + { + "$ref": "#/components/schemas/V2Event.server.connected" + } + ] + }, + "V2EventStream": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/V2Event" + }, + "contentMediaType": "application/json" + }, + "EventLog.Hint": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "log.hint" + ] + }, + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + } + }, + "required": [ + "type", + "aggregateID", + "seq" + ], + "additionalProperties": false, + "description": "Payload-free change hint: the aggregate's durable log advanced to at least seq. Hints coalesce under backpressure (latest per aggregate) and are never a delivery guarantee." + }, + "EventLog.SweepRequired": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "log.sweep_required" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Hints may have been lost; treat every aggregate as potentially dirty and recover via bounded sweep plus durable log reads. Emitted first on every (re)subscribe." + }, + "EventLog.Change": { + "anyOf": [ + { + "$ref": "#/components/schemas/EventLog.Hint" + }, + { + "$ref": "#/components/schemas/EventLog.SweepRequired" + } + ] + }, + "EventLog.ChangeStream": { + "type": "string", + "contentSchema": { + "$ref": "#/components/schemas/EventLog.Change" + }, + "contentMediaType": "application/json" + }, + "PtyNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "PtyNotFoundError" + ] + }, + "ptyID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "ptyID", + "message" + ], + "additionalProperties": false + }, + "PtyTicket.ConnectToken": { + "type": "object", + "properties": { + "ticket": { + "type": "string" + }, + "expires_in": { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + } + }, + "required": [ + "ticket", + "expires_in" + ], + "additionalProperties": false + }, + "ForbiddenError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ForbiddenError" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "message" + ], + "additionalProperties": false + }, + "Shell1": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + }, + "command": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "shell": { + "type": "string" + }, + "file": { + "type": "string" + }, + "pid": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "exit": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "metadata": { + "type": "object" + }, + "time": { + "type": "object", + "properties": { + "started": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + }, + "completed": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] + } + }, + "required": [ + "started" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "status", + "command", + "cwd", + "shell", + "file", + "metadata", + "time" + ], + "additionalProperties": false + }, + "ShellNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "ShellNotFoundError" + ] + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "id", + "message" + ], + "additionalProperties": false + }, + "QuestionV2.Request": { + "type": "object", + "properties": { + "id": { + "type": "string", + "allOf": [ + { + "pattern": "^que" + } + ] + }, + "sessionID": { + "type": "string", + "allOf": [ + { + "pattern": "^ses" + } + ] + }, + "questions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Info" + }, + "description": "Questions to ask" + }, + "tool": { + "$ref": "#/components/schemas/QuestionV2.Tool" + } + }, + "required": [ + "id", + "sessionID", + "questions" + ], + "additionalProperties": false + }, + "QuestionV2.Reply": { + "type": "object", + "properties": { + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2.Answer" + }, + "description": "User answers in order of questions (each answer is an array of selected labels)" + } + }, + "required": [ + "answers" + ], + "additionalProperties": false + }, + "QuestionNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "QuestionNotFoundError" + ] + }, + "requestID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "requestID", + "message" + ], + "additionalProperties": false + }, + "Reference.LocalSource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "local" + ] + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Reference.GitSource": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "git" + ] + }, + "repository": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": [ + "type", + "repository" + ], + "additionalProperties": false + }, + "Reference.Source": { + "anyOf": [ + { + "$ref": "#/components/schemas/Reference.LocalSource" + }, + { + "$ref": "#/components/schemas/Reference.GitSource" + } + ] + }, + "Reference.Info": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "source": { + "$ref": "#/components/schemas/Reference.Source" + } + }, + "required": [ + "name", + "path", + "source" + ], + "additionalProperties": false + }, + "ProjectCopy.Copy": { + "type": "object", + "properties": { + "directory": { + "type": "string" + } + }, + "required": [ + "directory" + ], + "additionalProperties": false + }, + "ProjectCopyError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": [ + "ProjectCopyError" + ] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "forceRequired": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "data" + ], + "additionalProperties": false + }, + "Vcs.FileStatus": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "additions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "deletions": { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + "status": { + "type": "string", + "enum": [ + "added", + "deleted", + "modified" + ] + } + }, + "required": [ + "file", + "additions", + "deletions", + "status" + ], + "additionalProperties": false + }, + "Vcs.Mode": { + "type": "string", + "enum": [ + "working", + "branch" + ] + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [ + { + "name": "server.health" + }, + { + "name": "server.location" + }, + { + "name": "server.agent" + }, + { + "name": "plugins", + "description": "Experimental plugin routes." + }, + { + "name": "sessions", + "description": "Experimental session routes." + }, + { + "name": "messages", + "description": "Experimental message routes." + }, + { + "name": "models", + "description": "Experimental model routes." + }, + { + "name": "generate", + "description": "Experimental one-shot generation routes." + }, + { + "name": "providers", + "description": "Experimental provider routes." + }, + { + "name": "integrations", + "description": "Integration discovery and authentication routes." + }, + { + "name": "mcp", + "description": "MCP server status routes." + }, + { + "name": "server.credential" + }, + { + "name": "projects", + "description": "Location-scoped project routes." + }, + { + "name": "forms", + "description": "Session form routes." + }, + { + "name": "permissions", + "description": "Experimental permission routes." + }, + { + "name": "filesystem", + "description": "Experimental location-scoped filesystem routes." + }, + { + "name": "commands", + "description": "Experimental command routes." + }, + { + "name": "skills", + "description": "Experimental skill routes." + }, + { + "name": "events", + "description": "Experimental event stream routes." + }, + { + "name": "pty", + "description": "Experimental location-scoped PTY routes." + }, + { + "name": "shell", + "description": "Experimental location-scoped shell command routes." + }, + { + "name": "session questions", + "description": "Experimental session question routes." + }, + { + "name": "reference", + "description": "Location-scoped project references." + }, + { + "name": "projectCopy", + "description": "Project copy management routes." + }, + { + "name": "vcs", + "description": "Location-scoped version control routes." + } + ] +} diff --git a/packages/codemode/test/openapi.test.ts b/packages/codemode/test/openapi.test.ts new file mode 100644 index 000000000000..99a089009132 --- /dev/null +++ b/packages/codemode/test/openapi.test.ts @@ -0,0 +1,958 @@ +import { describe, expect, test } from "bun:test" +import { Effect, Layer, Option } from "effect" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { CodeMode, OpenAPI } from "../src/index.js" +import { inputTypeScript, outputTypeScript, Tool } from "../src/tool.js" + +const baseUrl = "http://localhost:4096" +type Document = OpenAPI.Document + +type Recorded = { + readonly method: string + readonly url: string + readonly headers: Record + readonly body: unknown +} + +const opencodeSpec = async (): Promise => { + return Bun.file(new URL("./fixtures/opencode-v2-openapi.json", import.meta.url)).json() as Promise +} + +const happyPathSpec = async (): Promise => { + return Bun.file(new URL("./fixtures/openapi-happy-path.json", import.meta.url)).json() as Promise +} + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value) + +const toolAt = (tools: unknown, name: string) => + name.split(".").reduce((current, segment) => (isRecord(current) ? current[segment] : undefined), tools) + +const recordingClient = (respond: (request: HttpClientRequest.HttpClientRequest) => Response) => { + const requests: Array = [] + const layer = Layer.succeed(HttpClient.HttpClient)( + HttpClient.make((request) => + Effect.gen(function* () { + const body = + request.body._tag === "Uint8Array" ? JSON.parse(new TextDecoder().decode(request.body.body)) : undefined + const url = Option.map(HttpClientRequest.toUrl(request), (resolved) => resolved.toString()) + requests.push({ + method: request.method, + url: Option.getOrElse(url, () => request.url), + headers: { ...request.headers }, + body, + }) + return HttpClientResponse.fromWeb(request, respond(request)) + }), + ), + ) + return { requests, layer } +} + +const json = (value: unknown, status = 200) => + new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } }) + +const singleOperation = (operation: Record, method = "get"): Document => ({ + openapi: "3.1.0", + paths: { "/test": { [method]: { operationId: "test", responses: { 200: { description: "Success" } }, ...operation } } }, +}) + +describe("OpenAPI.fromSpec", () => { + test("covers a representative API from generation through execution", async () => { + const resolutions: Array = [] + const client = recordingClient((request) => { + const url = Option.getOrElse(HttpClientRequest.toUrl(request), () => new URL(request.url)) + if (request.method === "POST") { + return new Response( + JSON.stringify({ id: "user-2", name: "Grace", email: "grace@example.test", role: "admin" }), + { status: 201, headers: { "content-type": "application/vnd.example+json" } }, + ) + } + if (request.method === "DELETE") return new Response(null, { status: 204 }) + if (url.pathname === "/search") { + return new Response("2 matches", { headers: { "content-type": "text/plain" } }) + } + return json({ id: "user-1", name: "Ada", email: "ada@example.test", role: "member" }) + }) + const api = OpenAPI.fromSpec({ + spec: await happyPathSpec(), + baseUrl, + auth: { + resolve: ({ name }) => { + resolutions.push(name) + return Effect.succeed( + name === "BearerAuth" + ? { type: "bearer", token: "bearer-secret" } + : { type: "apiKey", value: "api-secret" }, + ) + }, + }, + }) + const get = toolAt(api.tools, "users.get") + const create = toolAt(api.tools, "users.create") + const search = toolAt(api.tools, "search.run") + const remove = toolAt(api.tools, "users.remove") + + expect(api.skipped).toEqual([]) + if (!Tool.isDefinition(get) || !Tool.isDefinition(create) || !Tool.isDefinition(search) || !Tool.isDefinition(remove)) { + throw new Error("happy-path fixture did not generate every operation") + } + expect(inputTypeScript(get)).toBe( + '{ userId: string; include?: Array; verbose?: boolean; "X-Trace-ID"?: string }', + ) + expect(inputTypeScript(create)).toBe('{ name: string; email: string; role?: "admin" | "member" }') + expect(inputTypeScript(search)).toBe("{ filter?: { query: string; page?: number }; tags?: Array }") + expect(inputTypeScript(remove)).toBe("{ userId: string }") + expect(outputTypeScript(get)).toContain("id: string") + expect(outputTypeScript(create)).toContain('role?: "admin" | "member"') + expect(outputTypeScript(search)).toBe("string") + expect(outputTypeScript(remove)).toBe("null") + + const result = await Effect.runPromise( + CodeMode.make({ tools: { api: api.tools } }) + .execute(` + const user = await tools.api.users.get({ + userId: "user-1", + include: ["profile", "permissions"], + verbose: true, + "X-Trace-ID": "trace-1", + }) + const created = await tools.api.users.create({ + name: "Grace", + email: "grace@example.test", + role: "admin", + }) + const summary = await tools.api.search.run({ + filter: { query: "effect", page: 2 }, + tags: ["typescript", "runtime"], + }) + const removed = await tools.api.users.remove({ userId: "user-1" }) + return { user, created, summary, removed } + `) + .pipe(Effect.provide(client.layer)), + ) + + expect(result).toMatchObject({ + ok: true, + value: { + user: { id: "user-1", name: "Ada" }, + created: { id: "user-2", name: "Grace" }, + summary: "2 matches", + removed: null, + }, + }) + expect(resolutions).toEqual(["BearerAuth", "ApiKey", "BearerAuth"]) + expect(client.requests).toHaveLength(4) + + const getUrl = new URL(client.requests[0]!.url) + expect(getUrl.pathname).toBe("/users/user-1") + expect(getUrl.searchParams.get("include")).toBe("profile,permissions") + expect(getUrl.searchParams.get("verbose")).toBe("true") + expect(client.requests[0]!.headers["x-trace-id"]).toBe("trace-1") + expect(client.requests[0]!.headers.authorization).toBe("Bearer bearer-secret") + + const createUrl = new URL(client.requests[1]!.url) + expect(createUrl.searchParams.get("api_key")).toBe("api-secret") + expect(client.requests[1]!.body).toEqual({ name: "Grace", email: "grace@example.test", role: "admin" }) + + const searchUrl = new URL(client.requests[2]!.url) + expect(searchUrl.searchParams.get("filter[query]")).toBe("effect") + expect(searchUrl.searchParams.get("filter[page]")).toBe("2") + expect(searchUrl.searchParams.getAll("tags")).toEqual(["typescript", "runtime"]) + expect(client.requests[2]!.headers.authorization).toBeUndefined() + expect(new URL(client.requests[3]!.url).pathname).toBe("/users/user-1") + expect(client.requests[3]!.headers.authorization).toBe("Bearer bearer-secret") + }) + + test("converts representative opencode operations into the expected tool shape", async () => { + const spec = await opencodeSpec() + const result = OpenAPI.fromSpec({ spec, baseUrl }) + + expect(result.skipped).toHaveLength(5) + expect(result.skipped).toContainEqual({ + method: "GET", + path: "/api/pty/{ptyID}/connect", + reason: "WebSocket operations are not supported", + }) + expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(3) + expect(result.skipped).toContainEqual({ + method: "GET", + path: "/api/fs/read/*", + reason: "binary responses are not supported", + }) + expect(toolAt(result.tools, "v2.health.get")).not.toBeUndefined() + expect(toolAt(result.tools, "v2.session.get")).not.toBeUndefined() + expect(toolAt(result.tools, "v2.session.create")).not.toBeUndefined() + + const sessionGet = toolAt(result.tools, "v2.session.get") + expect(Tool.isDefinition(sessionGet)).toBe(true) + if (!Tool.isDefinition(sessionGet)) throw new Error("v2.session.get was not generated") + expect(inputTypeScript(sessionGet)).toBe("{ sessionID: string }") + expect(outputTypeScript(sessionGet)).toContain("id: string") + expect(outputTypeScript(sessionGet)).toContain("additions: number") + + const switchAgent = toolAt(result.tools, "v2.session.switchAgent") + expect(Tool.isDefinition(switchAgent)).toBe(true) + if (!Tool.isDefinition(switchAgent)) throw new Error("v2.session.switchAgent was not generated") + expect(inputTypeScript(switchAgent)).toBe("{ sessionID: string; agent: string }") + + const contextEntryPut = toolAt(result.tools, "v2.session.contextEntry.put") + expect(Tool.isDefinition(contextEntryPut)).toBe(true) + if (!Tool.isDefinition(contextEntryPut)) throw new Error("v2.session.contextEntry.put was not generated") + expect(inputTypeScript(contextEntryPut)).toBe("{ sessionID: string; key: string; value: unknown }") + expect(toolAt(result.tools, "v2_session_context_entry_put_2")).toBeUndefined() + expect(toolAt(result.tools, "v2.pty.connect")).toBeUndefined() + expect(toolAt(result.tools, "v2.session.log")).toBeUndefined() + expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined() + expect(toolAt(result.tools, "v2.event.changes")).toBeUndefined() + expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined() + expect(toolAt(result.tools, "v2.pty.connectToken")).not.toBeUndefined() + }) + + test("preserves operation path sanitization and collision handling", () => { + const response = { responses: { 200: { description: "Success" } } } + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/first": { get: { ...response, operationId: "group.item" } }, + "/second": { get: { ...response, operationId: "group.item" } }, + "/third": { get: { ...response, operationId: "group..other" } }, + }, + }, + }) + + expect(Tool.isDefinition(toolAt(result.tools, "group.item"))).toBe(true) + expect(Tool.isDefinition(toolAt(result.tools, "group_item_2"))).toBe(true) + expect(Tool.isDefinition(toolAt(result.tools, "group.operation.other"))).toBe(true) + }) + + test("synthesizes flat operation IDs from methods and paths", () => { + const response = { responses: { 200: { description: "Success" } } } + const tools = OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/users": { get: response, post: response }, + "/users/{id}": { get: response, patch: response, delete: response }, + "/organizations/{organizationId}/users/{id}": { get: response }, + }, + }, + }).tools + + for (const path of [ + "getUsers", + "postUsers", + "getUsersById", + "patchUsersById", + "deleteUsersById", + "getOrganizationsByOrganizationidUsersById", + ]) { + expect(Tool.isDefinition(toolAt(tools, path))).toBe(true) + } + }) + + test("lets operation parameters override matching path parameters", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/test": { + parameters: [{ name: "limit", in: "query", schema: { type: "string" } }], + get: { + operationId: "test", + parameters: [{ name: "limit", in: "query", required: true, schema: { type: "number" } }], + responses: { 200: { description: "Success" } }, + }, + }, + }, + }, + }).tools, + "test", + ) + + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + expect(inputTypeScript(tool)).toBe("{ limit: number }") + }) + + test("normalizes OpenAPI 3.0 schemas with Effect", () => { + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.0.3", + paths: { + "/search": { + get: { + operationId: "search", + parameters: [ + { + in: "query", + name: "value", + schema: { type: "string", nullable: true, minLength: 2 }, + }, + ], + responses: { 200: { description: "Success" } }, + }, + }, + }, + }, + }) + const search = toolAt(result.tools, "search") + + expect(Tool.isDefinition(search)).toBe(true) + if (!Tool.isDefinition(search)) throw new Error("search was not generated") + expect(inputTypeScript(search)).toBe("{ value?: string | null }") + const schema: unknown = search.input + const input = isRecord(schema) ? schema : {} + const properties = isRecord(input.properties) ? input.properties : {} + const value = isRecord(properties.value) ? properties.value : {} + expect(value.minLength).toBe(2) + }) + + test("preserves schema-local definitions alongside component definitions", () => { + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/test": { + get: { + operationId: "test", + responses: { + 200: { + description: "Success", + content: { + "application/json": { + schema: { $ref: "#/$defs/Local", $defs: { Local: { type: "string" } } }, + }, + }, + }, + }, + }, + }, + }, + components: { schemas: { Global: { type: "number" } } }, + }, + }).tools, + "test", + ) + + if (!Tool.isDefinition(tool) || !isRecord(tool.output)) throw new Error("test output was not generated") + expect(tool.output.$defs).toMatchObject({ Local: { type: "string" }, Global: { type: "number" } }) + }) + + test("documents that the opencode fixture is unauthenticated", async () => { + const spec = await opencodeSpec() + const components = isRecord(spec.components) ? spec.components : {} + const result = OpenAPI.fromSpec({ spec, baseUrl }) + + expect(spec.security).toStrictEqual([]) + expect(isRecord(components.securitySchemes) ? Object.keys(components.securitySchemes) : []).toStrictEqual([]) + const health = toolAt(result.tools, "v2.health.get") + const healthInput = isRecord(health) ? health.input : undefined + expect(healthInput).toMatchObject({ type: "object", properties: {} }) + const input = isRecord(healthInput) ? healthInput : {} + expect(Object.keys(isRecord(input.properties) ? input.properties : {})).toStrictEqual([]) + }) + + test("exposes real opencode operations through CodeMode discovery", async () => { + const { layer } = recordingClient(() => json({})) + const runtime = CodeMode.make({ + tools: { opencode: OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools }, + }) + const result = await Effect.runPromise( + runtime + .execute( + ` + return await tools.$codemode.search({ query: "global health", namespace: "opencode", limit: 1 }) + `, + ) + .pipe(Effect.provide(layer)), + ) + + expect(result).toMatchObject({ ok: true }) + if (!result.ok) return + expect(result.value).toMatchObject({ + items: [ + { + path: "tools.opencode.v2.health.get", + description: "Check whether the API server is ready to accept requests.", + }, + ], + }) + expect(JSON.stringify(result.value)).toContain("healthy: true") + }) + + test("invokes real opencode path parameters and JSON request bodies", async () => { + const { requests, layer } = recordingClient((request) => { + if (request.method === "GET") return json({ id: "ses_123" }) + return json({ id: "ses_456" }) + }) + const runtime = CodeMode.make({ + tools: { opencode: OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools }, + }) + + const result = await Effect.runPromise( + runtime + .execute( + ` + const existing = await tools.opencode.v2.session.get({ sessionID: "ses_123" }) + const created = await tools.opencode.v2.session.create({ id: "ses_456" }) + return { existing, created } + `, + ) + .pipe(Effect.provide(layer)), + ) + + expect(result).toMatchObject({ ok: true }) + expect(requests).toHaveLength(2) + expect(requests[0]).toMatchObject({ method: "GET", body: undefined }) + expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_123") + expect(requests[1]).toMatchObject({ + method: "POST", + url: "http://localhost:4096/api/session", + body: { id: "ses_456" }, + }) + }) + + test("serializes deep-object query parameters from the opencode fixture", async () => { + const client = recordingClient(() => json({ directory: "/tmp" })) + const location = toolAt(OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools, "v2.location.get") + if (!Tool.isDefinition(location)) throw new Error("v2.location.get was not generated") + + await Effect.runPromise( + location.run({ location: { directory: "/tmp", workspace: "workspace-1" } }).pipe(Effect.provide(client.layer)), + ) + + const url = new URL(client.requests[0]!.url) + expect(url.searchParams.get("location[directory]")).toBe("/tmp") + expect(url.searchParams.get("location[workspace]")).toBe("workspace-1") + }) + + test("serializes supported simple and form parameter shapes", async () => { + const client = recordingClient(() => json({ ok: true })) + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + paths: { + "/items/{keys}": { + get: { + operationId: "items", + parameters: [ + { name: "keys", in: "path", required: true, schema: { type: "array", items: { type: "string" } } }, + { name: "tags", in: "query", style: "form", explode: false, schema: { type: "array" } }, + { name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } }, + { name: "nullable", in: "query", required: true, schema: { type: ["string", "null"] } }, + { name: "constructor", in: "query", schema: { type: "string" } }, + { name: "meta", in: "header", style: "simple", explode: true, schema: { type: "object" } }, + ], + responses: { 200: { description: "Success" } }, + }, + }, + }, + }, + }) + const tool = toolAt(result.tools, "items") + if (!Tool.isDefinition(tool)) throw new Error("items was not generated") + + await Effect.runPromise( + tool + .run({ + keys: ["a!", "b*"], + tags: ["x", "y"], + filter: { state: "open", page: 2 }, + nullable: null, + constructor_2: "safe", + meta: { a: "b", c: "d" }, + }) + .pipe(Effect.provide(client.layer)), + ) + + const url = new URL(client.requests[0]!.url) + expect(url.pathname).toBe("/items/a%21,b%2A") + expect(url.searchParams.get("tags")).toBe("x,y") + expect(url.searchParams.get("state")).toBe("open") + expect(url.searchParams.get("page")).toBe("2") + expect(url.searchParams.get("nullable")).toBe("null") + expect(url.searchParams.get("constructor")).toBe("safe") + expect(client.requests[0]!.headers.meta).toBe("a=b,c=d") + await expect( + Effect.runPromise(tool.run({ keys: [undefined] }).pipe(Effect.provide(client.layer))), + ).rejects.toThrow("unsupported nested value") + }) + + test("skips unsupported parameter encodings and malformed security", () => { + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + openapi: "3.1.0", + security: [{ bearer: [] }], + paths: { + "/cookie": { + get: { + operationId: "cookie", + parameters: [{ name: "session", in: "cookie", schema: { type: "string" } }], + responses: { 200: { description: "Success" } }, + }, + }, + "/reserved": { + get: { + operationId: "reserved", + parameters: [{ name: "query", in: "query", allowReserved: true, schema: { type: "string" } }], + responses: { 200: { description: "Success" } }, + }, + }, + "/invalid-style": { + get: { + operationId: "invalidStyle", + parameters: [{ name: "query", in: "query", style: 42, schema: { type: "string" } }], + responses: { 200: { description: "Success" } }, + }, + }, + "/security": { + get: { operationId: "security", security: null, responses: { 200: { description: "Success" } } }, + }, + }, + }, + }) + + expect(result.tools).toEqual({}) + expect(result.skipped.map((item) => item.reason)).toEqual([ + "cookie parameter 'session' is not supported", + "parameter 'query' uses unsupported allowReserved encoding", + "parameter 'query' has an invalid style", + "security declaration is not an array", + ]) + }) + + test("fails closed on prototype-named missing security schemes", () => { + const result = OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation({ security: [JSON.parse('{"__proto__":[]}')] }), + }) + + expect(result.tools).toEqual({}) + expect(result.skipped[0]?.reason).toBe("security requirement references missing or malformed scheme: __proto__") + }) + + test("resolves bearer authentication without exposing it as input", async () => { + const contexts: Array[0]> = [] + const client = recordingClient(() => json({ ok: true })) + const spec = { + ...singleOperation({ operationId: undefined }), + security: [{ bearer: [] }], + components: { securitySchemes: { bearer: { type: "http", scheme: "bearer" } } }, + } satisfies Document + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec, + auth: { + resolve: (context) => { + contexts.push(context) + return Effect.succeed({ type: "bearer", token: "secret" }) + }, + }, + }).tools, + "getTest", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer))) + + expect(inputTypeScript(tool)).toBe("{}") + expect(client.requests[0]!.headers.authorization).toBe("Bearer secret") + expect(contexts).toEqual([ + { + name: "bearer", + definition: { type: "http", scheme: "bearer" }, + scopes: [], + operation: { + operationId: undefined, + method: "GET", + path: "/test", + summary: undefined, + description: undefined, + }, + }, + ]) + }) + + test("applies authentication carriers without prototype or collision loss", async () => { + const client = recordingClient(() => json({ ok: true })) + const authenticated = (security: ReadonlyArray>>, schemes: Record) => + OpenAPI.fromSpec({ + baseUrl, + spec: { ...singleOperation({}), security, components: { securitySchemes: schemes } }, + auth: { resolve: () => Effect.succeed({ type: "apiKey", value: "secret" }) }, + }) + const prototype = toolAt( + authenticated([{ key: [] }], { key: { type: "apiKey", in: "query", name: "__proto__" } }).tools, + "test", + ) + if (!Tool.isDefinition(prototype)) throw new Error("prototype auth tool was not generated") + + await Effect.runPromise(prototype.run({}).pipe(Effect.provide(client.layer))) + expect(new URL(client.requests[0]!.url).searchParams.get("__proto__")).toBe("secret") + + const duplicate = toolAt( + authenticated( + [{ first: [], second: [] }], + { + first: { type: "apiKey", in: "header", name: "x-key" }, + second: { type: "apiKey", in: "header", name: "x-key" }, + }, + ).tools, + "test", + ) + if (!Tool.isDefinition(duplicate)) throw new Error("duplicate auth tool was not generated") + await expect(Effect.runPromise(duplicate.run({}).pipe(Effect.provide(client.layer)))).rejects.toThrow( + "multiple credentials", + ) + + const cookie = authenticated([{ key: [] }], { key: { type: "apiKey", in: "cookie", name: "session" } }) + expect(cookie.tools).toEqual({}) + expect(cookie.skipped[0]?.reason).toBe("cookie authentication 'key' is not supported") + + const alternative = OpenAPI.fromSpec({ + baseUrl, + spec: { + ...singleOperation({}), + security: [{ cookie: [] }, { bearer: [] }], + components: { + securitySchemes: { + cookie: { type: "apiKey", in: "cookie", name: "session" }, + bearer: { type: "http", scheme: "bearer" }, + }, + }, + }, + auth: { + resolve: ({ name }) => + Effect.succeed(name === "bearer" ? { type: "bearer", token: "secret" } : undefined), + }, + }) + const alternativeTool = toolAt(alternative.tools, "test") + if (!Tool.isDefinition(alternativeTool)) throw new Error("supported auth alternative was not generated") + await Effect.runPromise(alternativeTool.run({}).pipe(Effect.provide(client.layer))) + expect(client.requests.at(-1)?.headers.authorization).toBe("Bearer secret") + }) + + test("honors server precedence and rejects ambiguous base URLs", async () => { + const client = recordingClient(() => json({ ok: true })) + const spec = { + ...singleOperation({ servers: [{ url: "https://operation.example/v1" }] }), + servers: [{ url: "https://document.example" }], + } satisfies Document + const tool = toolAt(OpenAPI.fromSpec({ spec }).tools, "test") + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer))) + expect(client.requests[0]?.url).toBe("https://operation.example/v1/test") + + const invalid = OpenAPI.fromSpec({ spec, baseUrl: "https://example.com/api?tenant=one" }) + expect(invalid.tools).toEqual({}) + expect(invalid.skipped[0]?.reason).toContain("unsupported query string or fragment") + + const malformed = OpenAPI.fromSpec({ spec, baseUrl: "https:/example.com" }) + expect(malformed.tools).toEqual({}) + expect(malformed.skipped[0]?.reason).toContain("not an absolute HTTP(S) URL") + }) + + test("resolves chained response refs before detecting unsupported transports", () => { + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + ...singleOperation({ responses: { 200: { $ref: "#/components/responses/First" } } }), + components: { + responses: { + First: { $ref: "#/components/responses/Stream" }, + Stream: { content: { "text/event-stream": { schema: { type: "string" } } } }, + }, + }, + }, + }) + + expect(result.tools).toEqual({}) + expect(result.skipped[0]?.reason).toBe("SSE operations are not supported") + }) + + test("resolves response schemas before detecting binary output", () => { + const result = OpenAPI.fromSpec({ + baseUrl, + spec: { + ...singleOperation({ + responses: { + 200: { + content: { "text/plain": { schema: { $ref: "#/components/schemas/File" } } }, + }, + }, + }), + components: { schemas: { File: { type: "string", format: "binary" } } }, + }, + }) + + expect(result.tools).toEqual({}) + expect(result.skipped[0]?.reason).toBe("binary responses are not supported") + }) + + test("validates composite parameters before resolving auth", async () => { + const resolutions: Array = [] + const client = recordingClient(() => json({ ok: true })) + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: { + ...singleOperation({ + parameters: [{ name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } }], + }), + security: [{ bearer: [] }], + components: { securitySchemes: { bearer: { type: "http", scheme: "bearer" } } }, + }, + auth: { + resolve: ({ name }) => { + resolutions.push(name) + return Effect.succeed({ type: "bearer", token: "secret" }) + }, + }, + }).tools, + "test", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + await expect( + Effect.runPromise(tool.run({ filter: { value: undefined } }).pipe(Effect.provide(client.layer))), + ).rejects.toThrow("unsupported nested value") + expect(resolutions).toEqual([]) + expect(client.requests).toEqual([]) + }) + + test("preserves JSON media types and rejects unencodable bodies", async () => { + const client = recordingClient(() => json({ ok: true })) + const tool = toolAt( + OpenAPI.fromSpec({ + baseUrl, + spec: singleOperation( + { + requestBody: { + required: true, + content: { "application/merge-patch+json": { schema: { type: "object" } } }, + }, + }, + "post", + ), + }).tools, + "test", + ) + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + + await Effect.runPromise(tool.run({ body: { name: "updated" } }).pipe(Effect.provide(client.layer))) + expect(client.requests[0]!.headers["content-type"]).toBe("application/merge-patch+json") + const cyclic: Record = {} + cyclic.self = cyclic + await expect(Effect.runPromise(tool.run({ body: cyclic }).pipe(Effect.provide(client.layer)))).rejects.toThrow( + "Invalid JSON body", + ) + }) + + test("rejects oversized and malformed JSON responses", async () => { + const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: singleOperation({}) }).tools, "test") + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + const oversized = recordingClient( + () => new Response(null, { headers: { "content-length": String(50 * 1024 * 1024 + 1) } }), + ) + const malformed = recordingClient( + () => new Response("{", { headers: { "content-type": "application/json" } }), + ) + const chunked = recordingClient(() => new Response(new Uint8Array(50 * 1024 * 1024 + 1))) + + await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(oversized.layer)))).rejects.toThrow( + "response exceeds 50 MiB", + ) + await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(malformed.layer)))).rejects.toThrow( + "returned malformed JSON", + ) + await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(chunked.layer)))).rejects.toThrow( + "response exceeds 50 MiB", + ) + }) + + test("keeps non-JSON responses raw and unions every success output", async () => { + const spec = singleOperation({ + responses: { + 200: { description: "Text", content: { "text/plain": { schema: { type: "string" } } } }, + 204: { description: "Empty" }, + }, + }) + const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec }).tools, "test") + if (!Tool.isDefinition(tool)) throw new Error("test was not generated") + const client = recordingClient(() => new Response("123", { headers: { "content-type": "text/plain" } })) + + expect(outputTypeScript(tool)).toBe("string | null") + await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))).resolves.toBe("123") + }) + + test("fails missing required parameters before auth and network", async () => { + const { requests, layer } = recordingClient(() => json({})) + const runtime = CodeMode.make({ + tools: { opencode: OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools }, + }) + + const result = await Effect.runPromise( + runtime.execute("return await tools.opencode.v2.session.get({})").pipe(Effect.provide(layer)), + ) + + expect(result).toMatchObject({ ok: false }) + expect(JSON.stringify(result)).toContain("Missing required path parameter 'sessionID'") + expect(requests).toHaveLength(0) + }) + + test("prefixes cross-location collisions and reconstructs the HTTP request", async () => { + const spec = { + openapi: "3.1.0", + info: { title: "collision", version: "1.0.0" }, + paths: { + "/echo": { + post: { + operationId: "echo", + requestBody: { + required: true, + content: { "application/json": { schema: { type: "string" } } }, + }, + responses: { "204": { description: "Echoed" } }, + }, + }, + "/things/{id}": { + post: { + operationId: "things.update", + parameters: [ + { name: "id", in: "path", required: true, schema: { type: "string" } }, + { name: "id", in: "query", required: true, schema: { type: "string" } }, + { name: "path_id", in: "query", schema: { type: "string" } }, + { name: "id", in: "header", required: true, schema: { type: "string" } }, + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { id: { type: "string" } }, + required: ["id"], + additionalProperties: false, + }, + }, + }, + }, + responses: { "204": { description: "Updated" } }, + }, + }, + }, + } satisfies Document + const { requests, layer } = recordingClient(() => new Response(null, { status: 204 })) + const tools = OpenAPI.fromSpec({ spec, baseUrl }).tools + const update = toolAt(tools, "things.update") + const echo = toolAt(tools, "echo") + + expect(Tool.isDefinition(update)).toBe(true) + if (!Tool.isDefinition(update)) throw new Error("things.update was not generated") + expect(inputTypeScript(update)).toBe( + "{ path_id: string; query_id: string; path_id_2?: string; header_id: string; body_id: string }", + ) + expect(Tool.isDefinition(echo)).toBe(true) + if (!Tool.isDefinition(echo)) throw new Error("echo was not generated") + expect(inputTypeScript(echo)).toBe("{ body: string }") + + const runtime = CodeMode.make({ tools }) + const result = await Effect.runPromise( + runtime + .execute( + ` + const updated = await tools.things.update({ path_id: "path", query_id: "query", path_id_2: "literal", header_id: "header", body_id: "body" }) + const echoed = await tools.echo({ body: "hello" }) + return { updated, echoed } + `, + ) + .pipe(Effect.provide(layer)), + ) + + expect(result).toMatchObject({ ok: true }) + expect(requests).toHaveLength(2) + expect(new URL(requests[0]!.url).pathname).toBe("/things/path") + expect(new URL(requests[0]!.url).searchParams.get("id")).toBe("query") + expect(new URL(requests[0]!.url).searchParams.get("path_id")).toBe("literal") + expect(requests[0]!.headers.id).toBe("header") + expect(requests[0]!.body).toStrictEqual({ id: "body" }) + expect(requests[1]!.body).toBe("hello") + }) + + test("keeps bodies nested when flattening would lose schema semantics", () => { + const body = (schema: Record, required = true) => ({ + required, + content: { "application/json": { schema } }, + }) + const spec = { + openapi: "3.1.0", + info: { title: "bodies", version: "1.0.0" }, + paths: Object.fromEntries( + [ + [ + "optional", + body( + { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + additionalProperties: false, + }, + false, + ), + ], + ["dictionary", body({ type: "object", additionalProperties: { type: "string" } })], + [ + "composed", + body({ + type: "object", + allOf: [{ type: "object", properties: { name: { type: "string" } }, required: ["name"] }], + additionalProperties: false, + }), + ], + [ + "nullable", + body({ + type: ["object", "null"], + properties: { name: { type: "string" } }, + additionalProperties: false, + }), + ], + ].map(([name, requestBody]) => [ + `/body/${name}`, + { + post: { + operationId: `body.${name}`, + requestBody, + responses: { "204": { description: "Accepted" } }, + }, + }, + ]), + ), + } satisfies Document + const tools = OpenAPI.fromSpec({ spec, baseUrl }).tools + + for (const name of ["optional", "dictionary", "composed", "nullable"]) { + const tool = toolAt(tools, `body.${name}`) + expect(Tool.isDefinition(tool)).toBe(true) + if (!Tool.isDefinition(tool)) throw new Error(`body.${name} was not generated`) + const input = isRecord(tool.input) ? tool.input : {} + expect(Object.keys(isRecord(input.properties) ? input.properties : {})).toStrictEqual(["body"]) + } + const optional = toolAt(tools, "body.optional") + if (!Tool.isDefinition(optional)) throw new Error("body.optional was not generated") + expect(inputTypeScript(optional)).toBe("{ body?: { name: string } }") + }) +}) diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts index edf4c442c4dd..55b8ac020a36 100644 --- a/packages/codemode/test/signature.test.ts +++ b/packages/codemode/test/signature.test.ts @@ -159,8 +159,8 @@ describe("pretty signature rendering", () => { $ref: "#/$defs/Node", $defs: { Node: { type: "object", properties: { child: { $ref: "#/$defs/Node" }, name: { type: "string" } } } }, } as const - expect(jsonSchemaToTypeScript(cyclic)).toBe("{ child?: Node; name?: string }") - expect(jsonSchemaToTypeScript(cyclic, true)).toContain("child?: Node") + expect(jsonSchemaToTypeScript(cyclic)).toBe("{ child?: unknown; name?: string }") + expect(jsonSchemaToTypeScript(cyclic, true)).toContain("child?: unknown") let deep: Record = { type: "string" } for (let level = 0; level < 12; level += 1) deep = { type: "object", properties: { next: deep } } @@ -170,6 +170,43 @@ describe("pretty signature rendering", () => { expect(rendered).toContain("next?:") } }) + + test("intersects ref and union siblings instead of discarding them", () => { + expect( + jsonSchemaToTypeScript({ + $ref: "#/$defs/User", + properties: { active: { type: "boolean" } }, + required: ["active"], + $defs: { + User: { type: "object", properties: { id: { type: "string" } }, required: ["id"] }, + }, + }), + ).toBe("{ id: string } & { active: boolean }") + expect( + jsonSchemaToTypeScript({ + type: "object", + properties: { common: { type: "boolean" } }, + required: ["common"], + anyOf: [ + { type: "object", properties: { name: { type: "string" } }, required: ["name"] }, + { type: "object", properties: { count: { type: "number" } }, required: ["count"] }, + ], + }), + ).toBe("({ name: string } | { count: number }) & { common: boolean }") + expect(jsonSchemaToTypeScript({ $ref: "https://example.com/schema.json" })).toBe("unknown") + expect( + jsonSchemaToTypeScript({ + $ref: "#/$defs/User/properties/id", + $defs: { User: { type: "object" }, id: { type: "string" } }, + }), + ).toBe("unknown") + expect( + jsonSchemaToTypeScript({ + type: ["object", "null"], + properties: { name: { type: "string" } }, + }), + ).toBe("{ name?: string } | null") + }) }) describe("non-identifier property names render as quoted keys", () => { @@ -268,6 +305,32 @@ describe("union schemas render every alternative", () => { expect(inputTypeScript(tool)).toBe("{ value?: string | number }") expect(outputTypeScript(tool)).toBe("number | boolean") }) + + test("allOf renders intersections with parenthesized union members", () => { + const schema = { + allOf: [ + { type: "object", properties: { id: { type: "string" } } }, + { type: ["string", "null"] }, + ], + } as const + expect(jsonSchemaToTypeScript(schema)).toBe("{ id?: string } & (string | null)") + }) + + test("allOf does not discard an unresolved constraint", () => { + expect(jsonSchemaToTypeScript({ allOf: [{ type: "string" }, { $ref: "https://example.com/external.json" }] })).toBe( + "unknown", + ) + expect( + jsonSchemaToTypeScript({ allOf: [{ type: "string" }, { allOf: [{ $ref: "https://example.com/external.json" }] }] }), + ).toBe("unknown") + expect( + jsonSchemaToTypeScript({ + type: "string", + allOf: [{ $ref: "#/$defs/Constraint" }], + $defs: { Constraint: { description: "TypeScript-neutral constraint" } }, + }), + ).toBe("string") + }) }) describe("pretty signatures in search results", () => { diff --git a/packages/protocol/src/groups/pty.ts b/packages/protocol/src/groups/pty.ts index a6ec1b66b6f9..a40b6c4b5332 100644 --- a/packages/protocol/src/groups/pty.ts +++ b/packages/protocol/src/groups/pty.ts @@ -127,6 +127,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty") description: "Establish a WebSocket connection streaming PTY output and accepting terminal input.", transform: (operation) => ({ ...operation, + "x-websocket": true, parameters: [ ...(operation.parameters ?? []), ...["location[directory]", "location[workspace]", "cursor", PTY_CONNECT_TICKET_QUERY].map((name) => ({ From 1b9b2604581bfdac263a69a2d5846bd2a91da6cc Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 4 Jul 2026 21:18:39 +0000 Subject: [PATCH 03/34] chore: generate --- packages/codemode/README.md | 4 +- packages/codemode/src/openapi/runtime.ts | 26 +- packages/codemode/src/openapi/spec.ts | 10 +- .../test/fixtures/openapi-happy-path.json | 25 +- .../test/fixtures/opencode-v2-openapi.json | 5358 ++++------------- packages/codemode/test/openapi.test.ts | 46 +- packages/codemode/test/signature.test.ts | 9 +- packages/sdk/openapi.json | 1 + 8 files changed, 1121 insertions(+), 4358 deletions(-) diff --git a/packages/codemode/README.md b/packages/codemode/README.md index bd14a24fb08f..1230b162967c 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -165,9 +165,7 @@ const api = OpenAPI.fromSpec({ spec: await Bun.file("openapi.json").json(), // parsed document (no YAML) auth: { resolve: ({ name, scopes, operation }) => - name === "BearerAuth" - ? Effect.succeed({ type: "bearer", token }) - : Effect.succeed(undefined), + name === "BearerAuth" ? Effect.succeed({ type: "bearer", token }) : Effect.succeed(undefined), }, }) diff --git a/packages/codemode/src/openapi/runtime.ts b/packages/codemode/src/openapi/runtime.ts index 47312ae1623b..2515621792b2 100644 --- a/packages/codemode/src/openapi/runtime.ts +++ b/packages/codemode/src/openapi/runtime.ts @@ -46,9 +46,7 @@ export const invoke = (plan: Plan, input: unknown): Effect.Effect>): string return toolError(`Missing required path parameter '${field.inputName}'.`) } const fieldValue = serializeSimple(field, item, (value) => - encodeURIComponent(value).replace(/[!'()*]/g, (character) => - `%${character.charCodeAt(0).toString(16).toUpperCase()}`, + encodeURIComponent(value).replace( + /[!'()*]/g, + (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`, ), ) if (fieldValue instanceof ToolError) return fieldValue @@ -271,10 +270,7 @@ const serializeQuery = ( if (value.some((item) => item === undefined || (item !== null && typeof item === "object"))) { return toolError(`Query parameter '${field.inputName}' contains an unsupported nested value.`) } - return value.reduce( - (current, item) => HttpClientRequest.appendUrlParam(current, field.name, String(item)), - request, - ) + return value.reduce((current, item) => HttpClientRequest.appendUrlParam(current, field.name, String(item)), request) } if (isRecord(value) && field.explode) { return Object.entries(value).reduce((current, [name, item]) => { @@ -289,11 +285,15 @@ const serializeQuery = ( return rendered instanceof ToolError ? rendered : HttpClientRequest.appendUrlParam(request, field.name, rendered) } -const readResponseBody = (response: HttpClientResponse.HttpClientResponse, plan: Plan): Effect.Effect => +const readResponseBody = ( + response: HttpClientResponse.HttpClientResponse, + plan: Plan, +): Effect.Effect => Effect.gen(function* () { const contentLength = response.headers["content-length"] const parsedSize = contentLength === undefined ? undefined : Number.parseInt(contentLength, 10) - const declaredSize = parsedSize !== undefined && Number.isSafeInteger(parsedSize) && parsedSize >= 0 ? parsedSize : undefined + const declaredSize = + parsedSize !== undefined && Number.isSafeInteger(parsedSize) && parsedSize >= 0 ? parsedSize : undefined if (declaredSize !== undefined && declaredSize > maxResponseBodyBytes) { return yield* Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} response exceeds 50 MiB.`)) } @@ -304,7 +304,9 @@ const readResponseBody = (response: HttpClientResponse.HttpClientResponse, plan: return Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} response exceeds 50 MiB.`)) } if (size + chunk.byteLength > body.byteLength) { - const grown = Buffer.allocUnsafe(Math.min(maxResponseBodyBytes, Math.max(size + chunk.byteLength, body.byteLength * 2))) + const grown = Buffer.allocUnsafe( + Math.min(maxResponseBodyBytes, Math.max(size + chunk.byteLength, body.byteLength * 2)), + ) body.copy(grown, 0, 0, size) body = grown } diff --git a/packages/codemode/src/openapi/spec.ts b/packages/codemode/src/openapi/spec.ts index 22cf1535a832..bd4dc5aed63e 100644 --- a/packages/codemode/src/openapi/spec.ts +++ b/packages/codemode/src/openapi/spec.ts @@ -78,7 +78,9 @@ const isBinaryMediaType = (document: Document, mediaType: string, value: unknown return isRecord(schema) && schema.format === "binary" } -const jsonContent = (content: Record): { readonly mediaType: string; readonly schema: unknown } | undefined => { +const jsonContent = ( + content: Record, +): { readonly mediaType: string; readonly schema: unknown } | undefined => { const entry = Object.entries(content).find(([mediaType]) => isJsonMediaType(mediaType)) return entry !== undefined && isRecord(entry[1]) ? { mediaType: entry[0], schema: entry[1].schema } : undefined } @@ -344,7 +346,7 @@ export const operationOutput = ( if (outcomes.length === 0) return { ok: true, value: undefined } return { ok: true, - value: withDefinitions(outcomes.length === 1 ? outcomes[0] ?? {} : { anyOf: outcomes }, definitions), + value: withDefinitions(outcomes.length === 1 ? (outcomes[0] ?? {}) : { anyOf: outcomes }, definitions), } } @@ -380,7 +382,9 @@ export const operationPath = ( namespaces: ReadonlySet, ): ReadonlyArray => { const raw = nonEmptyString(operation.operationId) - const segments = (raw === undefined ? [fallbackOperationId(method, path)] : raw.split(".")).map(sanitizeOperationSegment) + const segments = (raw === undefined ? [fallbackOperationId(method, path)] : raw.split(".")).map( + sanitizeOperationSegment, + ) if (isOperationPathAvailable(segments, used, namespaces)) return segments const conflict = segments.slice(0, -1).findIndex((_, index) => used.has(segments.slice(0, index + 1).join("."))) if (conflict >= 0 && conflict + 1 < segments.length) { diff --git a/packages/codemode/test/fixtures/openapi-happy-path.json b/packages/codemode/test/fixtures/openapi-happy-path.json index dc3cbe5f8f44..8052dd739113 100644 --- a/packages/codemode/test/fixtures/openapi-happy-path.json +++ b/packages/codemode/test/fixtures/openapi-happy-path.json @@ -93,16 +93,10 @@ }, "role": { "type": "string", - "enum": [ - "admin", - "member" - ] + "enum": ["admin", "member"] } }, - "required": [ - "name", - "email" - ], + "required": ["name", "email"], "additionalProperties": false } } @@ -143,9 +137,7 @@ "type": "integer" } }, - "required": [ - "query" - ], + "required": ["query"], "additionalProperties": false } }, @@ -216,17 +208,10 @@ }, "role": { "type": "string", - "enum": [ - "admin", - "member" - ] + "enum": ["admin", "member"] } }, - "required": [ - "id", - "name", - "email" - ], + "required": ["id", "name", "email"], "additionalProperties": false } }, diff --git a/packages/codemode/test/fixtures/opencode-v2-openapi.json b/packages/codemode/test/fixtures/opencode-v2-openapi.json index 4d1d3af7082c..c78194e669fd 100644 --- a/packages/codemode/test/fixtures/opencode-v2-openapi.json +++ b/packages/codemode/test/fixtures/opencode-v2-openapi.json @@ -8,9 +8,7 @@ "paths": { "/api/health": { "get": { - "tags": [ - "server.health" - ], + "tags": ["server.health"], "operationId": "v2.health.get", "parameters": [], "security": [], @@ -24,14 +22,10 @@ "properties": { "healthy": { "type": "boolean", - "enum": [ - true - ] + "enum": [true] } }, - "required": [ - "healthy" - ], + "required": ["healthy"], "additionalProperties": false } } @@ -64,9 +58,7 @@ }, "/api/location": { "get": { - "tags": [ - "server.location" - ], + "tags": ["server.location"], "operationId": "v2.location.get", "parameters": [ { @@ -149,9 +141,7 @@ }, "/api/agent": { "get": { - "tags": [ - "server.agent" - ], + "tags": ["server.agent"], "operationId": "v2.agent.list", "parameters": [ { @@ -214,10 +204,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -250,9 +237,7 @@ }, "/api/plugin": { "get": { - "tags": [ - "plugins" - ], + "tags": ["plugins"], "operationId": "v2.plugin.list", "parameters": [ { @@ -315,10 +300,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -351,9 +333,7 @@ }, "/api/session": { "get": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.list", "parameters": [ { @@ -399,10 +379,7 @@ "anyOf": [ { "type": "string", - "enum": [ - "asc", - "desc" - ] + "enum": ["asc", "desc"] }, { "type": "null" @@ -444,9 +421,7 @@ }, { "type": "string", - "enum": [ - "null" - ] + "enum": ["null"] } ], "description": "Filter by parent session. Use null to return only root sessions." @@ -567,9 +542,7 @@ "summary": "List sessions" }, "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.create", "parameters": [], "security": [], @@ -585,9 +558,7 @@ "$ref": "#/components/schemas/SessionV2.Info" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -678,9 +649,7 @@ }, "/api/session/active": { "get": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.active", "parameters": [], "security": [], @@ -704,10 +673,7 @@ "$ref": "#/components/schemas/SessionWatermarks" } }, - "required": [ - "data", - "watermarks" - ], + "required": ["data", "watermarks"], "additionalProperties": false } } @@ -740,9 +706,7 @@ }, "/api/session/{sessionID}": { "get": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.get", "parameters": [ { @@ -772,9 +736,7 @@ "$ref": "#/components/schemas/SessionV2.Info" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -824,9 +786,7 @@ }, "/api/session/{sessionID}/fork": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.fork", "parameters": [ { @@ -856,9 +816,7 @@ "$ref": "#/components/schemas/SessionV2.Info" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -939,9 +897,7 @@ }, "/api/session/{sessionID}/agent": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.switchAgent", "parameters": [ { @@ -1013,9 +969,7 @@ "type": "string" } }, - "required": [ - "agent" - ], + "required": ["agent"], "additionalProperties": false } } @@ -1026,9 +980,7 @@ }, "/api/session/{sessionID}/model": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.switchModel", "parameters": [ { @@ -1100,9 +1052,7 @@ "$ref": "#/components/schemas/Model.Ref" } }, - "required": [ - "model" - ], + "required": ["model"], "additionalProperties": false } } @@ -1113,9 +1063,7 @@ }, "/api/session/{sessionID}/rename": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.rename", "parameters": [ { @@ -1187,9 +1135,7 @@ "type": "string" } }, - "required": [ - "title" - ], + "required": ["title"], "additionalProperties": false } } @@ -1200,9 +1146,7 @@ }, "/api/session/{sessionID}/prompt": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.prompt", "parameters": [ { @@ -1232,9 +1176,7 @@ "$ref": "#/components/schemas/SessionInput.Admitted" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -1318,10 +1260,7 @@ "anyOf": [ { "type": "string", - "enum": [ - "steer", - "queue" - ] + "enum": ["steer", "queue"] }, { "type": "null" @@ -1339,9 +1278,7 @@ ] } }, - "required": [ - "prompt" - ], + "required": ["prompt"], "additionalProperties": false } } @@ -1352,9 +1289,7 @@ }, "/api/session/{sessionID}/command": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.command", "parameters": [ { @@ -1384,9 +1319,7 @@ "$ref": "#/components/schemas/SessionInput.Admitted" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -1525,10 +1458,7 @@ "anyOf": [ { "type": "string", - "enum": [ - "steer", - "queue" - ] + "enum": ["steer", "queue"] }, { "type": "null" @@ -1546,9 +1476,7 @@ ] } }, - "required": [ - "command" - ], + "required": ["command"], "additionalProperties": false } } @@ -1559,9 +1487,7 @@ }, "/api/session/{sessionID}/skill": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.skill", "parameters": [ { @@ -1661,9 +1587,7 @@ ] } }, - "required": [ - "skill" - ], + "required": ["skill"], "additionalProperties": false } } @@ -1674,9 +1598,7 @@ }, "/api/session/{sessionID}/synthetic": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.synthetic", "parameters": [ { @@ -1761,9 +1683,7 @@ "type": "object" } }, - "required": [ - "text" - ], + "required": ["text"], "additionalProperties": false } } @@ -1774,9 +1694,7 @@ }, "/api/session/{sessionID}/compact": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.compact", "parameters": [ { @@ -1872,9 +1790,7 @@ }, "/api/session/{sessionID}/wait": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.wait", "parameters": [ { @@ -1950,9 +1866,7 @@ }, "/api/session/{sessionID}/revert/stage": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.revert.stage", "parameters": [ { @@ -1982,9 +1896,7 @@ "$ref": "#/components/schemas/Revert.State" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -2078,9 +1990,7 @@ ] } }, - "required": [ - "messageID" - ], + "required": ["messageID"], "additionalProperties": false } } @@ -2091,9 +2001,7 @@ }, "/api/session/{sessionID}/revert/clear": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.revert.clear", "parameters": [ { @@ -2178,9 +2086,7 @@ }, "/api/session/{sessionID}/revert/commit": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.revert.commit", "parameters": [ { @@ -2255,9 +2161,7 @@ }, "/api/session/{sessionID}/context": { "get": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.context", "parameters": [ { @@ -2290,9 +2194,7 @@ } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -2352,9 +2254,7 @@ }, "/api/session/{sessionID}/context-entry": { "get": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.context.entry.list", "parameters": [ { @@ -2387,9 +2287,7 @@ } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -2439,9 +2337,7 @@ }, "/api/session/{sessionID}/context-entry/{key}": { "put": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.context.entry.put", "parameters": [ { @@ -2519,9 +2415,7 @@ "properties": { "value": {} }, - "required": [ - "value" - ], + "required": ["value"], "additionalProperties": false } } @@ -2530,9 +2424,7 @@ } }, "delete": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.context.entry.remove", "parameters": [ { @@ -2606,9 +2498,7 @@ }, "/api/session/{sessionID}/log": { "get": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.log", "parameters": [ { @@ -2646,10 +2536,7 @@ "anyOf": [ { "type": "string", - "enum": [ - "true", - "false" - ] + "enum": ["true", "false"] }, { "type": "null" @@ -2685,11 +2572,7 @@ "$ref": "#/components/schemas/SessionLogItemStream" } }, - "required": [ - "id", - "event", - "data" - ], + "required": ["id", "event", "data"], "additionalProperties": false }, "x-effect-stream": { @@ -2703,18 +2586,13 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Fail" - ] + "enum": ["Fail"] }, "error": { "not": {} } }, - "required": [ - "_tag", - "error" - ], + "required": ["_tag", "error"], "additionalProperties": false }, { @@ -2722,16 +2600,11 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Die" - ] + "enum": ["Die"] }, "defect": {} }, - "required": [ - "_tag", - "defect" - ], + "required": ["_tag", "defect"], "additionalProperties": false }, { @@ -2739,9 +2612,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Interrupt" - ] + "enum": ["Interrupt"] }, "fiberId": { "anyOf": [ @@ -2754,10 +2625,7 @@ ] } }, - "required": [ - "_tag", - "fiberId" - ], + "required": ["_tag", "fiberId"], "additionalProperties": false } ] @@ -2815,9 +2683,7 @@ }, "/api/session/{sessionID}/interrupt": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.interrupt", "parameters": [ { @@ -2883,9 +2749,7 @@ }, "/api/session/{sessionID}/background": { "post": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.background", "parameters": [ { @@ -2951,9 +2815,7 @@ }, "/api/session/{sessionID}/message/{messageID}": { "get": { - "tags": [ - "sessions" - ], + "tags": ["sessions"], "operationId": "v2.session.message", "parameters": [ { @@ -2996,9 +2858,7 @@ "$ref": "#/components/schemas/Session.Message" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -3051,9 +2911,7 @@ }, "/api/session/{sessionID}/message": { "get": { - "tags": [ - "messages" - ], + "tags": ["messages"], "operationId": "v2.session.messages", "parameters": [ { @@ -3092,10 +2950,7 @@ "anyOf": [ { "type": "string", - "enum": [ - "asc", - "desc" - ] + "enum": ["asc", "desc"] }, { "type": "null" @@ -3195,9 +3050,7 @@ }, "/api/model": { "get": { - "tags": [ - "models" - ], + "tags": ["models"], "operationId": "v2.model.list", "parameters": [ { @@ -3260,10 +3113,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -3306,9 +3156,7 @@ }, "/api/model/default": { "get": { - "tags": [ - "models" - ], + "tags": ["models"], "operationId": "v2.model.default", "parameters": [ { @@ -3375,10 +3223,7 @@ ] } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -3421,9 +3266,7 @@ }, "/api/generate": { "post": { - "tags": [ - "generate" - ], + "tags": ["generate"], "operationId": "v2.generate.text", "parameters": [ { @@ -3539,9 +3382,7 @@ ] } }, - "required": [ - "prompt" - ], + "required": ["prompt"], "additionalProperties": false } } @@ -3552,9 +3393,7 @@ }, "/api/provider": { "get": { - "tags": [ - "providers" - ], + "tags": ["providers"], "operationId": "v2.provider.list", "parameters": [ { @@ -3617,10 +3456,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -3663,9 +3499,7 @@ }, "/api/provider/{providerID}": { "get": { - "tags": [ - "providers" - ], + "tags": ["providers"], "operationId": "v2.provider.get", "parameters": [ { @@ -3733,10 +3567,7 @@ "$ref": "#/components/schemas/ProviderV2.Info" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -3789,9 +3620,7 @@ }, "/api/integration": { "get": { - "tags": [ - "integrations" - ], + "tags": ["integrations"], "operationId": "v2.integration.list", "parameters": [ { @@ -3854,10 +3683,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -3890,9 +3716,7 @@ }, "/api/integration/{integrationID}": { "get": { - "tags": [ - "integrations" - ], + "tags": ["integrations"], "operationId": "v2.integration.get", "parameters": [ { @@ -3967,10 +3791,7 @@ ] } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -4003,9 +3824,7 @@ }, "/api/integration/{integrationID}/connect/key": { "post": { - "tags": [ - "integrations" - ], + "tags": ["integrations"], "operationId": "v2.integration.connect.key", "parameters": [ { @@ -4112,9 +3931,7 @@ ] } }, - "required": [ - "key" - ], + "required": ["key"], "additionalProperties": false } } @@ -4125,9 +3942,7 @@ }, "/api/integration/{integrationID}/connect/oauth": { "post": { - "tags": [ - "integrations" - ], + "tags": ["integrations"], "operationId": "v2.integration.connect.oauth", "parameters": [ { @@ -4195,10 +4010,7 @@ "$ref": "#/components/schemas/Integration.Attempt" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -4260,10 +4072,7 @@ ] } }, - "required": [ - "methodID", - "inputs" - ], + "required": ["methodID", "inputs"], "additionalProperties": false } } @@ -4274,9 +4083,7 @@ }, "/api/integration/attempt/{attemptID}": { "get": { - "tags": [ - "integrations" - ], + "tags": ["integrations"], "operationId": "v2.integration.attempt.status", "parameters": [ { @@ -4344,10 +4151,7 @@ "$ref": "#/components/schemas/Integration.AttemptStatus" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -4378,9 +4182,7 @@ "summary": "Get OAuth attempt status" }, "delete": { - "tags": [ - "integrations" - ], + "tags": ["integrations"], "operationId": "v2.integration.attempt.cancel", "parameters": [ { @@ -4464,9 +4266,7 @@ }, "/api/integration/attempt/{attemptID}/complete": { "post": { - "tags": [ - "integrations" - ], + "tags": ["integrations"], "operationId": "v2.integration.attempt.complete", "parameters": [ { @@ -4580,9 +4380,7 @@ }, "/api/mcp": { "get": { - "tags": [ - "mcp" - ], + "tags": ["mcp"], "operationId": "v2.mcp.list", "parameters": [ { @@ -4645,10 +4443,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -4681,9 +4476,7 @@ }, "/api/credential/{credentialID}": { "patch": { - "tags": [ - "server.credential" - ], + "tags": ["server.credential"], "operationId": "v2.credential.update", "parameters": [ { @@ -4773,9 +4566,7 @@ "type": "string" } }, - "required": [ - "label" - ], + "required": ["label"], "additionalProperties": false } } @@ -4784,9 +4575,7 @@ } }, "delete": { - "tags": [ - "server.credential" - ], + "tags": ["server.credential"], "operationId": "v2.credential.remove", "parameters": [ { @@ -4870,9 +4659,7 @@ }, "/api/project/current": { "get": { - "tags": [ - "projects" - ], + "tags": ["projects"], "operationId": "v2.project.current", "parameters": [ { @@ -4955,9 +4742,7 @@ }, "/api/project/{projectID}/directories": { "get": { - "tags": [ - "projects" - ], + "tags": ["projects"], "operationId": "v2.project.directories", "parameters": [ { @@ -5048,9 +4833,7 @@ }, "/api/form/request": { "get": { - "tags": [ - "forms" - ], + "tags": ["forms"], "operationId": "v2.form.request.list", "parameters": [ { @@ -5120,10 +4903,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -5156,9 +4936,7 @@ }, "/api/session/{sessionID}/form": { "get": { - "tags": [ - "forms" - ], + "tags": ["forms"], "operationId": "v2.session.form.list", "parameters": [ { @@ -5193,9 +4971,7 @@ } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -5243,9 +5019,7 @@ "summary": "List session forms" }, "post": { - "tags": [ - "forms" - ], + "tags": ["forms"], "operationId": "v2.session.form.create", "parameters": [ { @@ -5277,9 +5051,7 @@ ] } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -5356,9 +5128,7 @@ }, "/api/session/{sessionID}/form/{formID}": { "get": { - "tags": [ - "forms" - ], + "tags": ["forms"], "operationId": "v2.session.form.get", "parameters": [ { @@ -5403,9 +5173,7 @@ ] } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -5458,9 +5226,7 @@ }, "/api/session/{sessionID}/form/{formID}/state": { "get": { - "tags": [ - "forms" - ], + "tags": ["forms"], "operationId": "v2.session.form.state", "parameters": [ { @@ -5498,9 +5264,7 @@ "$ref": "#/components/schemas/Form.State" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -5553,9 +5317,7 @@ }, "/api/session/{sessionID}/form/{formID}/reply": { "post": { - "tags": [ - "forms" - ], + "tags": ["forms"], "operationId": "v2.session.form.reply", "parameters": [ { @@ -5659,9 +5421,7 @@ }, "/api/session/{sessionID}/form/{formID}/cancel": { "post": { - "tags": [ - "forms" - ], + "tags": ["forms"], "operationId": "v2.session.form.cancel", "parameters": [ { @@ -5748,9 +5508,7 @@ }, "/api/permission/request": { "get": { - "tags": [ - "permissions" - ], + "tags": ["permissions"], "operationId": "v2.permission.request.list", "parameters": [ { @@ -5813,10 +5571,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -5849,9 +5604,7 @@ }, "/api/permission/saved": { "get": { - "tags": [ - "permissions" - ], + "tags": ["permissions"], "operationId": "v2.permission.saved.list", "parameters": [ { @@ -5886,9 +5639,7 @@ } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -5921,9 +5672,7 @@ }, "/api/permission/saved/{id}": { "delete": { - "tags": [ - "permissions" - ], + "tags": ["permissions"], "operationId": "v2.permission.saved.remove", "parameters": [ { @@ -5967,9 +5716,7 @@ }, "/api/session/{sessionID}/permission": { "post": { - "tags": [ - "permissions" - ], + "tags": ["permissions"], "operationId": "v2.session.permission.create", "parameters": [ { @@ -6010,16 +5757,11 @@ "$ref": "#/components/schemas/PermissionV2.Effect" } }, - "required": [ - "id", - "effect" - ], + "required": ["id", "effect"], "additionalProperties": false } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -6118,10 +5860,7 @@ ] } }, - "required": [ - "action", - "resources" - ], + "required": ["action", "resources"], "additionalProperties": false } } @@ -6130,9 +5869,7 @@ } }, "get": { - "tags": [ - "permissions" - ], + "tags": ["permissions"], "operationId": "v2.session.permission.list", "parameters": [ { @@ -6165,9 +5902,7 @@ } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -6217,9 +5952,7 @@ }, "/api/session/{sessionID}/permission/{requestID}": { "get": { - "tags": [ - "permissions" - ], + "tags": ["permissions"], "operationId": "v2.session.permission.get", "parameters": [ { @@ -6262,9 +5995,7 @@ "$ref": "#/components/schemas/PermissionV2.Request" } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -6317,9 +6048,7 @@ }, "/api/session/{sessionID}/permission/{requestID}/reply": { "post": { - "tags": [ - "permissions" - ], + "tags": ["permissions"], "operationId": "v2.session.permission.reply", "parameters": [ { @@ -6417,9 +6146,7 @@ ] } }, - "required": [ - "reply" - ], + "required": ["reply"], "additionalProperties": false } } @@ -6430,9 +6157,7 @@ }, "/api/fs/read/*": { "get": { - "tags": [ - "filesystem" - ], + "tags": ["filesystem"], "operationId": "v2.fs.read", "parameters": [ { @@ -6516,9 +6241,7 @@ }, "/api/fs/list": { "get": { - "tags": [ - "filesystem" - ], + "tags": ["filesystem"], "operationId": "v2.fs.list", "parameters": [ { @@ -6596,10 +6319,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -6632,9 +6352,7 @@ }, "/api/fs/find": { "get": { - "tags": [ - "filesystem" - ], + "tags": ["filesystem"], "operationId": "v2.fs.find", "parameters": [ { @@ -6690,10 +6408,7 @@ "in": "query", "schema": { "type": "string", - "enum": [ - "file", - "directory" - ] + "enum": ["file", "directory"] }, "required": false }, @@ -6732,10 +6447,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -6768,9 +6480,7 @@ }, "/api/command": { "get": { - "tags": [ - "commands" - ], + "tags": ["commands"], "operationId": "v2.command.list", "parameters": [ { @@ -6833,10 +6543,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -6869,9 +6576,7 @@ }, "/api/skill": { "get": { - "tags": [ - "skills" - ], + "tags": ["skills"], "operationId": "v2.skill.list", "parameters": [ { @@ -6934,10 +6639,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -6970,9 +6672,7 @@ }, "/api/event": { "get": { - "tags": [ - "events" - ], + "tags": ["events"], "operationId": "v2.event.subscribe", "parameters": [], "security": [], @@ -7001,11 +6701,7 @@ "$ref": "#/components/schemas/V2EventStream" } }, - "required": [ - "id", - "event", - "data" - ], + "required": ["id", "event", "data"], "additionalProperties": false }, "x-effect-stream": { @@ -7019,18 +6715,13 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Fail" - ] + "enum": ["Fail"] }, "error": { "not": {} } }, - "required": [ - "_tag", - "error" - ], + "required": ["_tag", "error"], "additionalProperties": false }, { @@ -7038,16 +6729,11 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Die" - ] + "enum": ["Die"] }, "defect": {} }, - "required": [ - "_tag", - "defect" - ], + "required": ["_tag", "defect"], "additionalProperties": false }, { @@ -7055,9 +6741,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Interrupt" - ] + "enum": ["Interrupt"] }, "fiberId": { "anyOf": [ @@ -7070,10 +6754,7 @@ ] } }, - "required": [ - "_tag", - "fiberId" - ], + "required": ["_tag", "fiberId"], "additionalProperties": false } ] @@ -7114,9 +6795,7 @@ }, "/api/event/changes": { "get": { - "tags": [ - "events" - ], + "tags": ["events"], "operationId": "v2.event.changes", "parameters": [], "security": [], @@ -7145,11 +6824,7 @@ "$ref": "#/components/schemas/EventLog.ChangeStream" } }, - "required": [ - "id", - "event", - "data" - ], + "required": ["id", "event", "data"], "additionalProperties": false }, "x-effect-stream": { @@ -7163,18 +6838,13 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Fail" - ] + "enum": ["Fail"] }, "error": { "not": {} } }, - "required": [ - "_tag", - "error" - ], + "required": ["_tag", "error"], "additionalProperties": false }, { @@ -7182,16 +6852,11 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Die" - ] + "enum": ["Die"] }, "defect": {} }, - "required": [ - "_tag", - "defect" - ], + "required": ["_tag", "defect"], "additionalProperties": false }, { @@ -7199,9 +6864,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "Interrupt" - ] + "enum": ["Interrupt"] }, "fiberId": { "anyOf": [ @@ -7214,10 +6877,7 @@ ] } }, - "required": [ - "_tag", - "fiberId" - ], + "required": ["_tag", "fiberId"], "additionalProperties": false } ] @@ -7258,9 +6918,7 @@ }, "/api/pty": { "get": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.list", "parameters": [ { @@ -7323,10 +6981,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -7357,9 +7012,7 @@ "summary": "List PTY sessions" }, "post": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.create", "parameters": [ { @@ -7419,10 +7072,7 @@ "$ref": "#/components/schemas/Pty" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -7489,9 +7139,7 @@ }, "/api/pty/{ptyID}": { "get": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.get", "parameters": [ { @@ -7564,10 +7212,7 @@ "$ref": "#/components/schemas/Pty" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -7608,9 +7253,7 @@ "summary": "Get PTY session" }, "put": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.update", "parameters": [ { @@ -7683,10 +7326,7 @@ "$ref": "#/components/schemas/Pty" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -7754,10 +7394,7 @@ ] } }, - "required": [ - "rows", - "cols" - ], + "required": ["rows", "cols"], "additionalProperties": false } }, @@ -7769,9 +7406,7 @@ } }, "delete": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.remove", "parameters": [ { @@ -7870,9 +7505,7 @@ }, "/api/pty/{ptyID}/connect-token": { "post": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.connectToken", "parameters": [ { @@ -7945,10 +7578,7 @@ "$ref": "#/components/schemas/PtyTicket.ConnectToken" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8001,9 +7631,7 @@ }, "/api/pty/{ptyID}/connect": { "get": { - "tags": [ - "pty" - ], + "tags": ["pty"], "operationId": "v2.pty.connect", "x-websocket": true, "parameters": [ @@ -8108,9 +7736,7 @@ }, "/api/shell": { "get": { - "tags": [ - "shell" - ], + "tags": ["shell"], "operationId": "v2.shell.list", "parameters": [ { @@ -8173,10 +7799,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8207,9 +7830,7 @@ "summary": "List running shell commands" }, "post": { - "tags": [ - "shell" - ], + "tags": ["shell"], "operationId": "v2.shell.create", "parameters": [ { @@ -8269,10 +7890,7 @@ "$ref": "#/components/schemas/Shell1" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8325,9 +7943,7 @@ "type": "object" } }, - "required": [ - "command" - ], + "required": ["command"], "additionalProperties": false } } @@ -8338,9 +7954,7 @@ }, "/api/shell/{id}": { "get": { - "tags": [ - "shell" - ], + "tags": ["shell"], "operationId": "v2.shell.get", "parameters": [ { @@ -8413,10 +8027,7 @@ "$ref": "#/components/schemas/Shell1" } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8457,9 +8068,7 @@ "summary": "Get shell command" }, "delete": { - "tags": [ - "shell" - ], + "tags": ["shell"], "operationId": "v2.shell.remove", "parameters": [ { @@ -8558,9 +8167,7 @@ }, "/api/shell/{id}/output": { "get": { - "tags": [ - "shell" - ], + "tags": ["shell"], "operationId": "v2.shell.output", "parameters": [ { @@ -8681,19 +8288,11 @@ "type": "boolean" } }, - "required": [ - "output", - "cursor", - "size", - "truncated" - ], + "required": ["output", "cursor", "size", "truncated"], "additionalProperties": false } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8736,9 +8335,7 @@ }, "/api/question/request": { "get": { - "tags": [ - "session questions" - ], + "tags": ["session questions"], "operationId": "v2.question.request.list", "parameters": [ { @@ -8801,10 +8398,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -8837,9 +8431,7 @@ }, "/api/session/{sessionID}/question": { "get": { - "tags": [ - "session questions" - ], + "tags": ["session questions"], "operationId": "v2.session.question.list", "parameters": [ { @@ -8872,9 +8464,7 @@ } } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false } } @@ -8924,9 +8514,7 @@ }, "/api/session/{sessionID}/question/{requestID}/reply": { "post": { - "tags": [ - "session questions" - ], + "tags": ["session questions"], "operationId": "v2.session.question.reply", "parameters": [ { @@ -9018,9 +8606,7 @@ }, "/api/session/{sessionID}/question/{requestID}/reject": { "post": { - "tags": [ - "session questions" - ], + "tags": ["session questions"], "operationId": "v2.session.question.reject", "parameters": [ { @@ -9102,9 +8688,7 @@ }, "/api/reference": { "get": { - "tags": [ - "reference" - ], + "tags": ["reference"], "operationId": "v2.reference.list", "parameters": [ { @@ -9167,10 +8751,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -9203,9 +8784,7 @@ }, "/experimental/project/{projectID}/copy": { "post": { - "tags": [ - "projectCopy" - ], + "tags": ["projectCopy"], "operationId": "v2.projectCopy.create", "parameters": [ { @@ -9313,10 +8892,7 @@ "type": "string" } }, - "required": [ - "strategy", - "directory" - ], + "required": ["strategy", "directory"], "additionalProperties": false } } @@ -9325,9 +8901,7 @@ } }, "delete": { - "tags": [ - "projectCopy" - ], + "tags": ["projectCopy"], "operationId": "v2.projectCopy.remove", "parameters": [ { @@ -9425,10 +8999,7 @@ "type": "boolean" } }, - "required": [ - "directory", - "force" - ], + "required": ["directory", "force"], "additionalProperties": false } } @@ -9439,9 +9010,7 @@ }, "/experimental/project/{projectID}/copy/refresh": { "post": { - "tags": [ - "projectCopy" - ], + "tags": ["projectCopy"], "operationId": "v2.projectCopy.refresh", "parameters": [ { @@ -9530,9 +9099,7 @@ }, "/api/vcs/status": { "get": { - "tags": [ - "vcs" - ], + "tags": ["vcs"], "operationId": "v2.vcs.status", "parameters": [ { @@ -9595,10 +9162,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -9631,9 +9195,7 @@ }, "/api/vcs/diff": { "get": { - "tags": [ - "vcs" - ], + "tags": ["vcs"], "operationId": "v2.vcs.diff", "parameters": [ { @@ -9719,10 +9281,7 @@ } } }, - "required": [ - "location", - "data" - ], + "required": ["location", "data"], "additionalProperties": false } } @@ -9761,18 +9320,13 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "UnauthorizedError" - ] + "enum": ["UnauthorizedError"] }, "message": { "type": "string" } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "InvalidRequestError": { @@ -9780,9 +9334,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "InvalidRequestError" - ] + "enum": ["InvalidRequestError"] }, "message": { "type": "string" @@ -9808,10 +9360,7 @@ ] } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "Location.Info": { @@ -9838,17 +9387,11 @@ "type": "string" } }, - "required": [ - "id", - "directory" - ], + "required": ["id", "directory"], "additionalProperties": false } }, - "required": [ - "directory", - "project" - ], + "required": ["directory", "project"], "additionalProperties": false }, "Model.Ref": { @@ -9864,10 +9407,7 @@ "type": "string" } }, - "required": [ - "id", - "providerID" - ], + "required": ["id", "providerID"], "additionalProperties": false }, "Provider.Settings": { @@ -9889,11 +9429,7 @@ "type": "object" } }, - "required": [ - "settings", - "headers", - "body" - ], + "required": ["settings", "headers", "body"], "additionalProperties": false }, "Agent.Color": { @@ -9908,25 +9444,13 @@ }, { "type": "string", - "enum": [ - "primary", - "secondary", - "accent", - "success", - "warning", - "error", - "info" - ] + "enum": ["primary", "secondary", "accent", "success", "warning", "error", "info"] } ] }, "PermissionV2.Effect": { "type": "string", - "enum": [ - "allow", - "deny", - "ask" - ] + "enum": ["allow", "deny", "ask"] }, "PermissionV2.Rule": { "type": "object", @@ -9941,11 +9465,7 @@ "$ref": "#/components/schemas/PermissionV2.Effect" } }, - "required": [ - "action", - "resource", - "effect" - ], + "required": ["action", "resource", "effect"], "additionalProperties": false }, "PermissionV2.Ruleset": { @@ -9974,11 +9494,7 @@ }, "mode": { "type": "string", - "enum": [ - "subagent", - "primary", - "all" - ] + "enum": ["subagent", "primary", "all"] }, "hidden": { "type": "boolean" @@ -9998,13 +9514,7 @@ "$ref": "#/components/schemas/PermissionV2.Ruleset" } }, - "required": [ - "id", - "request", - "mode", - "hidden", - "permissions" - ], + "required": ["id", "request", "mode", "hidden", "permissions"], "additionalProperties": false }, "Plugin.Info": { @@ -10014,9 +9524,7 @@ "type": "string" } }, - "required": [ - "id" - ], + "required": ["id"], "additionalProperties": false }, "Location.Ref": { @@ -10034,9 +9542,7 @@ ] } }, - "required": [ - "directory" - ], + "required": ["directory"], "additionalProperties": false }, "File.Diff": { @@ -10047,11 +9553,7 @@ }, "status": { "type": "string", - "enum": [ - "added", - "modified", - "deleted" - ] + "enum": ["added", "modified", "deleted"] }, "additions": { "type": "integer", @@ -10073,13 +9575,7 @@ "type": "string" } }, - "required": [ - "path", - "status", - "additions", - "deletions", - "patch" - ], + "required": ["path", "status", "additions", "deletions", "patch"], "additionalProperties": false }, "Revert.State": { @@ -10109,9 +9605,7 @@ } } }, - "required": [ - "messageID" - ], + "required": ["messageID"], "additionalProperties": false }, "SessionV2.Info": { @@ -10167,19 +9661,11 @@ "type": "number" } }, - "required": [ - "read", - "write" - ], + "required": ["read", "write"], "additionalProperties": false } }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ], + "required": ["input", "output", "reasoning", "cache"], "additionalProperties": false }, "time": { @@ -10195,10 +9681,7 @@ "type": "number" } }, - "required": [ - "created", - "updated" - ], + "required": ["created", "updated"], "additionalProperties": false }, "title": { @@ -10214,15 +9697,7 @@ "$ref": "#/components/schemas/Revert.State" } }, - "required": [ - "id", - "projectID", - "cost", - "tokens", - "time", - "title", - "location" - ], + "required": ["id", "projectID", "cost", "tokens", "time", "title", "location"], "additionalProperties": false }, "SessionWatermarks": { @@ -10278,11 +9753,7 @@ "additionalProperties": false } }, - "required": [ - "data", - "watermarks", - "cursor" - ], + "required": ["data", "watermarks", "cursor"], "additionalProperties": false }, "InvalidCursorError": { @@ -10290,18 +9761,13 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "InvalidCursorError" - ] + "enum": ["InvalidCursorError"] }, "message": { "type": "string" } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "InvalidRequestError1": { @@ -10309,9 +9775,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "InvalidRequestError" - ] + "enum": ["InvalidRequestError"] }, "message": { "type": "string" @@ -10337,10 +9801,7 @@ ] } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "SessionActive": { @@ -10348,14 +9809,10 @@ "properties": { "type": { "type": "string", - "enum": [ - "running" - ] + "enum": ["running"] } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false }, "SessionNotFoundError": { @@ -10363,9 +9820,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "SessionNotFoundError" - ] + "enum": ["SessionNotFoundError"] }, "sessionID": { "type": "string" @@ -10374,11 +9829,7 @@ "type": "string" } }, - "required": [ - "_tag", - "sessionID", - "message" - ], + "required": ["_tag", "sessionID", "message"], "additionalProperties": false }, "MessageNotFoundError": { @@ -10386,9 +9837,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "MessageNotFoundError" - ] + "enum": ["MessageNotFoundError"] }, "sessionID": { "type": "string" @@ -10400,12 +9849,7 @@ "type": "string" } }, - "required": [ - "_tag", - "sessionID", - "messageID", - "message" - ], + "required": ["_tag", "sessionID", "messageID", "message"], "additionalProperties": false }, "Prompt.Source": { @@ -10421,11 +9865,7 @@ "type": "string" } }, - "required": [ - "start", - "end", - "text" - ], + "required": ["start", "end", "text"], "additionalProperties": false }, "PromptInput.FileAttachment": { @@ -10444,9 +9884,7 @@ "$ref": "#/components/schemas/Prompt.Source" } }, - "required": [ - "uri" - ], + "required": ["uri"], "additionalProperties": false }, "Prompt.AgentAttachment": { @@ -10459,9 +9897,7 @@ "$ref": "#/components/schemas/Prompt.Source" } }, - "required": [ - "name" - ], + "required": ["name"], "additionalProperties": false }, "PromptInput": { @@ -10483,9 +9919,7 @@ } } }, - "required": [ - "text" - ], + "required": ["text"], "additionalProperties": false }, "Prompt.FileAttachment": { @@ -10507,10 +9941,7 @@ "$ref": "#/components/schemas/Prompt.Source" } }, - "required": [ - "uri", - "mime" - ], + "required": ["uri", "mime"], "additionalProperties": false }, "Prompt": { @@ -10532,9 +9963,7 @@ } } }, - "required": [ - "text" - ], + "required": ["text"], "additionalProperties": false }, "SessionInput.Admitted": { @@ -10569,10 +9998,7 @@ }, "delivery": { "type": "string", - "enum": [ - "steer", - "queue" - ] + "enum": ["steer", "queue"] }, "timeCreated": { "type": "number" @@ -10586,14 +10012,7 @@ ] } }, - "required": [ - "admittedSeq", - "id", - "sessionID", - "prompt", - "delivery", - "timeCreated" - ], + "required": ["admittedSeq", "id", "sessionID", "prompt", "delivery", "timeCreated"], "additionalProperties": false }, "ConflictError": { @@ -10601,9 +10020,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "ConflictError" - ] + "enum": ["ConflictError"] }, "message": { "type": "string" @@ -10619,10 +10036,7 @@ ] } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "CommandNotFoundError": { @@ -10630,9 +10044,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "CommandNotFoundError" - ] + "enum": ["CommandNotFoundError"] }, "command": { "type": "string" @@ -10641,11 +10053,7 @@ "type": "string" } }, - "required": [ - "_tag", - "command", - "message" - ], + "required": ["_tag", "command", "message"], "additionalProperties": false }, "CommandEvaluationError": { @@ -10653,9 +10061,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "CommandEvaluationError" - ] + "enum": ["CommandEvaluationError"] }, "command": { "type": "string" @@ -10664,11 +10070,7 @@ "type": "string" } }, - "required": [ - "_tag", - "command", - "message" - ], + "required": ["_tag", "command", "message"], "additionalProperties": false }, "SkillNotFoundError": { @@ -10676,9 +10078,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "SkillNotFoundError" - ] + "enum": ["SkillNotFoundError"] }, "skill": { "type": "string" @@ -10687,11 +10087,7 @@ "type": "string" } }, - "required": [ - "_tag", - "skill", - "message" - ], + "required": ["_tag", "skill", "message"], "additionalProperties": false }, "SessionBusyError": { @@ -10699,9 +10095,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "SessionBusyError" - ] + "enum": ["SessionBusyError"] }, "sessionID": { "type": "string" @@ -10710,11 +10104,7 @@ "type": "string" } }, - "required": [ - "_tag", - "sessionID", - "message" - ], + "required": ["_tag", "sessionID", "message"], "additionalProperties": false }, "ServiceUnavailableError": { @@ -10722,9 +10112,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "ServiceUnavailableError" - ] + "enum": ["ServiceUnavailableError"] }, "message": { "type": "string" @@ -10740,10 +10128,7 @@ ] } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "UnknownError": { @@ -10751,9 +10136,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "UnknownError" - ] + "enum": ["UnknownError"] }, "message": { "type": "string" @@ -10769,10 +10152,7 @@ ] } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "Session.Message.AgentSwitched": { @@ -10796,27 +10176,18 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "agent-switched" - ] + "enum": ["agent-switched"] }, "agent": { "type": "string" } }, - "required": [ - "id", - "time", - "type", - "agent" - ], + "required": ["id", "time", "type", "agent"], "additionalProperties": false }, "Session.Message.ModelSwitched": { @@ -10840,27 +10211,18 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "model-switched" - ] + "enum": ["model-switched"] }, "model": { "$ref": "#/components/schemas/Model.Ref" } }, - "required": [ - "id", - "time", - "type", - "model" - ], + "required": ["id", "time", "type", "model"], "additionalProperties": false }, "Session.Message.User": { @@ -10884,9 +10246,7 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "text": { @@ -10906,17 +10266,10 @@ }, "type": { "type": "string", - "enum": [ - "user" - ] + "enum": ["user"] } }, - "required": [ - "id", - "time", - "text", - "type" - ], + "required": ["id", "time", "text", "type"], "additionalProperties": false }, "Session.Message.Synthetic": { @@ -10940,9 +10293,7 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "sessionID": { @@ -10961,18 +10312,10 @@ }, "type": { "type": "string", - "enum": [ - "synthetic" - ] + "enum": ["synthetic"] } }, - "required": [ - "id", - "time", - "sessionID", - "text", - "type" - ], + "required": ["id", "time", "sessionID", "text", "type"], "additionalProperties": false }, "Session.Message.System": { @@ -10996,27 +10339,18 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "system" - ] + "enum": ["system"] }, "text": { "type": "string" } }, - "required": [ - "id", - "time", - "type", - "text" - ], + "required": ["id", "time", "type", "text"], "additionalProperties": false }, "Session.Message.Skill": { @@ -11040,16 +10374,12 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "skill" - ] + "enum": ["skill"] }, "name": { "type": "string" @@ -11058,13 +10388,7 @@ "type": "string" } }, - "required": [ - "id", - "time", - "type", - "name", - "text" - ], + "required": ["id", "time", "type", "name", "text"], "additionalProperties": false }, "Session.Message.Shell": { @@ -11091,16 +10415,12 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "shell" - ] + "enum": ["shell"] }, "callID": { "type": "string" @@ -11112,14 +10432,7 @@ "type": "string" } }, - "required": [ - "id", - "time", - "type", - "callID", - "command", - "output" - ], + "required": ["id", "time", "type", "callID", "command", "output"], "additionalProperties": false }, "Session.Message.Assistant.Text": { @@ -11127,9 +10440,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "text" - ] + "enum": ["text"] }, "id": { "type": "string" @@ -11138,11 +10449,7 @@ "type": "string" } }, - "required": [ - "type", - "id", - "text" - ], + "required": ["type", "id", "text"], "additionalProperties": false }, "LLM.ProviderMetadata": { @@ -11156,9 +10463,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "reasoning" - ] + "enum": ["reasoning"] }, "id": { "type": "string" @@ -11179,17 +10484,11 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false } }, - "required": [ - "type", - "id", - "text" - ], + "required": ["type", "id", "text"], "additionalProperties": false }, "Session.Message.ToolState.Pending": { @@ -11197,18 +10496,13 @@ "properties": { "status": { "type": "string", - "enum": [ - "pending" - ] + "enum": ["pending"] }, "input": { "type": "string" } }, - "required": [ - "status", - "input" - ], + "required": ["status", "input"], "additionalProperties": false }, "Tool.TextContent": { @@ -11216,18 +10510,13 @@ "properties": { "type": { "type": "string", - "enum": [ - "text" - ] + "enum": ["text"] }, "text": { "type": "string" } }, - "required": [ - "type", - "text" - ], + "required": ["type", "text"], "additionalProperties": false }, "Tool.FileContent": { @@ -11235,9 +10524,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "file" - ] + "enum": ["file"] }, "uri": { "type": "string" @@ -11249,11 +10536,7 @@ "type": "string" } }, - "required": [ - "type", - "uri", - "mime" - ], + "required": ["type", "uri", "mime"], "additionalProperties": false }, "LLM.ToolContent": { @@ -11271,9 +10554,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "running" - ] + "enum": ["running"] }, "input": { "type": "object" @@ -11288,12 +10569,7 @@ } } }, - "required": [ - "status", - "input", - "structured", - "content" - ], + "required": ["status", "input", "structured", "content"], "additionalProperties": false }, "Session.Message.ToolState.Completed": { @@ -11301,9 +10577,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "completed" - ] + "enum": ["completed"] }, "input": { "type": "object" @@ -11331,12 +10605,7 @@ }, "result": {} }, - "required": [ - "status", - "input", - "content", - "structured" - ], + "required": ["status", "input", "content", "structured"], "additionalProperties": false }, "Session.Error.Unknown": { @@ -11344,18 +10613,13 @@ "properties": { "type": { "type": "string", - "enum": [ - "unknown" - ] + "enum": ["unknown"] }, "message": { "type": "string" } }, - "required": [ - "type", - "message" - ], + "required": ["type", "message"], "additionalProperties": false }, "Session.Message.ToolState.Error": { @@ -11363,9 +10627,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "error" - ] + "enum": ["error"] }, "input": { "type": "object" @@ -11384,13 +10646,7 @@ }, "result": {} }, - "required": [ - "status", - "input", - "content", - "structured", - "error" - ], + "required": ["status", "input", "content", "structured", "error"], "additionalProperties": false }, "Session.Message.Assistant.Tool": { @@ -11398,9 +10654,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "tool" - ] + "enum": ["tool"] }, "id": { "type": "string" @@ -11421,9 +10675,7 @@ "$ref": "#/components/schemas/LLM.ProviderMetadata" } }, - "required": [ - "executed" - ], + "required": ["executed"], "additionalProperties": false }, "state": { @@ -11458,19 +10710,11 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false } }, - "required": [ - "type", - "id", - "name", - "state", - "time" - ], + "required": ["type", "id", "name", "state", "time"], "additionalProperties": false }, "Session.Message.Assistant": { @@ -11497,16 +10741,12 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "type": { "type": "string", - "enum": [ - "assistant" - ] + "enum": ["assistant"] }, "agent": { "type": "string" @@ -11576,33 +10816,18 @@ "type": "number" } }, - "required": [ - "read", - "write" - ], + "required": ["read", "write"], "additionalProperties": false } }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ], + "required": ["input", "output", "reasoning", "cache"], "additionalProperties": false }, "error": { "$ref": "#/components/schemas/Session.Error.Unknown" } }, - "required": [ - "id", - "time", - "type", - "agent", - "model", - "content" - ], + "required": ["id", "time", "type", "agent", "model", "content"], "additionalProperties": false }, "Session.Message.Compaction": { @@ -11610,16 +10835,11 @@ "properties": { "type": { "type": "string", - "enum": [ - "compaction" - ] + "enum": ["compaction"] }, "reason": { "type": "string", - "enum": [ - "auto", - "manual" - ] + "enum": ["auto", "manual"] }, "summary": { "type": "string" @@ -11645,20 +10865,11 @@ "type": "number" } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false } }, - "required": [ - "type", - "reason", - "summary", - "recent", - "id", - "time" - ], + "required": ["type", "reason", "summary", "recent", "id", "time"], "additionalProperties": false }, "Session.Message": { @@ -11709,10 +10920,7 @@ }, "value": {} }, - "required": [ - "key", - "value" - ], + "required": ["key", "value"], "additionalProperties": false }, "session.next.agent.switched": { @@ -11731,9 +10939,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.agent.switched" - ] + "enum": ["session.next.agent.switched"] }, "durable": { "type": "object", @@ -11758,11 +10964,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -11794,20 +10996,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "messageID", - "agent" - ], + "required": ["timestamp", "sessionID", "messageID", "agent"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.model.switched": { @@ -11826,9 +11019,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.model.switched" - ] + "enum": ["session.next.model.switched"] }, "durable": { "type": "object", @@ -11853,11 +11044,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -11889,20 +11076,11 @@ "$ref": "#/components/schemas/Model.Ref" } }, - "required": [ - "timestamp", - "sessionID", - "messageID", - "model" - ], + "required": ["timestamp", "sessionID", "messageID", "model"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.moved": { @@ -11921,9 +11099,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.moved" - ] + "enum": ["session.next.moved"] }, "durable": { "type": "object", @@ -11948,11 +11124,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -11979,19 +11151,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "location" - ], + "required": ["timestamp", "sessionID", "location"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.renamed": { @@ -12010,9 +11174,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.renamed" - ] + "enum": ["session.next.renamed"] }, "durable": { "type": "object", @@ -12037,11 +11199,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12065,19 +11223,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "title" - ], + "required": ["timestamp", "sessionID", "title"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.forked": { @@ -12096,9 +11246,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.forked" - ] + "enum": ["session.next.forked"] }, "durable": { "type": "object", @@ -12123,11 +11271,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12164,19 +11308,11 @@ ] } }, - "required": [ - "timestamp", - "sessionID", - "parentID" - ], + "required": ["timestamp", "sessionID", "parentID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.prompted": { @@ -12195,9 +11331,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.prompted" - ] + "enum": ["session.next.prompted"] }, "durable": { "type": "object", @@ -12222,11 +11356,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12259,27 +11389,14 @@ }, "delivery": { "type": "string", - "enum": [ - "steer", - "queue" - ] + "enum": ["steer", "queue"] } }, - "required": [ - "timestamp", - "sessionID", - "messageID", - "prompt", - "delivery" - ], + "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.prompt.admitted": { @@ -12298,9 +11415,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.prompt.admitted" - ] + "enum": ["session.next.prompt.admitted"] }, "durable": { "type": "object", @@ -12325,11 +11440,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12362,27 +11473,14 @@ }, "delivery": { "type": "string", - "enum": [ - "steer", - "queue" - ] + "enum": ["steer", "queue"] } }, - "required": [ - "timestamp", - "sessionID", - "messageID", - "prompt", - "delivery" - ], + "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.context.updated": { @@ -12401,9 +11499,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.context.updated" - ] + "enum": ["session.next.context.updated"] }, "durable": { "type": "object", @@ -12428,11 +11524,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12464,20 +11556,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "messageID", - "text" - ], + "required": ["timestamp", "sessionID", "messageID", "text"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.synthetic": { @@ -12496,9 +11579,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.synthetic" - ] + "enum": ["session.next.synthetic"] }, "durable": { "type": "object", @@ -12523,11 +11604,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12565,20 +11642,11 @@ "type": "object" } }, - "required": [ - "timestamp", - "sessionID", - "messageID", - "text" - ], + "required": ["timestamp", "sessionID", "messageID", "text"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.skill.activated": { @@ -12597,9 +11665,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.skill.activated" - ] + "enum": ["session.next.skill.activated"] }, "durable": { "type": "object", @@ -12624,11 +11690,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12663,21 +11725,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "messageID", - "name", - "text" - ], + "required": ["timestamp", "sessionID", "messageID", "name", "text"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.shell.started": { @@ -12696,9 +11748,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.shell.started" - ] + "enum": ["session.next.shell.started"] }, "durable": { "type": "object", @@ -12723,11 +11773,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12762,21 +11808,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "messageID", - "callID", - "command" - ], + "required": ["timestamp", "sessionID", "messageID", "callID", "command"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.shell.ended": { @@ -12795,9 +11831,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.shell.ended" - ] + "enum": ["session.next.shell.ended"] }, "durable": { "type": "object", @@ -12822,11 +11856,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12853,20 +11883,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "callID", - "output" - ], + "required": ["timestamp", "sessionID", "callID", "output"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.step.started": { @@ -12885,9 +11906,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.step.started" - ] + "enum": ["session.next.step.started"] }, "durable": { "type": "object", @@ -12912,11 +11931,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -12954,21 +11969,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "agent", - "model" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "agent", "model"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.step.ended": { @@ -12987,9 +11992,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.step.ended" - ] + "enum": ["session.next.step.ended"] }, "durable": { "type": "object", @@ -13014,11 +12017,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13074,19 +12073,11 @@ "type": "number" } }, - "required": [ - "read", - "write" - ], + "required": ["read", "write"], "additionalProperties": false } }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ], + "required": ["input", "output", "reasoning", "cache"], "additionalProperties": false }, "snapshot": { @@ -13099,22 +12090,11 @@ } } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "finish", - "cost", - "tokens" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "finish", "cost", "tokens"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.step.failed": { @@ -13133,9 +12113,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.step.failed" - ] + "enum": ["session.next.step.failed"] }, "durable": { "type": "object", @@ -13160,11 +12138,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13196,20 +12170,11 @@ "$ref": "#/components/schemas/Session.Error.Unknown" } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "error" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "error"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.text.started": { @@ -13228,9 +12193,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.text.started" - ] + "enum": ["session.next.text.started"] }, "durable": { "type": "object", @@ -13255,11 +12218,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13291,20 +12250,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "textID" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "textID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.text.ended": { @@ -13323,9 +12273,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.text.ended" - ] + "enum": ["session.next.text.ended"] }, "durable": { "type": "object", @@ -13350,11 +12298,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13389,21 +12333,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "textID", - "text" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "text"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.tool.input.started": { @@ -13422,9 +12356,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.tool.input.started" - ] + "enum": ["session.next.tool.input.started"] }, "durable": { "type": "object", @@ -13449,11 +12381,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13488,21 +12416,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "callID", - "name" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "name"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.tool.input.ended": { @@ -13521,9 +12439,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.tool.input.ended" - ] + "enum": ["session.next.tool.input.ended"] }, "durable": { "type": "object", @@ -13548,11 +12464,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13587,21 +12499,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "callID", - "text" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "text"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "LLM.ProviderMetadata3": { @@ -13626,9 +12528,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.tool.called" - ] + "enum": ["session.next.tool.called"] }, "durable": { "type": "object", @@ -13653,11 +12553,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13704,29 +12600,15 @@ "$ref": "#/components/schemas/LLM.ProviderMetadata3" } }, - "required": [ - "executed" - ], + "required": ["executed"], "additionalProperties": false } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "callID", - "tool", - "input", - "provider" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "tool", "input", "provider"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.tool.progress": { @@ -13745,9 +12627,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.tool.progress" - ] + "enum": ["session.next.tool.progress"] }, "durable": { "type": "object", @@ -13772,11 +12652,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13817,22 +12693,11 @@ } } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "callID", - "structured", - "content" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "LLM.ProviderMetadata4": { @@ -13857,9 +12722,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.tool.success" - ] + "enum": ["session.next.tool.success"] }, "durable": { "type": "object", @@ -13884,11 +12747,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -13945,29 +12804,15 @@ "$ref": "#/components/schemas/LLM.ProviderMetadata4" } }, - "required": [ - "executed" - ], + "required": ["executed"], "additionalProperties": false } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "callID", - "structured", - "content", - "provider" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "structured", "content", "provider"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "LLM.ProviderMetadata5": { @@ -13992,9 +12837,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.tool.failed" - ] + "enum": ["session.next.tool.failed"] }, "durable": { "type": "object", @@ -14019,11 +12862,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14068,28 +12907,15 @@ "$ref": "#/components/schemas/LLM.ProviderMetadata5" } }, - "required": [ - "executed" - ], + "required": ["executed"], "additionalProperties": false } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "callID", - "error", - "provider" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "error", "provider"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "LLM.ProviderMetadata6": { @@ -14114,9 +12940,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.reasoning.started" - ] + "enum": ["session.next.reasoning.started"] }, "durable": { "type": "object", @@ -14141,11 +12965,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14180,20 +13000,11 @@ "$ref": "#/components/schemas/LLM.ProviderMetadata6" } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "reasoningID" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "LLM.ProviderMetadata7": { @@ -14218,9 +13029,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.reasoning.ended" - ] + "enum": ["session.next.reasoning.ended"] }, "durable": { "type": "object", @@ -14245,11 +13054,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14287,21 +13092,11 @@ "$ref": "#/components/schemas/LLM.ProviderMetadata7" } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "reasoningID", - "text" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "text"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.retry_error": { @@ -14332,10 +13127,7 @@ } } }, - "required": [ - "message", - "isRetryable" - ], + "required": ["message", "isRetryable"], "additionalProperties": false }, "session.next.retried": { @@ -14354,9 +13146,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.retried" - ] + "enum": ["session.next.retried"] }, "durable": { "type": "object", @@ -14381,11 +13171,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14412,20 +13198,11 @@ "$ref": "#/components/schemas/session.next.retry_error" } }, - "required": [ - "timestamp", - "sessionID", - "attempt", - "error" - ], + "required": ["timestamp", "sessionID", "attempt", "error"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.compaction.started": { @@ -14444,9 +13221,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.compaction.started" - ] + "enum": ["session.next.compaction.started"] }, "durable": { "type": "object", @@ -14471,11 +13246,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14505,26 +13276,14 @@ }, "reason": { "type": "string", - "enum": [ - "auto", - "manual" - ] + "enum": ["auto", "manual"] } }, - "required": [ - "timestamp", - "sessionID", - "messageID", - "reason" - ], + "required": ["timestamp", "sessionID", "messageID", "reason"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.compaction.ended": { @@ -14543,9 +13302,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.compaction.ended" - ] + "enum": ["session.next.compaction.ended"] }, "durable": { "type": "object", @@ -14570,11 +13327,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14604,10 +13357,7 @@ }, "reason": { "type": "string", - "enum": [ - "auto", - "manual" - ] + "enum": ["auto", "manual"] }, "text": { "type": "string" @@ -14616,22 +13366,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "messageID", - "reason", - "text", - "recent" - ], + "required": ["timestamp", "sessionID", "messageID", "reason", "text", "recent"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.revert.staged": { @@ -14650,9 +13389,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.revert.staged" - ] + "enum": ["session.next.revert.staged"] }, "durable": { "type": "object", @@ -14677,11 +13414,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14705,19 +13438,11 @@ "$ref": "#/components/schemas/Revert.State" } }, - "required": [ - "timestamp", - "sessionID", - "revert" - ], + "required": ["timestamp", "sessionID", "revert"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.revert.cleared": { @@ -14736,9 +13461,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.revert.cleared" - ] + "enum": ["session.next.revert.cleared"] }, "durable": { "type": "object", @@ -14763,11 +13486,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14788,18 +13507,11 @@ ] } }, - "required": [ - "timestamp", - "sessionID" - ], + "required": ["timestamp", "sessionID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.revert.committed": { @@ -14818,9 +13530,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.revert.committed" - ] + "enum": ["session.next.revert.committed"] }, "durable": { "type": "object", @@ -14845,11 +13555,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -14878,19 +13584,11 @@ ] } }, - "required": [ - "timestamp", - "sessionID", - "messageID" - ], + "required": ["timestamp", "sessionID", "messageID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "SessionDurableEvent": { @@ -14995,9 +13693,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "log.synced" - ] + "enum": ["log.synced"] }, "aggregateID": { "type": "string" @@ -15011,10 +13707,7 @@ ] } }, - "required": [ - "type", - "aggregateID" - ], + "required": ["type", "aggregateID"], "additionalProperties": false, "description": "Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq." }, @@ -15079,10 +13772,7 @@ "additionalProperties": false } }, - "required": [ - "data", - "cursor" - ], + "required": ["data", "cursor"], "additionalProperties": false }, "Model.Api": { @@ -15095,9 +13785,7 @@ }, "type": { "type": "string", - "enum": [ - "aisdk" - ] + "enum": ["aisdk"] }, "package": { "type": "string" @@ -15109,11 +13797,7 @@ "type": "object" } }, - "required": [ - "id", - "type", - "package" - ], + "required": ["id", "type", "package"], "additionalProperties": false }, { @@ -15124,9 +13808,7 @@ }, "type": { "type": "string", - "enum": [ - "native" - ] + "enum": ["native"] }, "url": { "type": "string" @@ -15135,11 +13817,7 @@ "type": "object" } }, - "required": [ - "id", - "type", - "settings" - ], + "required": ["id", "type", "settings"], "additionalProperties": false } ] @@ -15163,11 +13841,7 @@ } } }, - "required": [ - "tools", - "input", - "output" - ], + "required": ["tools", "input", "output"], "additionalProperties": false }, "Model.Cost": { @@ -15178,18 +13852,13 @@ "properties": { "type": { "type": "string", - "enum": [ - "context" - ] + "enum": ["context"] }, "size": { "type": "integer" } }, - "required": [ - "type", - "size" - ], + "required": ["type", "size"], "additionalProperties": false }, "input": { @@ -15208,18 +13877,11 @@ "type": "number" } }, - "required": [ - "read", - "write" - ], + "required": ["read", "write"], "additionalProperties": false } }, - "required": [ - "input", - "output", - "cache" - ], + "required": ["input", "output", "cache"], "additionalProperties": false }, "ModelV2.Info": { @@ -15262,11 +13924,7 @@ "type": "string" } }, - "required": [ - "settings", - "headers", - "body" - ], + "required": ["settings", "headers", "body"], "additionalProperties": false }, "variants": { @@ -15290,12 +13948,7 @@ "type": "object" } }, - "required": [ - "id", - "settings", - "headers", - "body" - ], + "required": ["id", "settings", "headers", "body"], "additionalProperties": false } }, @@ -15306,9 +13959,7 @@ "type": "number" } }, - "required": [ - "released" - ], + "required": ["released"], "additionalProperties": false }, "cost": { @@ -15319,12 +13970,7 @@ }, "status": { "type": "string", - "enum": [ - "alpha", - "beta", - "deprecated", - "active" - ] + "enum": ["alpha", "beta", "deprecated", "active"] }, "enabled": { "type": "boolean" @@ -15342,10 +13988,7 @@ "type": "integer" } }, - "required": [ - "context", - "output" - ], + "required": ["context", "output"], "additionalProperties": false } }, @@ -15375,15 +14018,11 @@ "type": "string" } }, - "required": [ - "text" - ], + "required": ["text"], "additionalProperties": false } }, - "required": [ - "data" - ], + "required": ["data"], "additionalProperties": false }, "Provider.AISDK": { @@ -15391,9 +14030,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "aisdk" - ] + "enum": ["aisdk"] }, "package": { "type": "string" @@ -15405,10 +14042,7 @@ "type": "object" } }, - "required": [ - "type", - "package" - ], + "required": ["type", "package"], "additionalProperties": false }, "Provider.Native": { @@ -15416,9 +14050,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "native" - ] + "enum": ["native"] }, "url": { "type": "string" @@ -15427,10 +14059,7 @@ "type": "object" } }, - "required": [ - "type", - "settings" - ], + "required": ["type", "settings"], "additionalProperties": false }, "Provider.Api": { @@ -15465,12 +14094,7 @@ "$ref": "#/components/schemas/Provider.Request" } }, - "required": [ - "id", - "name", - "api", - "request" - ], + "required": ["id", "name", "api", "request"], "additionalProperties": false }, "ProviderNotFoundError": { @@ -15478,9 +14102,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "ProviderNotFoundError" - ] + "enum": ["ProviderNotFoundError"] }, "providerID": { "type": "string" @@ -15489,11 +14111,7 @@ "type": "string" } }, - "required": [ - "_tag", - "providerID", - "message" - ], + "required": ["_tag", "providerID", "message"], "additionalProperties": false }, "Integration.When": { @@ -15504,20 +14122,13 @@ }, "op": { "type": "string", - "enum": [ - "eq", - "neq" - ] + "enum": ["eq", "neq"] }, "value": { "type": "string" } }, - "required": [ - "key", - "op", - "value" - ], + "required": ["key", "op", "value"], "additionalProperties": false }, "Integration.TextPrompt": { @@ -15525,9 +14136,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "text" - ] + "enum": ["text"] }, "key": { "type": "string" @@ -15542,11 +14151,7 @@ "$ref": "#/components/schemas/Integration.When" } }, - "required": [ - "type", - "key", - "message" - ], + "required": ["type", "key", "message"], "additionalProperties": false }, "Integration.SelectPrompt": { @@ -15554,9 +14159,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "select" - ] + "enum": ["select"] }, "key": { "type": "string" @@ -15579,10 +14182,7 @@ "type": "string" } }, - "required": [ - "label", - "value" - ], + "required": ["label", "value"], "additionalProperties": false } }, @@ -15590,12 +14190,7 @@ "$ref": "#/components/schemas/Integration.When" } }, - "required": [ - "type", - "key", - "message", - "options" - ], + "required": ["type", "key", "message", "options"], "additionalProperties": false }, "Integration.OAuthMethod": { @@ -15606,9 +14201,7 @@ }, "type": { "type": "string", - "enum": [ - "oauth" - ] + "enum": ["oauth"] }, "label": { "type": "string" @@ -15627,11 +14220,7 @@ } } }, - "required": [ - "id", - "type", - "label" - ], + "required": ["id", "type", "label"], "additionalProperties": false }, "Integration.KeyMethod": { @@ -15639,17 +14228,13 @@ "properties": { "type": { "type": "string", - "enum": [ - "key" - ] + "enum": ["key"] }, "label": { "type": "string" } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false }, "Integration.EnvMethod": { @@ -15657,9 +14242,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "env" - ] + "enum": ["env"] }, "names": { "type": "array", @@ -15668,10 +14251,7 @@ } } }, - "required": [ - "type", - "names" - ], + "required": ["type", "names"], "additionalProperties": false }, "Integration.Method": { @@ -15692,9 +14272,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "credential" - ] + "enum": ["credential"] }, "id": { "type": "string" @@ -15703,11 +14281,7 @@ "type": "string" } }, - "required": [ - "type", - "id", - "label" - ], + "required": ["type", "id", "label"], "additionalProperties": false }, "Connection.EnvInfo": { @@ -15715,18 +14289,13 @@ "properties": { "type": { "type": "string", - "enum": [ - "env" - ] + "enum": ["env"] }, "name": { "type": "string" } }, - "required": [ - "type", - "name" - ], + "required": ["type", "name"], "additionalProperties": false }, "Connection.Info": { @@ -15761,12 +14330,7 @@ } } }, - "required": [ - "id", - "name", - "methods", - "connections" - ], + "required": ["id", "name", "methods", "connections"], "additionalProperties": false }, "Integration.Attempt": { @@ -15783,10 +14347,7 @@ }, "mode": { "type": "string", - "enum": [ - "auto", - "code" - ] + "enum": ["auto", "code"] }, "time": { "type": "object", @@ -15800,31 +14361,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -15837,49 +14388,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "created", - "expires" - ], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": [ - "attemptID", - "url", - "instructions", - "mode", - "time" - ], + "required": ["attemptID", "url", "instructions", "mode", "time"], "additionalProperties": false }, "Integration.AttemptStatus": { @@ -15889,9 +14421,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "pending" - ] + "enum": ["pending"] }, "time": { "type": "object", @@ -15905,31 +14435,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -15942,46 +14462,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "created", - "expires" - ], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": [ - "status", - "time" - ], + "required": ["status", "time"], "additionalProperties": false }, { @@ -15989,9 +14493,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "complete" - ] + "enum": ["complete"] }, "time": { "type": "object", @@ -16005,31 +14507,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -16042,46 +14534,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "created", - "expires" - ], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": [ - "status", - "time" - ], + "required": ["status", "time"], "additionalProperties": false }, { @@ -16089,9 +14565,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "failed" - ] + "enum": ["failed"] }, "message": { "type": "string" @@ -16108,31 +14582,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -16145,47 +14609,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "created", - "expires" - ], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": [ - "status", - "message", - "time" - ], + "required": ["status", "message", "time"], "additionalProperties": false }, { @@ -16193,9 +14640,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "expired" - ] + "enum": ["expired"] }, "time": { "type": "object", @@ -16209,31 +14654,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -16246,46 +14681,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "created", - "expires" - ], + "required": ["created", "expires"], "additionalProperties": false } }, - "required": [ - "status", - "time" - ], + "required": ["status", "time"], "additionalProperties": false } ] @@ -16295,14 +14714,10 @@ "properties": { "status": { "type": "string", - "enum": [ - "connected" - ] + "enum": ["connected"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false }, "Mcp.Status.Disconnected": { @@ -16310,14 +14725,10 @@ "properties": { "status": { "type": "string", - "enum": [ - "disconnected" - ] + "enum": ["disconnected"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false }, "Mcp.Status.Disabled": { @@ -16325,14 +14736,10 @@ "properties": { "status": { "type": "string", - "enum": [ - "disabled" - ] + "enum": ["disabled"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false }, "Mcp.Status.Failed": { @@ -16340,18 +14747,13 @@ "properties": { "status": { "type": "string", - "enum": [ - "failed" - ] + "enum": ["failed"] }, "error": { "type": "string" } }, - "required": [ - "status", - "error" - ], + "required": ["status", "error"], "additionalProperties": false }, "Mcp.Status.NeedsAuth": { @@ -16359,14 +14761,10 @@ "properties": { "status": { "type": "string", - "enum": [ - "needs_auth" - ] + "enum": ["needs_auth"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false }, "Mcp.Status.NeedsClientRegistration": { @@ -16374,18 +14772,13 @@ "properties": { "status": { "type": "string", - "enum": [ - "needs_client_registration" - ] + "enum": ["needs_client_registration"] }, "error": { "type": "string" } }, - "required": [ - "status", - "error" - ], + "required": ["status", "error"], "additionalProperties": false }, "Mcp.Server": { @@ -16420,10 +14813,7 @@ "type": "string" } }, - "required": [ - "name", - "status" - ], + "required": ["name", "status"], "additionalProperties": false }, "Project.Current": { @@ -16436,10 +14826,7 @@ "type": "string" } }, - "required": [ - "id", - "directory" - ], + "required": ["id", "directory"], "additionalProperties": false }, "Project.Directory": { @@ -16452,9 +14839,7 @@ "type": "string" } }, - "required": [ - "directory" - ], + "required": ["directory"], "additionalProperties": false }, "Project.Directories": { @@ -16474,10 +14859,7 @@ }, "op": { "type": "string", - "enum": [ - "eq", - "neq" - ] + "enum": ["eq", "neq"] }, "value": { "anyOf": [ @@ -16493,31 +14875,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -16527,11 +14899,7 @@ ] } }, - "required": [ - "key", - "op", - "value" - ], + "required": ["key", "op", "value"], "additionalProperties": false }, "Form.Option": { @@ -16547,10 +14915,7 @@ "type": "string" } }, - "required": [ - "value", - "label" - ], + "required": ["value", "label"], "additionalProperties": false }, "Form.StringField": { @@ -16576,18 +14941,11 @@ }, "type": { "type": "string", - "enum": [ - "string" - ] + "enum": ["string"] }, "format": { "type": "string", - "enum": [ - "email", - "uri", - "date", - "date-time" - ] + "enum": ["email", "uri", "date", "date-time"] }, "minLength": { "type": "integer", @@ -16624,10 +14982,7 @@ "type": "boolean" } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.NumberField": { @@ -16653,9 +15008,7 @@ }, "type": { "type": "string", - "enum": [ - "number" - ] + "enum": ["number"] }, "minimum": { "anyOf": [ @@ -16666,31 +15019,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -16703,31 +15046,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -16740,39 +15073,26 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.IntegerField": { @@ -16798,9 +15118,7 @@ }, "type": { "type": "string", - "enum": [ - "integer" - ] + "enum": ["integer"] }, "minimum": { "anyOf": [ @@ -16811,31 +15129,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -16848,31 +15156,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -16885,39 +15183,26 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.BooleanField": { @@ -16943,18 +15228,13 @@ }, "type": { "type": "string", - "enum": [ - "boolean" - ] + "enum": ["boolean"] }, "default": { "type": "boolean" } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.MultiselectField": { @@ -16980,9 +15260,7 @@ }, "type": { "type": "string", - "enum": [ - "multiselect" - ] + "enum": ["multiselect"] }, "options": { "type": "array", @@ -17016,11 +15294,7 @@ } } }, - "required": [ - "key", - "type", - "options" - ], + "required": ["key", "type", "options"], "additionalProperties": false }, "Form.FormInfo": { @@ -17045,9 +15319,7 @@ }, "mode": { "type": "string", - "enum": [ - "form" - ] + "enum": ["form"] }, "fields": { "type": "array", @@ -17072,12 +15344,7 @@ } } }, - "required": [ - "id", - "sessionID", - "mode", - "fields" - ], + "required": ["id", "sessionID", "mode", "fields"], "additionalProperties": false }, "Form.UrlInfo": { @@ -17102,20 +15369,13 @@ }, "mode": { "type": "string", - "enum": [ - "url" - ] + "enum": ["url"] }, "url": { "type": "string" } }, - "required": [ - "id", - "sessionID", - "mode", - "url" - ], + "required": ["id", "sessionID", "mode", "url"], "additionalProperties": false }, "Form.CreatePayload": { @@ -17144,10 +15404,7 @@ }, "mode": { "type": "string", - "enum": [ - "form", - "url" - ] + "enum": ["form", "url"] }, "fields": { "anyOf": [ @@ -17189,9 +15446,7 @@ ] } }, - "required": [ - "mode" - ], + "required": ["mode"], "additionalProperties": false }, "FormNotFoundError": { @@ -17199,9 +15454,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "FormNotFoundError" - ] + "enum": ["FormNotFoundError"] }, "id": { "type": "string" @@ -17210,11 +15463,7 @@ "type": "string" } }, - "required": [ - "_tag", - "id", - "message" - ], + "required": ["_tag", "id", "message"], "additionalProperties": false }, "Form.Value": { @@ -17231,31 +15480,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -17283,14 +15522,10 @@ "properties": { "status": { "type": "string", - "enum": [ - "pending" - ] + "enum": ["pending"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false }, { @@ -17298,18 +15533,13 @@ "properties": { "status": { "type": "string", - "enum": [ - "answered" - ] + "enum": ["answered"] }, "answer": { "$ref": "#/components/schemas/Form.Answer" } }, - "required": [ - "status", - "answer" - ], + "required": ["status", "answer"], "additionalProperties": false }, { @@ -17317,14 +15547,10 @@ "properties": { "status": { "type": "string", - "enum": [ - "cancelled" - ] + "enum": ["cancelled"] } }, - "required": [ - "status" - ], + "required": ["status"], "additionalProperties": false } ] @@ -17336,9 +15562,7 @@ "$ref": "#/components/schemas/Form.Answer" } }, - "required": [ - "answer" - ], + "required": ["answer"], "additionalProperties": false }, "FormAlreadySettledError": { @@ -17346,9 +15570,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "FormAlreadySettledError" - ] + "enum": ["FormAlreadySettledError"] }, "id": { "type": "string" @@ -17357,11 +15579,7 @@ "type": "string" } }, - "required": [ - "_tag", - "id", - "message" - ], + "required": ["_tag", "id", "message"], "additionalProperties": false }, "FormInvalidAnswerError": { @@ -17369,9 +15587,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "FormInvalidAnswerError" - ] + "enum": ["FormInvalidAnswerError"] }, "id": { "type": "string" @@ -17380,11 +15596,7 @@ "type": "string" } }, - "required": [ - "_tag", - "id", - "message" - ], + "required": ["_tag", "id", "message"], "additionalProperties": false }, "PermissionV2.Source": { @@ -17394,9 +15606,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "tool" - ] + "enum": ["tool"] }, "messageID": { "type": "string" @@ -17405,11 +15615,7 @@ "type": "string" } }, - "required": [ - "type", - "messageID", - "callID" - ], + "required": ["type", "messageID", "callID"], "additionalProperties": false } ] @@ -17455,12 +15661,7 @@ "$ref": "#/components/schemas/PermissionV2.Source" } }, - "required": [ - "id", - "sessionID", - "action", - "resources" - ], + "required": ["id", "sessionID", "action", "resources"], "additionalProperties": false }, "PermissionSaved.Info": { @@ -17479,12 +15680,7 @@ "type": "string" } }, - "required": [ - "id", - "projectID", - "action", - "resource" - ], + "required": ["id", "projectID", "action", "resource"], "additionalProperties": false }, "PermissionNotFoundError": { @@ -17492,9 +15688,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "PermissionNotFoundError" - ] + "enum": ["PermissionNotFoundError"] }, "requestID": { "type": "string" @@ -17503,20 +15697,12 @@ "type": "string" } }, - "required": [ - "_tag", - "requestID", - "message" - ], + "required": ["_tag", "requestID", "message"], "additionalProperties": false }, "PermissionV2.Reply": { "type": "string", - "enum": [ - "once", - "always", - "reject" - ] + "enum": ["once", "always", "reject"] }, "FileSystem.Entry": { "type": "object", @@ -17526,16 +15712,10 @@ }, "type": { "type": "string", - "enum": [ - "file", - "directory" - ] + "enum": ["file", "directory"] } }, - "required": [ - "path", - "type" - ], + "required": ["path", "type"], "additionalProperties": false }, "CommandV2.Info": { @@ -17560,10 +15740,7 @@ "type": "boolean" } }, - "required": [ - "name", - "template" - ], + "required": ["name", "template"], "additionalProperties": false }, "SkillV2.Info": { @@ -17588,11 +15765,7 @@ "type": "string" } }, - "required": [ - "name", - "location", - "content" - ], + "required": ["name", "location", "content"], "additionalProperties": false }, "models-dev.refreshed": { @@ -17611,9 +15784,7 @@ }, "type": { "type": "string", - "enum": [ - "models-dev.refreshed" - ] + "enum": ["models-dev.refreshed"] }, "durable": { "type": "object", @@ -17638,11 +15809,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -17659,11 +15826,7 @@ ] } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "integration.updated": { @@ -17682,9 +15845,7 @@ }, "type": { "type": "string", - "enum": [ - "integration.updated" - ] + "enum": ["integration.updated"] }, "durable": { "type": "object", @@ -17709,11 +15870,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -17730,11 +15887,7 @@ ] } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "integration.connection.updated": { @@ -17753,9 +15906,7 @@ }, "type": { "type": "string", - "enum": [ - "integration.connection.updated" - ] + "enum": ["integration.connection.updated"] }, "durable": { "type": "object", @@ -17780,11 +15931,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -17797,17 +15944,11 @@ "type": "string" } }, - "required": [ - "integrationID" - ], + "required": ["integrationID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "catalog.updated": { @@ -17826,9 +15967,7 @@ }, "type": { "type": "string", - "enum": [ - "catalog.updated" - ] + "enum": ["catalog.updated"] }, "durable": { "type": "object", @@ -17853,11 +15992,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -17874,11 +16009,7 @@ ] } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "agent.updated": { @@ -17897,9 +16028,7 @@ }, "type": { "type": "string", - "enum": [ - "agent.updated" - ] + "enum": ["agent.updated"] }, "durable": { "type": "object", @@ -17924,11 +16053,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -17945,11 +16070,7 @@ ] } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "SnapshotFileDiff": { @@ -17969,26 +16090,15 @@ }, "status": { "type": "string", - "enum": [ - "added", - "deleted", - "modified" - ] + "enum": ["added", "deleted", "modified"] } }, - "required": [ - "additions", - "deletions" - ], + "required": ["additions", "deletions"], "additionalProperties": false }, "PermissionAction": { "type": "string", - "enum": [ - "allow", - "deny", - "ask" - ] + "enum": ["allow", "deny", "ask"] }, "PermissionRule": { "type": "object", @@ -18003,11 +16113,7 @@ "$ref": "#/components/schemas/PermissionAction" } }, - "required": [ - "permission", - "pattern", - "action" - ], + "required": ["permission", "pattern", "action"], "additionalProperties": false }, "PermissionRuleset": { @@ -18074,11 +16180,7 @@ } } }, - "required": [ - "additions", - "deletions", - "files" - ], + "required": ["additions", "deletions", "files"], "additionalProperties": false }, "cost": { @@ -18106,19 +16208,11 @@ "type": "number" } }, - "required": [ - "read", - "write" - ], + "required": ["read", "write"], "additionalProperties": false } }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ], + "required": ["input", "output", "reasoning", "cache"], "additionalProperties": false }, "share": { @@ -18128,9 +16222,7 @@ "type": "string" } }, - "required": [ - "url" - ], + "required": ["url"], "additionalProperties": false }, "title": { @@ -18152,10 +16244,7 @@ "type": "string" } }, - "required": [ - "id", - "providerID" - ], + "required": ["id", "providerID"], "additionalProperties": false }, "version": { @@ -18195,10 +16284,7 @@ "type": "number" } }, - "required": [ - "created", - "updated" - ], + "required": ["created", "updated"], "additionalProperties": false }, "permission": { @@ -18230,21 +16316,11 @@ "type": "string" } }, - "required": [ - "messageID" - ], + "required": ["messageID"], "additionalProperties": false } }, - "required": [ - "id", - "slug", - "projectID", - "directory", - "title", - "version", - "time" - ], + "required": ["id", "slug", "projectID", "directory", "title", "version", "time"], "additionalProperties": false }, "session.created": { @@ -18263,9 +16339,7 @@ }, "type": { "type": "string", - "enum": [ - "session.created" - ] + "enum": ["session.created"] }, "durable": { "type": "object", @@ -18290,11 +16364,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -18315,18 +16385,11 @@ "$ref": "#/components/schemas/Session" } }, - "required": [ - "sessionID", - "info" - ], + "required": ["sessionID", "info"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.updated": { @@ -18345,9 +16408,7 @@ }, "type": { "type": "string", - "enum": [ - "session.updated" - ] + "enum": ["session.updated"] }, "durable": { "type": "object", @@ -18372,11 +16433,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -18397,18 +16454,11 @@ "$ref": "#/components/schemas/Session" } }, - "required": [ - "sessionID", - "info" - ], + "required": ["sessionID", "info"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.deleted": { @@ -18427,9 +16477,7 @@ }, "type": { "type": "string", - "enum": [ - "session.deleted" - ] + "enum": ["session.deleted"] }, "durable": { "type": "object", @@ -18454,11 +16502,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -18479,18 +16523,11 @@ "$ref": "#/components/schemas/Session" } }, - "required": [ - "sessionID", - "info" - ], + "required": ["sessionID", "info"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "JSONSchema": { @@ -18503,14 +16540,10 @@ "properties": { "type": { "type": "string", - "enum": [ - "text" - ] + "enum": ["text"] } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false }, { @@ -18518,9 +16551,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "json_schema" - ] + "enum": ["json_schema"] }, "schema": { "$ref": "#/components/schemas/JSONSchema" @@ -18548,10 +16579,7 @@ ] } }, - "required": [ - "type", - "schema" - ], + "required": ["type", "schema"], "additionalProperties": false } ] @@ -18577,9 +16605,7 @@ }, "role": { "type": "string", - "enum": [ - "user" - ] + "enum": ["user"] }, "time": { "type": "object", @@ -18593,9 +16619,7 @@ ] } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "format": { @@ -18640,9 +16664,7 @@ } } }, - "required": [ - "diffs" - ], + "required": ["diffs"], "additionalProperties": false }, { @@ -18673,10 +16695,7 @@ ] } }, - "required": [ - "providerID", - "modelID" - ], + "required": ["providerID", "modelID"], "additionalProperties": false }, "system": { @@ -18703,14 +16722,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "role", - "time", - "agent", - "model" - ], + "required": ["id", "sessionID", "role", "time", "agent", "model"], "additionalProperties": false }, "ProviderAuthError": { @@ -18718,9 +16730,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "ProviderAuthError" - ] + "enum": ["ProviderAuthError"] }, "data": { "type": "object", @@ -18732,17 +16742,11 @@ "type": "string" } }, - "required": [ - "providerID", - "message" - ], + "required": ["providerID", "message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "UnknownError1": { @@ -18750,9 +16754,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "UnknownError" - ] + "enum": ["UnknownError"] }, "data": { "type": "object", @@ -18771,16 +16773,11 @@ ] } }, - "required": [ - "message" - ], + "required": ["message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "MessageOutputLengthError": { @@ -18788,9 +16785,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "MessageOutputLengthError" - ] + "enum": ["MessageOutputLengthError"] }, "data": { "anyOf": [ @@ -18803,10 +16798,7 @@ ] } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "MessageAbortedError": { @@ -18814,9 +16806,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "MessageAbortedError" - ] + "enum": ["MessageAbortedError"] }, "data": { "type": "object", @@ -18825,16 +16815,11 @@ "type": "string" } }, - "required": [ - "message" - ], + "required": ["message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "StructuredOutputError": { @@ -18842,9 +16827,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "StructuredOutputError" - ] + "enum": ["StructuredOutputError"] }, "data": { "type": "object", @@ -18861,17 +16844,11 @@ ] } }, - "required": [ - "message", - "retries" - ], + "required": ["message", "retries"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "ContextOverflowError": { @@ -18879,9 +16856,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "ContextOverflowError" - ] + "enum": ["ContextOverflowError"] }, "data": { "type": "object", @@ -18900,16 +16875,11 @@ ] } }, - "required": [ - "message" - ], + "required": ["message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "ContentFilterError": { @@ -18917,9 +16887,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "ContentFilterError" - ] + "enum": ["ContentFilterError"] }, "data": { "type": "object", @@ -18928,16 +16896,11 @@ "type": "string" } }, - "required": [ - "message" - ], + "required": ["message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "APIError": { @@ -18945,9 +16908,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "APIError" - ] + "enum": ["APIError"] }, "data": { "type": "object", @@ -19010,17 +16971,11 @@ ] } }, - "required": [ - "message", - "isRetryable" - ], + "required": ["message", "isRetryable"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "AssistantMessage": { @@ -19044,9 +16999,7 @@ }, "role": { "type": "string", - "enum": [ - "assistant" - ] + "enum": ["assistant"] }, "time": { "type": "object", @@ -19075,9 +17028,7 @@ ] } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false }, "error": { @@ -19145,10 +17096,7 @@ "type": "string" } }, - "required": [ - "cwd", - "root" - ], + "required": ["cwd", "root"], "additionalProperties": false }, "summary": { @@ -19196,19 +17144,11 @@ "type": "number" } }, - "required": [ - "read", - "write" - ], + "required": ["read", "write"], "additionalProperties": false } }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ], + "required": ["input", "output", "reasoning", "cache"], "additionalProperties": false }, "structured": { @@ -19282,9 +17222,7 @@ }, "type": { "type": "string", - "enum": [ - "message.updated" - ] + "enum": ["message.updated"] }, "durable": { "type": "object", @@ -19309,11 +17247,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -19334,18 +17268,11 @@ "$ref": "#/components/schemas/Message" } }, - "required": [ - "sessionID", - "info" - ], + "required": ["sessionID", "info"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "message.removed": { @@ -19364,9 +17291,7 @@ }, "type": { "type": "string", - "enum": [ - "message.removed" - ] + "enum": ["message.removed"] }, "durable": { "type": "object", @@ -19391,11 +17316,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -19421,18 +17342,11 @@ ] } }, - "required": [ - "sessionID", - "messageID" - ], + "required": ["sessionID", "messageID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "TextPart": { @@ -19464,9 +17378,7 @@ }, "type": { "type": "string", - "enum": [ - "text" - ] + "enum": ["text"] }, "text": { "type": "string" @@ -19520,9 +17432,7 @@ ] } }, - "required": [ - "start" - ], + "required": ["start"], "additionalProperties": false }, { @@ -19541,13 +17451,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "text" - ], + "required": ["id", "sessionID", "messageID", "type", "text"], "additionalProperties": false }, "SubtaskPart": { @@ -19579,9 +17483,7 @@ }, "type": { "type": "string", - "enum": [ - "subtask" - ] + "enum": ["subtask"] }, "prompt": { "type": "string" @@ -19604,10 +17506,7 @@ "type": "string" } }, - "required": [ - "providerID", - "modelID" - ], + "required": ["providerID", "modelID"], "additionalProperties": false }, { @@ -19626,15 +17525,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "prompt", - "description", - "agent" - ], + "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"], "additionalProperties": false }, "ReasoningPart": { @@ -19666,9 +17557,7 @@ }, "type": { "type": "string", - "enum": [ - "reasoning" - ] + "enum": ["reasoning"] }, "text": { "type": "string" @@ -19710,20 +17599,11 @@ ] } }, - "required": [ - "start" - ], + "required": ["start"], "additionalProperties": false } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "text", - "time" - ], + "required": ["id", "sessionID", "messageID", "type", "text", "time"], "additionalProperties": false }, "FilePartSourceText": { @@ -19739,11 +17619,7 @@ "type": "number" } }, - "required": [ - "value", - "start", - "end" - ], + "required": ["value", "start", "end"], "additionalProperties": false }, "FileSource": { @@ -19754,19 +17630,13 @@ }, "type": { "type": "string", - "enum": [ - "file" - ] + "enum": ["file"] }, "path": { "type": "string" } }, - "required": [ - "text", - "type", - "path" - ], + "required": ["text", "type", "path"], "additionalProperties": false }, "Range": { @@ -19792,10 +17662,7 @@ ] } }, - "required": [ - "line", - "character" - ], + "required": ["line", "character"], "additionalProperties": false }, "end": { @@ -19818,17 +17685,11 @@ ] } }, - "required": [ - "line", - "character" - ], + "required": ["line", "character"], "additionalProperties": false } }, - "required": [ - "start", - "end" - ], + "required": ["start", "end"], "additionalProperties": false }, "SymbolSource": { @@ -19839,9 +17700,7 @@ }, "type": { "type": "string", - "enum": [ - "symbol" - ] + "enum": ["symbol"] }, "path": { "type": "string" @@ -19861,14 +17720,7 @@ ] } }, - "required": [ - "text", - "type", - "path", - "range", - "name", - "kind" - ], + "required": ["text", "type", "path", "range", "name", "kind"], "additionalProperties": false }, "ResourceSource": { @@ -19879,9 +17731,7 @@ }, "type": { "type": "string", - "enum": [ - "resource" - ] + "enum": ["resource"] }, "clientName": { "type": "string" @@ -19890,12 +17740,7 @@ "type": "string" } }, - "required": [ - "text", - "type", - "clientName", - "uri" - ], + "required": ["text", "type", "clientName", "uri"], "additionalProperties": false }, "FilePartSource": { @@ -19940,9 +17785,7 @@ }, "type": { "type": "string", - "enum": [ - "file" - ] + "enum": ["file"] }, "mime": { "type": "string" @@ -19971,14 +17814,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "mime", - "url" - ], + "required": ["id", "sessionID", "messageID", "type", "mime", "url"], "additionalProperties": false }, "ToolStatePending": { @@ -19986,9 +17822,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "pending" - ] + "enum": ["pending"] }, "input": { "type": "object" @@ -19997,11 +17831,7 @@ "type": "string" } }, - "required": [ - "status", - "input", - "raw" - ], + "required": ["status", "input", "raw"], "additionalProperties": false }, "ToolStateRunning": { @@ -20009,9 +17839,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "running" - ] + "enum": ["running"] }, "input": { "type": "object" @@ -20048,17 +17876,11 @@ ] } }, - "required": [ - "start" - ], + "required": ["start"], "additionalProperties": false } }, - "required": [ - "status", - "input", - "time" - ], + "required": ["status", "input", "time"], "additionalProperties": false }, "ToolStateCompleted": { @@ -20066,9 +17888,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "completed" - ] + "enum": ["completed"] }, "input": { "type": "object" @@ -20117,10 +17937,7 @@ ] } }, - "required": [ - "start", - "end" - ], + "required": ["start", "end"], "additionalProperties": false }, "attachments": { @@ -20137,14 +17954,7 @@ ] } }, - "required": [ - "status", - "input", - "output", - "title", - "metadata", - "time" - ], + "required": ["status", "input", "output", "title", "metadata", "time"], "additionalProperties": false }, "ToolStateError": { @@ -20152,9 +17962,7 @@ "properties": { "status": { "type": "string", - "enum": [ - "error" - ] + "enum": ["error"] }, "input": { "type": "object" @@ -20192,19 +18000,11 @@ ] } }, - "required": [ - "start", - "end" - ], + "required": ["start", "end"], "additionalProperties": false } }, - "required": [ - "status", - "input", - "error", - "time" - ], + "required": ["status", "input", "error", "time"], "additionalProperties": false }, "ToolState": { @@ -20252,9 +18052,7 @@ }, "type": { "type": "string", - "enum": [ - "tool" - ] + "enum": ["tool"] }, "callID": { "type": "string" @@ -20276,15 +18074,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "callID", - "tool", - "state" - ], + "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"], "additionalProperties": false }, "StepStartPart": { @@ -20316,9 +18106,7 @@ }, "type": { "type": "string", - "enum": [ - "step-start" - ] + "enum": ["step-start"] }, "snapshot": { "anyOf": [ @@ -20331,12 +18119,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type" - ], + "required": ["id", "sessionID", "messageID", "type"], "additionalProperties": false }, "StepFinishPart": { @@ -20368,9 +18151,7 @@ }, "type": { "type": "string", - "enum": [ - "step-finish" - ] + "enum": ["step-finish"] }, "reason": { "type": "string" @@ -20420,31 +18201,15 @@ "type": "number" } }, - "required": [ - "read", - "write" - ], + "required": ["read", "write"], "additionalProperties": false } }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ], + "required": ["input", "output", "reasoning", "cache"], "additionalProperties": false } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "reason", - "cost", - "tokens" - ], + "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"], "additionalProperties": false }, "SnapshotPart": { @@ -20476,21 +18241,13 @@ }, "type": { "type": "string", - "enum": [ - "snapshot" - ] + "enum": ["snapshot"] }, "snapshot": { "type": "string" } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "snapshot" - ], + "required": ["id", "sessionID", "messageID", "type", "snapshot"], "additionalProperties": false }, "PatchPart": { @@ -20522,9 +18279,7 @@ }, "type": { "type": "string", - "enum": [ - "patch" - ] + "enum": ["patch"] }, "hash": { "type": "string" @@ -20536,14 +18291,7 @@ } } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "hash", - "files" - ], + "required": ["id", "sessionID", "messageID", "type", "hash", "files"], "additionalProperties": false }, "AgentPart": { @@ -20575,9 +18323,7 @@ }, "type": { "type": "string", - "enum": [ - "agent" - ] + "enum": ["agent"] }, "name": { "type": "string" @@ -20607,11 +18353,7 @@ ] } }, - "required": [ - "value", - "start", - "end" - ], + "required": ["value", "start", "end"], "additionalProperties": false }, { @@ -20620,13 +18362,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "name" - ], + "required": ["id", "sessionID", "messageID", "type", "name"], "additionalProperties": false }, "RetryPart": { @@ -20658,9 +18394,7 @@ }, "type": { "type": "string", - "enum": [ - "retry" - ] + "enum": ["retry"] }, "attempt": { "type": "integer", @@ -20685,21 +18419,11 @@ ] } }, - "required": [ - "created" - ], + "required": ["created"], "additionalProperties": false } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "attempt", - "error", - "time" - ], + "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"], "additionalProperties": false }, "CompactionPart": { @@ -20731,9 +18455,7 @@ }, "type": { "type": "string", - "enum": [ - "compaction" - ] + "enum": ["compaction"] }, "auto": { "type": "boolean" @@ -20764,13 +18486,7 @@ ] } }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "auto" - ], + "required": ["id", "sessionID", "messageID", "type", "auto"], "additionalProperties": false }, "Part": { @@ -20829,9 +18545,7 @@ }, "type": { "type": "string", - "enum": [ - "message.part.updated" - ] + "enum": ["message.part.updated"] }, "durable": { "type": "object", @@ -20856,11 +18570,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -20884,19 +18594,11 @@ "type": "number" } }, - "required": [ - "sessionID", - "part", - "time" - ], + "required": ["sessionID", "part", "time"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "message.part.removed": { @@ -20915,9 +18617,7 @@ }, "type": { "type": "string", - "enum": [ - "message.part.removed" - ] + "enum": ["message.part.removed"] }, "durable": { "type": "object", @@ -20942,11 +18642,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -20980,19 +18676,11 @@ ] } }, - "required": [ - "sessionID", - "messageID", - "partID" - ], + "required": ["sessionID", "messageID", "partID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.execution.settled": { @@ -21011,9 +18699,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.execution.settled" - ] + "enum": ["session.next.execution.settled"] }, "durable": { "type": "object", @@ -21038,11 +18724,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -21064,29 +18746,17 @@ }, "outcome": { "type": "string", - "enum": [ - "success", - "failure", - "interrupted" - ] + "enum": ["success", "failure", "interrupted"] }, "error": { "$ref": "#/components/schemas/Session.Error.Unknown" } }, - "required": [ - "timestamp", - "sessionID", - "outcome" - ], + "required": ["timestamp", "sessionID", "outcome"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.text.delta": { @@ -21105,9 +18775,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.text.delta" - ] + "enum": ["session.next.text.delta"] }, "durable": { "type": "object", @@ -21132,11 +18800,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -21171,21 +18835,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "textID", - "delta" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "textID", "delta"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.reasoning.delta": { @@ -21204,9 +18858,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.reasoning.delta" - ] + "enum": ["session.next.reasoning.delta"] }, "durable": { "type": "object", @@ -21231,11 +18883,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -21270,21 +18918,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "reasoningID", - "delta" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "reasoningID", "delta"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.tool.input.delta": { @@ -21303,9 +18941,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.tool.input.delta" - ] + "enum": ["session.next.tool.input.delta"] }, "durable": { "type": "object", @@ -21330,11 +18966,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -21369,21 +19001,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "assistantMessageID", - "callID", - "delta" - ], + "required": ["timestamp", "sessionID", "assistantMessageID", "callID", "delta"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.next.compaction.delta": { @@ -21402,9 +19024,7 @@ }, "type": { "type": "string", - "enum": [ - "session.next.compaction.delta" - ] + "enum": ["session.next.compaction.delta"] }, "durable": { "type": "object", @@ -21429,11 +19049,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -21465,20 +19081,11 @@ "type": "string" } }, - "required": [ - "timestamp", - "sessionID", - "messageID", - "text" - ], + "required": ["timestamp", "sessionID", "messageID", "text"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "file.edited": { @@ -21497,9 +19104,7 @@ }, "type": { "type": "string", - "enum": [ - "file.edited" - ] + "enum": ["file.edited"] }, "durable": { "type": "object", @@ -21524,11 +19129,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -21541,17 +19142,11 @@ "type": "string" } }, - "required": [ - "file" - ], + "required": ["file"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "reference.updated": { @@ -21570,9 +19165,7 @@ }, "type": { "type": "string", - "enum": [ - "reference.updated" - ] + "enum": ["reference.updated"] }, "durable": { "type": "object", @@ -21597,11 +19190,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -21618,11 +19207,7 @@ ] } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "permission.v2.asked": { @@ -21641,9 +19226,7 @@ }, "type": { "type": "string", - "enum": [ - "permission.v2.asked" - ] + "enum": ["permission.v2.asked"] }, "durable": { "type": "object", @@ -21668,11 +19251,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -21719,20 +19298,11 @@ "$ref": "#/components/schemas/PermissionV2.Source" } }, - "required": [ - "id", - "sessionID", - "action", - "resources" - ], + "required": ["id", "sessionID", "action", "resources"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "permission.v2.replied": { @@ -21751,9 +19321,7 @@ }, "type": { "type": "string", - "enum": [ - "permission.v2.replied" - ] + "enum": ["permission.v2.replied"] }, "durable": { "type": "object", @@ -21778,11 +19346,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -21811,19 +19375,11 @@ "$ref": "#/components/schemas/PermissionV2.Reply" } }, - "required": [ - "sessionID", - "requestID", - "reply" - ], + "required": ["sessionID", "requestID", "reply"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "plugin.added": { @@ -21842,9 +19398,7 @@ }, "type": { "type": "string", - "enum": [ - "plugin.added" - ] + "enum": ["plugin.added"] }, "durable": { "type": "object", @@ -21869,11 +19423,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -21886,17 +19436,11 @@ "type": "string" } }, - "required": [ - "id" - ], + "required": ["id"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "project.directories.updated": { @@ -21915,9 +19459,7 @@ }, "type": { "type": "string", - "enum": [ - "project.directories.updated" - ] + "enum": ["project.directories.updated"] }, "durable": { "type": "object", @@ -21942,11 +19484,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -21959,17 +19497,11 @@ "type": "string" } }, - "required": [ - "projectID" - ], + "required": ["projectID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "command.updated": { @@ -21988,9 +19520,7 @@ }, "type": { "type": "string", - "enum": [ - "command.updated" - ] + "enum": ["command.updated"] }, "durable": { "type": "object", @@ -22015,11 +19545,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22036,11 +19562,7 @@ ] } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "skill.updated": { @@ -22059,9 +19581,7 @@ }, "type": { "type": "string", - "enum": [ - "skill.updated" - ] + "enum": ["skill.updated"] }, "durable": { "type": "object", @@ -22086,11 +19606,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22107,11 +19623,7 @@ ] } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "file.watcher.updated": { @@ -22130,9 +19642,7 @@ }, "type": { "type": "string", - "enum": [ - "file.watcher.updated" - ] + "enum": ["file.watcher.updated"] }, "durable": { "type": "object", @@ -22157,11 +19667,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22175,25 +19681,14 @@ }, "event": { "type": "string", - "enum": [ - "add", - "change", - "unlink" - ] + "enum": ["add", "change", "unlink"] } }, - "required": [ - "file", - "event" - ], + "required": ["file", "event"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "Pty": { @@ -22224,10 +19719,7 @@ }, "status": { "type": "string", - "enum": [ - "running", - "exited" - ] + "enum": ["running", "exited"] }, "pid": { "type": "integer", @@ -22246,15 +19738,7 @@ ] } }, - "required": [ - "id", - "title", - "command", - "args", - "cwd", - "status", - "pid" - ], + "required": ["id", "title", "command", "args", "cwd", "status", "pid"], "additionalProperties": false }, "pty.created": { @@ -22273,9 +19757,7 @@ }, "type": { "type": "string", - "enum": [ - "pty.created" - ] + "enum": ["pty.created"] }, "durable": { "type": "object", @@ -22300,11 +19782,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22317,17 +19795,11 @@ "$ref": "#/components/schemas/Pty" } }, - "required": [ - "info" - ], + "required": ["info"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "pty.updated": { @@ -22346,9 +19818,7 @@ }, "type": { "type": "string", - "enum": [ - "pty.updated" - ] + "enum": ["pty.updated"] }, "durable": { "type": "object", @@ -22373,11 +19843,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22390,17 +19856,11 @@ "$ref": "#/components/schemas/Pty" } }, - "required": [ - "info" - ], + "required": ["info"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "pty.exited": { @@ -22419,9 +19879,7 @@ }, "type": { "type": "string", - "enum": [ - "pty.exited" - ] + "enum": ["pty.exited"] }, "durable": { "type": "object", @@ -22446,11 +19904,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22476,18 +19930,11 @@ ] } }, - "required": [ - "id", - "exitCode" - ], + "required": ["id", "exitCode"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "pty.deleted": { @@ -22506,9 +19953,7 @@ }, "type": { "type": "string", - "enum": [ - "pty.deleted" - ] + "enum": ["pty.deleted"] }, "durable": { "type": "object", @@ -22533,11 +19978,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22555,17 +19996,11 @@ ] } }, - "required": [ - "id" - ], + "required": ["id"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "Shell": { @@ -22581,12 +20016,7 @@ }, "status": { "type": "string", - "enum": [ - "running", - "exited", - "timeout", - "killed" - ] + "enum": ["running", "exited", "timeout", "killed"] }, "command": { "type": "string" @@ -22615,21 +20045,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -22646,21 +20070,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -22671,41 +20089,24 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] } }, - "required": [ - "started" - ], + "required": ["started"], "additionalProperties": false } }, - "required": [ - "id", - "status", - "command", - "cwd", - "shell", - "file", - "metadata", - "time" - ], + "required": ["id", "status", "command", "cwd", "shell", "file", "metadata", "time"], "additionalProperties": false }, "shell.created": { @@ -22724,9 +20125,7 @@ }, "type": { "type": "string", - "enum": [ - "shell.created" - ] + "enum": ["shell.created"] }, "durable": { "type": "object", @@ -22751,11 +20150,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22768,17 +20163,11 @@ "$ref": "#/components/schemas/Shell" } }, - "required": [ - "info" - ], + "required": ["info"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "shell.exited": { @@ -22797,9 +20186,7 @@ }, "type": { "type": "string", - "enum": [ - "shell.exited" - ] + "enum": ["shell.exited"] }, "durable": { "type": "object", @@ -22824,11 +20211,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22852,46 +20235,28 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, "status": { "type": "string", - "enum": [ - "running", - "exited", - "timeout", - "killed" - ] + "enum": ["running", "exited", "timeout", "killed"] } }, - "required": [ - "id", - "status" - ], + "required": ["id", "status"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "shell.deleted": { @@ -22910,9 +20275,7 @@ }, "type": { "type": "string", - "enum": [ - "shell.deleted" - ] + "enum": ["shell.deleted"] }, "durable": { "type": "object", @@ -22937,11 +20300,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -22959,17 +20318,11 @@ ] } }, - "required": [ - "id" - ], + "required": ["id"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "QuestionV2.Option": { @@ -22984,10 +20337,7 @@ "description": "Explanation of choice" } }, - "required": [ - "label", - "description" - ], + "required": ["label", "description"], "additionalProperties": false }, "QuestionV2.Info": { @@ -23015,11 +20365,7 @@ "type": "boolean" } }, - "required": [ - "question", - "header", - "options" - ], + "required": ["question", "header", "options"], "additionalProperties": false }, "QuestionV2.Tool": { @@ -23032,10 +20378,7 @@ "type": "string" } }, - "required": [ - "messageID", - "callID" - ], + "required": ["messageID", "callID"], "additionalProperties": false }, "question.v2.asked": { @@ -23054,9 +20397,7 @@ }, "type": { "type": "string", - "enum": [ - "question.v2.asked" - ] + "enum": ["question.v2.asked"] }, "durable": { "type": "object", @@ -23081,11 +20422,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -23121,19 +20458,11 @@ "$ref": "#/components/schemas/QuestionV2.Tool" } }, - "required": [ - "id", - "sessionID", - "questions" - ], + "required": ["id", "sessionID", "questions"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "QuestionV2.Answer": { @@ -23158,9 +20487,7 @@ }, "type": { "type": "string", - "enum": [ - "question.v2.replied" - ] + "enum": ["question.v2.replied"] }, "durable": { "type": "object", @@ -23185,11 +20512,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -23221,19 +20544,11 @@ } } }, - "required": [ - "sessionID", - "requestID", - "answers" - ], + "required": ["sessionID", "requestID", "answers"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "question.v2.rejected": { @@ -23252,9 +20567,7 @@ }, "type": { "type": "string", - "enum": [ - "question.v2.rejected" - ] + "enum": ["question.v2.rejected"] }, "durable": { "type": "object", @@ -23279,11 +20592,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -23309,18 +20618,11 @@ ] } }, - "required": [ - "sessionID", - "requestID" - ], + "required": ["sessionID", "requestID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "Form.Metadata1": { @@ -23334,10 +20636,7 @@ }, "op": { "type": "string", - "enum": [ - "eq", - "neq" - ] + "enum": ["eq", "neq"] }, "value": { "anyOf": [ @@ -23351,21 +20650,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -23375,11 +20668,7 @@ ] } }, - "required": [ - "key", - "op", - "value" - ], + "required": ["key", "op", "value"], "additionalProperties": false }, "Form.StringField1": { @@ -23405,18 +20694,11 @@ }, "type": { "type": "string", - "enum": [ - "string" - ] + "enum": ["string"] }, "format": { "type": "string", - "enum": [ - "email", - "uri", - "date", - "date-time" - ] + "enum": ["email", "uri", "date", "date-time"] }, "minLength": { "type": "integer", @@ -23452,11 +20734,8 @@ "custom": { "type": "boolean" } - }, - "required": [ - "key", - "type" - ], + }, + "required": ["key", "type"], "additionalProperties": false }, "Form.NumberField1": { @@ -23482,9 +20761,7 @@ }, "type": { "type": "string", - "enum": [ - "number" - ] + "enum": ["number"] }, "minimum": { "anyOf": [ @@ -23493,21 +20770,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -23518,21 +20789,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -23543,29 +20808,20 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.IntegerField1": { @@ -23591,9 +20847,7 @@ }, "type": { "type": "string", - "enum": [ - "integer" - ] + "enum": ["integer"] }, "minimum": { "anyOf": [ @@ -23602,21 +20856,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -23627,21 +20875,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -23652,29 +20894,20 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.BooleanField1": { @@ -23700,18 +20933,13 @@ }, "type": { "type": "string", - "enum": [ - "boolean" - ] + "enum": ["boolean"] }, "default": { "type": "boolean" } }, - "required": [ - "key", - "type" - ], + "required": ["key", "type"], "additionalProperties": false }, "Form.MultiselectField1": { @@ -23737,9 +20965,7 @@ }, "type": { "type": "string", - "enum": [ - "multiselect" - ] + "enum": ["multiselect"] }, "options": { "type": "array", @@ -23773,11 +20999,7 @@ } } }, - "required": [ - "key", - "type", - "options" - ], + "required": ["key", "type", "options"], "additionalProperties": false }, "Form.FormInfo1": { @@ -23802,9 +21024,7 @@ }, "mode": { "type": "string", - "enum": [ - "form" - ] + "enum": ["form"] }, "fields": { "type": "array", @@ -23829,12 +21049,7 @@ } } }, - "required": [ - "id", - "sessionID", - "mode", - "fields" - ], + "required": ["id", "sessionID", "mode", "fields"], "additionalProperties": false }, "Form.UrlInfo1": { @@ -23859,20 +21074,13 @@ }, "mode": { "type": "string", - "enum": [ - "url" - ] + "enum": ["url"] }, "url": { "type": "string" } }, - "required": [ - "id", - "sessionID", - "mode", - "url" - ], + "required": ["id", "sessionID", "mode", "url"], "additionalProperties": false }, "form.created": { @@ -23891,9 +21099,7 @@ }, "type": { "type": "string", - "enum": [ - "form.created" - ] + "enum": ["form.created"] }, "durable": { "type": "object", @@ -23918,11 +21124,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -23942,17 +21144,11 @@ ] } }, - "required": [ - "form" - ], + "required": ["form"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "Form.Value1": { @@ -23967,21 +21163,15 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, @@ -24018,9 +21208,7 @@ }, "type": { "type": "string", - "enum": [ - "form.replied" - ] + "enum": ["form.replied"] }, "durable": { "type": "object", @@ -24045,11 +21233,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -24073,19 +21257,11 @@ "$ref": "#/components/schemas/Form.Answer1" } }, - "required": [ - "id", - "sessionID", - "answer" - ], + "required": ["id", "sessionID", "answer"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "form.cancelled": { @@ -24104,9 +21280,7 @@ }, "type": { "type": "string", - "enum": [ - "form.cancelled" - ] + "enum": ["form.cancelled"] }, "durable": { "type": "object", @@ -24131,11 +21305,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -24156,18 +21326,11 @@ "type": "string" } }, - "required": [ - "id", - "sessionID" - ], + "required": ["id", "sessionID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "Todo": { @@ -24186,11 +21349,7 @@ "description": "Priority level of the task: high, medium, low" } }, - "required": [ - "content", - "status", - "priority" - ], + "required": ["content", "status", "priority"], "additionalProperties": false }, "todo.updated": { @@ -24209,9 +21368,7 @@ }, "type": { "type": "string", - "enum": [ - "todo.updated" - ] + "enum": ["todo.updated"] }, "durable": { "type": "object", @@ -24236,11 +21393,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -24264,18 +21417,11 @@ } } }, - "required": [ - "sessionID", - "todos" - ], + "required": ["sessionID", "todos"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "SessionStatus": { @@ -24285,14 +21431,10 @@ "properties": { "type": { "type": "string", - "enum": [ - "idle" - ] + "enum": ["idle"] } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false }, { @@ -24300,9 +21442,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "retry" - ] + "enum": ["retry"] }, "attempt": { "type": "integer", @@ -24337,13 +21477,7 @@ "type": "string" } }, - "required": [ - "reason", - "provider", - "title", - "message", - "label" - ], + "required": ["reason", "provider", "title", "message", "label"], "additionalProperties": false }, "next": { @@ -24355,12 +21489,7 @@ ] } }, - "required": [ - "type", - "attempt", - "message", - "next" - ], + "required": ["type", "attempt", "message", "next"], "additionalProperties": false }, { @@ -24368,14 +21497,10 @@ "properties": { "type": { "type": "string", - "enum": [ - "busy" - ] + "enum": ["busy"] } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false } ] @@ -24396,9 +21521,7 @@ }, "type": { "type": "string", - "enum": [ - "session.status" - ] + "enum": ["session.status"] }, "durable": { "type": "object", @@ -24423,11 +21546,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -24448,18 +21567,11 @@ "$ref": "#/components/schemas/SessionStatus" } }, - "required": [ - "sessionID", - "status" - ], + "required": ["sessionID", "status"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.idle": { @@ -24478,9 +21590,7 @@ }, "type": { "type": "string", - "enum": [ - "session.idle" - ] + "enum": ["session.idle"] }, "durable": { "type": "object", @@ -24505,11 +21615,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -24527,17 +21633,11 @@ ] } }, - "required": [ - "sessionID" - ], + "required": ["sessionID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "tui.prompt.append": { @@ -24556,9 +21656,7 @@ }, "type": { "type": "string", - "enum": [ - "tui.prompt.append" - ] + "enum": ["tui.prompt.append"] }, "durable": { "type": "object", @@ -24583,11 +21681,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -24600,17 +21694,11 @@ "type": "string" } }, - "required": [ - "text" - ], + "required": ["text"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "tui.command.execute": { @@ -24629,9 +21717,7 @@ }, "type": { "type": "string", - "enum": [ - "tui.command.execute" - ] + "enum": ["tui.command.execute"] }, "durable": { "type": "object", @@ -24656,11 +21742,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -24699,17 +21781,11 @@ ] } }, - "required": [ - "command" - ], + "required": ["command"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "tui.toast.show": { @@ -24728,9 +21804,7 @@ }, "type": { "type": "string", - "enum": [ - "tui.toast.show" - ] + "enum": ["tui.toast.show"] }, "durable": { "type": "object", @@ -24755,11 +21829,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -24776,12 +21846,7 @@ }, "variant": { "type": "string", - "enum": [ - "info", - "success", - "warning", - "error" - ] + "enum": ["info", "success", "warning", "error"] }, "duration": { "anyOf": [ @@ -24799,18 +21864,11 @@ ] } }, - "required": [ - "message", - "variant" - ], + "required": ["message", "variant"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "tui.session.select": { @@ -24829,9 +21887,7 @@ }, "type": { "type": "string", - "enum": [ - "tui.session.select" - ] + "enum": ["tui.session.select"] }, "durable": { "type": "object", @@ -24856,11 +21912,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -24879,17 +21931,11 @@ ] } }, - "required": [ - "sessionID" - ], + "required": ["sessionID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "installation.updated": { @@ -24908,9 +21954,7 @@ }, "type": { "type": "string", - "enum": [ - "installation.updated" - ] + "enum": ["installation.updated"] }, "durable": { "type": "object", @@ -24935,11 +21979,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -24952,17 +21992,11 @@ "type": "string" } }, - "required": [ - "version" - ], + "required": ["version"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "installation.update-available": { @@ -24981,9 +22015,7 @@ }, "type": { "type": "string", - "enum": [ - "installation.update-available" - ] + "enum": ["installation.update-available"] }, "durable": { "type": "object", @@ -25008,11 +22040,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -25025,17 +22053,11 @@ "type": "string" } }, - "required": [ - "version" - ], + "required": ["version"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "vcs.branch.updated": { @@ -25054,9 +22076,7 @@ }, "type": { "type": "string", - "enum": [ - "vcs.branch.updated" - ] + "enum": ["vcs.branch.updated"] }, "durable": { "type": "object", @@ -25081,11 +22101,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -25101,11 +22117,7 @@ "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "mcp.status.changed": { @@ -25124,9 +22136,7 @@ }, "type": { "type": "string", - "enum": [ - "mcp.status.changed" - ] + "enum": ["mcp.status.changed"] }, "durable": { "type": "object", @@ -25151,11 +22161,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -25168,17 +22174,11 @@ "type": "string" } }, - "required": [ - "server" - ], + "required": ["server"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "permission.asked": { @@ -25197,9 +22197,7 @@ }, "type": { "type": "string", - "enum": [ - "permission.asked" - ] + "enum": ["permission.asked"] }, "durable": { "type": "object", @@ -25224,11 +22222,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -25283,10 +22277,7 @@ "type": "string" } }, - "required": [ - "messageID", - "callID" - ], + "required": ["messageID", "callID"], "additionalProperties": false }, { @@ -25295,22 +22286,11 @@ ] } }, - "required": [ - "id", - "sessionID", - "permission", - "patterns", - "metadata", - "always" - ], + "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "permission.replied": { @@ -25329,9 +22309,7 @@ }, "type": { "type": "string", - "enum": [ - "permission.replied" - ] + "enum": ["permission.replied"] }, "durable": { "type": "object", @@ -25356,11 +22334,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -25387,26 +22361,14 @@ }, "reply": { "type": "string", - "enum": [ - "once", - "always", - "reject" - ] + "enum": ["once", "always", "reject"] } }, - "required": [ - "sessionID", - "requestID", - "reply" - ], + "required": ["sessionID", "requestID", "reply"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "QuestionOption": { @@ -25421,10 +22383,7 @@ "description": "Explanation of choice" } }, - "required": [ - "label", - "description" - ], + "required": ["label", "description"], "additionalProperties": false }, "QuestionInfo": { @@ -25468,11 +22427,7 @@ "description": "Allow typing a custom answer (default: true)" } }, - "required": [ - "question", - "header", - "options" - ], + "required": ["question", "header", "options"], "additionalProperties": false }, "QuestionTool": { @@ -25490,10 +22445,7 @@ "type": "string" } }, - "required": [ - "messageID", - "callID" - ], + "required": ["messageID", "callID"], "additionalProperties": false }, "question.asked": { @@ -25512,9 +22464,7 @@ }, "type": { "type": "string", - "enum": [ - "question.asked" - ] + "enum": ["question.asked"] }, "durable": { "type": "object", @@ -25539,11 +22489,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -25586,19 +22532,11 @@ ] } }, - "required": [ - "id", - "sessionID", - "questions" - ], + "required": ["id", "sessionID", "questions"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "QuestionAnswer": { @@ -25623,9 +22561,7 @@ }, "type": { "type": "string", - "enum": [ - "question.replied" - ] + "enum": ["question.replied"] }, "durable": { "type": "object", @@ -25650,11 +22586,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -25686,19 +22618,11 @@ } } }, - "required": [ - "sessionID", - "requestID", - "answers" - ], + "required": ["sessionID", "requestID", "answers"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "question.rejected": { @@ -25717,9 +22641,7 @@ }, "type": { "type": "string", - "enum": [ - "question.rejected" - ] + "enum": ["question.rejected"] }, "durable": { "type": "object", @@ -25744,11 +22666,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -25774,18 +22692,11 @@ ] } }, - "required": [ - "sessionID", - "requestID" - ], + "required": ["sessionID", "requestID"], "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "session.error": { @@ -25804,9 +22715,7 @@ }, "type": { "type": "string", - "enum": [ - "session.error" - ] + "enum": ["session.error"] }, "durable": { "type": "object", @@ -25831,11 +22740,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, "location": { @@ -25898,11 +22803,7 @@ "additionalProperties": false } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "V2Event.server.connected": { @@ -25951,11 +22852,7 @@ ] } }, - "required": [ - "aggregateID", - "seq", - "version" - ], + "required": ["aggregateID", "seq", "version"], "additionalProperties": false }, { @@ -25975,9 +22872,7 @@ }, "type": { "type": "string", - "enum": [ - "server.connected" - ] + "enum": ["server.connected"] }, "data": { "anyOf": [ @@ -25990,11 +22885,7 @@ ] } }, - "required": [ - "id", - "type", - "data" - ], + "required": ["id", "type", "data"], "additionalProperties": false }, "V2Event": { @@ -26277,9 +23168,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "log.hint" - ] + "enum": ["log.hint"] }, "aggregateID": { "type": "string" @@ -26293,11 +23182,7 @@ ] } }, - "required": [ - "type", - "aggregateID", - "seq" - ], + "required": ["type", "aggregateID", "seq"], "additionalProperties": false, "description": "Payload-free change hint: the aggregate's durable log advanced to at least seq. Hints coalesce under backpressure (latest per aggregate) and are never a delivery guarantee." }, @@ -26306,14 +23191,10 @@ "properties": { "type": { "type": "string", - "enum": [ - "log.sweep_required" - ] + "enum": ["log.sweep_required"] } }, - "required": [ - "type" - ], + "required": ["type"], "additionalProperties": false, "description": "Hints may have been lost; treat every aggregate as potentially dirty and recover via bounded sweep plus durable log reads. Emitted first on every (re)subscribe." }, @@ -26339,9 +23220,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "PtyNotFoundError" - ] + "enum": ["PtyNotFoundError"] }, "ptyID": { "type": "string" @@ -26350,11 +23229,7 @@ "type": "string" } }, - "required": [ - "_tag", - "ptyID", - "message" - ], + "required": ["_tag", "ptyID", "message"], "additionalProperties": false }, "PtyTicket.ConnectToken": { @@ -26372,10 +23247,7 @@ ] } }, - "required": [ - "ticket", - "expires_in" - ], + "required": ["ticket", "expires_in"], "additionalProperties": false }, "ForbiddenError": { @@ -26383,18 +23255,13 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "ForbiddenError" - ] + "enum": ["ForbiddenError"] }, "message": { "type": "string" } }, - "required": [ - "_tag", - "message" - ], + "required": ["_tag", "message"], "additionalProperties": false }, "Shell1": { @@ -26410,12 +23277,7 @@ }, "status": { "type": "string", - "enum": [ - "running", - "exited", - "timeout", - "killed" - ] + "enum": ["running", "exited", "timeout", "killed"] }, "command": { "type": "string" @@ -26446,31 +23308,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -26489,31 +23341,21 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] }, @@ -26526,51 +23368,30 @@ }, { "type": "string", - "enum": [ - "NaN" - ] + "enum": ["NaN"] }, { "type": "string", - "enum": [ - "Infinity" - ] + "enum": ["Infinity"] }, { "type": "string", - "enum": [ - "-Infinity" - ] + "enum": ["-Infinity"] } ] }, { "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "enum": ["Infinity", "-Infinity", "NaN"] } ] } }, - "required": [ - "started" - ], + "required": ["started"], "additionalProperties": false } }, - "required": [ - "id", - "status", - "command", - "cwd", - "shell", - "file", - "metadata", - "time" - ], + "required": ["id", "status", "command", "cwd", "shell", "file", "metadata", "time"], "additionalProperties": false }, "ShellNotFoundError": { @@ -26578,9 +23399,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "ShellNotFoundError" - ] + "enum": ["ShellNotFoundError"] }, "id": { "type": "string" @@ -26589,11 +23408,7 @@ "type": "string" } }, - "required": [ - "_tag", - "id", - "message" - ], + "required": ["_tag", "id", "message"], "additionalProperties": false }, "QuestionV2.Request": { @@ -26626,11 +23441,7 @@ "$ref": "#/components/schemas/QuestionV2.Tool" } }, - "required": [ - "id", - "sessionID", - "questions" - ], + "required": ["id", "sessionID", "questions"], "additionalProperties": false }, "QuestionV2.Reply": { @@ -26644,9 +23455,7 @@ "description": "User answers in order of questions (each answer is an array of selected labels)" } }, - "required": [ - "answers" - ], + "required": ["answers"], "additionalProperties": false }, "QuestionNotFoundError": { @@ -26654,9 +23463,7 @@ "properties": { "_tag": { "type": "string", - "enum": [ - "QuestionNotFoundError" - ] + "enum": ["QuestionNotFoundError"] }, "requestID": { "type": "string" @@ -26665,11 +23472,7 @@ "type": "string" } }, - "required": [ - "_tag", - "requestID", - "message" - ], + "required": ["_tag", "requestID", "message"], "additionalProperties": false }, "Reference.LocalSource": { @@ -26677,9 +23480,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "local" - ] + "enum": ["local"] }, "path": { "type": "string" @@ -26691,10 +23492,7 @@ "type": "boolean" } }, - "required": [ - "type", - "path" - ], + "required": ["type", "path"], "additionalProperties": false }, "Reference.GitSource": { @@ -26702,9 +23500,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "git" - ] + "enum": ["git"] }, "repository": { "type": "string" @@ -26719,10 +23515,7 @@ "type": "boolean" } }, - "required": [ - "type", - "repository" - ], + "required": ["type", "repository"], "additionalProperties": false }, "Reference.Source": { @@ -26754,11 +23547,7 @@ "$ref": "#/components/schemas/Reference.Source" } }, - "required": [ - "name", - "path", - "source" - ], + "required": ["name", "path", "source"], "additionalProperties": false }, "ProjectCopy.Copy": { @@ -26768,9 +23557,7 @@ "type": "string" } }, - "required": [ - "directory" - ], + "required": ["directory"], "additionalProperties": false }, "ProjectCopyError": { @@ -26778,9 +23565,7 @@ "properties": { "name": { "type": "string", - "enum": [ - "ProjectCopyError" - ] + "enum": ["ProjectCopyError"] }, "data": { "type": "object", @@ -26799,16 +23584,11 @@ ] } }, - "required": [ - "message" - ], + "required": ["message"], "additionalProperties": false } }, - "required": [ - "name", - "data" - ], + "required": ["name", "data"], "additionalProperties": false }, "Vcs.FileStatus": { @@ -26835,27 +23615,15 @@ }, "status": { "type": "string", - "enum": [ - "added", - "deleted", - "modified" - ] + "enum": ["added", "deleted", "modified"] } }, - "required": [ - "file", - "additions", - "deletions", - "status" - ], + "required": ["file", "additions", "deletions", "status"], "additionalProperties": false }, "Vcs.Mode": { "type": "string", - "enum": [ - "working", - "branch" - ] + "enum": ["working", "branch"] } }, "securitySchemes": {} diff --git a/packages/codemode/test/openapi.test.ts b/packages/codemode/test/openapi.test.ts index 99a089009132..24291ee14d0e 100644 --- a/packages/codemode/test/openapi.test.ts +++ b/packages/codemode/test/openapi.test.ts @@ -54,7 +54,9 @@ const json = (value: unknown, status = 200) => const singleOperation = (operation: Record, method = "get"): Document => ({ openapi: "3.1.0", - paths: { "/test": { [method]: { operationId: "test", responses: { 200: { description: "Success" } }, ...operation } } }, + paths: { + "/test": { [method]: { operationId: "test", responses: { 200: { description: "Success" } }, ...operation } }, + }, }) describe("OpenAPI.fromSpec", () => { @@ -94,7 +96,12 @@ describe("OpenAPI.fromSpec", () => { const remove = toolAt(api.tools, "users.remove") expect(api.skipped).toEqual([]) - if (!Tool.isDefinition(get) || !Tool.isDefinition(create) || !Tool.isDefinition(search) || !Tool.isDefinition(remove)) { + if ( + !Tool.isDefinition(get) || + !Tool.isDefinition(create) || + !Tool.isDefinition(search) || + !Tool.isDefinition(remove) + ) { throw new Error("happy-path fixture did not generate every operation") } expect(inputTypeScript(get)).toBe( @@ -110,7 +117,8 @@ describe("OpenAPI.fromSpec", () => { const result = await Effect.runPromise( CodeMode.make({ tools: { api: api.tools } }) - .execute(` + .execute( + ` const user = await tools.api.users.get({ userId: "user-1", include: ["profile", "permissions"], @@ -128,7 +136,8 @@ describe("OpenAPI.fromSpec", () => { }) const removed = await tools.api.users.remove({ userId: "user-1" }) return { user, created, summary, removed } - `) + `, + ) .pipe(Effect.provide(client.layer)), ) @@ -482,9 +491,9 @@ describe("OpenAPI.fromSpec", () => { expect(url.searchParams.get("nullable")).toBe("null") expect(url.searchParams.get("constructor")).toBe("safe") expect(client.requests[0]!.headers.meta).toBe("a=b,c=d") - await expect( - Effect.runPromise(tool.run({ keys: [undefined] }).pipe(Effect.provide(client.layer))), - ).rejects.toThrow("unsupported nested value") + await expect(Effect.runPromise(tool.run({ keys: [undefined] }).pipe(Effect.provide(client.layer)))).rejects.toThrow( + "unsupported nested value", + ) }) test("skips unsupported parameter encodings and malformed security", () => { @@ -586,7 +595,10 @@ describe("OpenAPI.fromSpec", () => { test("applies authentication carriers without prototype or collision loss", async () => { const client = recordingClient(() => json({ ok: true })) - const authenticated = (security: ReadonlyArray>>, schemes: Record) => + const authenticated = ( + security: ReadonlyArray>>, + schemes: Record, + ) => OpenAPI.fromSpec({ baseUrl, spec: { ...singleOperation({}), security, components: { securitySchemes: schemes } }, @@ -602,13 +614,10 @@ describe("OpenAPI.fromSpec", () => { expect(new URL(client.requests[0]!.url).searchParams.get("__proto__")).toBe("secret") const duplicate = toolAt( - authenticated( - [{ first: [], second: [] }], - { - first: { type: "apiKey", in: "header", name: "x-key" }, - second: { type: "apiKey", in: "header", name: "x-key" }, - }, - ).tools, + authenticated([{ first: [], second: [] }], { + first: { type: "apiKey", in: "header", name: "x-key" }, + second: { type: "apiKey", in: "header", name: "x-key" }, + }).tools, "test", ) if (!Tool.isDefinition(duplicate)) throw new Error("duplicate auth tool was not generated") @@ -633,8 +642,7 @@ describe("OpenAPI.fromSpec", () => { }, }, auth: { - resolve: ({ name }) => - Effect.succeed(name === "bearer" ? { type: "bearer", token: "secret" } : undefined), + resolve: ({ name }) => Effect.succeed(name === "bearer" ? { type: "bearer", token: "secret" } : undefined), }, }) const alternativeTool = toolAt(alternative.tools, "test") @@ -766,9 +774,7 @@ describe("OpenAPI.fromSpec", () => { const oversized = recordingClient( () => new Response(null, { headers: { "content-length": String(50 * 1024 * 1024 + 1) } }), ) - const malformed = recordingClient( - () => new Response("{", { headers: { "content-type": "application/json" } }), - ) + const malformed = recordingClient(() => new Response("{", { headers: { "content-type": "application/json" } })) const chunked = recordingClient(() => new Response(new Uint8Array(50 * 1024 * 1024 + 1))) await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(oversized.layer)))).rejects.toThrow( diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts index 55b8ac020a36..4f25645de22c 100644 --- a/packages/codemode/test/signature.test.ts +++ b/packages/codemode/test/signature.test.ts @@ -308,10 +308,7 @@ describe("union schemas render every alternative", () => { test("allOf renders intersections with parenthesized union members", () => { const schema = { - allOf: [ - { type: "object", properties: { id: { type: "string" } } }, - { type: ["string", "null"] }, - ], + allOf: [{ type: "object", properties: { id: { type: "string" } } }, { type: ["string", "null"] }], } as const expect(jsonSchemaToTypeScript(schema)).toBe("{ id?: string } & (string | null)") }) @@ -321,7 +318,9 @@ describe("union schemas render every alternative", () => { "unknown", ) expect( - jsonSchemaToTypeScript({ allOf: [{ type: "string" }, { allOf: [{ $ref: "https://example.com/external.json" }] }] }), + jsonSchemaToTypeScript({ + allOf: [{ type: "string" }, { allOf: [{ $ref: "https://example.com/external.json" }] }], + }), ).toBe("unknown") expect( jsonSchemaToTypeScript({ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index b30c3beb3500..d97885b55390 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -14507,6 +14507,7 @@ }, "description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.", "summary": "Connect to PTY session", + "x-websocket": true, "x-codeSamples": [ { "lang": "js", From a12d50e15a6cc54949818d5b86728ce1955a3ee2 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:49:46 +1000 Subject: [PATCH 04/34] fix(app): hydrate timeline message parents (#35269) --- ...session-parent-hydration-benchmark.spec.ts | 146 +++++++++ .../timeline/session-tab-switch-metrics.ts | 5 +- .../timeline/session-tab-switch-probe.ts | 56 +++- .../e2e/performance/unit/mock-server.test.ts | 46 +++ .../unit/session-tab-switch-metrics.test.ts | 32 ++ .../unit/session-tab-switch-probe.test.ts | 45 +++ .../session-timeline-history-root.spec.ts | 220 +++++++++++++ packages/app/e2e/utils/mock-server.ts | 15 +- .../app/src/context/server-session.test.ts | 308 ++++++++++++++++++ packages/app/src/context/server-session.ts | 139 ++++++-- .../src/pages/session/timeline/model.test.ts | 8 +- .../app/src/pages/session/timeline/model.ts | 6 +- 12 files changed, 984 insertions(+), 42 deletions(-) create mode 100644 packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts create mode 100644 packages/app/e2e/performance/unit/mock-server.test.ts create mode 100644 packages/app/e2e/performance/unit/session-tab-switch-probe.test.ts create mode 100644 packages/app/e2e/regression/session-timeline-history-root.spec.ts diff --git a/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts new file mode 100644 index 000000000000..77c8491efda3 --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts @@ -0,0 +1,146 @@ +import type { Page } from "@playwright/test" +import { expectSessionTitle } from "../../utils/waits" +import { mockOpenCodeServer } from "../../utils/mock-server" +import { benchmark, expect, withBenchmarkPage } from "../benchmark" +import { fixture } from "./session-timeline-stress.fixture" +import { installStressSessionTabs, stressSessionHref } from "./timeline-test-helpers" +import { measureSessionSwitch, waitForStableTimeline } from "./session-tab-switch-probe" + +type ParentHydrationBenchmarkMode = "natural" | "candidate" + +const mode = process.env.SESSION_PARENT_HYDRATION_BENCHMARK_MODE ?? "natural" +if (mode !== "natural" && mode !== "candidate") throw new Error(`Unknown parent hydration benchmark mode: ${mode}`) +const userID = "msg_parent_hydration_user" +const user = { + ...fixture.messages[fixture.targetID][0]!, + info: { ...fixture.messages[fixture.targetID][0]!.info, id: userID, time: { created: 1700001000000 } }, + parts: fixture.messages[fixture.targetID][0]!.parts.map((part, index) => ({ + ...part, + id: `prt_parent_hydration_user_${index}`, + messageID: userID, + })), +} +const assistantSeed = fixture.messages[fixture.targetID][3]! +const assistants = Array.from({ length: 14 }, (_, index) => { + const messageID = `msg_parent_hydration_${String(index).padStart(2, "0")}` + return { + ...assistantSeed, + info: { + ...assistantSeed.info, + id: messageID, + parentID: userID, + time: { created: 1700001001000 + index * 1_000, completed: 1700001001500 + index * 1_000 }, + }, + parts: assistantSeed.parts.map((part, partIndex) => ({ + ...part, + id: `prt_parent_hydration_${String(index).padStart(2, "0")}_${partIndex}`, + messageID, + })), + } +}) +const messages = [user, ...assistants] +const target = fixture.sessions.find((session) => session.id === fixture.targetID)! +const lastID = userID +const lastPartID = assistants.at(-1)!.parts.at(-1)!.id + +benchmark("hydrates an orphaned latest turn after a cold session click", async ({ browser, report }, testInfo) => { + benchmark.setTimeout(180_000) + const results = [] as Awaited>[] + for (let run = 0; run < 5; run++) { + results.push( + await withBenchmarkPage(browser, `session-parent-hydration-${mode}-${run}`, (page) => trial(page, mode), testInfo), + ) + } + const timing = results.map((result) => result.metrics.firstCorrectObservedMs!).sort((a, b) => a - b) + report( + { + results: results.map((result) => ({ ...result.metrics, historyGateCount: result.historyGateCount })), + summary: { + firstCorrectObservedMs: { min: timing[0], median: timing[2], max: timing.at(-1) }, + blankSamples: results.map((result) => result.metrics.blankSamples), + requestCounts: { + list: results.map((result) => result.requestCounts.list), + parent: results.map((result) => result.requestCounts.parent), + }, + historyGateCount: results.map((result) => result.historyGateCount), + }, + }, + { mode }, + ) +}) + +async function trial(page: Page, mode: ParentHydrationBenchmarkMode) { + const requests: { type: "list" | "parent"; before?: string }[] = [] + const history = mode === "candidate" ? Promise.withResolvers() : undefined + let historyGates = 0 + await mockOpenCodeServer(page, { + sessions: fixture.sessions.filter((session) => session.id === fixture.sourceID), + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + messageDelay: 50, + onMessages: (request) => { + if (request.sessionID === fixture.targetID && request.phase === "start") + requests.push({ type: "list", before: request.before }) + }, + beforeMessagesResponse: (request) => { + if (mode !== "candidate" || request.sessionID !== fixture.targetID || !request.before) return Promise.resolve() + historyGates++ + return history!.promise + }, + onMessage: (request) => { + if (request.sessionID === fixture.targetID && request.messageID === userID) requests.push({ type: "parent" }) + }, + message: (sessionID, messageID) => { + if (sessionID !== fixture.targetID || messageID !== userID) return + return user + }, + pageMessages: (sessionID, limit, before) => { + const items = sessionID === fixture.targetID ? messages : fixture.messages[fixture.sourceID] + const end = before ? items.findIndex((message) => message.info.id === before) : items.length + const start = Math.max(0, end - limit) + return { items: items.slice(start, end), cursor: start > 0 ? items[start]!.info.id : undefined } + }, + }) + await page.route(`**/session/${fixture.targetID}`, (route) => + route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(target) }), + ) + await installStressSessionTabs(page, { sessionIDs: [fixture.sourceID] }) + await page.goto(stressSessionHref(fixture.sourceID)) + await expectSessionTitle(page, fixture.expected.sourceTitle) + await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!) + + const href = stressSessionHref(fixture.targetID) + await page.evaluate( + ({ href, title }) => { + const link = document.createElement("a") + link.id = "parent-hydration-target" + link.href = href + link.textContent = title + document.body.append(link) + }, + { href, title: target.title }, + ) + const metrics = await measureSessionSwitch(page, { + destinationIDs: messages.map((message) => message.info.id), + sourceIDs: fixture.messages[fixture.sourceID].map((message) => message.info.id), + lastID, + requiredPartID: lastPartID, + requireBottomAnchor: false, + href, + switch: async () => { + await page.locator("#parent-hydration-target").click() + await expectSessionTitle(page, target.title) + }, + }).finally(() => history?.resolve()) + expect(metrics.firstCorrectObservedMs).not.toBeNull() + const requestCounts = { + list: requests.filter((request) => request.type === "list").length, + parent: requests.filter((request) => request.type === "parent").length, + } + if (mode === "candidate") { + expect(requestCounts.parent).toBe(1) + expect(historyGates).toBe(1) + } + return { metrics, requestCounts, historyGateCount: historyGates } +} diff --git a/packages/app/e2e/performance/timeline/session-tab-switch-metrics.ts b/packages/app/e2e/performance/timeline/session-tab-switch-metrics.ts index 05a0dae4963c..5298b9b0b7df 100644 --- a/packages/app/e2e/performance/timeline/session-tab-switch-metrics.ts +++ b/packages/app/e2e/performance/timeline/session-tab-switch-metrics.ts @@ -4,6 +4,8 @@ export type SessionSwitchSample = { source: string[] hasVisibleRows: boolean last: boolean + requiredPartVisible?: boolean + bottomAnchorRequired?: boolean bottomErrorPx?: number review?: { fileHost: boolean @@ -41,7 +43,8 @@ export function isCorrectDestination(sample: SessionSwitchSample) { sample.destination.length > 0 && sample.source.length === 0 && sample.last && - Math.abs(sample.bottomErrorPx ?? Infinity) <= 1 + sample.requiredPartVisible !== false && + (sample.bottomAnchorRequired === false || Math.abs(sample.bottomErrorPx ?? Infinity) <= 1) ) } diff --git a/packages/app/e2e/performance/timeline/session-tab-switch-probe.ts b/packages/app/e2e/performance/timeline/session-tab-switch-probe.ts index f61160f7a741..955da80367d7 100644 --- a/packages/app/e2e/performance/timeline/session-tab-switch-probe.ts +++ b/packages/app/e2e/performance/timeline/session-tab-switch-probe.ts @@ -8,9 +8,16 @@ type SessionSwitchProbe = { async function installSessionSwitchProbe( page: Page, - input: { destinationIDs: string[]; sourceIDs: string[]; lastID: string; href: string }, + input: { + destinationIDs: string[] + sourceIDs: string[] + lastID: string + requiredPartID?: string + requireBottomAnchor?: boolean + href: string + }, ) { - await page.evaluate(({ destinationIDs, sourceIDs, lastID, href }) => { + await page.evaluate(({ destinationIDs, sourceIDs, lastID, requiredPartID, requireBottomAnchor, href }) => { const destination = new Set(destinationIDs) const source = new Set(sourceIDs) const samples: SessionSwitchSample[] = [] @@ -66,6 +73,13 @@ async function installSessionSwitchProbe( const rect = element.getBoundingClientRect() return rect.bottom > view.top && rect.top < view.bottom }) + const requiredPartVisible = requiredPartID + ? [...root.querySelectorAll("[data-timeline-part-id]")].some((element) => { + if (element.dataset.timelinePartId !== requiredPartID) return false + const rect = element.getBoundingClientRect() + return rect.width > 0 && rect.height > 0 && rect.bottom > view.top && rect.top < view.bottom + }) + : undefined const spacer = root.querySelector('[data-timeline-row="bottom-spacer"]')?.getBoundingClientRect() samples.push({ observedAtMs, @@ -73,11 +87,22 @@ async function installSessionSwitchProbe( source: visible.filter((id) => source.has(id)), hasVisibleRows, last: visible.includes(lastID), + requiredPartVisible, + bottomAnchorRequired: requireBottomAnchor !== false, bottomErrorPx: spacer ? spacer.bottom - view.bottom : undefined, review, }) } else { - samples.push({ observedAtMs, destination: [], source: [], hasVisibleRows: false, last: false, review }) + samples.push({ + observedAtMs, + destination: [], + source: [], + hasVisibleRows: false, + last: false, + requiredPartVisible: requiredPartID ? false : undefined, + bottomAnchorRequired: requireBottomAnchor !== false, + review, + }) } requestAnimationFrame(sample) }, 0) @@ -117,7 +142,8 @@ async function waitForStableSessionSwitch(page: Page) { sample.destination.length > 0 && sample.source.length === 0 && sample.last && - Math.abs(sample.bottomErrorPx ?? Infinity) <= 1, + sample.requiredPartVisible !== false && + (sample.bottomAnchorRequired === false || Math.abs(sample.bottomErrorPx ?? Infinity) <= 1), ) ) }) @@ -135,13 +161,27 @@ async function collectSessionSwitchResult(page: Page) { export async function measureSessionSwitch( page: Page, - input: { destinationIDs: string[]; sourceIDs: string[]; lastID: string; href: string; switch: () => Promise }, + input: { + destinationIDs: string[] + sourceIDs: string[] + lastID: string + requiredPartID?: string + requireBottomAnchor?: boolean + href: string + switch: () => Promise + }, ) { const { switch: run, ...probe } = input await installSessionSwitchProbe(page, probe) - await run() - await waitForStableSessionSwitch(page) - return collectSessionSwitchResult(page) + try { + await run() + await waitForStableSessionSwitch(page) + return await collectSessionSwitchResult(page) + } finally { + await page.evaluate(() => { + ;(window as Window & { __sessionSwitchProbe?: SessionSwitchProbe }).__sessionSwitchProbe?.stop() + }) + } } export async function waitForStableTimeline(page: Page, lastID: string) { diff --git a/packages/app/e2e/performance/unit/mock-server.test.ts b/packages/app/e2e/performance/unit/mock-server.test.ts new file mode 100644 index 000000000000..83308c0a866b --- /dev/null +++ b/packages/app/e2e/performance/unit/mock-server.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from "bun:test" +import type { Page, Route } from "@playwright/test" +import { mockOpenCodeServer } from "../../utils/mock-server" + +test("applies message latency after a list response gate is released", async () => { + const events: string[] = [] + const gate = Promise.withResolvers() + let handler: ((route: Route) => Promise) | undefined + const page = { + route: (_url: string, callback: (route: Route) => Promise) => { + handler = callback + return Promise.resolve() + }, + } as unknown as Page + await mockOpenCodeServer(page, { + provider: {}, + directory: "C:/OpenCode", + project: {}, + sessions: [{ id: "session" }], + messageDelay: 25, + beforeMessagesResponse: () => { + events.push("before") + return gate.promise + }, + onMessages: (request) => events.push(request.phase), + pageMessages: () => { + events.push("page") + return { items: [] } + }, + }) + + const response = handler!({ + request: () => ({ url: () => "http://127.0.0.1:4096/session/session/message" }), + fulfill: () => { + events.push("fulfill") + return Promise.resolve() + }, + } as unknown as Route) + expect(events).toEqual(["start", "before"]) + + const released = performance.now() + gate.resolve() + await response + expect(performance.now() - released).toBeGreaterThanOrEqual(20) + expect(events).toEqual(["start", "before", "page", "end", "fulfill"]) +}) diff --git a/packages/app/e2e/performance/unit/session-tab-switch-metrics.test.ts b/packages/app/e2e/performance/unit/session-tab-switch-metrics.test.ts index dd771b7d57c9..4b824d9dfb0d 100644 --- a/packages/app/e2e/performance/unit/session-tab-switch-metrics.test.ts +++ b/packages/app/e2e/performance/unit/session-tab-switch-metrics.test.ts @@ -52,3 +52,35 @@ test("reports missing correctness without throwing", () => { expect(result.firstCorrectObservedMs).toBeNull() expect(result.stableObservedMs).toBeNull() }) + +test("requires an explicitly tracked part to be visible", () => { + const result = classifySessionSwitch([ + { + observedAtMs: 16, + destination: ["destination"], + source: [], + hasVisibleRows: true, + last: true, + requiredPartVisible: false, + bottomErrorPx: 0, + }, + ]) + + expect(result.firstCorrectObservedMs).toBeNull() +}) + +test("can measure content correctness without requiring a bottom anchor", () => { + const result = classifySessionSwitch([ + { + observedAtMs: 16, + destination: ["destination"], + source: [], + hasVisibleRows: true, + last: true, + requiredPartVisible: true, + bottomAnchorRequired: false, + }, + ]) + + expect(result.firstCorrectObservedMs).toBe(16) +}) diff --git a/packages/app/e2e/performance/unit/session-tab-switch-probe.test.ts b/packages/app/e2e/performance/unit/session-tab-switch-probe.test.ts new file mode 100644 index 000000000000..3b71dc5c4631 --- /dev/null +++ b/packages/app/e2e/performance/unit/session-tab-switch-probe.test.ts @@ -0,0 +1,45 @@ +import { expect, test } from "bun:test" +import type { Page } from "@playwright/test" +import { measureSessionSwitch } from "../timeline/session-tab-switch-probe" + +function testPage(waitFailure?: Error) { + const stops: unknown[] = [] + const page = { + evaluate: async (_callback: unknown, input?: unknown) => { + if (input) return + stops.push(undefined) + }, + waitForFunction: async () => { + if (waitFailure) throw waitFailure + }, + } as unknown as Page + return { page, stops } +} + +function input(run: () => Promise) { + return { + destinationIDs: ["destination"], + sourceIDs: ["source"], + lastID: "destination", + href: "/session/destination", + switch: run, + } +} + +test("stops sampling when the session switch fails", async () => { + const failure = new Error("switch failed") + const context = testPage() + + await expect(measureSessionSwitch(context.page, input(async () => Promise.reject(failure)))).rejects.toBe(failure) + + expect(context.stops).toHaveLength(1) +}) + +test("stops sampling when the stable wait fails", async () => { + const failure = new Error("stable wait failed") + const context = testPage(failure) + + await expect(measureSessionSwitch(context.page, input(async () => {}))).rejects.toBe(failure) + + expect(context.stops).toHaveLength(1) +}) diff --git a/packages/app/e2e/regression/session-timeline-history-root.spec.ts b/packages/app/e2e/regression/session-timeline-history-root.spec.ts new file mode 100644 index 000000000000..15375cafed59 --- /dev/null +++ b/packages/app/e2e/regression/session-timeline-history-root.spec.ts @@ -0,0 +1,220 @@ +import { base64Encode } from "@opencode-ai/core/util/encode" +import { expect, test, type Page } from "@playwright/test" +import { + assistantMessage, + directory, + messageUpdated, + project, + session, + sessionID, + status, + textPart, + title, + userID, + userMessage, +} from "../performance/timeline-stability/fixture" +import { mockOpenCodeServer } from "../utils/mock-server" +import { installSseTransport } from "../utils/sse-transport" +import { expectSessionTitle } from "../utils/waits" + +const assistants = Array.from({ length: 14 }, (_, index) => + assistantMessage([textPart(`prt_history_root_${index}`, `Assistant response ${index}`)], { + id: `msg_${String(index + 1001).padStart(4, "0")}_history_root_assistant`, + parentID: userID, + created: 1700000001000 + index * 1_000, + completed: index < 13, + }), +) +const messages = [userMessage(), ...assistants] +const lastAssistant = assistants.at(-1)! +const lastPartID = assistants.at(-1)!.parts[0]!.id +const userPartID = `prt_${userID}_text` +const completed = { + ...lastAssistant.info, + time: { ...lastAssistant.info.time, completed: lastAssistant.info.time.created + 15_000 }, +} +const scenarios = [ + { name: "completion", info: completed, idleFirst: false, interrupted: false }, + { + name: "interruption", + info: { ...completed, error: { name: "MessageAbortedError", data: { message: "Stopped" } } }, + idleFirst: true, + interrupted: true, + }, +] as const + +test.use({ viewport: { width: 646, height: 1385 } }) + +for (const scenario of scenarios) { + test(`keeps the latest user turn visible through ${scenario.name}`, async ({ page }) => { + const requests: { before?: string; phase: "start" | "end" }[] = [] + const pages: { before?: string; limit: number }[] = [] + const roots: { sessionID: string; messageID: string }[] = [] + const sequence: string[] = [] + const history = Promise.withResolvers() + const transport = await installSseTransport<{ directory: string; payload: Record }>(page, { + server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`, + retry: 20, + }) + await mockOpenCodeServer(page, { + directory, + project: project(), + provider: { + all: [ + { + id: "opencode", + name: "OpenCode", + models: { + "claude-opus-4-6": { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + limit: { context: 200_000 }, + }, + }, + }, + ], + connected: ["opencode"], + default: { providerID: "opencode", modelID: "claude-opus-4-6" }, + }, + sessions: [session()], + sessionStatus: { [sessionID]: { type: "busy" } }, + beforeMessagesResponse: (request) => (request.before ? history.promise : Promise.resolve()), + onMessages: (request) => { + requests.push(request) + sequence.push(`messages:${request.phase}:${request.before ?? "latest"}`) + }, + onMessage: (request) => { + roots.push(request) + sequence.push(`message:${request.messageID}`) + }, + message: (requestedSessionID, messageID) => { + if (requestedSessionID !== sessionID) return + return messages.find((item) => item.info.id === messageID) + }, + pageMessages: (_, limit, before) => { + pages.push({ before, limit }) + const end = before ? messages.findIndex((message) => message.info.id === before) : messages.length + const start = Math.max(0, end - limit) + return { + items: messages.slice(start, end), + cursor: start > 0 ? messages[start]!.info.id : undefined, + } + }, + }) + await page.addInitScript( + ({ userPartID, lastPartID }) => { + const state = { armed: false, hidden: false, samples: 0, stop: false } + ;(window as Window & { __historyRootProbe?: typeof state }).__historyRootProbe = state + const sample = () => { + if (state.armed) { + const virtual = document.querySelector("[data-timeline-virtual-content]") + const viewport = virtual?.closest(".scroll-view__viewport") + const view = viewport?.getBoundingClientRect() + const visible = (partID: string) => { + const part = viewport?.querySelector(`[data-timeline-part-id="${partID}"]`) + const rect = part?.getBoundingClientRect() + return ( + !!rect && + !!view && + rect.width > 0 && + rect.height > 0 && + rect.bottom > view.top && + rect.top < view.bottom + ) + } + if (!virtual || !visible(userPartID) || !visible(lastPartID)) state.hidden = true + state.samples++ + } + if (!state.stop) requestAnimationFrame(() => setTimeout(sample, 0)) + } + requestAnimationFrame(() => setTimeout(sample, 0)) + }, + { userPartID, lastPartID }, + ) + + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await transport.waitForConnection() + await expectSessionTitle(page, title) + await expect(page.locator(`[data-timeline-part-id="${lastPartID}"]`)).toBeVisible() + await expect(page.locator(`[data-timeline-part-id="${userPartID}"]`)).toBeVisible() + await expect.poll(() => requests.filter((request) => request.phase === "start").length).toBe(2) + expect(requests.filter((request) => request.phase === "end")).toHaveLength(1) + expect(sequence.slice(0, 4)).toEqual([ + "messages:start:latest", + "messages:end:latest", + `message:${userID}`, + `messages:start:${messages.at(-2)!.info.id}`, + ]) + await page.evaluate(() => { + ;( + window as Window & { + __historyRootProbe?: { armed: boolean } + } + ).__historyRootProbe!.armed = true + }) + await waitForProbeSamples(page, 0) + expect(await historyRootHidden(page)).toBe(false) + const beforeHistory = await probeSamples(page) + history.resolve() + await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(14) + await expect(page.getByRole("button", { name: "Stop" })).toBeVisible() + await waitForProbeSamples(page, beforeHistory) + expect(pages[0]).toEqual({ before: undefined, limit: 2 }) + expect(roots).toEqual([{ sessionID, messageID: userID }]) + + const message = messageUpdated(scenario.info) + const idle = status("idle") + for (const event of scenario.idleFirst ? [idle, message] : [message, idle]) { + const beforeEvent = await probeSamples(page) + await transport.send(event) + if (event === idle) await expect(page.getByRole("button", { name: "Stop" })).toHaveCount(0) + if (event === message && scenario.interrupted) + await expect(page.getByText("Interrupted", { exact: true })).toBeVisible() + await waitForProbeSamples(page, beforeEvent) + const current = await timelineState(page) + expect(current, JSON.stringify(current)).toMatchObject({ virtual: true }) + expect(current.rows, JSON.stringify(current)).toBeGreaterThan(0) + } + + expect(requests[0]).toEqual({ before: undefined, phase: "start", sessionID }) + expect(requests[1]).toEqual({ before: undefined, phase: "end", sessionID }) + await expect(page.getByRole("button", { name: "Stop" })).toHaveCount(0) + await expect(page.locator('[data-timeline-row="bottom-spacer"]')).toBeVisible() + if (scenario.interrupted) await expect(page.getByText("Interrupted", { exact: true })).toBeVisible() + expect( + await page.evaluate(() => { + const state = (window as Window & { __historyRootProbe?: { hidden: boolean; stop: boolean } }) + .__historyRootProbe! + state.stop = true + return state.hidden + }), + ).toBe(false) + }) +} + +function timelineState(page: Page) { + return page.evaluate(() => ({ + virtual: !!document.querySelector("[data-timeline-virtual-content]"), + rows: document.querySelectorAll("[data-timeline-key]").length, + })) +} + +function probeSamples(page: Page) { + return page.evaluate( + () => (window as Window & { __historyRootProbe?: { samples: number } }).__historyRootProbe!.samples, + ) +} + +async function waitForProbeSamples(page: Page, after: number) { + await page.waitForFunction( + (after) => + (window as Window & { __historyRootProbe?: { samples: number } }).__historyRootProbe!.samples >= after + 3, + after, + ) +} + +function historyRootHidden(page: Page) { + return page.evaluate( + () => (window as Window & { __historyRootProbe?: { hidden: boolean } }).__historyRootProbe!.hidden, + ) +} diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index dc0a8d0c41c6..ce216028fb88 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -11,7 +11,10 @@ export interface MockServerConfig { pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string } vcsDiff?: unknown[] messageDelay?: number + beforeMessagesResponse?: (input: { sessionID: string; before?: string }) => Promise onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void + message?: (sessionID: string, messageID: string) => unknown + onMessage?: (input: { sessionID: string; messageID: string }) => void events?: () => unknown[] eventRetry?: number todos?: (sessionID: string) => unknown[] @@ -72,6 +75,15 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { return json(route, session ?? {}) } + const messageMatch = path.match(/^\/session\/([^/]+)\/message\/([^/]+)$/) + if (messageMatch) { + config.onMessage?.({ sessionID: messageMatch[1]!, messageID: messageMatch[2]! }) + if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay)) + const message = config.message?.(messageMatch[1]!, messageMatch[2]!) + if (message === undefined) return json(route, { error: "Message not found" }, undefined, 404) + return json(route, message) + } + const todoMatch = path.match(/^\/session\/([^/]+)\/todo$/) if (todoMatch) return json(route, config.todos?.(todoMatch[1]!) ?? []) if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, []) @@ -82,7 +94,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { const before = token ? cursors.get(token) : undefined if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400) config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "start" }) - if (config.messageDelay) await new Promise((resolve) => setTimeout(resolve, config.messageDelay)) + await config.beforeMessagesResponse?.({ sessionID: messagesMatch[1]!, before }) + if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay)) const limit = Number(url.searchParams.get("limit") ?? 80) const pageData = config.pageMessages(messagesMatch[1], limit, before) config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "end" }) diff --git a/packages/app/src/context/server-session.test.ts b/packages/app/src/context/server-session.test.ts index 01f517478924..77871d347315 100644 --- a/packages/app/src/context/server-session.test.ts +++ b/packages/app/src/context/server-session.test.ts @@ -15,11 +15,13 @@ const session = (id: string, parentID?: string): Session => ({ }) type UserMessage = Extract +type AssistantMessage = Extract type TextPart = Extract type MessageResponse = { data: { info: Message; parts: Part[] }[] response: { headers: Headers } } +type SingleMessageResponse = { data: MessageResponse["data"][number] } const userMessage = (id: string, input: Partial = {}): UserMessage => ({ id, @@ -31,6 +33,22 @@ const userMessage = (id: string, input: Partial = {}): UserMessage ...input, }) +const assistantMessage = (id: string, parentID: string, input: Partial = {}): AssistantMessage => ({ + id, + sessionID: "child", + role: "assistant", + time: { created: Number(id.at(-1)), completed: Number(id.at(-1)) }, + parentID, + modelID: "model", + providerID: "provider", + mode: "build", + agent: "build", + path: { cwd: "/repo", root: "/repo" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + ...input, +}) + const textPart = (messageID: string, input: Partial = {}): TextPart => ({ id: "part", sessionID: "child", @@ -45,6 +63,8 @@ const response = (data: MessageResponse["data"] = [], cursor?: string): MessageR response: { headers: new Headers(cursor ? { "x-next-cursor": cursor } : undefined) }, }) +const singleResponse = (info: Message, parts: Part[] = []): SingleMessageResponse => ({ data: { info, parts } }) + const deferredResponse = () => Promise.withResolvers() function messageClient(...responses: Array>) { @@ -71,6 +91,40 @@ function messageClient(...responses: Array>, + roots: Array>, +) { + let pageIndex = 0 + let rootIndex = 0 + const requests: unknown[] = [] + const rootRequests: unknown[] = [] + const rootWaiting = new Map void>() + const client = { + session: { + get: async () => ({ data: session("child", "root") }), + messages: (input: unknown) => { + requests.push(input) + return pages[pageIndex++] + }, + message: (input: unknown) => { + rootRequests.push(input) + rootWaiting.get(rootRequests.length)?.() + rootWaiting.delete(rootRequests.length) + return roots[rootIndex++] + }, + }, + } as unknown as OpencodeClient + return Object.assign(client, { + requests, + rootRequests, + rootRequested(count: number) { + if (rootRequests.length >= count) return Promise.resolve() + return new Promise((resolve) => rootWaiting.set(count, resolve)) + }, + }) +} + const retryImmediately: typeof retry = async (task, options = {}) => { const attempts = options.attempts ?? 3 for (let attempt = 0; ; attempt++) { @@ -124,6 +178,240 @@ describe("server session", () => { expect(ctx.store.data.message.root).toEqual([]) }) + test("backfills an assistant-only initial page through its user root", async () => { + const user = userMessage("message-1") + const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] + const client = rootMessageClient( + [ + response( + assistants.map((info) => ({ info, parts: [] })), + "older", + ), + ], + [singleResponse(user)], + ) + const store = createServerSession(client) + + await store.sync("child") + + expect(client.requests).toEqual([ + { sessionID: "child", limit: 2, before: undefined }, + ]) + expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }]) + expect(store.data.message.child).toEqual([user, ...assistants]) + expect(store.history.more("child")).toBe(true) + }) + + test("does not let an optimistic user suppress initial root backfill", async () => { + const user = userMessage("message-1") + const part = textPart(user.id) + const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] + const client = rootMessageClient( + [ + response( + assistants.map((info) => ({ info, parts: [] })), + "older", + ), + ], + [singleResponse(user)], + ) + const store = createServerSession(client) + store.optimistic.add({ sessionID: "child", message: user, parts: [part] }) + + await store.sync("child") + store.optimistic.remove({ sessionID: "child", messageID: user.id }) + + expect(client.requests).toHaveLength(1) + expect(client.rootRequests).toHaveLength(1) + expect(store.data.message.child).toEqual([user, ...assistants]) + }) + + test("backfills the parent of fetched assistants when another user is cached", async () => { + const unrelated = userMessage("message-0", { time: { created: 0 } }) + const user = userMessage("message-1") + const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] + const client = rootMessageClient( + [ + response([{ info: unrelated, parts: [] }]), + response( + assistants.map((info) => ({ info, parts: [] })), + "older", + ), + ], + [singleResponse(user)], + ) + const store = createServerSession(client) + await store.sync("child") + + await store.sync("child", { force: true }) + + expect(client.requests).toHaveLength(2) + expect(client.rootRequests).toHaveLength(1) + expect(store.data.message.child).toEqual([unrelated, user, ...assistants]) + }) + + test("preserves cached history between an injected parent and the page boundary", async () => { + const user = userMessage("message-1") + const cached = userMessage("message-3", { time: { created: 3 } }) + const assistant = assistantMessage("message-4", user.id) + const client = rootMessageClient( + [response([{ info: cached, parts: [] }]), response([{ info: assistant, parts: [] }], "older")], + [singleResponse(user)], + ) + const store = createServerSession(client) + await store.sync("child") + + await store.sync("child", { force: true }) + + expect(store.data.message.child).toEqual([user, cached, assistant]) + }) + + test("refreshes a cached parent omitted by an assistant-only replacement page", async () => { + const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } }) + const fresh = { ...stale, summary: { title: "fresh", diffs: [] } } + const stalePart = textPart(stale.id, { text: "stale" }) + const freshPart = { ...stalePart, text: "fresh" } + const assistant = assistantMessage("message-2", stale.id) + const client = rootMessageClient( + [response([{ info: stale, parts: [stalePart] }]), response([{ info: assistant, parts: [] }], "older")], + [singleResponse(fresh, [freshPart])], + ) + const store = createServerSession(client) + await store.sync("child") + + await store.sync("child", { force: true }) + + expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }]) + expect(store.data.message.child).toEqual([fresh, assistant]) + expect(store.data.part[stale.id]).toEqual([freshPart]) + }) + + test("refreshes a confirmed optimistic parent while preserving pending parts", async () => { + const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } }) + const fresh = { ...stale, summary: { title: "fresh", diffs: [] } } + const confirmed = textPart(stale.id, { id: "confirmed", text: "stale" }) + const refreshed = { ...confirmed, text: "fresh" } + const pending = textPart(stale.id, { id: "pending", text: "pending" }) + const assistant = assistantMessage("message-2", stale.id) + const client = rootMessageClient( + [response([{ info: stale, parts: [confirmed] }]), response([{ info: assistant, parts: [] }], "older")], + [singleResponse(fresh, [refreshed])], + ) + const store = createServerSession(client) + store.optimistic.add({ sessionID: "child", message: stale, parts: [confirmed, pending] }) + await store.sync("child") + + await store.sync("child", { force: true }) + + expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }]) + expect(store.data.message.child).toEqual([fresh, assistant]) + expect(store.data.part[stale.id]).toEqual([refreshed, pending]) + }) + + test("uses a parent received by SSE during the replacement load", async () => { + const pending = deferredResponse() + const user = userMessage("message-1") + const assistant = assistantMessage("message-2", user.id) + const client = rootMessageClient([pending.promise], []) + const store = createServerSession(client) + const loading = store.sync("child") + + store.apply({ type: "message.updated", properties: { info: user } }) + pending.resolve(response([{ info: assistant, parts: [] }], "older")) + await loading + + expect(client.rootRequests).toEqual([]) + expect(store.data.message.child).toEqual([user, assistant]) + }) + + test("uses a successful retry over events received by a failed backfill attempt", async () => { + const failed = deferredResponse() + const user = userMessage("message-1") + const live = { ...user, agent: "stale" } + const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] + const client = rootMessageClient( + [ + response( + assistants.map((info) => ({ info, parts: [] })), + "older", + ), + ], + [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)], + ) + const store = createServerSession(client, { retry: retryImmediately }) + const loading = store.sync("child") + await client.rootRequested(1) + + store.apply({ type: "message.updated", properties: { info: live } }) + failed.reject(new Error("retry")) + await loading + + expect(client.requests).toHaveLength(1) + expect(client.rootRequests).toHaveLength(2) + expect(store.data.message.child).toEqual([user, ...assistants]) + }) + + test("preserves newer-page events across a failed parent retry", async () => { + const failed = deferredResponse() + const user = userMessage("message-1") + const assistant = assistantMessage("message-2", user.id) + const live = { ...assistant, cost: 1 } + const client = rootMessageClient( + [response([{ info: assistant, parts: [] }], "older")], + [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)], + ) + const store = createServerSession(client, { retry: retryImmediately }) + const loading = store.sync("child") + await client.rootRequested(1) + + store.apply({ type: "message.updated", properties: { info: live } }) + failed.reject(new Error("retry")) + await loading + + expect(store.data.message.child).toEqual([user, live]) + }) + + test("preserves unrelated message events across a failed parent retry", async () => { + const failed = deferredResponse() + const user = userMessage("message-1") + const assistant = assistantMessage("message-2", user.id) + const live = userMessage("message-4", { time: { created: 4 } }) + const client = rootMessageClient( + [response([{ info: assistant, parts: [] }], "older")], + [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)], + ) + const store = createServerSession(client, { retry: retryImmediately }) + const loading = store.sync("child") + await client.rootRequested(1) + + store.apply({ type: "message.updated", properties: { info: live } }) + failed.reject(new Error("retry")) + await loading + + expect(store.data.message.child).toEqual([user, assistant, live]) + }) + + test("preserves newer-page part events across a failed parent retry", async () => { + const failed = deferredResponse() + const user = userMessage("message-1") + const assistant = assistantMessage("message-2", user.id) + const stale = textPart(assistant.id, { text: "stale" }) + const live = { ...stale, text: "live" } + const client = rootMessageClient( + [response([{ info: assistant, parts: [stale] }], "older")], + [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)], + ) + const store = createServerSession(client, { retry: retryImmediately }) + const loading = store.sync("child") + await client.rootRequested(1) + + store.apply({ type: "message.part.updated", properties: { sessionID: "child", part: live, time: 2 } }) + failed.reject(new Error("retry")) + await loading + + expect(store.data.part[assistant.id]).toEqual([live]) + }) + test("merges live events into the initial page", async () => { const pending = deferredResponse() const user = userMessage("message-1") @@ -905,6 +1193,26 @@ describe("server session", () => { expect(store.data.message.child).toEqual([latest]) }) + test("does not scan cached messages for user roots during history prepend", async () => { + const guard = { active: false } + const latest = new Proxy(userMessage("message-2", { time: { created: 2 } }), { + get(target, property, receiver) { + if (guard.active && property === "role") throw new Error("cached role accessed") + return Reflect.get(target, property, receiver) + }, + }) + const older = userMessage("message-1") + const store = createServerSession( + messageClient(response([{ info: latest, parts: [] }], "older"), response([{ info: older, parts: [] }])), + ) + await store.sync("child") + guard.active = true + + await store.history.loadMore("child") + + expect(store.data.message.child).toEqual([older, latest]) + }) + test("preserves loaded history during an incomplete refresh", async () => { const older = userMessage("message-1") const latest = userMessage("message-2", { time: { created: 2 } }) diff --git a/packages/app/src/context/server-session.ts b/packages/app/src/context/server-session.ts index 36cf56c70803..6898cc230497 100644 --- a/packages/app/src/context/server-session.ts +++ b/packages/app/src/context/server-session.ts @@ -54,6 +54,11 @@ type MessageLoadState = { clearedMessageParts: Set } +type MessageLoadBaseline = Pick< + MessageLoadState, + "touchedMessages" | "retainedMessages" | "touchedParts" | "clearedMessageParts" +> + function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) { if (items.length === 0) return { ...page, observed: [] as { messageID: string; parts: Part[] }[] } const session = [...page.session] @@ -347,7 +352,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?: load.touchedParts.set(messageID, new Set([partID])) } - const resetMessageLoad = (sessionID: string, load: MessageLoadState) => { + const resetMessageLoad = (sessionID: string, load: MessageLoadState, baseline?: MessageLoadBaseline) => { load.touchedMessages.clear() load.retainedMessages.clear() load.touchedParts.clear() @@ -380,8 +385,27 @@ export function createServerSession(client: OpencodeClient, options?: { retry?: parts.forEach((partID) => touched.add(partID)) load.touchedParts.set(messageID, touched) } + baseline?.touchedMessages.forEach((messageID) => load.touchedMessages.add(messageID)) + baseline?.retainedMessages.forEach((messageID) => load.retainedMessages.add(messageID)) + baseline?.clearedMessageParts.forEach((messageID) => load.clearedMessageParts.add(messageID)) + baseline?.touchedParts.forEach((parts, messageID) => { + const touched = load.touchedParts.get(messageID) ?? new Set() + parts.forEach((partID) => touched.add(partID)) + load.touchedParts.set(messageID, touched) + }) } + const messageLoadBaseline = (load: MessageLoadState, exclude: string): MessageLoadBaseline => ({ + touchedMessages: new Set([...load.touchedMessages].filter((messageID) => messageID !== exclude)), + retainedMessages: new Set([...load.retainedMessages].filter((messageID) => messageID !== exclude)), + touchedParts: new Map( + [...load.touchedParts] + .filter(([messageID]) => messageID !== exclude) + .map(([messageID, parts]) => [messageID, new Set(parts)]), + ), + clearedMessageParts: new Set([...load.clearedMessageParts].filter((messageID) => messageID !== exclude)), + }) + const evict = (sessionIDs: string[]) => { if (sessionIDs.length === 0) return const evicted = new Set(sessionIDs) @@ -460,6 +484,18 @@ export function createServerSession(client: OpencodeClient, options?: { retry?: } } + const fetchMessage = async (sessionID: string, messageID: string, onAttempt?: () => void) => { + const response = await (options?.retry ?? retry)(() => { + onAttempt?.() + return client.session.message({ sessionID, messageID }) + }) + if (!response.data?.info?.id) throw new Error(`Message not found: ${messageID}`) + return { + message: cleanMessage(response.data.info), + parts: response.data.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)), + } + } + const replaceMessages = (sessionID: string, messages: Message[]) => { const messageIDs = new Set(messages.map((message) => message.id)) const dropped = (data.message[sessionID] ?? []).filter((message) => !messageIDs.has(message.id)) @@ -578,36 +614,79 @@ export function createServerSession(client: OpencodeClient, options?: { retry?: messageLoads.set(sessionID, load) setMeta("loading", sessionID, true) let applied = false - await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load)) - .then((page) => { - if (generations.get(sessionID) !== active) return - const first = page.session.reduce( - (oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest), - undefined, - ) - const preserveUnfetched = - mode === "prepend" || (!page.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0))) - applyMessagePage( - sessionID, - page, - messageLoads.get(sessionID) === load ? load : undefined, - preserveUnfetched, - mode !== "prepend", - ) - applied = true - }) - .finally(() => { - if (!applied && generations.get(sessionID) === active && messageLoads.get(sessionID) === load) { - for (const messageID of load.orphanParents) { - if (!orphanParts.get(sessionID)?.has(messageID)) continue - setData(produce((draft) => deleteMessageParts(draft, messageID))) - orphanParts.get(sessionID)?.delete(messageID) - } - if (orphanParts.get(sessionID)?.size === 0) orphanParts.delete(sessionID) + try { + const page = await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load)) + const first = page.session.reduce( + (oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest), + undefined, + ) + if (generations.get(sessionID) !== active) return + + const parents = [] as Awaited>[] + if (mode !== "prepend") { + const users = new Set([ + ...page.session.filter((message) => message.role === "user").map((message) => message.id), + ...(data.message[sessionID] ?? []) + .filter((message) => { + if (message.role !== "user") return false + const item = optimistic.get(sessionID)?.get(message.id) + return load.touchedMessages.has(message.id) && (!item || item.confirmedMessage === true) + }) + .map((message) => message.id), + ]) + const parentIDs = [ + ...new Set( + page.session.flatMap((message) => + message.role === "assistant" && !users.has(message.parentID) ? [message.parentID] : [], + ), + ), + ] + for (const parentID of parentIDs) { + if (generations.get(sessionID) !== active) break + const parent = await fetchMessage(sessionID, parentID, () => + resetMessageLoad(sessionID, load, messageLoadBaseline(load, parentID)), + ) + if (parent.message.role !== "user") throw new Error(`Assistant parent is not a user message: ${parentID}`) + parents.push(parent) } - if (messageLoads.get(sessionID) === load) messageLoads.delete(sessionID) - if (generations.get(sessionID) === active) setMeta("loading", sessionID, false) - }) + } + if (generations.get(sessionID) !== active) return + const result = + mode === "prepend" + ? page + : { + ...page, + session: merge( + page.session, + parents.map((parent) => parent.message), + ), + part: merge( + page.part, + parents.map((parent) => ({ id: parent.message.id, part: parent.parts })), + ), + } + const preserveUnfetched = + mode === "prepend" || (!result.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0))) + applyMessagePage( + sessionID, + result, + messageLoads.get(sessionID) === load ? load : undefined, + preserveUnfetched, + mode !== "prepend", + ) + applied = true + } finally { + if (!applied && generations.get(sessionID) === active && messageLoads.get(sessionID) === load) { + for (const messageID of load.orphanParents) { + if (!orphanParts.get(sessionID)?.has(messageID)) continue + setData(produce((draft) => deleteMessageParts(draft, messageID))) + orphanParts.get(sessionID)?.delete(messageID) + } + if (orphanParts.get(sessionID)?.size === 0) orphanParts.delete(sessionID) + } + if (messageLoads.get(sessionID) === load) messageLoads.delete(sessionID) + if (generations.get(sessionID) === active) setMeta("loading", sessionID, false) + } } const sync = (sessionID: string, options?: { force?: boolean; messageLimit?: number }) => { diff --git a/packages/app/src/pages/session/timeline/model.test.ts b/packages/app/src/pages/session/timeline/model.test.ts index 09f24ff5ef75..24612072c3d4 100644 --- a/packages/app/src/pages/session/timeline/model.test.ts +++ b/packages/app/src/pages/session/timeline/model.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import type { AssistantMessage, Message, UserMessage } from "@opencode-ai/sdk/v2" -import { loadOlderTimeline, selectUserMessages, selectVisibleUserMessages } from "./model" +import { isTimelineReady, loadOlderTimeline, selectUserMessages, selectVisibleUserMessages } from "./model" const user = (id: string) => ({ id, role: "user" }) as UserMessage const assistant = (id: string) => ({ id, role: "assistant" }) as AssistantMessage @@ -15,6 +15,12 @@ describe("timeline model", () => { expect(selectVisibleUserMessages(users)).toBe(users) }) + test("waits for an assistant-only load to hydrate its user root", () => { + expect(isTimelineReady([assistant("msg_2")], true)).toBe(false) + expect(isTimelineReady([user("msg_1"), assistant("msg_2")], true)).toBe(true) + expect(isTimelineReady([], false)).toBe(true) + }) + test("loads exactly one opaque cursor page", async () => { let calls = 0 const anchors: Array = [] diff --git a/packages/app/src/pages/session/timeline/model.ts b/packages/app/src/pages/session/timeline/model.ts index 91bfc0eaf781..7eebee608078 100644 --- a/packages/app/src/pages/session/timeline/model.ts +++ b/packages/app/src/pages/session/timeline/model.ts @@ -45,7 +45,7 @@ export function createTimelineModel(input: { }) const ready = createMemo(() => { const id = input.sessionID() - return !id || sync().data.message[id] !== undefined + return !id || isTimelineReady(sync().data.message[id], serverSync().session.history.loading(id)) }) const userMessages = createMemo(() => selectUserMessages(messages()), emptyUserMessages, { equals: same }) const visibleUserMessages = createMemo( @@ -98,6 +98,10 @@ export function selectUserMessages(messages: Message[]) { return messages.filter((message): message is UserMessage => message.role === "user") } +export function isTimelineReady(messages: Message[] | undefined, loading: boolean) { + return messages !== undefined && (messages.some((message) => message.role === "user") || !loading) +} + export function selectVisibleUserMessages(messages: UserMessage[], revertMessageID?: string) { if (!revertMessageID) return messages return messages.filter((message) => message.id < revertMessageID) From 476e3fc3820ae48f3de948c8ee78d6f3cc4ec0eb Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 5 Jul 2026 00:51:28 +0000 Subject: [PATCH 05/34] chore: generate --- .../session-parent-hydration-benchmark.spec.ts | 7 ++++++- .../unit/session-tab-switch-probe.test.ts | 14 ++++++++++++-- packages/app/src/context/server-session.test.ts | 4 +--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts index 77c8491efda3..2a214831daa8 100644 --- a/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts +++ b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts @@ -48,7 +48,12 @@ benchmark("hydrates an orphaned latest turn after a cold session click", async ( const results = [] as Awaited>[] for (let run = 0; run < 5; run++) { results.push( - await withBenchmarkPage(browser, `session-parent-hydration-${mode}-${run}`, (page) => trial(page, mode), testInfo), + await withBenchmarkPage( + browser, + `session-parent-hydration-${mode}-${run}`, + (page) => trial(page, mode), + testInfo, + ), ) } const timing = results.map((result) => result.metrics.firstCorrectObservedMs!).sort((a, b) => a - b) diff --git a/packages/app/e2e/performance/unit/session-tab-switch-probe.test.ts b/packages/app/e2e/performance/unit/session-tab-switch-probe.test.ts index 3b71dc5c4631..d5d7f09a69ef 100644 --- a/packages/app/e2e/performance/unit/session-tab-switch-probe.test.ts +++ b/packages/app/e2e/performance/unit/session-tab-switch-probe.test.ts @@ -30,7 +30,12 @@ test("stops sampling when the session switch fails", async () => { const failure = new Error("switch failed") const context = testPage() - await expect(measureSessionSwitch(context.page, input(async () => Promise.reject(failure)))).rejects.toBe(failure) + await expect( + measureSessionSwitch( + context.page, + input(async () => Promise.reject(failure)), + ), + ).rejects.toBe(failure) expect(context.stops).toHaveLength(1) }) @@ -39,7 +44,12 @@ test("stops sampling when the stable wait fails", async () => { const failure = new Error("stable wait failed") const context = testPage(failure) - await expect(measureSessionSwitch(context.page, input(async () => {}))).rejects.toBe(failure) + await expect( + measureSessionSwitch( + context.page, + input(async () => {}), + ), + ).rejects.toBe(failure) expect(context.stops).toHaveLength(1) }) diff --git a/packages/app/src/context/server-session.test.ts b/packages/app/src/context/server-session.test.ts index 77871d347315..46a0dc826658 100644 --- a/packages/app/src/context/server-session.test.ts +++ b/packages/app/src/context/server-session.test.ts @@ -194,9 +194,7 @@ describe("server session", () => { await store.sync("child") - expect(client.requests).toEqual([ - { sessionID: "child", limit: 2, before: undefined }, - ]) + expect(client.requests).toEqual([{ sessionID: "child", limit: 2, before: undefined }]) expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }]) expect(store.data.message.child).toEqual([user, ...assistants]) expect(store.history.more("child")).toBe(true) From 82db86e2a1e834555ac4e203b3a4bff13a01cba2 Mon Sep 17 00:00:00 2001 From: usrnk1 <7547651+usrnk1@users.noreply.github.com> Date: Sun, 5 Jul 2026 03:37:59 +0200 Subject: [PATCH 06/34] feat(desktop): reopen closed tabs and background tab open (#35010) Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> --- packages/app/src/components/titlebar.tsx | 11 ++- packages/app/src/context/closed-tabs.ts | 40 +++++++++ packages/app/src/context/tabs.test.ts | 87 +++++++++++++++++++ packages/app/src/context/tabs.tsx | 59 ++++++++++++- packages/app/src/i18n/en.ts | 1 + .../app/src/pages/home-session-open.test.ts | 12 +++ packages/app/src/pages/home-session-open.ts | 11 +++ packages/app/src/pages/home.tsx | 41 ++++++--- 8 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 packages/app/src/context/closed-tabs.ts create mode 100644 packages/app/src/pages/home-session-open.test.ts create mode 100644 packages/app/src/pages/home-session-open.ts diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 703082d55370..3592bbe6227f 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -374,9 +374,16 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { keybind: "mod+w", hidden: true, onSelect: () => { - tabsStoreActions.removeTab(tabsStore.findIndex((tab) => current === tab)) + tabsStoreActions.closeTab(tabsStore.findIndex((tab) => current === tab)) }, }, + { + id: "tab.reopenClosed", + category: language.t("command.category.file"), + title: language.t("command.tab.reopenClosed"), + keybind: "mod+shift+t", + onSelect: () => tabsStoreActions.reopenClosedTab(), + }, { id: `tab.prev`, category: "tab", @@ -464,7 +471,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { }} onClose={(tab) => { const index = tabsStore.findIndex((item) => tabKey(item) === tabKey(tab)) - if (index !== -1) tabsStoreActions.removeTab(index) + if (index !== -1) tabsStoreActions.closeTab(index) }} onReorder={(keys) => tabsStoreActions.reorder(keys)} /> diff --git a/packages/app/src/context/closed-tabs.ts b/packages/app/src/context/closed-tabs.ts new file mode 100644 index 000000000000..5c49b94b6fb2 --- /dev/null +++ b/packages/app/src/context/closed-tabs.ts @@ -0,0 +1,40 @@ +import type { SessionTab, Tab } from "./tabs" + +export type ClosedTab = { + tab: SessionTab + index: number +} + +const CLOSED_TAB_LIMIT = 25 + +// Only session tabs are recorded; closing a draft tab deletes its persisted +// state, so a reopened draft would come back empty anyway. +export function pushClosedTab(stack: ClosedTab[], tab: Tab, index: number): ClosedTab[] { + if (tab.type !== "session") return stack + return [...stack, { tab: { ...tab }, index }].slice(-CLOSED_TAB_LIMIT) +} + +// Pops the most recently closed tab that is not open again, +// discarding stale entries along the way. +export function takeClosedTab(stack: ClosedTab[], tabs: Tab[]): { entry?: ClosedTab; stack: ClosedTab[] } { + const remaining = [...stack] + while (remaining.length) { + const entry = remaining.pop() + if (entry && !isOpen(tabs, entry.tab)) return { entry, stack: remaining } + } + return { stack: remaining } +} + +export function removeClosedTabs(stack: ClosedTab[], server: SessionTab["server"], sessionIDs: string[]) { + const removed = new Set(sessionIDs) + return stack.filter((entry) => entry.tab.server !== server || !removed.has(entry.tab.sessionId)) +} + +export function nextTabAfterClose(tabs: Tab[], index: number, active: boolean) { + if (!active) return undefined + return tabs[index + 1] ?? tabs[index - 1] ?? null +} + +function isOpen(tabs: Tab[], tab: SessionTab) { + return tabs.some((item) => item.type === "session" && item.server === tab.server && item.sessionId === tab.sessionId) +} diff --git a/packages/app/src/context/tabs.test.ts b/packages/app/src/context/tabs.test.ts index 27525af63d9d..fbc40ea09e6a 100644 --- a/packages/app/src/context/tabs.test.ts +++ b/packages/app/src/context/tabs.test.ts @@ -1,6 +1,21 @@ import { describe, expect, test } from "bun:test" import { createRoot, getOwner, onCleanup } from "solid-js" import { createTabMemory } from "./tab-memory" +import { + nextTabAfterClose, + pushClosedTab, + removeClosedTabs, + takeClosedTab, + type ClosedTab, +} from "./closed-tabs" +import type { SessionTab, Tab } from "./tabs" +import type { ServerConnection } from "./server" + +const server = "local\nhttp://localhost:4096" as ServerConnection.Key + +function sessionTab(sessionId: string): SessionTab { + return { type: "session", server, sessionId } +} describe("tab memory", () => { test("keeps state until its tab is removed", () => { @@ -22,3 +37,75 @@ describe("tab memory", () => { }) }) }) + +describe("closed tab stack", () => { + test("records session tabs with their index", () => { + const stack = pushClosedTab([], sessionTab("a"), 2) + + expect(stack).toEqual([{ tab: sessionTab("a"), index: 2 }]) + }) + + test("ignores draft tabs", () => { + const draft: Tab = { type: "draft", draftID: "d1", server, directory: "/tmp" } + + expect(pushClosedTab([], draft, 0)).toEqual([]) + }) + + test("caps the stack size", () => { + const stack = Array.from({ length: 30 }, (_, i) => i).reduce( + (acc, i) => pushClosedTab(acc, sessionTab(`s${i}`), i), + [], + ) + + expect(stack).toHaveLength(25) + expect(stack[0]?.tab.sessionId).toBe("s5") + expect(stack.at(-1)?.tab.sessionId).toBe("s29") + }) + + test("pops the most recently closed tab", () => { + const stack = [ + { tab: sessionTab("a"), index: 0 }, + { tab: sessionTab("b"), index: 1 }, + ] + const result = takeClosedTab(stack, []) + + expect(result.entry?.tab.sessionId).toBe("b") + expect(result.stack).toEqual([{ tab: sessionTab("a"), index: 0 }]) + }) + + test("skips entries whose tab is already open", () => { + const stack = [ + { tab: sessionTab("a"), index: 0 }, + { tab: sessionTab("b"), index: 1 }, + ] + const result = takeClosedTab(stack, [sessionTab("b")]) + + expect(result.entry?.tab.sessionId).toBe("a") + expect(result.stack).toEqual([]) + }) + + test("returns no entry when everything is open or empty", () => { + expect(takeClosedTab([], []).entry).toBeUndefined() + + const result = takeClosedTab([{ tab: sessionTab("a"), index: 0 }], [sessionTab("a")]) + expect(result.entry).toBeUndefined() + expect(result.stack).toEqual([]) + }) + + test("purges removed sessions", () => { + const stack = [ + { tab: sessionTab("a"), index: 0 }, + { tab: sessionTab("b"), index: 1 }, + ] + + expect(removeClosedTabs(stack, server, ["a"])).toEqual([{ tab: sessionTab("b"), index: 1 }]) + }) + + test("does not navigate when a background tab closes", () => { + const tabs = [sessionTab("a"), sessionTab("b"), sessionTab("c")] + + expect(nextTabAfterClose(tabs, 1, false)).toBeUndefined() + expect(nextTabAfterClose(tabs, 1, true)).toEqual(sessionTab("c")) + expect(nextTabAfterClose([sessionTab("a")], 0, true)).toBeNull() + }) +}) diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index 6a73d7736dc7..11155d2ca479 100644 --- a/packages/app/src/context/tabs.tsx +++ b/packages/app/src/context/tabs.tsx @@ -10,6 +10,7 @@ import { uuid } from "@/utils/uuid" import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events" import { sessionHref } from "@/utils/session-route" import { createTabMemory } from "./tab-memory" +import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs" export type SessionTab = { type: "session" @@ -63,6 +64,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ createStore([]), ) const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore({})) + const [closed, setClosed, , closedReady] = persisted(Persist.window("tabs.closed"), createStore([])) const params = useParams() const navigate = useNavigate() @@ -87,6 +89,15 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ }) } + const updateClosed = (update: (stack: ClosedTab[]) => ClosedTab[]) => { + const apply = () => setClosed((stack) => update(stack)) + if (closedReady()) { + apply() + return + } + void closedReady.promise?.then(apply) + } + const removeDraftPersisted = (draftID: string) => { for (const key of draftPersistedKeys()) removePersisted(Persist.draft(draftID, key), platform) } @@ -106,6 +117,13 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ if (recent.key && !next.some((tab) => tabKey(tab) === recent.key)) setRecentKey(undefined) }) + createEffect(() => { + if (!closedReady()) return + const servers = new Set(server.list.map(ServerConnection.key)) + const next = closed.filter((entry) => servers.has(entry.tab.server)) + if (next.length !== closed.length) setClosed(() => next) + }) + const navigateTab = (tab: Tab) => { const href = tabHref(tab) setRecentKey(tabKey(tab)) @@ -117,7 +135,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ if (!tab) return const key = tabKey(tab) const draftID = tab.type === "draft" ? tab.draftID : undefined - const nextTab = store[index + 1] ?? store[index - 1] + const nextTab = nextTabAfterClose(store, index, recentKey() === key && location.pathname !== "/") closing.add(key) void startTransition(() => { setStore( @@ -125,9 +143,11 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ tabs.splice(index, 1) }), ) - if (recent.key === key) setRecentKey(nextTab && tabKey(nextTab)) + if (nextTab === null) { + setRecentKey(undefined) + navigate("/") + } if (nextTab) navigateTab(nextTab) - else navigate("/") }).finally(() => closing.delete(key)) memory.remove(key) if (draftID) removeDraftPersisted(draftID) @@ -201,13 +221,45 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ removeDraftPersisted(draftID) }, removeTab, + // User-initiated close: records the tab so it can be reopened. + // Cleanup paths (missing sessions, archive, server removal) go through + // removeTab and friends directly and are not recorded. + closeTab(index: number) { + const tab = store[index] + if (!tab) return + if (tab.type === "session") updateClosed((stack) => pushClosedTab(stack, tab, index)) + removeTab(index) + }, + reopenClosedTab() { + if (!closedReady()) { + void closedReady.promise?.then(() => actions.reopenClosedTab()) + return + } + const result = takeClosedTab(closed, store) + if (result.stack.length === closed.length) return + setClosed(() => result.stack) + const entry = result.entry + if (!entry) return + const index = Math.min(entry.index, store.length) + void startTransition(() => { + setStore( + produce((tabs) => { + if (tabs.some((item) => tabKey(item) === tabKey(entry.tab))) return + tabs.splice(index, 0, entry.tab) + }), + ) + navigateTab(entry.tab) + }) + }, removeSessionTab(input: Omit) { + updateClosed((stack) => removeClosedTabs(stack, input.server, [input.sessionId])) const index = store.findIndex( (tab) => tab.type === "session" && tab.server === input.server && tab.sessionId === input.sessionId, ) if (index !== -1) removeTab(index) }, removeServer(key: ServerConnection.Key) { + updateClosed((stack) => stack.filter((entry) => entry.tab.server !== key)) const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : [])) const removed = store.filter((tab) => tab.server === key).map(tabKey) setStore((tabs) => tabs.filter((tab) => tab.server !== key)) @@ -218,6 +270,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ }, removeSessions: (input: SessionTabsRemovedDetail) => { const targetServer = input.server ?? server.key + updateClosed((stack) => removeClosedTabs(stack, targetServer, input.sessionIDs)) const removed = store .filter( (tab) => tab.type === "session" && tab.server === targetServer && input.sessionIDs.includes(tab.sessionId), diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index e5255df54218..66e60df30e09 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -48,6 +48,7 @@ export const dict = { "command.session.new": "New session", "command.file.open": "Open file", "command.tab.close": "Close tab", + "command.tab.reopenClosed": "Reopen closed tab", "command.context.addSelection": "Add selection to context", "command.context.addSelection.description": "Add selected lines from the current file", "command.input.focus": "Focus input", diff --git a/packages/app/src/pages/home-session-open.test.ts b/packages/app/src/pages/home-session-open.test.ts new file mode 100644 index 000000000000..a71d58ff0723 --- /dev/null +++ b/packages/app/src/pages/home-session-open.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "bun:test" +import { shouldOpenSessionInBackground } from "./home-session-open" + +describe("shouldOpenSessionInBackground", () => { + test("requires only the platform primary modifier", () => { + expect(shouldOpenSessionInBackground({ mac: true, meta: true, ctrl: false, shift: false, alt: false })).toBe(true) + expect(shouldOpenSessionInBackground({ mac: false, meta: false, ctrl: true, shift: false, alt: false })).toBe(true) + expect(shouldOpenSessionInBackground({ mac: true, meta: true, ctrl: false, shift: true, alt: false })).toBe(false) + expect(shouldOpenSessionInBackground({ mac: false, meta: false, ctrl: true, shift: false, alt: true })).toBe(false) + expect(shouldOpenSessionInBackground({ mac: false, meta: true, ctrl: false, shift: false, alt: false })).toBe(false) + }) +}) diff --git a/packages/app/src/pages/home-session-open.ts b/packages/app/src/pages/home-session-open.ts new file mode 100644 index 000000000000..8e32efb559d2 --- /dev/null +++ b/packages/app/src/pages/home-session-open.ts @@ -0,0 +1,11 @@ +export function shouldOpenSessionInBackground(input: { + mac: boolean + meta: boolean + ctrl: boolean + shift: boolean + alt: boolean +}) { + if (input.shift || input.alt) return false + if (input.mac) return input.meta && !input.ctrl + return input.ctrl && !input.meta +} diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 6222817ab66d..efbab7809ee9 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -66,6 +66,7 @@ import { Persist, persisted } from "@/utils/persist" import { useMarked } from "@opencode-ai/ui/context/marked" import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache" import { archiveHomeSession } from "./home-session-archive" +import { shouldOpenSessionInBackground } from "./home-session-open" import { showToast } from "@/utils/toast" const HOME_SESSION_LIMIT = 64 @@ -238,6 +239,20 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) { return { setViewport, setContentRef, setHeaderRef, update, titleOpacity } } +// Cmd+click on macOS (Ctrl+click elsewhere) opens a session tab in the +// background without navigating, matching browser conventions. +function isBackgroundOpen(event: MouseEvent) { + return shouldOpenSessionInBackground({ + mac: typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform), + meta: event.metaKey, + ctrl: event.ctrlKey, + shift: event.shiftKey, + alt: event.altKey, + }) +} + +type OpenSessionOptions = { background?: boolean } + export function NewHome() { const sync = useServerSync() const layout = useLayout() @@ -377,9 +392,11 @@ export function NewHome() { setState("searchFocused", false) } - function selectSearchSession(session: Session) { - openSession(session) - closeSearch() + function selectSearchSession(session: Session, options?: OpenSessionOptions) { + openSession(session, options) + // Background opens keep the search visible so several results can be + // opened in a row. + if (!options?.background) closeSearch() } command.register("home", () => [ @@ -464,13 +481,17 @@ export function NewHome() { .forEach((directory) => state.project.markViewed(directory)) } - function openSession(session: Session) { + function openSession(session: Session, options?: OpenSessionOptions) { const project = projectForSession(session, projects(), projectByID()) const conn = focusedServer() if (!conn) return const directory = project?.worktree ?? session.directory const ctx = global.ensureServerCtx(conn) ctx.projects.open(directory) + if (options?.background) { + tabs.addSessionTab({ server: ServerConnection.key(conn), sessionId: session.id }) + return + } ctx.projects.touch(directory) startTransition(() => { const tab = tabs.addSessionTab({ server: ServerConnection.key(conn), sessionId: session.id }) @@ -1130,7 +1151,7 @@ function HomeSessionSearch(props: { onInput: (value: string) => void onFocus: () => void onClose: () => void - onSelect: (session: Session) => void + onSelect: (session: Session, options?: OpenSessionOptions) => void }) { const language = useLanguage() const [store, setStore] = createStore({ active: "" }) @@ -1244,7 +1265,7 @@ function HomeSessionSearch(props: { server={props.server} selected={store.active === homeSessionSearchKey(record)} onHighlight={() => setStore("active", homeSessionSearchKey(record))} - onSelect={(session) => props.onSelect(session)} + onSelect={(session, options) => props.onSelect(session, options)} /> )} @@ -1324,7 +1345,7 @@ function HomeSessionSearchResultRow(props: { server: ServerConnection.Key selected: boolean onHighlight: () => void - onSelect: (session: Session) => void + onSelect: (session: Session, options?: OpenSessionOptions) => void }) { const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) const showProjectName = () => props.showProjectName && props.record.projectName @@ -1345,7 +1366,7 @@ function HomeSessionSearchResultRow(props: { group: !!showProjectName(), }} onMouseEnter={() => props.onHighlight()} - onClick={() => props.onSelect(props.record.session)} + onClick={(event) => props.onSelect(props.record.session, { background: isBackgroundOpen(event) })} > void + openSession: (session: Session, options?: OpenSessionOptions) => void archiveSession: (session: Session) => Promise }) { const language = useLanguage() @@ -1405,7 +1426,7 @@ function HomeSessionRow(props: { type="button" data-component="home-session-row" class={`${HOME_ROW} h-10 min-w-0 flex-1 gap-2 py-3 pl-3 pr-10`} - onClick={() => props.openSession(props.record.session)} + onClick={(event) => props.openSession(props.record.session, { background: isBackgroundOpen(event) })} > Date: Sun, 5 Jul 2026 01:39:09 +0000 Subject: [PATCH 07/34] chore: generate --- packages/app/src/context/tabs.test.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/app/src/context/tabs.test.ts b/packages/app/src/context/tabs.test.ts index fbc40ea09e6a..b4bff4caff21 100644 --- a/packages/app/src/context/tabs.test.ts +++ b/packages/app/src/context/tabs.test.ts @@ -1,13 +1,7 @@ import { describe, expect, test } from "bun:test" import { createRoot, getOwner, onCleanup } from "solid-js" import { createTabMemory } from "./tab-memory" -import { - nextTabAfterClose, - pushClosedTab, - removeClosedTabs, - takeClosedTab, - type ClosedTab, -} from "./closed-tabs" +import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs" import type { SessionTab, Tab } from "./tabs" import type { ServerConnection } from "./server" From 7cf8d1ca6d377ac692c89523ffb23d0f98a97a70 Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Sun, 5 Jul 2026 07:31:30 +0530 Subject: [PATCH 08/34] feat(app): terminal improvements (#34747) Co-authored-by: Brendan Allan --- .../e2e/regression/terminal-hidden.spec.ts | 5 +- packages/app/e2e/utils/mock-server.ts | 11 +- .../session-sortable-terminal-tab-v2.tsx | 271 +++++++++++++ .../app/src/components/titlebar-tab-strip.tsx | 2 - packages/app/src/pages/session.tsx | 104 ++++- .../session/session-panel-layout.test.ts | 19 + .../src/pages/session/session-panel-layout.ts | 6 + .../src/pages/session/session-side-panel.tsx | 5 +- .../src/pages/session/terminal-panel-v2.tsx | 364 ++++++++++++++++++ packages/ui/src/components/tabs.css | 48 ++- 10 files changed, 810 insertions(+), 25 deletions(-) create mode 100644 packages/app/src/components/session/session-sortable-terminal-tab-v2.tsx create mode 100644 packages/app/src/pages/session/session-panel-layout.test.ts create mode 100644 packages/app/src/pages/session/session-panel-layout.ts create mode 100644 packages/app/src/pages/session/terminal-panel-v2.tsx diff --git a/packages/app/e2e/regression/terminal-hidden.spec.ts b/packages/app/e2e/regression/terminal-hidden.spec.ts index 357d9ac203e3..73821580af07 100644 --- a/packages/app/e2e/regression/terminal-hidden.spec.ts +++ b/packages/app/e2e/regression/terminal-hidden.spec.ts @@ -7,7 +7,7 @@ const projectID = "proj_hidden_terminal_regression" const sessionID = "ses_hidden_terminal_regression" const title = "Hidden terminal regression" -test("unmounts the terminal renderer while the pane is hidden", async ({ page }) => { +test("unmounts the terminal panel while it is hidden", async ({ page }) => { await page.setViewportSize({ width: 1400, height: 900 }) await mockOpenCodeServer(page, { directory, @@ -64,13 +64,14 @@ test("unmounts the terminal renderer while the pane is hidden", async ({ page }) await expect(page.locator('[data-component="terminal"]')).toBeVisible() await page.keyboard.press("Control+Backquote") - await expect(panel).toHaveAttribute("aria-hidden", "true") + await expect(panel).toHaveCount(0) await expect(page.locator('[data-component="terminal"]')).toHaveCount(0) await page.setViewportSize({ width: 1200, height: 700 }) await expect(page.locator('[data-component="terminal"]')).toHaveCount(0) await page.keyboard.press("Control+Backquote") + await expect(panel).toBeVisible() await expect(page.locator('[data-component="terminal"]')).toBeVisible() }) diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index ce216028fb88..06602c279217 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -1,7 +1,7 @@ import type { Page, Route } from "@playwright/test" const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/status", "/vcs/diff"]) -const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp"]) +const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/experimental/resource"]) export interface MockServerConfig { provider: unknown @@ -55,6 +55,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { const path = url.pathname if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry) if (path === "/global/health") return json(route, { healthy: true }) + if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: false }) if (path === "/permission") return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])) if (path === "/question") @@ -65,6 +66,11 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { return json(route, await config.fileList(url.searchParams.get("path") ?? "")) if (path === "/file/content" && config.fileContent) return json(route, await config.fileContent(url.searchParams.get("path") ?? "")) + if (path === "/api/reference") + return json(route, { + location: { directory: config.directory, project: { id: (config.project as { id?: string }).id, directory: config.directory } }, + data: [], + }) if (emptyObject.has(path)) return json(route, {}) if (emptyList.has(path)) return json(route, []) if (path in staticRoutes) return json(route, staticRoutes[path]) @@ -75,6 +81,9 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { return json(route, session ?? {}) } + const projectMatch = path.match(/^\/project\/([^/]+)$/) + if (projectMatch) return json(route, config.project) + const messageMatch = path.match(/^\/session\/([^/]+)\/message\/([^/]+)$/) if (messageMatch) { config.onMessage?.({ sessionID: messageMatch[1]!, messageID: messageMatch[2]! }) diff --git a/packages/app/src/components/session/session-sortable-terminal-tab-v2.tsx b/packages/app/src/components/session/session-sortable-terminal-tab-v2.tsx new file mode 100644 index 000000000000..f0cdf41ac1f5 --- /dev/null +++ b/packages/app/src/components/session/session-sortable-terminal-tab-v2.tsx @@ -0,0 +1,271 @@ +import type { JSX } from "solid-js" +import { Show, createEffect, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" +import { useSortable } from "@dnd-kit/solid/sortable" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Tabs } from "@opencode-ai/ui/tabs" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { Icon } from "@opencode-ai/ui/icon" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title" +import { useTerminal, type LocalPTY } from "@/context/terminal" +import { useLanguage } from "@/context/language" +import { focusTerminalById } from "@/pages/session/helpers" + +export function SortableTerminalTabV2(props: { + terminal: LocalPTY + index: () => number + newLayout: boolean + onClose?: () => void +}): JSX.Element { + const terminal = useTerminal() + const language = useLanguage() + const sortable = useSortable({ + get id() { + return props.terminal.id + }, + get index() { + return props.index() + }, + }) + const [store, setStore] = createStore({ + editing: false, + title: props.terminal.title, + menuOpen: false, + menuPosition: { x: 0, y: 0 }, + blurEnabled: false, + }) + let input: HTMLInputElement | undefined + let blurFrame: number | undefined + let editRequested = false + + const isDefaultTitle = () => { + const number = props.terminal.titleNumber + if (!Number.isFinite(number) || number <= 0) return false + return isDefaultTerminalTitle(props.terminal.title, number) + } + + const label = () => { + language.locale() + if (props.terminal.title && !isDefaultTitle()) return props.terminal.title + + const number = props.terminal.titleNumber + if (Number.isFinite(number) && number > 0) return language.t("terminal.title.numbered", { number }) + if (props.terminal.title) return props.terminal.title + return language.t("terminal.title") + } + + const close = () => { + const count = terminal.all().length + void terminal.close(props.terminal.id) + if (count === 1) { + props.onClose?.() + } + } + + const focus = () => { + if (store.editing) return + terminal.open(props.terminal.id) + if (document.activeElement instanceof HTMLElement) document.activeElement.blur() + focusTerminalById(props.terminal.id) + } + + const edit = (e?: Event) => { + if (e) { + e.stopPropagation() + e.preventDefault() + } + + setStore("blurEnabled", false) + setStore("title", props.terminal.title) + setStore("editing", true) + } + + const save = () => { + if (!store.blurEnabled) return + + const value = store.title.trim() + if (value && value !== props.terminal.title) { + terminal.update({ id: props.terminal.id, title: value }) + } + setStore("editing", false) + } + + const keydown = (e: KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault() + save() + return + } + if (e.key === "Escape") { + e.preventDefault() + setStore("editing", false) + } + } + + const menu = (e: MouseEvent) => { + e.preventDefault() + setStore("menuPosition", { x: e.clientX, y: e.clientY }) + setStore("menuOpen", true) + } + + createEffect(() => { + if (!store.editing) return + if (!input) return + input.focus() + input.select() + if (blurFrame !== undefined) cancelAnimationFrame(blurFrame) + blurFrame = requestAnimationFrame(() => { + blurFrame = undefined + setStore("blurEnabled", true) + }) + }) + + onCleanup(() => { + if (blurFrame === undefined) return + cancelAnimationFrame(blurFrame) + }) + + return ( +
+ + e.preventDefault()} + onContextMenu={menu} + class="!shadow-none" + classes={{ + button: "border-0 outline-none focus:outline-none focus-visible:outline-none !shadow-none !ring-0", + }} + closeButton={ + { + e.stopPropagation() + close() + }} + aria-label={language.t("terminal.close")} + /> + } + > + + {label()} + + + +
+ setStore("title", e.currentTarget.value)} + onBlur={save} + onKeyDown={keydown} + onMouseDown={(e) => e.stopPropagation()} + class="bg-transparent border-none outline-none text-sm min-w-0 flex-1" + /> +
+
+ setStore("menuOpen", open)}> + + { + if (!editRequested) return + e.preventDefault() + editRequested = false + requestAnimationFrame(() => edit()) + }} + > + (editRequested = true)}> + + {language.t("common.rename")} + + + + {language.t("common.close")} + + + + +
+ } + > + + + { + e.stopPropagation() + close() + }} + aria-label={language.t("terminal.close")} + /> + } + hideCloseButton + onMiddleClick={close} + > + + {label()} + + + +
+ setStore("title", e.currentTarget.value)} + onBlur={save} + onKeyDown={keydown} + onMouseDown={(e) => e.stopPropagation()} + class="bg-transparent border-none outline-none min-w-0 flex-1 p-0 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-base [font-weight:440] [font-variation-settings:'slnt'_0] [font-variant-numeric:tabular-nums]" + /> +
+
+
+ + { + if (!editRequested) return + e.preventDefault() + editRequested = false + requestAnimationFrame(() => edit()) + }} + > + (editRequested = true)}>{language.t("common.rename")} + {language.t("common.close")} + + +
+ + + ) +} diff --git a/packages/app/src/components/titlebar-tab-strip.tsx b/packages/app/src/components/titlebar-tab-strip.tsx index 68032e83c998..6705ac1919f0 100644 --- a/packages/app/src/components/titlebar-tab-strip.tsx +++ b/packages/app/src/components/titlebar-tab-strip.tsx @@ -17,8 +17,6 @@ import { createTabPromptState } from "@/context/prompt" import { base64Encode } from "@opencode-ai/core/util/encode" import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture" -const sortableTransition = { duration: 0 } - function SessionTabSlot(props: { tab: SessionTab id: string diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 30b6a5ebce96..db2c1f03e5d6 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -71,11 +71,13 @@ import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/ import { useSessionLayout } from "@/pages/session/session-layout" import { syncSessionModel } from "@/pages/session/session-model-helpers" import { SessionSidePanel } from "@/pages/session/session-side-panel" +import { sessionPanelLayout } from "@/pages/session/session-panel-layout" import { SessionReviewEmptyChangesV2 } from "@opencode-ai/session-ui/v2/session-review-empty-changes-v2" import { SessionReviewEmptyNoGitV2 } from "@opencode-ai/session-ui/v2/session-review-empty-no-git-v2" import { ReviewPanelV2 } from "@/pages/session/v2/review-panel-v2" import { createReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state" import { TerminalPanel } from "@/pages/session/terminal-panel" +import { TerminalPanelV2 } from "@/pages/session/terminal-panel-v2" import { useComposerCommands } from "@/pages/session/use-composer-commands" import { useSessionCommands } from "@/pages/session/use-session-commands" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" @@ -427,6 +429,12 @@ export default function Page() { const isDesktop = createMediaQuery("(min-width: 768px)") const size = createSizing() const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) + const desktopV2ReviewOpen = createMemo(() => newSessionDesign() && desktopReviewOpen() && !!params.id) + const terminalOpen = createMemo(() => view().terminal.opened()) + const desktopTerminalOpen = createMemo(() => isDesktop() && terminalOpen()) + const desktopInlineTerminalOnlyOpen = createMemo( + () => newSessionDesign() && desktopTerminalOpen() && !desktopV2ReviewOpen(), + ) const desktopFileTreeOpen = createMemo( () => isDesktop() && @@ -435,13 +443,23 @@ export default function Page() { opened: layout.fileTree.opened(), }), ) - const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen()) + const desktopSessionResizeOpen = createMemo( + () => (newSessionDesign() ? desktopV2ReviewOpen() || desktopTerminalOpen() : desktopReviewOpen()), + ) + const desktopSidePanelOpen = createMemo(() => desktopSessionResizeOpen() || desktopFileTreeOpen()) const sessionPanelWidth = createMemo(() => { if (!desktopSidePanelOpen()) return "100%" - if (desktopReviewOpen()) return `${layout.session.width()}px` + if (desktopSessionResizeOpen()) return `${layout.session.width()}px` return `calc(100% - ${layout.fileTree.width()}px)` }) const centered = createMemo(() => isDesktop() && !desktopReviewOpen()) + const desktopV2PanelLayout = createMemo(() => + sessionPanelLayout({ + review: desktopV2ReviewOpen(), + terminal: desktopTerminalOpen(), + files: desktopFileTreeOpen(), + }), + ) function normalizeTab(tab: string) { if (!tab.startsWith("file://")) return tab @@ -2093,7 +2111,7 @@ export default function Page() { classList={{ "@container relative shrink-0 flex flex-col min-h-0 h-full flex-1 md:flex-none transition-[width]": true, "duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none": - !size.active() && !ui.reviewSnap, + !size.active() && !ui.reviewSnap && !desktopInlineTerminalOnlyOpen(), }} style={{ width: sessionPanelWidth(), @@ -2113,7 +2131,7 @@ export default function Page() { )} - +
size.start()}>
- (newSessionDesign() ? reviewPanelV2() : reviewPanel())} - activeDiff={tree.activeDiff} - focusReviewDiff={focusReviewDiff} - reviewSnap={ui.reviewSnap} - size={size} - /> + + + + + +
+ +
+ +
+
+ +
size.start()}> + { + size.touch() + layout.terminal.resize(height) + }} + onCollapse={() => view().terminal.close()} + /> +
+
+ +
+ +
+
+
+
+
- + + + ) } diff --git a/packages/app/src/pages/session/session-panel-layout.test.ts b/packages/app/src/pages/session/session-panel-layout.test.ts new file mode 100644 index 000000000000..499dd7411a0b --- /dev/null +++ b/packages/app/src/pages/session/session-panel-layout.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "bun:test" +import { sessionPanelLayout } from "./session-panel-layout" + +describe("sessionPanelLayout", () => { + test("keeps one V2 owner while changing panel geometry", () => { + expect(sessionPanelLayout({ review: false, terminal: false, files: false })).toEqual({ + visible: false, + stacked: false, + }) + expect(sessionPanelLayout({ review: false, terminal: true, files: false })).toEqual({ + visible: true, + stacked: false, + }) + expect(sessionPanelLayout({ review: true, terminal: true, files: false })).toEqual({ + visible: true, + stacked: true, + }) + }) +}) diff --git a/packages/app/src/pages/session/session-panel-layout.ts b/packages/app/src/pages/session/session-panel-layout.ts new file mode 100644 index 000000000000..49feaebedeef --- /dev/null +++ b/packages/app/src/pages/session/session-panel-layout.ts @@ -0,0 +1,6 @@ +export function sessionPanelLayout(input: { review: boolean; terminal: boolean; files: boolean }) { + return { + visible: input.review || input.terminal || input.files, + stacked: input.review && input.terminal, + } +} diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 097df2219376..690262102039 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -51,6 +51,7 @@ export function SessionSidePanel(props: { focusReviewDiff: (path: string) => void reviewSnap: boolean size: Sizing + stacked?: boolean }) { const layout = useLayout() const settings = useSettings() @@ -219,8 +220,10 @@ export function SessionSidePanel(props: { aria-label={language.t("session.panel.reviewAndFiles")} aria-hidden={!open()} inert={!open()} - class="relative min-w-0 h-full flex shrink-0 overflow-hidden bg-background-base" + class="relative min-w-0 flex overflow-hidden bg-background-base" classList={{ + "h-full shrink-0": !props.stacked, + "min-h-0 flex-1": props.stacked, "pointer-events-none": !open(), "transition-[width] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none": !props.size.active() && !props.reviewSnap, diff --git a/packages/app/src/pages/session/terminal-panel-v2.tsx b/packages/app/src/pages/session/terminal-panel-v2.tsx new file mode 100644 index 000000000000..aec999d5dc15 --- /dev/null +++ b/packages/app/src/pages/session/terminal-panel-v2.tsx @@ -0,0 +1,364 @@ +import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" +import { createMediaQuery } from "@solid-primitives/media" +import { DragDropProvider, PointerSensor } from "@dnd-kit/solid" +import { isSortable } from "@dnd-kit/solid/sortable" +import { Accessibility, AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom" +import { RestrictToHorizontalAxis } from "@dnd-kit/abstract/modifiers" +import { RestrictToElement } from "@dnd-kit/dom/modifiers" +import { Tabs } from "@opencode-ai/ui/tabs" +import { ResizeHandle } from "@opencode-ai/ui/resize-handle" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" + +import { SortableTerminalTabV2 } from "@/components/session/session-sortable-terminal-tab-v2" +import { Terminal } from "@/components/terminal" +import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { useSettings } from "@/context/settings" +import { useTerminal } from "@/context/terminal" +import { useSDK } from "@/context/sdk" +import { terminalTabLabel } from "@/pages/session/terminal-label" +import { createSizing, focusTerminalById } from "@/pages/session/helpers" +import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" +import { useSessionLayout } from "@/pages/session/session-layout" + +export function TerminalPanelV2(props: { stacked?: boolean } = {}) { + const delays = [120, 240] + const layout = useLayout() + const terminal = useTerminal() + const sdk = useSDK() + const language = useLanguage() + const command = useCommand() + const settings = useSettings() + const { workspaceKey, view } = useSessionLayout() + + const isDesktop = createMediaQuery("(min-width: 768px)") + const newLayout = createMemo(() => settings.general.newLayoutDesigns()) + const opened = createMemo(() => view().terminal.opened()) + const size = createSizing() + const height = createMemo(() => layout.terminal.height()) + const close = () => view().terminal.close() + let root: HTMLDivElement | undefined + let tabList: HTMLDivElement | undefined + + const [store, setStore] = createStore({ + autoCreated: false, + recovered: {} as Record, + view: typeof window === "undefined" ? 1000 : (window.visualViewport?.height ?? window.innerHeight), + }) + + const max = () => store.view * 0.6 + const pane = () => Math.min(height(), max()) + const stacked = createMemo(() => isDesktop() && props.stacked) + const panelHeight = createMemo(() => (isDesktop() ? (stacked() ? `${pane()}px` : "100%") : opened() ? `${pane()}px` : "0px")) + const contentHeight = createMemo(() => (isDesktop() ? (stacked() ? `${pane()}px` : "100%") : `${pane()}px`)) + const newTerminalKeybind = createMemo(() => command.keybindParts("terminal.new")) + + onMount(() => { + if (typeof window === "undefined") return + + const sync = () => setStore("view", window.visualViewport?.height ?? window.innerHeight) + const port = window.visualViewport + + sync() + makeEventListener(window, "resize", sync) + if (port) makeEventListener(port, "resize", sync) + }) + + createEffect(() => { + if (!opened()) { + setStore("autoCreated", false) + return + } + + if (!terminal.ready() || terminal.all().length !== 0 || store.autoCreated) return + terminal.new() + setStore("autoCreated", true) + }) + + createEffect( + on( + () => terminal.all().length, + (count, prevCount) => { + if (prevCount === undefined || prevCount <= 0 || count !== 0) return + if (!opened()) return + close() + }, + ), + ) + + const focus = (id: string) => { + focusTerminalById(id) + + const frame = requestAnimationFrame(() => { + if (!opened()) return + if (terminal.active() !== id) return + focusTerminalById(id) + }) + + const timers = delays.map((ms) => + window.setTimeout(() => { + if (!opened()) return + if (terminal.active() !== id) return + focusTerminalById(id) + }, ms), + ) + + return () => { + cancelAnimationFrame(frame) + for (const timer of timers) clearTimeout(timer) + } + } + + createEffect( + on( + () => [opened(), terminal.active()] as const, + ([next, id]) => { + if (!next || !id) return + const stop = focus(id) + onCleanup(stop) + }, + ), + ) + + createEffect(() => { + if (opened()) return + const active = document.activeElement + if (!(active instanceof HTMLElement)) return + if (!root?.contains(active)) return + active.blur() + }) + + createEffect(() => { + const dir = sdk().directory + if (!dir) return + if (!terminal.ready()) return + language.locale() + + setTerminalHandoff( + workspaceKey(), + terminal.all().map((pty) => + terminalTabLabel({ + title: pty.title, + titleNumber: pty.titleNumber, + t: language.t as (key: string, vars?: Record) => string, + }), + ), + ) + }) + + const handoff = createMemo(() => { + const dir = sdk().directory + if (!dir) return [] + return getTerminalHandoff(workspaceKey()) ?? [] + }) + + const all = terminal.all + const ids = createMemo(() => all().map((pty) => pty.id)) + + const recoverTerminal = (key: string, id: string, clone: (id: string) => Promise) => { + if (store.recovered[key]) return + setStore("recovered", key, true) + void clone(id) + } + + const terminalRecoveryKey = (pty: { id: string; title: string; titleNumber: number }) => { + return String(pty.titleNumber || pty.title || pty.id) + } + + const markTerminalConnected = (key: string, id: string, trim: (id: string) => void) => { + setStore("recovered", key, false) + trim(id) + } + + const handleTerminalDragEnd = () => { + const activeId = terminal.active() + if (!activeId) return + requestAnimationFrame(() => { + if (terminal.active() !== activeId) return + focusTerminalById(activeId) + }) + } + + return ( + + ) +} diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index 036533c10fb8..fe450da85a5e 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -135,7 +135,8 @@ } } - #review-panel &[data-variant="normal"][data-orientation="horizontal"] { + #review-panel &[data-variant="normal"][data-orientation="horizontal"], + #terminal-panel &[data-variant="normal"][data-orientation="horizontal"] { background-color: var(--background-stronger); [data-slot="tabs-list"] { @@ -267,6 +268,51 @@ } } + #terminal-panel &[data-variant="normal"][data-orientation="horizontal"] { + [data-slot="tabs-list"] { + height: 52px; + padding-right: 12px; + gap: 8px; + } + + [data-slot="tabs-trigger-wrapper"] { + height: 28px; + padding-inline: 8px; + border: 0; + background-color: transparent; + box-shadow: none; + + &::after { + display: none; + } + + [data-slot="tabs-trigger"] { + padding: 0 !important; + } + + &:has([data-slot="tabs-trigger-close-button"]) { + padding-right: 8px; + } + + &:has([data-selected]) { + background-color: var(--v2-background-bg-layer-01); + box-shadow: inset 0 0 0 0.5px var(--v2-border-border-muted); + } + } + + [data-slot="terminal-tab-title"] { + font-family: Inter, sans-serif; + font-style: normal; + font-weight: 440; + font-size: 13px; + line-height: 16px; + letter-spacing: -0.04px; + color: var(--v2-text-text-base); + font-variation-settings: "slnt" 0; + font-variant-numeric: tabular-nums; + } + } + &[data-variant="alt"] { [data-slot="tabs-list"] { padding-left: 24px; From 7135bc4fb5a44b3ca5f9cf194de34a5527ea9e55 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 5 Jul 2026 02:02:39 +0000 Subject: [PATCH 09/34] chore: generate --- packages/app/e2e/utils/mock-server.ts | 5 ++++- packages/app/src/pages/session.tsx | 4 ++-- packages/app/src/pages/session/terminal-panel-v2.tsx | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index 06602c279217..e5df40c7d2de 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -68,7 +68,10 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { return json(route, await config.fileContent(url.searchParams.get("path") ?? "")) if (path === "/api/reference") return json(route, { - location: { directory: config.directory, project: { id: (config.project as { id?: string }).id, directory: config.directory } }, + location: { + directory: config.directory, + project: { id: (config.project as { id?: string }).id, directory: config.directory }, + }, data: [], }) if (emptyObject.has(path)) return json(route, {}) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index db2c1f03e5d6..8b422f713edc 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -443,8 +443,8 @@ export default function Page() { opened: layout.fileTree.opened(), }), ) - const desktopSessionResizeOpen = createMemo( - () => (newSessionDesign() ? desktopV2ReviewOpen() || desktopTerminalOpen() : desktopReviewOpen()), + const desktopSessionResizeOpen = createMemo(() => + newSessionDesign() ? desktopV2ReviewOpen() || desktopTerminalOpen() : desktopReviewOpen(), ) const desktopSidePanelOpen = createMemo(() => desktopSessionResizeOpen() || desktopFileTreeOpen()) const sessionPanelWidth = createMemo(() => { diff --git a/packages/app/src/pages/session/terminal-panel-v2.tsx b/packages/app/src/pages/session/terminal-panel-v2.tsx index aec999d5dc15..e59513cde779 100644 --- a/packages/app/src/pages/session/terminal-panel-v2.tsx +++ b/packages/app/src/pages/session/terminal-panel-v2.tsx @@ -55,7 +55,9 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) { const max = () => store.view * 0.6 const pane = () => Math.min(height(), max()) const stacked = createMemo(() => isDesktop() && props.stacked) - const panelHeight = createMemo(() => (isDesktop() ? (stacked() ? `${pane()}px` : "100%") : opened() ? `${pane()}px` : "0px")) + const panelHeight = createMemo(() => + isDesktop() ? (stacked() ? `${pane()}px` : "100%") : opened() ? `${pane()}px` : "0px", + ) const contentHeight = createMemo(() => (isDesktop() ? (stacked() ? `${pane()}px` : "100%") : `${pane()}px`)) const newTerminalKeybind = createMemo(() => command.keybindParts("terminal.new")) From efd5f0a80f3c7a4eeb54cfcdb5767cc78ea0ef21 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:47:37 +1000 Subject: [PATCH 10/34] fix(app): preserve timeline selection autoscroll (#35383) --- .../scroll-interaction.spec.ts | 34 +++++++++++++++++++ .../session/timeline/message-timeline.tsx | 9 +++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts b/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts index 4c2e99ffaa8e..e67545104d48 100644 --- a/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts @@ -48,6 +48,40 @@ test("does not reverse visible rows when the user wheels during shell remeasurem await reportVisualStability(testInfo, "wheel-during-resize", trace, rowPairPlan(regions, 1)) }) +test("keeps moving upward while drag-selecting above the timeline", async ({ page }) => { + await setupTimeline(page, { + messages: history(80), + viewport: { width: 1400, height: 700 }, + reducedMotion: true, + }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + const text = page.getByText("History 79.", { exact: false }) + await expect(text).toBeVisible() + await scroller.evaluate((element) => { + element.dataset.selectionLength = "0" + document.addEventListener("selectionchange", () => { + element.dataset.selectionLength = String( + Math.max(Number(element.dataset.selectionLength), window.getSelection()?.toString().length ?? 0), + ) + }) + }) + const textBox = await text.boundingBox() + const scrollBox = await scroller.boundingBox() + expect(textBox).not.toBeNull() + expect(scrollBox).not.toBeNull() + if (!textBox || !scrollBox) return + + await page.mouse.move(textBox.x + textBox.width - 10, textBox.y + textBox.height / 2) + await page.mouse.down() + await page.mouse.move(textBox.x + 20, scrollBox.y - 120, { steps: 30 }) + + await expect.poll(() => scroller.evaluate((element) => Number(element.dataset.selectionLength))).toBeGreaterThan(0) + await expect + .poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeGreaterThan(500) + await page.mouse.up() +}) + test("does not pull a keyboard-scrolled user during shell remeasurement", async ({ page }, testInfo) => { const shellID = "prt_keyboard_01_shell" const followingID = "prt_keyboard_02_following" diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 9d8d82bcc9be..2e2c65af3b33 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -598,8 +598,12 @@ export function MessageTimeline(props: { const handleListPointerDown = (event: PointerEvent & { currentTarget: HTMLDivElement }) => { if (!prependLoading) clearPrependAnchor() - if (event.target !== event.currentTarget) return - props.onMarkScrollGesture(event.currentTarget) + props.onMarkScrollGesture(event.target) + } + + const handleListPointerMove = (event: PointerEvent) => { + if (event.buttons !== 1) return + props.onMarkScrollGesture(event.target) } const handleListKeyDown = (event: KeyboardEvent & { currentTarget: HTMLDivElement }) => { @@ -1356,6 +1360,7 @@ export function MessageTimeline(props: { onTouchEnd={handleListTouchEnd} onTouchCancel={handleListTouchEnd} onPointerDown={handleListPointerDown} + onPointerMove={handleListPointerMove} onKeyDown={handleListKeyDown} onScroll={handleListScroll} onClick={props.onAutoScrollInteraction} From b7e4f1ef7433f83ba009eefa2997aeb81017f6ed Mon Sep 17 00:00:00 2001 From: Jack Date: Sun, 5 Jul 2026 14:00:14 +0800 Subject: [PATCH 11/34] fix: update Feishu community link (#35392) --- README.zh.md | 2 +- README.zht.md | 2 +- packages/console/app/src/routes/feishu.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.zh.md b/README.zh.md index 93f81e83cd0c..352e13ccaa1d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -125,4 +125,4 @@ OpenCode 内置两种 Agent,可用 `Tab` 键快速切换: --- -**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode) +**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=52ao9352-5623-4fa0-b7dd-3407c392c1af&qr_code=true) | [X.com](https://x.com/opencode) diff --git a/README.zht.md b/README.zht.md index 59d9709bd39f..f09358b1330b 100644 --- a/README.zht.md +++ b/README.zht.md @@ -125,4 +125,4 @@ OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。 --- -**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode) +**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=52ao9352-5623-4fa0-b7dd-3407c392c1af&qr_code=true) | [X.com](https://x.com/opencode) diff --git a/packages/console/app/src/routes/feishu.ts b/packages/console/app/src/routes/feishu.ts index 3366e7208be4..2b13fb49bfe4 100644 --- a/packages/console/app/src/routes/feishu.ts +++ b/packages/console/app/src/routes/feishu.ts @@ -2,6 +2,6 @@ import { redirect } from "@solidjs/router" export async function GET() { return redirect( - "https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true", + "https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=52ao9352-5623-4fa0-b7dd-3407c392c1af&qr_code=true", ) } From f14eafe9db38a487681f76d12e8c469a7826fa0d Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 5 Jul 2026 09:30:46 -0500 Subject: [PATCH 12/34] refactor(codemode): remove generic agent tool (#35417) --- packages/codemode/README.md | 10 ++----- packages/codemode/codemode.md | 4 +-- packages/codemode/src/codemode.ts | 35 ++++++------------------- packages/codemode/src/index.ts | 3 +-- packages/codemode/test/codemode.test.ts | 31 ++++++---------------- 5 files changed, 21 insertions(+), 62 deletions(-) diff --git a/packages/codemode/README.md b/packages/codemode/README.md index 1230b162967c..54d31b4e312c 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -4,7 +4,7 @@ Effect-native confined code execution over explicit, schema-described tools. CodeMode lets a model write a small JavaScript program that can call only the tools supplied by the host. The program can sequence calls, transform plain data, branch, loop, and run independent calls in parallel without receiving ambient filesystem, process, network, module, or application authority. -The package is currently private to this workspace. Its API is designed around three uses: +The package is currently private to this workspace. Its API is designed around one-shot and reusable execution: ```ts // One execution @@ -13,9 +13,6 @@ yield * CodeMode.execute({ tools, code }) // A reusable runtime const runtime = CodeMode.make({ tools, limits }) yield * runtime.execute(code) - -// One agent-facing code tool -const codeTool = runtime.agentTool() ``` ## Install @@ -117,10 +114,9 @@ const runtime = CodeMode.make({ runtime.catalog() // structured tool descriptions runtime.instructions() // model-facing syntax and tool guide runtime.execute(source) // ExecuteResult -runtime.agentTool() // { name, description, input, output, execute } ``` -`catalog`, `instructions`, and `agentTool` are projections of the same configured tool tree. `agentTool().description` is exactly `instructions()`. +`CodeMode.Input` and `CodeMode.Result` are Effect schemas for the execution request and result. Hosts can combine them with `runtime.instructions()` and `runtime.execute()` when constructing a framework-specific agent tool. ### Results @@ -333,8 +329,6 @@ A program cannot gain authority through prose or generated code. It can only exe The public contract is guided by these equivalences: - `CodeMode.execute({ ...options, code })` is equivalent to `CodeMode.make(options).execute(code)`. -- `CodeMode.make(options).agentTool().execute({ code })` is equivalent to `CodeMode.make(options).execute(code)`. -- `CodeMode.make(options).agentTool().description` equals `CodeMode.make(options).instructions()`. - A tool implementation is not invoked unless its input has decoded successfully. - A tool result is not visible to the program unless its output has decoded and crossed the plain-data boundary successfully. - Unknown host failures do not become model-visible diagnostics; `ToolError` is the explicit safe-message channel. diff --git a/packages/codemode/codemode.md b/packages/codemode/codemode.md index 41b801044dcf..2ae45c56bf00 100644 --- a/packages/codemode/codemode.md +++ b/packages/codemode/codemode.md @@ -246,7 +246,7 @@ maxOutputBytes? }` (defaults 10_000 / 100 / 32_000). This wave kept the other kn serialized values become truncated text + ` [result truncated: N bytes exceeds the M-byte output limit; return a smaller value]`; logs keep leading lines within the remaining budget - `[logs truncated: showing K of N lines]`; result gains `truncated: true` (also added to - `ExecuteResultSchema`). UTF-8-safe truncation (no split code points). (The in-sandbox + `CodeMode.Result`). UTF-8-safe truncation (no split code points). (The in-sandbox `maxDataBytes` check that used to throw first on oversized raw values died in Fix 5 - truncation is now the only result-size mechanism.) - **Search polish**: default limit 12 -> **10** (`defaultSearchLimit`); exact-path lookup - a @@ -575,7 +575,7 @@ configurable knobs; the internal limit system dies): `maxCollectionLength` (every array-length/object-field-count check - this knob was actively harmful: an MCP tool returning 20k rows failed). The `OperationLimitExceeded` and `AuditLimitExceeded` diagnostic kinds are gone from the `DiagnosticKind` union and - `ExecuteResultSchema` (fine - the package is unreleased). + `CodeMode.Result` (fine - the package is unreleased). - **Fixed constants, not knobs**: `TOOL_CALL_CONCURRENCY = 8` (codemode.ts; the fork semaphore) and `MAX_VALUE_DEPTH = 32` (tool-runtime.ts; the `copyIn` depth check - kept only because it produces a clearer error than a native stack-overflow RangeError; still diff --git a/packages/codemode/src/codemode.ts b/packages/codemode/src/codemode.ts index f8a01f23b0b0..6aa030e52109 100644 --- a/packages/codemode/src/codemode.ts +++ b/packages/codemode/src/codemode.ts @@ -108,14 +108,14 @@ export type ExecuteFailure = { /** Result of executing a CodeMode program. Program failures are data, not Effect failures. */ export type ExecuteResult = ExecuteSuccess | ExecuteFailure -/** Reusable CodeMode configuration shared by `execute` and `agentTool`. */ +/** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */ export type CodeModeOptions = {}> = Omit, "code"> & { /** Progressive-disclosure configuration for the agent-facing tool catalog. */ readonly discovery?: DiscoveryOptions } -/** Input schema for the single agent-facing tool produced by `runtime.agentTool()`. */ -export const ExecuteInputSchema = Schema.Struct({ code: Schema.String }) +/** Schema for a CodeMode execution request. */ +const Input = Schema.Struct({ code: Schema.String }) const DiagnosticKindSchema = Schema.Literals([ "ParseError", @@ -130,8 +130,8 @@ const DiagnosticKindSchema = Schema.Literals([ "ExecutionFailure", ]) -/** Structured success or diagnostic result schema returned by CodeMode execution. */ -export const ExecuteResultSchema = Schema.Union([ +/** Schema for the structured success or diagnostic returned by CodeMode execution. */ +const Result = Schema.Union([ Schema.Struct({ ok: Schema.Literal(true), value: Schema.Json, @@ -153,23 +153,12 @@ export const ExecuteResultSchema = Schema.Union([ }), ]) -/** Agent-facing projection of a configured CodeMode runtime. */ -export type AgentToolDefinition = { - readonly name: "code" - readonly description: string - readonly input: typeof ExecuteInputSchema - readonly output: typeof ExecuteResultSchema - readonly execute: (input: { readonly code: string }) => Effect.Effect -} - /** Reusable confined runtime over one explicit tool tree. */ export type CodeModeRuntime = { /** Lists schema-described tool paths provided by the host. */ readonly catalog: () => ReadonlyArray /** Builds model-facing syntax guidance and visible tool signatures. */ readonly instructions: () => string - /** Projects the configured runtime as one agent-facing `code` tool. */ - readonly agentTool: () => AgentToolDefinition /** Executes a program using this runtime's configured host tools. */ readonly execute: (code: string) => Effect.Effect } @@ -4088,13 +4077,12 @@ export const execute = >( /** * Creates an Effect-native runtime over explicit, schema-described tools. * - * Use `execute` for host-driven execution or `agentTool` to expose one confined code tool to an - * agent framework. Tool requirements remain in the returned Effect environment. + * Use `execute` for host-driven execution. Tool requirements remain in the returned Effect environment. * * @example * ```ts * const runtime = CodeMode.make({ tools: { orders: { lookup } } }) - * const code = runtime.agentTool() + * const result = runtime.execute("return await tools.orders.lookup({ id: 'order_42' })") * ``` */ export const make = = {}>( @@ -4111,16 +4099,9 @@ export const make = = {}>( return { catalog: () => catalog, instructions: () => instructions, - agentTool: () => ({ - name: "code", - description: instructions, - input: ExecuteInputSchema, - output: ExecuteResultSchema, - execute: ({ code }) => executeProgram(code), - }), execute: executeProgram, } } /** Constructors for one-shot and reusable CodeMode execution. */ -export const CodeMode = { make, execute } +export const CodeMode = { Input, Result, make, execute } diff --git a/packages/codemode/src/index.ts b/packages/codemode/src/index.ts index 1aea6644da27..9a21e9f640a1 100644 --- a/packages/codemode/src/index.ts +++ b/packages/codemode/src/index.ts @@ -1,10 +1,9 @@ -export { ToolError, CodeMode, ExecuteInputSchema, ExecuteResultSchema, toolError } from "./codemode.js" +export { ToolError, CodeMode, toolError } from "./codemode.js" export { Tool } from "./tool.js" export * as OpenAPI from "./openapi/index.js" export type { Definition as ToolDefinition, JsonSchema, ToolSchema } from "./tool.js" export type { ToolCallEnded, ToolCallHooks } from "./tool-runtime.js" export type { - AgentToolDefinition, CodeModeOptions, CodeModeRuntime, DataValue, diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index 4fda88b384fb..9db7739296c2 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -1,13 +1,6 @@ import { describe, expect, test } from "bun:test" import { Cause, Effect, Schema } from "effect" -import { - CodeMode, - ExecuteInputSchema, - ExecuteResultSchema, - Tool, - toolError, - type ExecutionLimits, -} from "../src/index.js" +import { CodeMode, Tool, toolError, type ExecutionLimits } from "../src/index.js" import type { Definition } from "../src/tool.js" const run = (tool: Definition) => @@ -235,7 +228,7 @@ describe("CodeMode console capture", () => { logs: ['Thread info: {"name":"Demo","count":2}', "[warn] careful"], toolCalls: [], }) - expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) + expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) }) test("keeps logs captured before failures", async () => { @@ -371,7 +364,7 @@ describe("CodeMode output budget", () => { expect(result.value).toMatch( /^\{"data":"x+ \[result truncated: \d+ bytes exceeds the 40-byte output limit; return a smaller value\]$/, ) - expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) + expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) }) test("keeps leading logs within the remaining budget and marks the cut", async () => { @@ -501,24 +494,16 @@ describe("CodeMode public contract", () => { const tools = { orders: { lookup } } const source = `return await tools.orders.lookup({ id: "order_42" })` - test("keeps one-shot, reusable, and agent-tool execution equivalent", async () => { + test("keeps one-shot and reusable execution equivalent", async () => { const runtime = CodeMode.make({ tools }) - const agentTool = runtime.agentTool() - const [oneShot, reusable, projected] = await Promise.all([ + const [oneShot, reusable] = await Promise.all([ Effect.runPromise(CodeMode.execute({ tools, code: source })), Effect.runPromise(runtime.execute(source)), - Effect.runPromise(agentTool.execute({ code: source })), ]) expect(reusable).toStrictEqual(oneShot) - expect(projected).toStrictEqual(oneShot) - expect(agentTool.name).toBe("code") - expect(agentTool.input).toBe(ExecuteInputSchema) - expect(agentTool.output).toBe(ExecuteResultSchema) - expect(agentTool.description).toBe(runtime.instructions()) - expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(projected)))).toStrictEqual( - projected, - ) + expect(Schema.decodeUnknownSync(CodeMode.Input)({ code: source })).toStrictEqual({ code: source }) + expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(reusable)))).toStrictEqual(reusable) }) test("inlines a COMPLETE small catalog and keeps search registered but unadvertised", async () => { @@ -1035,7 +1020,7 @@ describe("CodeMode public contract", () => { value: { top: null, nested: [1, null] }, toolCalls: [], }) - expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) + expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) }) test("rejects invalid configuration and discovery limits", async () => { From be73f465df6b20e0c3091f49ab83e89c0ede3b35 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:34:58 -0500 Subject: [PATCH 13/34] refactor(codemode): namespace public types (#35425) --- packages/codemode/README.md | 28 ++--- packages/codemode/codemode.md | 14 +-- packages/codemode/src/codemode.ts | 135 +++++++++--------------- packages/codemode/src/index.ts | 23 +--- packages/codemode/src/tool-api.ts | 2 + packages/codemode/test/codemode.test.ts | 12 +-- packages/codemode/test/parity.test.ts | 2 +- packages/codemode/test/promise.test.ts | 11 +- packages/opencode/src/tool/code-mode.ts | 17 +-- 9 files changed, 98 insertions(+), 146 deletions(-) create mode 100644 packages/codemode/src/tool-api.ts diff --git a/packages/codemode/README.md b/packages/codemode/README.md index 54d31b4e312c..0be6d769c7cb 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -60,7 +60,7 @@ const result = `) ``` -`result` is always an `ExecuteResult`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption. +`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption. Successful result values are JSON-safe data. A program that returns `undefined`, including by reaching the end without `return`, produces `null`; nested `undefined` values are normalized to `null` as well. @@ -83,6 +83,8 @@ const tool = Tool.make({ The description and schemas are part of the model-visible tool contract. Keep descriptions concrete and put authorization in `run` or in the service it calls. +Public tool types are grouped under the same namespace: `Tool.Definition`, `Tool.Options`, `Tool.SchemaType`, and `Tool.JsonSchema`. + ### `CodeMode.execute` Use `CodeMode.execute` for a single execution: @@ -113,30 +115,32 @@ const runtime = CodeMode.make({ runtime.catalog() // structured tool descriptions runtime.instructions() // model-facing syntax and tool guide -runtime.execute(source) // ExecuteResult +runtime.execute(source) // CodeMode.Result ``` -`CodeMode.Input` and `CodeMode.Result` are Effect schemas for the execution request and result. Hosts can combine them with `runtime.instructions()` and `runtime.execute()` when constructing a framework-specific agent tool. +`CodeMode.Input`, `CodeMode.Result`, `CodeMode.Success`, `CodeMode.Failure`, `CodeMode.Diagnostic`, and `CodeMode.DiagnosticKind` are both Effect schemas and their inferred TypeScript types. Hosts can combine `CodeMode.Input` and `CodeMode.Result` with `runtime.instructions()` and `runtime.execute()` when constructing a framework-specific agent tool. + +All other CodeMode types use the same namespace: `CodeMode.Options`, `CodeMode.ExecuteOptions`, `CodeMode.Runtime`, `CodeMode.ExecutionLimits`, `CodeMode.DiscoveryOptions`, `CodeMode.DataValue`, `CodeMode.ToolDescription`, and the `CodeMode.ToolCall*` observation types. ### Results ```ts -type ExecuteResult = ExecuteSuccess | ExecuteFailure +type Result = Success | Failure -interface ExecuteSuccess { +interface Success { readonly ok: true - readonly value: Schema.Json + readonly value: CodeMode.DataValue readonly logs?: ReadonlyArray readonly truncated?: boolean - readonly toolCalls: ReadonlyArray + readonly toolCalls: ReadonlyArray } -interface ExecuteFailure { +interface Failure { readonly ok: false - readonly error: Diagnostic + readonly error: CodeMode.Diagnostic readonly logs?: ReadonlyArray readonly truncated?: boolean - readonly toolCalls: ReadonlyArray + readonly toolCalls: ReadonlyArray } ``` @@ -300,7 +304,7 @@ import { toolError } from "@opencode-ai/codemode" run: ({ id }) => (authorized(id) ? loadOrder(id) : Effect.fail(toolError("Order is unavailable"))) ``` -Only the supplied message is model-visible. The optional cause is never returned in `ExecuteResult`; hosts should perform any required internal logging before crossing this boundary. +Only the supplied message is model-visible. The optional cause is never returned in `CodeMode.Result`; hosts should perform any required internal logging before crossing this boundary. ## Authority Boundary @@ -332,7 +336,7 @@ The public contract is guided by these equivalences: - A tool implementation is not invoked unless its input has decoded successfully. - A tool result is not visible to the program unless its output has decoded and crossed the plain-data boundary successfully. - Unknown host failures do not become model-visible diagnostics; `ToolError` is the explicit safe-message channel. -- Host interruption remains interruption rather than an `ExecuteFailure`. +- Host interruption remains interruption rather than a `CodeMode.Failure`. ## Non-Goals diff --git a/packages/codemode/codemode.md b/packages/codemode/codemode.md index 2ae45c56bf00..732c565b72dc 100644 --- a/packages/codemode/codemode.md +++ b/packages/codemode/codemode.md @@ -222,7 +222,7 @@ wave; both packages typecheck clean. result is exposed as-is. Discrimination via `Schema.isSchema`. New helpers exported from `tool.ts`: `inputTypeScript`/`outputTypeScript`/`decodeInput`/`decodeOutput`/ `jsonSchemaToTypeScript`; `tool-runtime.ts` consumes them (no direct `Schema.*` use there - anymore). Types `JsonSchema`/`ToolSchema` exported from the index. Note: an empty + anymore). Types `Tool.JsonSchema`/`Tool.SchemaType` exported from the index. Note: an empty `Schema.Struct({})` renders as `{ } | Array` (effect's JSON Schema emission) - cosmetic, fixed in Wave 4. - **`output.*` API deleted**: `OutputItem`(+Schema), result `output` fields, the `output` @@ -237,7 +237,7 @@ wave; both packages typecheck clean. so failures are typed and observable). `message` is the model-safe failure message (`ToolError`/`ToolRuntimeError` message, else "Tool execution failed"). Interrupted calls fire no end event (timeout kills the whole execution anyway). -- **Limits collapse**: public `ExecutionLimits` = `{ timeoutMs?, maxToolCalls?, +- **Limits collapse**: public `CodeMode.ExecutionLimits` = `{ timeoutMs?, maxToolCalls?, maxOutputBytes? }` (defaults 10_000 / 100 / 32_000). This wave kept the other knobs as internal defaults reachable through an `@internal` `InternalExecutionLimits` type; Fix 5 later deleted that type and the internal limit system entirely. @@ -313,7 +313,7 @@ real MCP config. Package still 101 tests / 0 fail; opencode adapter suites still packages typecheck clean. - **Budgeted catalog** (`discoveryPlan` in `tool-runtime.ts`): the all-or-nothing - inline/search modes are gone - `DiscoveryMode` deleted, `DiscoveryOptions` is just + inline/search modes are gone - `DiscoveryMode` deleted, `CodeMode.DiscoveryOptions` is just `{ maxInlineCatalogBytes? }` (default 16,000 UTF-8 bytes; later converted to `maxInlineCatalogTokens`, default 4,000 estimated tokens - see Post-wave fixes). Port of the old opencode @@ -542,7 +542,7 @@ budget; namespaces must always be present): - `src/token.ts` added: copy of `@opencode-ai/core/util/token` (`round(chars / 4)`), so the package stays dependency-free; keep in sync if the core heuristic changes. -- `DiscoveryOptions.maxInlineCatalogBytes` -> `maxInlineCatalogTokens` (default 4,000 +- `CodeMode.DiscoveryOptions.maxInlineCatalogBytes` -> `maxInlineCatalogTokens` (default 4,000 estimated tokens ~ the old 16,000 bytes at 4 chars/token - behavior parity, not a size reduction). `discoveryPlan` charges `estimate(catalogLine(tool))` per line; cheapest-first - stop-on-first-miss unchanged at the time (stop-on-first-miss replaced by round-robin in @@ -558,7 +558,7 @@ budget; namespaces must always be present): **Fix 5 - internal limits removed** (user direction: only the three PUBLIC limits survive as configurable knobs; the internal limit system dies): -- `ExecutionLimits` (`timeoutMs` 10_000 / `maxToolCalls` 100 / `maxOutputBytes` 32_000 at +- `CodeMode.ExecutionLimits` (`timeoutMs` 10_000 / `maxToolCalls` 100 / `maxOutputBytes` 32_000 at the time; Fix 6 later removed the first two defaults. Same validation: safe integers, timeoutMs >= 1, others >= 0, RangeError otherwise) is now the ENTIRE limit surface - exactly the shape section 2's original locked spec named. @@ -602,7 +602,7 @@ configurable knobs; the internal limit system dies): enumeration operation-budget, codemode maxDataBytes/maxSourceBytes/maxOperations/ maxConcurrency-RangeError assertions, and the adapter's runaway-loop-via-operation-limit test - superseded by the package timeout regression test); rewrote the helpers that used - `InternalExecutionLimits` as a convenience to plain `ExecutionLimits` + `InternalExecutionLimits` as a convenience to plain `CodeMode.ExecutionLimits` (promise/enumeration/stdlib run helpers). Package suite: 154 pass / 0 fail; adapter suites: 34 + 16. @@ -849,7 +849,7 @@ section 4 outer-truncation item the OPPOSITE way from "kill the outer one"): that relied on the old default now asserts the oversized result reaches the shared wrapper un-truncated. Suites: 210 + 50, tsgo clean both. -**Docs polish** (post-API-review): stale `DiscoveryOptions` JSDoc fixed (claimed default +**Docs polish** (post-API-review): stale `CodeMode.DiscoveryOptions` JSDoc fixed (claimed default 4,000 and alphabetical cheapest-first - now 2,000 and round-robin, matching Fix 8/9 reality) and the README's incorrect "`effect` as a peer dependency" line corrected (`effect` is a regular dependency; hosts depend on it themselves because the API surface is Effect-typed). diff --git a/packages/codemode/src/codemode.ts b/packages/codemode/src/codemode.ts index 6aa030e52109..b207efe46a8c 100644 --- a/packages/codemode/src/codemode.ts +++ b/packages/codemode/src/codemode.ts @@ -19,8 +19,7 @@ import { ToolError } from "./tool-error.js" import { isSandboxValue, SandboxDate, SandboxMap, SandboxPromise, SandboxRegExp, SandboxSet } from "./values.js" /** A tool call admitted during an execution. */ -export type { ToolCall, ToolCallStarted, ToolDescription } from "./tool-runtime.js" -export { ToolError, toolError } from "./tool-error.js" +export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescription } from "./tool-runtime.js" /** Resource budgets enforced independently during each CodeMode program execution. */ export type ExecutionLimits = { @@ -74,50 +73,20 @@ export type ExecuteOptions = {}> = { onToolCallEnd?: (call: ToolRuntime.ToolCallEnded) => Effect.Effect> } -/** A normalized program diagnostic safe to return across an agent tool boundary. */ -export type Diagnostic = { - readonly kind: DiagnosticKind - readonly message: string - readonly location?: { readonly line: number; readonly column: number } - readonly suggestions?: ReadonlyArray -} - /** A JSON value that can cross the confined interpreter boundary. */ export type DataValue = Schema.Json -/** Successful execution after the result has crossed the plain-data boundary. */ -export type ExecuteSuccess = { - readonly ok: true - readonly value: DataValue - readonly logs?: ReadonlyArray - /** Present when the value or logs were truncated to fit `maxOutputBytes`. */ - readonly truncated?: boolean - readonly toolCalls: ReadonlyArray -} - -/** Failed execution with calls admitted before the diagnostic was produced. */ -export type ExecuteFailure = { - readonly ok: false - readonly error: Diagnostic - readonly logs?: ReadonlyArray - /** Present when the logs were truncated to fit `maxOutputBytes`. */ - readonly truncated?: boolean - readonly toolCalls: ReadonlyArray -} - -/** Result of executing a CodeMode program. Program failures are data, not Effect failures. */ -export type ExecuteResult = ExecuteSuccess | ExecuteFailure - /** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */ -export type CodeModeOptions = {}> = Omit, "code"> & { +export type Options = {}> = Omit, "code"> & { /** Progressive-disclosure configuration for the agent-facing tool catalog. */ readonly discovery?: DiscoveryOptions } -/** Schema for a CodeMode execution request. */ -const Input = Schema.Struct({ code: Schema.String }) +/** Schema for a host tool input containing CodeMode source. */ +export const Input = Schema.Struct({ code: Schema.String }) +export type Input = typeof Input.Type -const DiagnosticKindSchema = Schema.Literals([ +export const DiagnosticKind = Schema.Literals([ "ParseError", "UnsupportedSyntax", "UnknownTool", @@ -129,38 +98,52 @@ const DiagnosticKindSchema = Schema.Literals([ "ToolFailure", "ExecutionFailure", ]) +/** Stable categories produced by program, schema, tool, and limit failures. */ +export type DiagnosticKind = typeof DiagnosticKind.Type + +export const Diagnostic = Schema.Struct({ + kind: DiagnosticKind, + message: Schema.String, + location: Schema.optionalKey(Schema.Struct({ line: Schema.Number, column: Schema.Number })), + suggestions: Schema.optionalKey(Schema.Array(Schema.String)), +}) +/** A normalized program diagnostic safe to return across an agent tool boundary. */ +export type Diagnostic = typeof Diagnostic.Type + +const ToolCallSchema = Schema.Struct({ name: Schema.String }) +export const Success = Schema.Struct({ + ok: Schema.Literal(true), + value: Schema.Json, + logs: Schema.optionalKey(Schema.Array(Schema.String)), + truncated: Schema.optionalKey(Schema.Boolean), + toolCalls: Schema.Array(ToolCallSchema), +}) +/** Successful execution after the result has crossed the plain-data boundary. */ +export type Success = typeof Success.Type + +export const Failure = Schema.Struct({ + ok: Schema.Literal(false), + error: Diagnostic, + logs: Schema.optionalKey(Schema.Array(Schema.String)), + truncated: Schema.optionalKey(Schema.Boolean), + toolCalls: Schema.Array(ToolCallSchema), +}) +/** Failed execution with calls admitted before the diagnostic was produced. */ +export type Failure = typeof Failure.Type /** Schema for the structured success or diagnostic returned by CodeMode execution. */ -const Result = Schema.Union([ - Schema.Struct({ - ok: Schema.Literal(true), - value: Schema.Json, - logs: Schema.optionalKey(Schema.Array(Schema.String)), - truncated: Schema.optionalKey(Schema.Boolean), - toolCalls: Schema.Array(Schema.Struct({ name: Schema.String })), - }), - Schema.Struct({ - ok: Schema.Literal(false), - error: Schema.Struct({ - kind: DiagnosticKindSchema, - message: Schema.String, - location: Schema.optionalKey(Schema.Struct({ line: Schema.Number, column: Schema.Number })), - suggestions: Schema.optionalKey(Schema.Array(Schema.String)), - }), - logs: Schema.optionalKey(Schema.Array(Schema.String)), - truncated: Schema.optionalKey(Schema.Boolean), - toolCalls: Schema.Array(Schema.Struct({ name: Schema.String })), - }), -]) +export const Result = Schema.Union([Success, Failure]) +/** Result of executing a CodeMode program. Program failures are data, not Effect failures. */ +export type Result = typeof Result.Type /** Reusable confined runtime over one explicit tool tree. */ -export type CodeModeRuntime = { +export type Runtime = { /** Lists schema-described tool paths provided by the host. */ readonly catalog: () => ReadonlyArray /** Builds model-facing syntax guidance and visible tool signatures. */ readonly instructions: () => string /** Executes a program using this runtime's configured host tools. */ - readonly execute: (code: string) => Effect.Effect + readonly execute: (code: string) => Effect.Effect } type SourcePosition = { @@ -275,19 +258,6 @@ const errorBrandName = (value: unknown): string | undefined => ? ((value as Record)[ErrorBrand] as string | undefined) : undefined -/** Stable categories produced by program, schema, tool, and limit failures. */ -export type DiagnosticKind = - | "ParseError" - | "UnsupportedSyntax" - | "UnknownTool" - | "InvalidToolInput" - | "InvalidToolOutput" - | "InvalidDataValue" - | "ToolCallLimitExceeded" - | "TimeoutExceeded" - | "ToolFailure" - | "ExecutionFailure" - const arrayMethods = new Set([ "map", "filter", @@ -3943,7 +3913,7 @@ const executeWithLimits = >( options: ExecuteOptions, limits: ResolvedExecutionLimits, searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"], -): Effect.Effect> => { +): Effect.Effect> => { const hooks = { ...(options.onToolCallStart === undefined ? {} : { onToolCallStart: options.onToolCallStart }), ...(options.onToolCallEnd === undefined ? {} : { onToolCallEnd: options.onToolCallEnd }), @@ -3975,7 +3945,7 @@ const executeWithLimits = >( value: result, ...logged(), toolCalls: tools.calls, - } satisfies ExecuteResult + } satisfies Result }).pipe((program) => { const timeoutMs = limits.timeoutMs if (timeoutMs === undefined) return program @@ -3988,7 +3958,7 @@ const executeWithLimits = >( error: { kind: "TimeoutExceeded", message: `Execution timed out after ${timeoutMs}ms.` }, ...logged(), toolCalls: tools.calls, - } satisfies ExecuteResult), + } satisfies Result), }), ) }) @@ -4002,7 +3972,7 @@ const executeWithLimits = >( error: normalizeError(Cause.squash(cause)), ...logged(), toolCalls: tools.calls, - } satisfies ExecuteResult), + } satisfies Result), ), Effect.map((result) => (limits.maxOutputBytes === undefined ? result : boundOutput(result, limits.maxOutputBytes))), ) @@ -4026,7 +3996,7 @@ const utf8Truncate = (value: string, maxBytes: number): string => { * fails the execution; `truncated: true` marks affected results. Only runs when the host set * `maxOutputBytes` - with the limit absent, output passes through unbounded. */ -const boundOutput = (result: ExecuteResult, maxOutputBytes: number): ExecuteResult => { +const boundOutput = (result: Result, maxOutputBytes: number): Result => { let truncated = false let value: DataValue = null @@ -4068,7 +4038,7 @@ const boundOutput = (result: ExecuteResult, maxOutputBytes: number): ExecuteResu export const execute = >( options: ExecuteOptions, -): Effect.Effect> => { +): Effect.Effect> => { const tools = (options.tools ?? {}) as HostTools> ToolRuntime.assertValidTools(tools) return executeWithLimits(options, resolveExecutionLimits(options.limits), ToolRuntime.searchIndex(tools)) @@ -4086,8 +4056,8 @@ export const execute = >( * ``` */ export const make = = {}>( - options: CodeModeOptions = {} as CodeModeOptions, -): CodeModeRuntime> => { + options: Options = {} as Options, +): Runtime> => { const tools = (options.tools ?? {}) as HostTools> ToolRuntime.assertValidTools(tools) const limits = resolveExecutionLimits(options.limits) @@ -4102,6 +4072,3 @@ export const make = = {}>( execute: executeProgram, } } - -/** Constructors for one-shot and reusable CodeMode execution. */ -export const CodeMode = { Input, Result, make, execute } diff --git a/packages/codemode/src/index.ts b/packages/codemode/src/index.ts index 9a21e9f640a1..3fdfcd45e7fd 100644 --- a/packages/codemode/src/index.ts +++ b/packages/codemode/src/index.ts @@ -1,21 +1,4 @@ -export { ToolError, CodeMode, toolError } from "./codemode.js" -export { Tool } from "./tool.js" +export * as CodeMode from "./codemode.js" +export * as Tool from "./tool-api.js" export * as OpenAPI from "./openapi/index.js" -export type { Definition as ToolDefinition, JsonSchema, ToolSchema } from "./tool.js" -export type { ToolCallEnded, ToolCallHooks } from "./tool-runtime.js" -export type { - CodeModeOptions, - CodeModeRuntime, - DataValue, - Diagnostic, - DiagnosticKind, - DiscoveryOptions, - ExecuteFailure, - ExecuteOptions, - ExecuteResult, - ExecuteSuccess, - ExecutionLimits, - ToolCall, - ToolCallStarted, - ToolDescription, -} from "./codemode.js" +export { ToolError, toolError } from "./tool-error.js" diff --git a/packages/codemode/src/tool-api.ts b/packages/codemode/src/tool-api.ts new file mode 100644 index 000000000000..96ce68d046b0 --- /dev/null +++ b/packages/codemode/src/tool-api.ts @@ -0,0 +1,2 @@ +export { isDefinition, make } from "./tool.js" +export type { Definition, JsonSchema, Options, ToolSchema as SchemaType } from "./tool.js" diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index 9db7739296c2..087678c778cb 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -1,9 +1,8 @@ import { describe, expect, test } from "bun:test" import { Cause, Effect, Schema } from "effect" -import { CodeMode, Tool, toolError, type ExecutionLimits } from "../src/index.js" -import type { Definition } from "../src/tool.js" +import { CodeMode, Tool, toolError } from "../src/index.js" -const run = (tool: Definition) => +const run = (tool: Tool.Definition) => Effect.runPromise(CodeMode.make({ tools: { host: { call: tool } } }).execute("return await tools.host.call({})")) class UnsafeHostError extends Schema.TaggedErrorClass()("UnsafeHostError", { @@ -349,7 +348,7 @@ describe("CodeMode output budget", () => { }) test("truncates an oversized result value with a marker instead of failing", async () => { - const limits: ExecutionLimits = { maxOutputBytes: 40 } + const limits: CodeMode.ExecutionLimits = { maxOutputBytes: 40 } const result = await Effect.runPromise( CodeMode.execute({ code: `return { data: "${"x".repeat(200)}" }`, @@ -368,7 +367,7 @@ describe("CodeMode output budget", () => { }) test("keeps leading logs within the remaining budget and marks the cut", async () => { - const limits: ExecutionLimits = { maxOutputBytes: 40 } + const limits: CodeMode.ExecutionLimits = { maxOutputBytes: 40 } const result = await Effect.runPromise( CodeMode.execute({ code: ` @@ -502,7 +501,8 @@ describe("CodeMode public contract", () => { ]) expect(reusable).toStrictEqual(oneShot) - expect(Schema.decodeUnknownSync(CodeMode.Input)({ code: source })).toStrictEqual({ code: source }) + const input: CodeMode.Input = { code: source } + expect(Schema.decodeUnknownSync(CodeMode.Input)(input)).toStrictEqual(input) expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(reusable)))).toStrictEqual(reusable) }) diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index e5c1d8382219..dfa85831837c 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -3,7 +3,7 @@ import { Effect } from "effect" import { CodeMode } from "../src/index.js" import { ToolRuntime } from "../src/tool-runtime.js" -// Runs a CodeMode program with no host tools and returns the ExecuteResult. These tests pin the +// Runs a CodeMode program with no host tools and returns the CodeMode.Result. These tests pin the // JS-parity behaviors for the "99% of ordinary defensive JavaScript just works" goal: cases where // a strict interpreter would throw but idiomatic JS yields undefined / succeeds. // diff --git a/packages/codemode/test/promise.test.ts b/packages/codemode/test/promise.test.ts index 952b2fdc504f..545d463abfac 100644 --- a/packages/codemode/test/promise.test.ts +++ b/packages/codemode/test/promise.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Effect, Schema } from "effect" -import { CodeMode, Tool, toolError, type ExecuteResult, type ExecutionLimits } from "../src/index.js" +import { CodeMode, Tool, toolError } from "../src/index.js" // Wave 5 acceptance suite: first-class promise values. Un-awaited tool calls start eagerly on // supervised fibers, `await` settles them, and Promise.all/allSettled/race/resolve/reject are @@ -48,7 +48,10 @@ const failingTool = Tool.make({ run: () => Effect.fail(toolError("Lookup refused")), }) -const run = (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}): Promise => { +const run = ( + code: string, + options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}, +): Promise => { const trace = options.trace ?? makeTrace() return Effect.runPromise( CodeMode.execute({ @@ -59,13 +62,13 @@ const run = (code: string, options: { trace?: Trace; limits?: ExecutionLimits } ) } -const value = async (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}) => { +const value = async (code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}) => { const result = await run(code, options) if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) return result.value } -const error = async (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}) => { +const error = async (code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}) => { const result = await run(code, options) if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`) return result.error diff --git a/packages/opencode/src/tool/code-mode.ts b/packages/opencode/src/tool/code-mode.ts index a97dee93eff9..0f566a49ade0 100644 --- a/packages/opencode/src/tool/code-mode.ts +++ b/packages/opencode/src/tool/code-mode.ts @@ -1,14 +1,7 @@ import * as Tool from "./tool" import { CallToolResultSchema, type CallToolResult } from "@modelcontextprotocol/sdk/types.js" import { Cause, Effect, Schema } from "effect" -import { - CodeMode, - Tool as SandboxTool, - toolError, - type ExecuteResult, - type JsonSchema, - type ToolDefinition, -} from "@opencode-ai/codemode" +import { CodeMode, Tool as SandboxTool, toolError } from "@opencode-ai/codemode" import { MCP } from "@/mcp" import { McpCatalog } from "@/mcp/catalog" import { Agent } from "@/agent/agent" @@ -132,13 +125,13 @@ function projectMcpResult(result: CallToolResult, collect: (attachment: Attachme type Run = (input: unknown) => Effect.Effect function toolTree(catalog: readonly CatalogEntry[], run: (entry: CatalogEntry) => Run) { - const tree: Record> = {} + const tree: Record> = {} for (const entry of catalog) { const namespace = (tree[entry.server] ??= {}) namespace[entry.local] = SandboxTool.make({ description: entry.tool.def.description ?? "", - input: entry.tool.def.inputSchema as JsonSchema, - output: entry.tool.def.outputSchema as JsonSchema | undefined, + input: entry.tool.def.inputSchema as SandboxTool.JsonSchema, + output: entry.tool.def.outputSchema as SandboxTool.JsonSchema | undefined, run: run(entry), }) } @@ -279,7 +272,7 @@ export const CodeModeTool = Tool.define( ctx.abort.addEventListener("abort", handler, { once: true }) return Effect.sync(() => ctx.abort.removeEventListener("abort", handler)) }) - const cancelled = (): ExecuteResult => ({ + const cancelled = (): CodeMode.Result => ({ ok: false, error: { kind: "ExecutionFailure", message: "Execution cancelled." }, toolCalls: calls.map((call) => ({ name: call.tool })), From d3459eb7403cbb33c197621777409954e9a1312f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:49:31 -0500 Subject: [PATCH 14/34] test(mcp): replace module mocks with real servers (#35450) --- packages/opencode/src/mcp/browser.ts | 37 + packages/opencode/src/mcp/index.ts | 22 +- .../test/fixture/mcp-lifecycle-stdio.ts | 26 + packages/opencode/test/mcp/headers.test.ts | 163 +- packages/opencode/test/mcp/lifecycle.test.ts | 1746 +++++------------ .../test/mcp/oauth-auto-connect.test.ts | 551 +++--- .../opencode/test/mcp/oauth-browser.test.ts | 402 ++-- 7 files changed, 1071 insertions(+), 1876 deletions(-) create mode 100644 packages/opencode/src/mcp/browser.ts create mode 100644 packages/opencode/test/fixture/mcp-lifecycle-stdio.ts diff --git a/packages/opencode/src/mcp/browser.ts b/packages/opencode/src/mcp/browser.ts new file mode 100644 index 000000000000..5760d8cbf9a0 --- /dev/null +++ b/packages/opencode/src/mcp/browser.ts @@ -0,0 +1,37 @@ +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { Context, Effect, Layer } from "effect" +import open from "open" + +export interface Interface { + readonly open: (url: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/McpBrowser") {} + +const layer = Layer.succeed( + Service, + Service.of({ + open: Effect.fn("McpBrowser.open")(function* (url: string) { + const subprocess = yield* Effect.tryPromise({ + try: () => open(url), + catch: (error) => (error instanceof Error ? error : new Error(String(error))), + }) + yield* Effect.callback((resume) => { + const timer = setTimeout(() => resume(Effect.void), 500) + subprocess.on("error", (error) => { + clearTimeout(timer) + resume(Effect.fail(error)) + }) + subprocess.on("exit", (code) => { + if (code === null || code === 0) return + clearTimeout(timer) + resume(Effect.fail(new Error(`Browser open failed with exit code ${code}`))) + }) + }) + }), + }), +) + +export const node = LayerNode.make({ service: Service, layer, deps: [] }) + +export * as McpBrowser from "./browser" diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 0d6c8c65dcdc..05f12fa2ee45 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -26,7 +26,6 @@ import { McpOAuthCallback } from "./oauth-callback" import { McpAuth } from "./auth" import { EventV2Bridge } from "@/event-v2-bridge" import { TuiEvent } from "@/server/tui-event" -import open from "open" import { Cause, Effect, Exit, Layer, Context, Schema, Stream } from "effect" import { EffectBridge } from "@/effect/bridge" import { InstanceState } from "@/effect/instance-state" @@ -34,6 +33,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { McpCatalog } from "./catalog" import { McpEvent } from "@opencode-ai/schema/mcp-event" +import { McpBrowser } from "./browser" const DEFAULT_TIMEOUT = 30_000 const CLIENT_OPTIONS = { @@ -207,6 +207,7 @@ const layer = Layer.effect( const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const auth = yield* McpAuth.Service const events = yield* EventV2Bridge.Service + const browser = yield* McpBrowser.Service type Transport = StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport @@ -897,22 +898,7 @@ const layer = Layer.effect( const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName) onAuthorization?.(result.authorizationUrl) - yield* Effect.tryPromise(() => open(result.authorizationUrl)).pipe( - Effect.flatMap((subprocess) => - Effect.callback((resume) => { - const timer = setTimeout(() => resume(Effect.void), 500) - subprocess.on("error", (err) => { - clearTimeout(timer) - resume(Effect.fail(err)) - }) - subprocess.on("exit", (code) => { - if (code !== null && code !== 0) { - clearTimeout(timer) - resume(Effect.fail(new Error(`Browser open failed with exit code ${code}`))) - } - }) - }), - ), + yield* browser.open(result.authorizationUrl).pipe( Effect.catch(() => { return events.publish(BrowserOpenFailed, { mcpName, url: result.authorizationUrl }).pipe(Effect.ignore) }), @@ -1012,7 +998,7 @@ export type AuthStatus = "authenticated" | "expired" | "not_authenticated" export const node = LayerNode.make({ service: Service, layer: layer, - deps: [CrossSpawnSpawner.node, McpAuth.node, EventV2Bridge.node, Config.node], + deps: [CrossSpawnSpawner.node, McpAuth.node, EventV2Bridge.node, Config.node, McpBrowser.node], }) export * as MCP from "." diff --git a/packages/opencode/test/fixture/mcp-lifecycle-stdio.ts b/packages/opencode/test/fixture/mcp-lifecycle-stdio.ts new file mode 100644 index 000000000000..b01ed921cfd4 --- /dev/null +++ b/packages/opencode/test/fixture/mcp-lifecycle-stdio.ts @@ -0,0 +1,26 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" +import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" + +if (process.argv.includes("--hang")) { + const pidFile = process.env.MCP_LIFECYCLE_PID_FILE + if (!pidFile) throw new Error("MCP_LIFECYCLE_PID_FILE is required") + await Bun.write(pidFile, String(process.pid)) + await new Promise(() => {}) +} + +const server = new Server({ name: "mcp-lifecycle-stdio", version: "1.0.0" }, { capabilities: { tools: {} } }) + +server.setRequestHandler(ListToolsRequestSchema, () => + Promise.resolve({ + tools: [ + { + name: "current_directory", + description: process.cwd(), + inputSchema: { type: "object", properties: {} }, + }, + ], + }), +) + +await server.connect(new StdioServerTransport()) diff --git a/packages/opencode/test/mcp/headers.test.ts b/packages/opencode/test/mcp/headers.test.ts index e6b83d678ec8..31cfc20d51c6 100644 --- a/packages/opencode/test/mcp/headers.test.ts +++ b/packages/opencode/test/mcp/headers.test.ts @@ -1,126 +1,101 @@ -import { describe, expect, mock, beforeEach } from "bun:test" +import { describe, expect } from "bun:test" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" +import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Effect } from "effect" import { testEffect } from "../lib/effect" +import { MCP } from "../../src/mcp/index" -// Track what options were passed to each transport constructor -const transportCalls: Array<{ - type: "streamable" | "sse" - url: string - options: { authProvider?: unknown; requestInit?: RequestInit } -}> = [] - -// Mock the transport constructors to capture their arguments -void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ - StreamableHTTPClientTransport: class MockStreamableHTTP { - constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) { - transportCalls.push({ - type: "streamable", - url: url.toString(), - options: options ?? {}, - }) - } - async start() { - throw new Error("Mock transport cannot connect") - } - }, -})) +const it = testEffect(LayerNode.compile(MCP.node)) -void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ - SSEClientTransport: class MockSSE { - constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) { - transportCalls.push({ - type: "sse", - url: url.toString(), - options: options ?? {}, - }) +const serve = Effect.acquireRelease( + Effect.promise(async () => { + const requests: Headers[] = [] + const protocol = new Server({ name: "headers", version: "1.0.0" }, { capabilities: { tools: {} } }) + protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] })) + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + enableJsonResponse: true, + }) + await protocol.connect(transport) + const http = Bun.serve({ + port: 0, + fetch(request) { + requests.push(new Headers(request.headers)) + return transport.handleRequest(request) + }, + }) + return { + requests, + url: http.url.toString(), + close: async () => { + await http.stop(true) + await protocol.close() + }, } - async start() { - throw new Error("Mock transport cannot connect") - } - }, -})) - -beforeEach(() => { - transportCalls.length = 0 -}) - -// Import MCP after mocking -const { MCP } = await import("../../src/mcp/index") -const it = testEffect(LayerNode.compile(MCP.node)) + }), + (server) => Effect.promise(server.close), +) describe("mcp.headers", () => { it.instance("headers are passed to transports when oauth is enabled (default)", () => Effect.gen(function* () { + const server = yield* serve const mcp = yield* MCP.Service - yield* mcp - .add("test-server", { - type: "remote", - url: "https://example.com/mcp", - headers: { - Authorization: "Bearer test-token", - "X-Custom-Header": "custom-value", - }, - }) - .pipe(Effect.catch(() => Effect.void)) - - // Both transports should have been created with headers - expect(transportCalls.length).toBeGreaterThanOrEqual(1) - - for (const call of transportCalls) { - expect(call.options.requestInit).toBeDefined() - expect(call.options.requestInit?.headers).toEqual({ + const result = yield* mcp.add("test-server", { + type: "remote", + url: server.url, + headers: { Authorization: "Bearer test-token", "X-Custom-Header": "custom-value", - }) - // OAuth should be enabled by default, so authProvider should exist - expect(call.options.authProvider).toBeDefined() + }, + }) + + expect(result.status).toMatchObject({ "test-server": { status: "connected" } }) + expect(server.requests.length).toBeGreaterThan(0) + for (const headers of server.requests) { + expect(headers.get("authorization")).toBe("Bearer test-token") + expect(headers.get("x-custom-header")).toBe("custom-value") } }), ) it.instance("headers are passed to transports when oauth is explicitly disabled", () => Effect.gen(function* () { + const server = yield* serve const mcp = yield* MCP.Service - yield* mcp - .add("test-server-no-oauth", { - type: "remote", - url: "https://example.com/mcp", - oauth: false, - headers: { - Authorization: "Bearer test-token", - }, - }) - .pipe(Effect.catch(() => Effect.void)) - - expect(transportCalls.length).toBeGreaterThanOrEqual(1) - - for (const call of transportCalls) { - expect(call.options.requestInit).toBeDefined() - expect(call.options.requestInit?.headers).toEqual({ + const result = yield* mcp.add("test-server-no-oauth", { + type: "remote", + url: server.url, + oauth: false, + headers: { Authorization: "Bearer test-token", - }) - // OAuth is disabled, so no authProvider - expect(call.options.authProvider).toBeUndefined() + }, + }) + + expect(result.status).toMatchObject({ "test-server-no-oauth": { status: "connected" } }) + expect(server.requests.length).toBeGreaterThan(0) + for (const headers of server.requests) { + expect(headers.get("authorization")).toBe("Bearer test-token") } }), ) it.instance("no requestInit when headers are not provided", () => Effect.gen(function* () { + const server = yield* serve const mcp = yield* MCP.Service - yield* mcp - .add("test-server-no-headers", { - type: "remote", - url: "https://example.com/mcp", - }) - .pipe(Effect.catch(() => Effect.void)) - - expect(transportCalls.length).toBeGreaterThanOrEqual(1) + const result = yield* mcp.add("test-server-no-headers", { + type: "remote", + url: server.url, + }) - for (const call of transportCalls) { - // No headers means requestInit should be undefined - expect(call.options.requestInit).toBeUndefined() + expect(result.status).toMatchObject({ "test-server-no-headers": { status: "connected" } }) + expect(server.requests.length).toBeGreaterThan(0) + for (const headers of server.requests) { + expect(headers.has("authorization")).toBe(false) + expect(headers.has("x-custom-header")).toBe(false) } }), ) diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index aabb171f9396..80c8fd22f886 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -1,1304 +1,560 @@ import path from "node:path" import { pathToFileURL } from "node:url" -import { expect, mock, beforeEach } from "bun:test" -import { ListRootsRequestSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js" +import { expect } from "bun:test" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" +import { + GetPromptRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListResourceTemplatesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, + type ServerCapabilities, + type Tool, +} from "@modelcontextprotocol/sdk/types.js" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Cause, Effect, Exit } from "effect" import type { MCP as MCPNS } from "../../src/mcp/index" -import { testEffect } from "../lib/effect" +import { MCP } from "../../src/mcp/index" +import { McpOAuthCallback } from "../../src/mcp/oauth-callback" import { TestInstance } from "../fixture/fixture" +import { pollWithTimeout, testEffect } from "../lib/effect" -// --- Mock infrastructure --- +const it = testEffect(LayerNode.compile(MCP.node)) +const stdioFixture = path.join(import.meta.dir, "../fixture/mcp-lifecycle-stdio.ts") + +type Page = { items: T[]; nextCursor?: string } -// Per-client state for controlling mock behavior -interface MockClientState { - capabilities: { tools?: object; prompts?: object; resources?: object } - capabilitiesShouldThrow: boolean - instructions?: string - tools: Array<{ name: string; description?: string; inputSchema: object; outputSchema?: object }> - listToolsCalls: number - listPromptsCalls: number - listResourcesCalls: number - listResourceTemplatesCalls: number - getPromptTimeout?: number - readResourceTimeout?: number - requestCalls: number - listToolsShouldFail: boolean - listToolsError: string - listPromptsShouldFail: boolean - listResourcesShouldFail: boolean +interface LifecycleServerState { + tools: Tool[] prompts: Array<{ name: string; description?: string }> resources: Array<{ name: string; uri: string; description?: string }> resourceTemplates: Array<{ name: string; uriTemplate: string; description?: string }> - toolPages: Record< - string, - { - tools: Array<{ name: string; description?: string; inputSchema: object; outputSchema?: object }> - nextCursor?: string - } - > - promptPages: Record; nextCursor?: string }> - resourcePages: Record< - string, - { resources: Array<{ name: string; uri: string; description?: string }>; nextCursor?: string } - > - resourceTemplatePages: Record< - string, - { resourceTemplates: Array<{ name: string; uriTemplate: string; description?: string }>; nextCursor?: string } - > - closed: boolean - clientOptions?: { capabilities?: { roots?: { listChanged?: boolean } } } - requestHandlers: Map Promise> - notificationHandlers: Map any> -} - -const clientStates = new Map() -let lastCreatedClientName: string | undefined -let connectShouldFail = false -let connectShouldHang = false -let connectError = "Mock transport cannot connect" -// Tracks how many Client instances were created (detects leaks) -let clientCreateCount = 0 -// Tracks how many times transport.close() is called across all mock transports -let transportCloseCount = 0 -// Captures the opts passed to each MockStdioTransport, keyed by lastCreatedClientName -const stdioOptsByName = new Map() - -function getOrCreateClientState(name?: string): MockClientState { - const key = name ?? "default" - let state = clientStates.get(key) - if (!state) { - state = { - capabilities: { tools: {}, prompts: {}, resources: {} }, - capabilitiesShouldThrow: false, - tools: [{ name: "test_tool", description: "A test tool", inputSchema: { type: "object", properties: {} } }], - listToolsCalls: 0, - listPromptsCalls: 0, - listResourcesCalls: 0, - listResourceTemplatesCalls: 0, - requestCalls: 0, - listToolsShouldFail: false, - listToolsError: "listTools failed", - listPromptsShouldFail: false, - listResourcesShouldFail: false, - prompts: [], - resources: [], - resourceTemplates: [], - toolPages: {}, - promptPages: {}, - resourcePages: {}, - resourceTemplatePages: {}, - closed: false, - requestHandlers: new Map(), - notificationHandlers: new Map(), - } - clientStates.set(key, state) - } - return state -} - -// Mock transport that succeeds or fails based on connectShouldFail / connectShouldHang -class MockStdioTransport { - stderr: null = null - pid = 12345 - constructor(opts: any) { - if (lastCreatedClientName) stdioOptsByName.set(lastCreatedClientName, opts) - } - async start() { - if (connectShouldHang) return new Promise(() => {}) // never resolves - if (connectShouldFail) throw new Error(connectError) - } - async close() { - transportCloseCount++ - } -} - -class MockStreamableHTTP { - // oxlint-disable-next-line no-useless-constructor - constructor(_url: URL, _opts?: any) {} - async start() { - if (connectShouldHang) return new Promise(() => {}) // never resolves - if (connectShouldFail) throw new Error(connectError) - } - async close() { - transportCloseCount++ - } - async finishAuth() {} -} - -class MockSSE { - // oxlint-disable-next-line no-useless-constructor - constructor(_url: URL, _opts?: any) {} - async start() { - if (connectShouldHang) return new Promise(() => {}) // never resolves - if (connectShouldFail) throw new Error(connectError) - } - async close() { - transportCloseCount++ - } + toolPages?: Record> + promptPages?: Record> + resourcePages?: Record> + resourceTemplatePages?: Record> + listToolsError?: string + requestDelay?: number + roots?: Array<{ uri: string; name?: string }> + requests: string[] + aborted: number } -void mock.module("@modelcontextprotocol/sdk/client/stdio.js", () => ({ - StdioClientTransport: MockStdioTransport, -})) - -void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ - StreamableHTTPClientTransport: MockStreamableHTTP, -})) - -void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ - SSEClientTransport: MockSSE, -})) - -void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ - UnauthorizedError: class extends Error { - constructor() { - super("Unauthorized") - } - }, -})) - -// Mock Client that delegates to per-name MockClientState -void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ - Client: class MockClient { - _state!: MockClientState - transport: any - - constructor(_info: any, options?: MockClientState["clientOptions"]) { - clientCreateCount++ - this._state = getOrCreateClientState(lastCreatedClientName) - this._state.clientOptions = options - } - - async connect(transport: { start: () => Promise }) { - this.transport = transport - await transport.start() - // After successful connect, bind to the last-created client name - this._state = getOrCreateClientState(lastCreatedClientName) - } - - setRequestHandler(schema: unknown, handler: (...args: any[]) => Promise) { - this._state.requestHandlers.set(schema, handler) - } - - setNotificationHandler(schema: unknown, handler: (...args: any[]) => any) { - this._state?.notificationHandlers.set(schema, handler) - } - - getServerCapabilities() { - if (this._state?.capabilitiesShouldThrow) throw new Error("capability discovery failed") - return this._state?.capabilities - } - - getInstructions() { - return this._state?.instructions - } - - async listTools(params?: { cursor?: string }) { - if (this._state) this._state.listToolsCalls++ - if (this._state?.listToolsShouldFail) { - throw new Error(this._state.listToolsError) +function lifecycleServer(input?: { capabilities?: ServerCapabilities; instructions?: string; requestRoots?: boolean }) { + const capabilities = input?.capabilities ?? { tools: {}, prompts: {}, resources: {} } + return Effect.acquireRelease( + Effect.promise(async () => { + const state: LifecycleServerState = { + tools: [{ name: "test_tool", description: "A test tool", inputSchema: { type: "object", properties: {} } }], + prompts: [], + resources: [], + resourceTemplates: [], + requests: [], + aborted: 0, } - const page = this._state?.toolPages[params === undefined ? "initial" : (params.cursor ?? "")] - if (page) return page - return { tools: this._state?.tools ?? [] } - } - async request( - request: { method: string; params?: { cursor?: string } }, - schema: { parse: (value: unknown) => unknown }, - ) { - if (this._state) this._state.requestCalls++ - if (request.method === "tools/list") { - return schema.parse( - this._state?.toolPages[request.params === undefined ? "initial" : (request.params.cursor ?? "")] ?? { - tools: this._state?.tools ?? [], - }, + const makeProtocol = async () => { + const protocol = new Server( + { name: "mcp-lifecycle", version: "1.0.0" }, + { capabilities, instructions: input?.instructions }, ) - } - throw new Error(`unsupported request: ${request.method}`) - } - - async listPrompts(params?: { cursor?: string }) { - if (this._state) this._state.listPromptsCalls++ - if (this._state?.listPromptsShouldFail) { - throw new Error("listPrompts failed") - } - const page = this._state?.promptPages[params === undefined ? "initial" : (params.cursor ?? "")] - if (page) return page - return { prompts: this._state?.prompts ?? [] } - } - - async listResources(params?: { cursor?: string }) { - if (this._state) this._state.listResourcesCalls++ - if (this._state?.listResourcesShouldFail) { - throw new Error("listResources failed") - } - const page = this._state?.resourcePages[params === undefined ? "initial" : (params.cursor ?? "")] - if (page) return page - return { resources: this._state?.resources ?? [] } - } - - async listResourceTemplates(params?: { cursor?: string }) { - if (this._state) this._state.listResourceTemplatesCalls++ - const page = this._state?.resourceTemplatePages[params === undefined ? "initial" : (params.cursor ?? "")] - if (page) return page - return { resourceTemplates: this._state?.resourceTemplates ?? [] } - } - - async getPrompt(_params: unknown, options?: { timeout?: number }) { - if (this._state) this._state.getPromptTimeout = options?.timeout - return { messages: [] } - } - - async readResource(params: { uri: string }, options?: { timeout?: number }) { - if (this._state) this._state.readResourceTimeout = options?.timeout - return { contents: [{ uri: params.uri, text: "test" }] } - } - - async close() { - if (this._state) this._state.closed = true - } - }, -})) - -beforeEach(() => { - clientStates.clear() - lastCreatedClientName = undefined - connectShouldFail = false - connectShouldHang = false - connectError = "Mock transport cannot connect" - clientCreateCount = 0 - transportCloseCount = 0 -}) - -// Import after mocks -const { MCP } = await import("../../src/mcp/index") -const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback") - -const it = testEffect(LayerNode.compile(MCP.node)) - -function statusName(status: Record | MCPNS.Status, server: string) { - if ("status" in status) return status.status - return status[server]?.status -} - -it.instance( - "advertises and lists the instance directory as its root", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - const { directory } = yield* TestInstance - lastCreatedClientName = "roots" - yield* mcp.add("roots", { type: "local", command: ["echo", "test"] }) - - const state = getOrCreateClientState("roots") - expect(state.clientOptions?.capabilities?.roots).toEqual({}) - expect(state.clientOptions?.capabilities?.roots?.listChanged).toBeUndefined() - - const handler = state.requestHandlers.get(ListRootsRequestSchema) - expect(handler).toBeDefined() - const result = yield* Effect.promise(() => handler?.() ?? Promise.reject(new Error("roots handler missing"))) - expect(result).toEqual({ roots: [{ uri: pathToFileURL(directory).href }] }) - }), - ), - { config: { mcp: {} } }, -) - -it.instance( - "local mcp cwd resolves relative paths against instance directory", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - const { directory } = yield* TestInstance - lastCreatedClientName = "rel-cwd" - yield* mcp.add("rel-cwd", { type: "local", command: ["echo", "test"], cwd: "plugins/sub" }) - expect(stdioOptsByName.get("rel-cwd")?.cwd).toBe(path.resolve(directory, "plugins/sub")) - }), - ), - { config: { mcp: {} } }, -) - -// ======================================================================== -// Test: tools() are cached after connect -// ======================================================================== - -it.instance( - "tools() reuses cached tool definitions after connect", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "my-server" - const serverState = getOrCreateClientState("my-server") - serverState.tools = [ - { name: "do_thing", description: "does a thing", inputSchema: { type: "object", properties: {} } }, - ] - - // First: add the server successfully - const addResult = yield* mcp.add("my-server", { - type: "local", - command: ["echo", "test"], - }) - expect((addResult.status as any)["my-server"]?.status ?? (addResult.status as any).status).toBe("connected") - - expect(serverState.listToolsCalls).toBe(1) - - const toolsA = yield* mcp.tools() - const toolsB = yield* mcp.tools() - expect(Object.keys(toolsA).length).toBeGreaterThan(0) - expect(Object.keys(toolsB).length).toBeGreaterThan(0) - expect(serverState.listToolsCalls).toBe(1) - }), - ), - { config: { mcp: {} } }, -) - -it.instance( - "instructions() returns connected server instructions with tool names", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "guide-server" - const serverState = getOrCreateClientState("guide-server") - serverState.instructions = "Use lookup before mutate." - - yield* mcp.add("guide-server", { - type: "local", - command: ["echo", "test"], + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + enableJsonResponse: true, }) - expect(yield* mcp.instructions()).toContainEqual({ - name: "guide-server", - instructions: "Use lookup before mutate.", - tools: ["guide-server_test_tool"], - }) - }), - ), - { config: { mcp: {} } }, -) - -it.instance( - "instructions() omits empty and disconnected server instructions", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "temporary-server" - getOrCreateClientState("temporary-server").instructions = "Temporary guidance." - - yield* mcp.add("temporary-server", { - type: "local", - command: ["echo", "test"], - }) - yield* mcp.disconnect("temporary-server") - - lastCreatedClientName = "blank-server" - getOrCreateClientState("blank-server").instructions = " " - - yield* mcp.add("blank-server", { - type: "local", - command: ["echo", "test"], - }) - - const instructions = yield* mcp.instructions() - expect(instructions.some((item) => item.name === "temporary-server")).toBe(false) - expect(instructions.some((item) => item.name === "blank-server")).toBe(false) - }), - ), - { config: { mcp: {} } }, -) - -it.instance( - "follows cursors when listing tools, prompts, and resources", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "paged-server" - const serverState = getOrCreateClientState("paged-server") - serverState.toolPages = { - initial: { - tools: [{ name: "tool-one", inputSchema: { type: "object", properties: {} } }], - nextCursor: "tools-2", - }, - "tools-2": { tools: [{ name: "tool-two", inputSchema: { type: "object", properties: {} } }] }, + if (capabilities.tools) { + protocol.setRequestHandler(ListToolsRequestSchema, (request) => { + if (state.listToolsError) throw new Error(state.listToolsError) + const page = state.toolPages?.[request.params?.cursor ?? "initial"] + return Promise.resolve({ tools: page?.items ?? state.tools, nextCursor: page?.nextCursor }) + }) } - serverState.promptPages = { - initial: { prompts: [{ name: "prompt-one" }], nextCursor: "prompts-2" }, - "prompts-2": { prompts: [{ name: "prompt-two" }] }, + if (capabilities.prompts) { + protocol.setRequestHandler(ListPromptsRequestSchema, (request) => { + const page = state.promptPages?.[request.params?.cursor ?? "initial"] + return Promise.resolve({ prompts: page?.items ?? state.prompts, nextCursor: page?.nextCursor }) + }) + protocol.setRequestHandler(GetPromptRequestSchema, async () => { + if (state.requestDelay) await Bun.sleep(state.requestDelay) + return { messages: [{ role: "user", content: { type: "text", text: "prompt result" } }] } + }) } - serverState.resourcePages = { - initial: { resources: [{ name: "resource-one", uri: "test://one" }], nextCursor: "resources-2" }, - "resources-2": { resources: [{ name: "resource-two", uri: "test://two" }] }, + if (capabilities.resources) { + protocol.setRequestHandler(ListResourcesRequestSchema, (request) => { + const page = state.resourcePages?.[request.params?.cursor ?? "initial"] + return Promise.resolve({ resources: page?.items ?? state.resources, nextCursor: page?.nextCursor }) + }) + protocol.setRequestHandler(ListResourceTemplatesRequestSchema, (request) => { + const page = state.resourceTemplatePages?.[request.params?.cursor ?? "initial"] + return Promise.resolve({ + resourceTemplates: page?.items ?? state.resourceTemplates, + nextCursor: page?.nextCursor, + }) + }) + protocol.setRequestHandler(ReadResourceRequestSchema, async (request) => { + if (state.requestDelay) await Bun.sleep(state.requestDelay) + return { contents: [{ uri: request.params.uri, text: "resource result" }] } + }) } - serverState.resourceTemplatePages = { - initial: { - resourceTemplates: [{ name: "template-one", uriTemplate: "test://one/{id}" }], - nextCursor: "resource-templates-2", - }, - "resource-templates-2": { resourceTemplates: [{ name: "template-two", uriTemplate: "test://two/{id}" }] }, - } - - yield* mcp.add("paged-server", { - type: "local", - command: ["echo", "test"], - }) - - expect(Object.keys(yield* mcp.tools())).toEqual(["paged-server_tool-one", "paged-server_tool-two"]) - expect(Object.keys(yield* mcp.prompts())).toEqual(["paged-server:prompt-one", "paged-server:prompt-two"]) - expect(Object.keys(yield* mcp.resources())).toEqual(["paged-server:test://one", "paged-server:test://two"]) - expect(Object.keys(yield* mcp.resourceTemplates())).toEqual([ - "paged-server:test://one/{id}", - "paged-server:test://two/{id}", - ]) - expect(serverState.listToolsCalls).toBe(2) - expect(serverState.listPromptsCalls).toBe(2) - expect(serverState.listResourcesCalls).toBe(2) - expect(serverState.listResourceTemplatesCalls).toBe(2) - }), - ), - { config: { mcp: {} } }, -) -it.instance( - "stops listing when a server repeats a cursor", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "looping-server" - const serverState = getOrCreateClientState("looping-server") - serverState.toolPages = { - initial: { tools: [], nextCursor: "repeat" }, - repeat: { tools: [], nextCursor: "repeat" }, - } - - yield* mcp.add("looping-server", { - type: "local", - command: ["echo", "test"], - }) - - expect(serverState.listToolsCalls).toBe(2) - expect(yield* mcp.tools()).toEqual({}) - }), - ), - { config: { mcp: {} } }, -) - -it.instance( - "follows empty cursors", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "empty-cursor-server" - const serverState = getOrCreateClientState("empty-cursor-server") - serverState.promptPages = { - initial: { prompts: [{ name: "prompt-one" }], nextCursor: "" }, - "": { prompts: [{ name: "prompt-two" }] }, + protocol.oninitialized = () => { + if (!input?.requestRoots) return + if (!protocol.getClientCapabilities()?.roots) return + void Bun.sleep(25) + .then(() => protocol.listRoots()) + .then((result) => { + state.roots = result.roots + }) + .catch(() => {}) } + await protocol.connect(transport) + return { protocol, transport } + } - yield* mcp.add("empty-cursor-server", { - type: "local", - command: ["echo", "test"], - }) - - expect(Object.keys(yield* mcp.prompts())).toEqual([ - "empty-cursor-server:prompt-one", - "empty-cursor-server:prompt-two", - ]) - expect(serverState.listPromptsCalls).toBe(2) - }), - ), - { config: { mcp: {} } }, -) - -// ======================================================================== -// Test: tool change notifications refresh the cache -// ======================================================================== - -it.instance( - "tool change notifications refresh cached tool definitions", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "status-server" - const serverState = getOrCreateClientState("status-server") - - yield* mcp.add("status-server", { - type: "local", - command: ["echo", "test"], - }) - - const before = yield* mcp.tools() - expect(Object.keys(before).some((key) => key.includes("test_tool"))).toBe(true) - expect(serverState.listToolsCalls).toBe(1) - - serverState.tools = [ - { name: "next_tool", description: "next", inputSchema: { type: "object", properties: {} } }, - ] - - const handler = serverState.notificationHandlers.get(ToolListChangedNotificationSchema) - expect(handler).toBeDefined() - yield* Effect.promise(() => handler?.()) - - const after = yield* mcp.tools() - expect(Object.keys(after).some((key) => key.includes("next_tool"))).toBe(true) - expect(Object.keys(after).some((key) => key.includes("test_tool"))).toBe(false) - expect(serverState.listToolsCalls).toBe(2) - }), - ), - { config: { mcp: {} } }, -) - -// ======================================================================== -// Test: connect() / disconnect() lifecycle -// ======================================================================== - -it.instance( - "disconnect sets status to disabled and removes client", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "disc-server" - getOrCreateClientState("disc-server") - - yield* mcp.add("disc-server", { - type: "local", - command: ["echo", "test"], - }) - - const statusBefore = yield* mcp.status() - expect(statusBefore["disc-server"]?.status).toBe("connected") - - yield* mcp.disconnect("disc-server") - - const statusAfter = yield* mcp.status() - expect(statusAfter["disc-server"]?.status).toBe("disabled") - - const tools = yield* mcp.tools() - const serverTools = Object.keys(tools).filter((k) => k.startsWith("disc-server")) - expect(serverTools.length).toBe(0) - }), - ), - { - config: { - mcp: { - "disc-server": { - type: "local", - command: ["echo", "test"], + let current = await makeProtocol() + const http = Bun.serve({ + port: 0, + fetch(request) { + state.requests.push(request.method) + request.signal.addEventListener("abort", () => state.aborted++) + return current.transport.handleRequest(request) }, - }, - }, - }, -) - -it.instance( - "connect() after disconnect() re-establishes the server", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "reconn-server" - const serverState = getOrCreateClientState("reconn-server") - serverState.tools = [ - { name: "my_tool", description: "a tool", inputSchema: { type: "object", properties: {} } }, - ] - - yield* mcp.add("reconn-server", { - type: "local", - command: ["echo", "test"], - }) - - yield* mcp.disconnect("reconn-server") - expect((yield* mcp.status())["reconn-server"]?.status).toBe("disabled") - - yield* mcp.connect("reconn-server") - expect((yield* mcp.status())["reconn-server"]?.status).toBe("connected") - - const tools = yield* mcp.tools() - expect(Object.keys(tools).some((k) => k.includes("my_tool"))).toBe(true) - }), - ), - { - config: { - mcp: { - "reconn-server": { - type: "local", - command: ["echo", "test"], + }) + + return { + state, + url: http.url.toString(), + sendToolListChanged: () => current.protocol.sendToolListChanged(), + restart: async () => { + current = await makeProtocol() }, - }, - }, - }, -) - -// ======================================================================== -// Test: add() closes existing client before replacing -// ======================================================================== - -it.instance( - "add() closes the old client when replacing a server", - // Don't put the server in config — add it dynamically so we control - // exactly which client instance is "first" vs "second". - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "replace-server" - const firstState = getOrCreateClientState("replace-server") - - yield* mcp.add("replace-server", { - type: "local", - command: ["echo", "test"], - }) - - expect(firstState.closed).toBe(false) - - // Create new state for second client - clientStates.delete("replace-server") - const secondState = getOrCreateClientState("replace-server") - - // Re-add should close the first client - yield* mcp.add("replace-server", { - type: "local", - command: ["echo", "test"], - }) - - expect(firstState.closed).toBe(true) - expect(secondState.closed).toBe(false) - }), - ), - { config: { mcp: {} } }, -) - -// ======================================================================== -// Test: state init with mixed success/failure -// ======================================================================== - -it.instance( - "init connects available servers even when one fails", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - // Set up good server - const goodState = getOrCreateClientState("good-server") - goodState.tools = [{ name: "good_tool", description: "works", inputSchema: { type: "object", properties: {} } }] - - // Set up bad server - will fail on listTools during create() - const badState = getOrCreateClientState("bad-server") - badState.listToolsShouldFail = true - - // Add good server first - lastCreatedClientName = "good-server" - yield* mcp.add("good-server", { - type: "local", - command: ["echo", "good"], - }) - - // Add bad server - should fail but not affect good server - lastCreatedClientName = "bad-server" - yield* mcp.add("bad-server", { - type: "local", - command: ["echo", "bad"], - }) - - const status = yield* mcp.status() - expect(status["good-server"]?.status).toBe("connected") - expect(status["bad-server"]?.status).toBe("failed") - - // Good server's tools should still be available - const tools = yield* mcp.tools() - expect(Object.keys(tools).some((k) => k.includes("good_tool"))).toBe(true) - }), - ), - { - config: { - mcp: { - "good-server": { - type: "local", - command: ["echo", "good"], - }, - "bad-server": { - type: "local", - command: ["echo", "bad"], + close: async () => { + await current.protocol.close().catch(() => {}) + http.stop(true) }, - }, - }, - }, -) - -it.instance( - "returns failed and closes the client when SDK initialization throws", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "defective-server" - const serverState = getOrCreateClientState("defective-server") - serverState.capabilitiesShouldThrow = true - - const result = yield* mcp.add("defective-server", { - type: "local", - command: ["echo", "test"], - }) - - expect(statusName(result.status, "defective-server")).toBe("failed") - expect((yield* mcp.status())["defective-server"]).toEqual({ - status: "failed", - error: "capability discovery failed", - }) - expect(serverState.closed).toBe(true) - }), - ), - { config: { mcp: {} } }, -) - -it.instance( - "falls back when MCP output schema refs fail SDK tool discovery", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "stitch-like-server" - const serverState = getOrCreateClientState("stitch-like-server") - serverState.listToolsShouldFail = true - serverState.listToolsError = "can't resolve reference #/$defs/ScreenInstance from id #" - serverState.tools = [ - { - name: "render_screen", - description: "renders a screen", - inputSchema: { type: "object", properties: { prompt: { type: "string" } }, required: ["prompt"] }, - outputSchema: { type: "object", properties: { screen: { $ref: "#/$defs/ScreenInstance" } } }, - }, - ] - - const addResult = yield* mcp.add("stitch-like-server", { - type: "local", - command: ["echo", "test"], - }) - - expect(statusName(addResult.status, "stitch-like-server")).toBe("connected") - - const tools = yield* mcp.tools() - expect(Object.keys(tools).some((key) => key.includes("render_screen"))).toBe(true) - expect(serverState.listToolsCalls).toBe(1) - expect(serverState.requestCalls).toBe(1) - }), - ), - { config: { mcp: {} } }, -) - -it.instance( - "does not fall back for non-schema MCP tool discovery errors", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "broken-server" - const serverState = getOrCreateClientState("broken-server") - serverState.listToolsShouldFail = true - serverState.listToolsError = "transport closed" - - const addResult = yield* mcp.add("broken-server", { - type: "local", - command: ["echo", "test"], - }) - - expect(statusName(addResult.status, "broken-server")).toBe("failed") - expect(serverState.listToolsCalls).toBe(1) - expect(serverState.requestCalls).toBe(0) - }), - ), - { config: { mcp: {} } }, -) - -// ======================================================================== -// Test: disabled server via config -// ======================================================================== - -it.instance( - "disabled server is marked as disabled without attempting connection", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - const countBefore = clientCreateCount - - yield* mcp.add("disabled-server", { - type: "local", - command: ["echo", "test"], - enabled: false, - } as any) - - // No client should have been created - expect(clientCreateCount).toBe(countBefore) - - const status = yield* mcp.status() - expect(status["disabled-server"]?.status).toBe("disabled") - }), - ), - { - config: { - mcp: { - "disabled-server": { - type: "local", - command: ["echo", "test"], - enabled: false, - }, - }, - }, - }, -) - -// ======================================================================== -// Test: prompts() and resources() -// ======================================================================== - -it.instance( - "prompts() returns prompts from connected servers", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "prompt-server" - const serverState = getOrCreateClientState("prompt-server") - serverState.prompts = [{ name: "my-prompt", description: "A test prompt" }] - - yield* mcp.add("prompt-server", { - type: "local", - command: ["echo", "test"], - }) + } + }), + (server) => Effect.promise(server.close), + ) +} - const prompts = yield* mcp.prompts() - expect(Object.keys(prompts).length).toBe(1) - const key = Object.keys(prompts)[0] - expect(key).toContain("prompt-server") - expect(key).toContain("my-prompt") - }), - ), - { - config: { - mcp: { - "prompt-server": { - type: "local", - command: ["echo", "test"], +function hangingLifecycleServer() { + return Effect.acquireRelease( + Effect.promise(async () => { + const protocol = new Server({ name: "mcp-lifecycle-hanging", version: "1.0.0" }, { capabilities: { tools: {} } }) + protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] })) + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + enableJsonResponse: true, + }) + await protocol.connect(transport) + const requests: string[] = [] + let aborted = 0 + const http = Bun.serve({ + port: 0, + fetch(request) { + requests.push(request.method) + request.signal.addEventListener("abort", () => aborted++) + return new Promise(() => {}) }, - }, - }, - }, -) - -it.instance( - "resources() returns resources from connected servers", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "resource-server" - const serverState = getOrCreateClientState("resource-server") - serverState.resources = [ - { name: "my-resource", uri: "file:///test.txt", description: "A test resource" }, - { name: "my-resource", uri: "ui://component-state", description: "A second resource with same name" }, - ] - - yield* mcp.add("resource-server", { - type: "local", - command: ["echo", "test"], - }) - - const resources = yield* mcp.resources() - expect(Object.keys(resources)).toEqual([ - "resource-server:file:///test.txt", - "resource-server:ui://component-state", - ]) - }), - ), - { - config: { - mcp: { - "resource-server": { - type: "local", - command: ["echo", "test"], + }) + return { + requests, + aborted: () => aborted, + url: http.url.toString(), + close: async () => { + await protocol.close().catch(() => {}) + http.stop(true) }, - }, - }, - }, -) - -it.instance( - "uses per-server timeouts for prompt and resource requests", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "timeout-server" - const serverState = getOrCreateClientState("timeout-server") - - yield* mcp.add("timeout-server", { - type: "local", - command: ["echo", "test"], - timeout: 2500, - }) - yield* mcp.getPrompt("timeout-server", "test") - yield* mcp.readResource("timeout-server", "test://resource") - - expect(serverState.getPromptTimeout).toBe(2500) - expect(serverState.readResourceTimeout).toBe(2500) - }), - ), - { config: { mcp: {}, experimental: { mcp_timeout: 5000 } } }, -) - -it.instance( - "resource-only servers connect without listing tools", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "resource-only-server" - const serverState = getOrCreateClientState("resource-only-server") - serverState.capabilities = { resources: {} } - serverState.resources = [{ name: "docs", uri: "docs://readme" }] - - const result = yield* mcp.add("resource-only-server", { - type: "local", - command: ["echo", "test"], - }) - - expect(statusName(result.status, "resource-only-server")).toBe("connected") - expect(serverState.listToolsCalls).toBe(0) - expect(Object.keys(yield* mcp.tools())).toHaveLength(0) - expect(Object.keys(yield* mcp.resources())).toEqual(["resource-only-server:docs://readme"]) - expect(serverState.listResourcesCalls).toBe(1) - expect(serverState.listPromptsCalls).toBe(0) - }), - ), - { config: { mcp: {} } }, -) - -it.instance( - "prompt-only servers connect without listing tools", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "prompt-only-server" - const serverState = getOrCreateClientState("prompt-only-server") - serverState.capabilities = { prompts: {} } - serverState.prompts = [{ name: "review" }] - - const result = yield* mcp.add("prompt-only-server", { - type: "local", - command: ["echo", "test"], - }) + } + }), + (server) => Effect.promise(server.close), + ) +} - expect(statusName(result.status, "prompt-only-server")).toBe("connected") - expect(serverState.listToolsCalls).toBe(0) - expect(Object.keys(yield* mcp.tools())).toHaveLength(0) - expect(Object.keys(yield* mcp.prompts())).toEqual(["prompt-only-server:review"]) - expect(serverState.listPromptsCalls).toBe(1) - expect(serverState.listResourcesCalls).toBe(0) - }), - ), - { config: { mcp: {} } }, -) +function statusName(status: Record | MCPNS.Status, server: string) { + if ("status" in status) return status.status + return status[server]?.status +} -it.instance( - "tools-only servers skip optional prompt and resource discovery", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "tools-only-server" - const serverState = getOrCreateClientState("tools-only-server") - serverState.capabilities = { tools: {} } +const remote = (url: string, timeout?: number) => ({ type: "remote" as const, url, oauth: false as const, timeout }) - const result = yield* mcp.add("tools-only-server", { - type: "local", - command: ["echo", "test"], - }) +it.instance("advertises and lists the instance directory as its root", () => + Effect.gen(function* () { + const server = yield* lifecycleServer({ requestRoots: true }) + const mcp = yield* MCP.Service + const test = yield* TestInstance + yield* mcp.add("roots", remote(server.url)) - expect(statusName(result.status, "tools-only-server")).toBe("connected") - expect(serverState.listToolsCalls).toBe(1) - expect(Object.keys(yield* mcp.tools())).toEqual(["tools-only-server_test_tool"]) - expect(yield* mcp.prompts()).toEqual({}) - expect(yield* mcp.resources()).toEqual({}) - expect(serverState.listPromptsCalls).toBe(0) - expect(serverState.listResourcesCalls).toBe(0) - }), - ), - { config: { mcp: {} } }, + const roots = yield* pollWithTimeout( + Effect.sync(() => server.state.roots), + "server did not receive roots", + ) + expect(roots).toEqual([{ uri: pathToFileURL(test.directory).href }]) + }), ) it.instance( - "prompts() skips disconnected servers", + "local mcp cwd resolves relative paths against instance directory", () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "prompt-disc-server" - const serverState = getOrCreateClientState("prompt-disc-server") - serverState.prompts = [{ name: "hidden-prompt", description: "Should not appear" }] - - yield* mcp.add("prompt-disc-server", { - type: "local", - command: ["echo", "test"], - }) - - yield* mcp.disconnect("prompt-disc-server") - - const prompts = yield* mcp.prompts() - expect(Object.keys(prompts).length).toBe(0) - }), - ), - { - config: { - mcp: { - "prompt-disc-server": { - type: "local", - command: ["echo", "test"], - }, + Effect.gen(function* () { + const mcp = yield* MCP.Service + const test = yield* TestInstance + yield* mcp.add("rel-cwd", { + type: "local", + command: [process.execPath, stdioFixture], + cwd: "plugins/sub", + }) + + expect((yield* mcp.tools())["rel-cwd_current_directory"]?.def.description).toBe( + path.resolve(test.directory, "plugins/sub"), + ) + }), + { init: (directory) => Effect.promise(() => Bun.$`mkdir -p ${path.join(directory, "plugins/sub")}`.quiet()) }, +) + +it.instance("tools() reuses cached definitions until a protocol notification", () => + Effect.gen(function* () { + const server = yield* lifecycleServer({ capabilities: { tools: { listChanged: true } } }) + const mcp = yield* MCP.Service + yield* mcp.add("cache-server", remote(server.url)) + server.state.tools = [{ name: "next_tool", inputSchema: { type: "object", properties: {} } }] + + expect(Object.keys(yield* mcp.tools())).toEqual(["cache-server_test_tool"]) + yield* Effect.promise(server.sendToolListChanged) + yield* pollWithTimeout( + Effect.gen(function* () { + const keys = Object.keys(yield* mcp.tools()) + return keys.includes("cache-server_next_tool") ? keys : undefined + }), + "tool cache did not refresh", + ) + expect(Object.keys(yield* mcp.tools())).toEqual(["cache-server_next_tool"]) + }), +) + +it.instance("instructions() returns non-empty connected server instructions with tool names", () => + Effect.gen(function* () { + const guide = yield* lifecycleServer({ instructions: "Use lookup before mutate." }) + const blank = yield* lifecycleServer({ instructions: " " }) + const mcp = yield* MCP.Service + yield* mcp.add("guide-server", remote(guide.url)) + yield* mcp.add("blank-server", remote(blank.url)) + + expect(yield* mcp.instructions()).toEqual([ + { name: "guide-server", instructions: "Use lookup before mutate.", tools: ["guide-server_test_tool"] }, + ]) + yield* mcp.disconnect("guide-server") + expect(yield* mcp.instructions()).toEqual([]) + }), +) + +it.instance("follows cursors when listing tools, prompts, resources, and templates", () => + Effect.gen(function* () { + const server = yield* lifecycleServer() + server.state.toolPages = { + initial: { items: [{ name: "tool-one", inputSchema: { type: "object" } }], nextCursor: "tools-2" }, + "tools-2": { items: [{ name: "tool-two", inputSchema: { type: "object" } }] }, + } + server.state.promptPages = { + initial: { items: [{ name: "prompt-one" }], nextCursor: "prompts-2" }, + "prompts-2": { items: [{ name: "prompt-two" }] }, + } + server.state.resourcePages = { + initial: { items: [{ name: "resource-one", uri: "test://one" }], nextCursor: "resources-2" }, + "resources-2": { items: [{ name: "resource-two", uri: "test://two" }] }, + } + server.state.resourceTemplatePages = { + initial: { items: [{ name: "template-one", uriTemplate: "test://one/{id}" }], nextCursor: "templates-2" }, + "templates-2": { items: [{ name: "template-two", uriTemplate: "test://two/{id}" }] }, + } + const mcp = yield* MCP.Service + yield* mcp.add("paged-server", remote(server.url)) + + expect(Object.keys(yield* mcp.tools())).toEqual(["paged-server_tool-one", "paged-server_tool-two"]) + expect(Object.keys(yield* mcp.prompts())).toEqual(["paged-server:prompt-one", "paged-server:prompt-two"]) + expect(Object.keys(yield* mcp.resources())).toEqual(["paged-server:test://one", "paged-server:test://two"]) + expect(Object.keys(yield* mcp.resourceTemplates())).toEqual([ + "paged-server:test://one/{id}", + "paged-server:test://two/{id}", + ]) + }), +) + +it.instance("accepts empty cursors and rejects repeated cursors", () => + Effect.gen(function* () { + const empty = yield* lifecycleServer({ capabilities: { prompts: {} } }) + empty.state.promptPages = { + initial: { items: [{ name: "prompt-one" }], nextCursor: "" }, + "": { items: [{ name: "prompt-two" }] }, + } + const looping = yield* lifecycleServer({ capabilities: { tools: {} } }) + looping.state.toolPages = { + initial: { items: [], nextCursor: "repeat" }, + repeat: { items: [], nextCursor: "repeat" }, + } + const mcp = yield* MCP.Service + yield* mcp.add("empty-cursor", remote(empty.url)) + const result = yield* mcp.add("looping-cursor", remote(looping.url)) + + expect(Object.keys(yield* mcp.prompts())).toEqual(["empty-cursor:prompt-one", "empty-cursor:prompt-two"]) + expect(statusName(result.status, "looping-cursor")).toBe("failed") + }), +) + +it.instance("disconnect removes protocol data and reconnect establishes a new session", () => + Effect.gen(function* () { + const server = yield* lifecycleServer() + const mcp = yield* MCP.Service + yield* mcp.add("reconnect-server", remote(server.url)) + expect((yield* mcp.status())["reconnect-server"]?.status).toBe("connected") + + yield* mcp.disconnect("reconnect-server") + expect((yield* mcp.status())["reconnect-server"]?.status).toBe("disabled") + expect(Object.keys(yield* mcp.tools())).toEqual([]) + yield* pollWithTimeout( + Effect.sync(() => (server.state.aborted > 0 ? server.state.aborted : undefined)), + "disconnected HTTP session was not aborted", + ) + + yield* Effect.promise(server.restart) + yield* mcp.connect("reconnect-server") + expect((yield* mcp.status())["reconnect-server"]?.status).toBe("connected") + expect(Object.keys(yield* mcp.tools())).toEqual(["reconnect-server_test_tool"]) + }), +) + +it.instance("add() closes the old protocol session when replacing a server", () => + Effect.gen(function* () { + const first = yield* lifecycleServer() + const second = yield* lifecycleServer() + const mcp = yield* MCP.Service + yield* mcp.add("replace-server", remote(first.url)) + yield* mcp.add("replace-server", remote(second.url)) + + yield* pollWithTimeout( + Effect.sync(() => (first.state.aborted > 0 ? first.state.aborted : undefined)), + "replaced HTTP session was not aborted", + ) + expect(second.state.aborted).toBe(0) + expect(Object.keys(yield* mcp.tools())).toEqual(["replace-server_test_tool"]) + }), +) + +it.instance("one failed server does not affect another connected server", () => + Effect.gen(function* () { + const good = yield* lifecycleServer() + good.state.tools = [{ name: "good_tool", inputSchema: { type: "object" } }] + const bad = yield* lifecycleServer() + bad.state.listToolsError = "listTools failed" + const mcp = yield* MCP.Service + yield* mcp.add("good-server", remote(good.url)) + yield* mcp.add("bad-server", remote(bad.url)) + + expect((yield* mcp.status())["good-server"]?.status).toBe("connected") + expect((yield* mcp.status())["bad-server"]?.status).toBe("failed") + expect(Object.keys(yield* mcp.tools())).toEqual(["good-server_good_tool"]) + }), +) + +it.instance("falls back when output schema refs fail SDK tool discovery", () => + Effect.gen(function* () { + const server = yield* lifecycleServer({ capabilities: { tools: {} } }) + server.state.tools = [ + { + name: "render_screen", + inputSchema: { type: "object", properties: { prompt: { type: "string" } }, required: ["prompt"] }, + outputSchema: { type: "object", properties: { screen: { $ref: "#/$defs/ScreenInstance" } } }, }, - }, - }, -) - -// ======================================================================== -// Test: connect() on nonexistent server -// ======================================================================== - -it.instance( - "connect() on nonexistent server fails with NotFoundError", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - const exit = yield* mcp.connect("nonexistent").pipe(Effect.exit) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) { - expect(Cause.squash(exit.cause)).toMatchObject({ _tag: "MCP.NotFoundError", name: "nonexistent" }) - } - const status = yield* mcp.status() - expect(status["nonexistent"]).toBeUndefined() - }), - ), - { config: { mcp: {} } }, -) - -// ======================================================================== -// Test: disconnect() on nonexistent server -// ======================================================================== - -it.instance( - "disconnect() on nonexistent server fails with NotFoundError", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - const exit = yield* mcp.disconnect("nonexistent").pipe(Effect.exit) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) { - expect(Cause.squash(exit.cause)).toMatchObject({ _tag: "MCP.NotFoundError", name: "nonexistent" }) + ] + const mcp = yield* MCP.Service + const result = yield* mcp.add("schema-server", remote(server.url)) + + expect(statusName(result.status, "schema-server")).toBe("connected") + expect(Object.keys(yield* mcp.tools())).toEqual(["schema-server_render_screen"]) + }), +) + +it.instance("does not fall back for protocol tool discovery errors", () => + Effect.gen(function* () { + const server = yield* lifecycleServer({ capabilities: { tools: {} } }) + server.state.listToolsError = "transport closed" + const mcp = yield* MCP.Service + const result = yield* mcp.add("broken-server", remote(server.url)) + + expect(statusName(result.status, "broken-server")).toBe("failed") + }), +) + +it.instance("disabled server is marked disabled without opening a protocol session", () => + Effect.gen(function* () { + const server = yield* lifecycleServer() + const mcp = yield* MCP.Service + yield* mcp.add("disabled-server", { ...remote(server.url), enabled: false }) + + expect((yield* mcp.status())["disabled-server"]?.status).toBe("disabled") + expect(server.state.requests).toEqual([]) + }), +) + +it.instance("returns prompts and URI-keyed resources from connected servers", () => + Effect.gen(function* () { + const server = yield* lifecycleServer() + server.state.prompts = [{ name: "my-prompt", description: "A test prompt" }] + server.state.resources = [ + { name: "same-name", uri: "file:///test.txt" }, + { name: "same-name", uri: "ui://component-state" }, + ] + const mcp = yield* MCP.Service + yield* mcp.add("content-server", remote(server.url)) + + expect(Object.keys(yield* mcp.prompts())).toEqual(["content-server:my-prompt"]) + expect(Object.keys(yield* mcp.resources())).toEqual([ + "content-server:file:///test.txt", + "content-server:ui://component-state", + ]) + yield* mcp.disconnect("content-server") + expect(yield* mcp.prompts()).toEqual({}) + }), +) + +it.instance("uses per-server timeouts for prompt and resource requests", () => + Effect.gen(function* () { + const server = yield* lifecycleServer() + server.state.requestDelay = 200 + const mcp = yield* MCP.Service + yield* mcp.add("timeout-server", remote(server.url, 50)) + + expect(yield* mcp.getPrompt("timeout-server", "test")).toBeUndefined() + expect(yield* mcp.readResource("timeout-server", "test://resource")).toBeUndefined() + }), +) + +it.instance("connects resource-only, prompt-only, and tools-only servers", () => + Effect.gen(function* () { + const resources = yield* lifecycleServer({ capabilities: { resources: {} } }) + resources.state.resources = [{ name: "docs", uri: "docs://readme" }] + const prompts = yield* lifecycleServer({ capabilities: { prompts: {} } }) + prompts.state.prompts = [{ name: "review" }] + const tools = yield* lifecycleServer({ capabilities: { tools: {} } }) + const mcp = yield* MCP.Service + yield* mcp.add("resource-only", remote(resources.url)) + yield* mcp.add("prompt-only", remote(prompts.url)) + yield* mcp.add("tools-only", remote(tools.url)) + + expect(Object.keys(yield* mcp.tools())).toEqual(["tools-only_test_tool"]) + expect(Object.keys(yield* mcp.prompts())).toEqual(["prompt-only:review"]) + expect(Object.keys(yield* mcp.resources())).toEqual(["resource-only:docs://readme"]) + }), +) + +it.instance("connect and disconnect fail for unknown servers", () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + for (const operation of [mcp.connect("missing"), mcp.disconnect("missing")]) { + const exit = yield* operation.pipe(Effect.exit) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) { + expect(Cause.squash(exit.cause)).toMatchObject({ _tag: "MCP.NotFoundError", name: "missing" }) + } + } + expect(yield* mcp.status()).toEqual({}) + expect(yield* mcp.tools()).toEqual({}) + }), +) + +it.instance("unavailable remote server is marked failed without tools", () => + Effect.gen(function* () { + const server = yield* Effect.acquireRelease( + Effect.sync(() => Bun.serve({ port: 0, fetch: () => new Response("unavailable", { status: 503 }) })), + (http) => Effect.promise(() => http.stop(true)), + ) + const mcp = yield* MCP.Service + yield* mcp.add("unavailable", remote(server.url.toString(), 500)) + + expect((yield* mcp.status()).unavailable?.status).toBe("failed") + expect(yield* mcp.tools()).toEqual({}) + }), +) + +it.instance("tools() prefixes sanitized server and tool names", () => + Effect.gen(function* () { + const server = yield* lifecycleServer({ capabilities: { tools: {} } }) + server.state.tools = [ + { name: "tool-a", inputSchema: { type: "object" } }, + { name: "tool.b", inputSchema: { type: "object" } }, + ] + const mcp = yield* MCP.Service + yield* mcp.add("my.special-server", remote(server.url)) + + expect(Object.keys(yield* mcp.tools())).toEqual(["my_special-server_tool-a", "my_special-server_tool_b"]) + }), +) + +it.instance("local stdio timeout terminates the real server process", () => + Effect.gen(function* () { + const test = yield* TestInstance + const pidFile = path.join(test.directory, "mcp.pid") + const mcp = yield* MCP.Service + const result = yield* mcp.add("hanging-stdio", { + type: "local", + command: [process.execPath, stdioFixture, "--hang"], + environment: { MCP_LIFECYCLE_PID_FILE: pidFile }, + timeout: 100, + }) + + expect(statusName(result.status, "hanging-stdio")).toBe("failed") + const pid = yield* pollWithTimeout( + Effect.promise(async () => { + const file = Bun.file(pidFile) + return (await file.exists()) ? Number(await file.text()) : undefined + }), + "stdio fixture did not publish its pid", + ) + yield* pollWithTimeout( + Effect.sync(() => { + try { + process.kill(pid, 0) + return undefined + } catch { + return true } }), - ), - { config: { mcp: {} } }, + "stdio fixture process was not terminated", + ) + }), ) -// ======================================================================== -// Test: tools() with no MCP servers configured -// ======================================================================== +it.instance("remote timeout aborts both real HTTP transport attempts", () => + Effect.gen(function* () { + const server = yield* hangingLifecycleServer() + const mcp = yield* MCP.Service + const result = yield* mcp.add("hanging-remote", remote(server.url, 100)) -it.instance( - "tools() returns empty when no MCP servers are configured", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - const tools = yield* mcp.tools() - expect(Object.keys(tools).length).toBe(0) - }), - ), - { config: { mcp: {} } }, + expect(statusName(result.status, "hanging-remote")).toBe("failed") + yield* pollWithTimeout( + Effect.sync(() => (server.aborted() >= 2 ? server.aborted() : undefined)), + "remote transport requests were not aborted", + ) + expect(server.requests).toEqual(["POST", "GET"]) + }), ) -// ======================================================================== -// Test: connect failure during create() -// ======================================================================== - -it.instance( - "server that fails to connect is marked as failed", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "fail-connect" - getOrCreateClientState("fail-connect") - connectShouldFail = true - connectError = "Connection refused" - - yield* mcp.add("fail-connect", { - type: "local", - command: ["echo", "test"], - }) - - const status = yield* mcp.status() - expect(status["fail-connect"]?.status).toBe("failed") - if (status["fail-connect"]?.status === "failed") { - expect(status["fail-connect"].error).toContain("Connection refused") - } - - // No tools should be available - const tools = yield* mcp.tools() - expect(Object.keys(tools).length).toBe(0) - }), - ), - { - config: { - mcp: { - "fail-connect": { - type: "local", - command: ["echo", "test"], - }, - }, - }, - }, -) - -// ======================================================================== -// Bug #5: McpOAuthCallback.cancelPending uses wrong key -// ======================================================================== - -it.live("McpOAuthCallback.cancelPending is keyed by mcpName but pendingAuths uses oauthState", () => +it.live("McpOAuthCallback.cancelPending rejects the pending callback", () => Effect.acquireUseRelease( Effect.sync(() => McpOAuthCallback.waitForCallback("abc123hexstate", "my-mcp-server")), (callback) => Effect.gen(function* () { McpOAuthCallback.cancelPending("my-mcp-server") - const exit = yield* Effect.tryPromise({ try: () => callback, catch: (error) => (error instanceof Error ? error : new Error(String(error))), - }).pipe( - Effect.timeoutOrElse({ - duration: "1 second", - orElse: () => Effect.fail(new Error("timed out waiting for OAuth cancellation")), - }), - Effect.exit, - ) - + }).pipe(Effect.exit) expect(Exit.isFailure(exit)).toBe(true) }), () => Effect.promise(() => McpOAuthCallback.stop()).pipe(Effect.ignore), ), ) - -// ======================================================================== -// Test: multiple tools from same server get correct name prefixes -// ======================================================================== - -it.instance( - "tools() prefixes tool names with sanitized server name", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "my.special-server" - const serverState = getOrCreateClientState("my.special-server") - serverState.tools = [ - { name: "tool-a", description: "Tool A", inputSchema: { type: "object", properties: {} } }, - { name: "tool.b", description: "Tool B", inputSchema: { type: "object", properties: {} } }, - ] - - yield* mcp.add("my.special-server", { - type: "local", - command: ["echo", "test"], - }) - - const tools = yield* mcp.tools() - const keys = Object.keys(tools) - - // Server name dots should be replaced with underscores - expect(keys.some((k) => k.startsWith("my_special-server_"))).toBe(true) - // Tool name dots should be replaced with underscores - expect(keys.some((k) => k.endsWith("tool_b"))).toBe(true) - expect(keys.length).toBe(2) - }), - ), - { - config: { - mcp: { - "my.special-server": { - type: "local", - command: ["echo", "test"], - }, - }, - }, - }, -) - -// ======================================================================== -// Test: transport leak — local stdio timeout (#19168) -// ======================================================================== - -it.instance( - "local stdio transport is closed when connect times out (no process leak)", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "hanging-server" - getOrCreateClientState("hanging-server") - connectShouldHang = true - - const addResult = yield* mcp.add("hanging-server", { - type: "local", - command: ["node", "fake.js"], - timeout: 100, - }) - - const serverStatus = (addResult.status as any)["hanging-server"] ?? addResult.status - expect(serverStatus.status).toBe("failed") - expect(serverStatus.error).toContain("timed out") - // Transport must be closed to avoid orphaned child process - expect(transportCloseCount).toBeGreaterThanOrEqual(1) - }), - ), - { config: { mcp: {} } }, -) - -// ======================================================================== -// Test: transport leak — remote timeout (#19168) -// ======================================================================== - -it.instance( - "remote transport is closed when connect times out", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "hanging-remote" - getOrCreateClientState("hanging-remote") - connectShouldHang = true - - const addResult = yield* mcp.add("hanging-remote", { - type: "remote", - url: "http://localhost:9999/mcp", - timeout: 100, - oauth: false, - }) - - const serverStatus = (addResult.status as any)["hanging-remote"] ?? addResult.status - expect(serverStatus.status).toBe("failed") - // Transport must be closed to avoid leaked HTTP connections - expect(transportCloseCount).toBeGreaterThanOrEqual(1) - }), - ), - { config: { mcp: {} } }, -) - -// ======================================================================== -// Test: transport leak — failed remote transports not closed (#19168) -// ======================================================================== - -it.instance( - "failed remote transport is closed before trying next transport", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "fail-remote" - getOrCreateClientState("fail-remote") - connectShouldFail = true - connectError = "Connection refused" - - const addResult = yield* mcp.add("fail-remote", { - type: "remote", - url: "http://localhost:9999/mcp", - timeout: 5000, - oauth: false, - }) - - const serverStatus = (addResult.status as any)["fail-remote"] ?? addResult.status - expect(serverStatus.status).toBe("failed") - // Both StreamableHTTP and SSE transports should be closed - expect(transportCloseCount).toBeGreaterThanOrEqual(2) - }), - ), - { config: { mcp: {} } }, -) diff --git a/packages/opencode/test/mcp/oauth-auto-connect.test.ts b/packages/opencode/test/mcp/oauth-auto-connect.test.ts index febbe0d0bdc7..5f8889068c33 100644 --- a/packages/opencode/test/mcp/oauth-auto-connect.test.ts +++ b/packages/opencode/test/mcp/oauth-auto-connect.test.ts @@ -1,199 +1,155 @@ -import { expect, mock, beforeEach } from "bun:test" +import { expect } from "bun:test" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" +import { ListResourcesRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { LayerNode } from "@opencode-ai/core/effect/layer-node" -import { Effect, Layer } from "effect" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Effect } from "effect" +import { Config } from "../../src/config/config" +import { EventV2Bridge } from "../../src/event-v2-bridge" +import { McpAuth } from "../../src/mcp/auth" +import { MCP } from "../../src/mcp/index" +import { McpOAuthCallback } from "../../src/mcp/oauth-callback" +import { McpOAuthPendingProvider, McpOAuthProvider } from "../../src/mcp/oauth-provider" import { testEffect } from "../lib/effect" -// Mock UnauthorizedError to match the SDK's class -class MockUnauthorizedError extends Error { - constructor(message?: string) { - super(message ?? "Unauthorized") - this.name = "UnauthorizedError" - } +const mcpTest = testEffect( + LayerNode.compile( + LayerNode.group([MCP.node, McpAuth.node, EventV2Bridge.node, Config.node, CrossSpawnSpawner.node, FSUtil.node]), + ), +) + +interface OAuthMcpOptions { + capabilities?: "tools" | "resources" } -// Track what options were passed to each transport constructor -const transportCalls: Array<{ - type: "streamable" | "sse" - url: string - options: { authProvider?: unknown } -}> = [] - -// Controls whether the mock transport simulates a 401 that triggers the SDK -// auth flow (which calls provider.state()) or a simple UnauthorizedError. -let simulateAuthFlow = true -let connectSucceedsImmediately = false -let serverCapabilities: { tools?: object; resources?: object } = { tools: {} } -let listToolsCalls = 0 -let finishAuthFails = false -let finishAuthStoresCredentials = false - -// Mock the transport constructors to simulate OAuth auto-auth on 401 -void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ - StreamableHTTPClientTransport: class MockStreamableHTTP { - authProvider: - | { - state?: () => Promise - redirectToAuthorization?: (url: URL) => Promise - saveCodeVerifier?: (v: string) => Promise - tokens?: () => Promise<{ access_token: string } | undefined> - clientInformation?: () => Promise<{ client_id: string } | undefined> - saveClientInformation?: (info: { client_id: string; client_secret?: string }) => Promise - saveTokens?: (tokens: { access_token: string; token_type: string }) => Promise - } - | undefined - constructor(url: URL, options?: { authProvider?: unknown }) { - this.authProvider = options?.authProvider as typeof this.authProvider - transportCalls.push({ - type: "streamable", - url: url.toString(), - options: options ?? {}, +function serveOAuthMcp(options: OAuthMcpOptions = {}) { + return Effect.acquireRelease( + Effect.promise(async () => { + const capabilities = options.capabilities ?? "tools" + const protocol = new Server( + { name: "oauth-auto-connect", version: "1.0.0" }, + { capabilities: capabilities === "tools" ? { tools: {} } : { resources: {} } }, + ) + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + enableJsonResponse: true, }) - } - async start() { - if (connectSucceedsImmediately) return - - // Simulate what the real SDK transport does on 401: - // It calls auth() which eventually calls provider.state(), then - // provider.redirectToAuthorization(), then throws UnauthorizedError. - if (simulateAuthFlow && this.authProvider) { - if (await this.authProvider.tokens?.()) throw new MockUnauthorizedError() - if (await this.authProvider.clientInformation?.()) throw new MockUnauthorizedError() - // The SDK calls provider.state() to get the OAuth state parameter - if (this.authProvider.state) { - await this.authProvider.state() - } - // The SDK calls saveCodeVerifier before redirecting - if (this.authProvider.saveCodeVerifier) { - await this.authProvider.saveCodeVerifier("test-verifier") - } - // The SDK calls redirectToAuthorization to redirect the user - if (this.authProvider.redirectToAuthorization) { - await this.authProvider.redirectToAuthorization(new URL("https://auth.example.com/authorize?state=test")) - } - throw new MockUnauthorizedError() + let listToolsCalls = 0 + let requiresAuth = true + + if (capabilities === "tools") { + protocol.setRequestHandler(ListToolsRequestSchema, () => { + listToolsCalls++ + return Promise.resolve({ tools: [{ name: "test_tool", inputSchema: { type: "object" } }] }) + }) } - throw new MockUnauthorizedError() - } - async finishAuth(_code: string) { - if (finishAuthFails) throw new Error("Token exchange failed") - if (finishAuthStoresCredentials) { - await this.authProvider?.saveClientInformation?.({ client_id: "replacement-client" }) - await this.authProvider?.saveTokens?.({ access_token: "replacement-token", token_type: "Bearer" }) + if (capabilities === "resources") { + protocol.setRequestHandler(ListResourcesRequestSchema, () => + Promise.resolve({ resources: [{ name: "docs", uri: "docs://readme" }] }), + ) } - } - async close() {} - }, -})) - -void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ - SSEClientTransport: class MockSSE { - constructor(url: URL, options?: { authProvider?: unknown }) { - transportCalls.push({ - type: "sse", - url: url.toString(), - options: options ?? {}, - }) - } - async start() { - throw new Error("Mock SSE transport cannot connect") - } - }, -})) - -// Mock the MCP SDK Client -void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ - Client: class MockClient { - setRequestHandler() {} - - async connect(transport: { start: () => Promise }) { - await transport.start() - } - - setNotificationHandler() {} - - getServerCapabilities() { - return serverCapabilities - } - - getInstructions() {} - - async listTools() { - listToolsCalls++ - return { tools: [{ name: "test_tool", inputSchema: { type: "object", properties: {} } }] } - } - - async listResources() { - return { resources: [{ name: "docs", uri: "docs://readme" }] } - } - - async close() {} - }, -})) - -// Mock UnauthorizedError in the auth module so instanceof checks work -void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ - UnauthorizedError: MockUnauthorizedError, -})) - -beforeEach(() => { - transportCalls.length = 0 - simulateAuthFlow = true - connectSucceedsImmediately = false - serverCapabilities = { tools: {} } - listToolsCalls = 0 - finishAuthFails = false - finishAuthStoresCredentials = false -}) -// Import modules after mocking -const { MCP } = await import("../../src/mcp/index") -const { EventV2Bridge } = await import("../../src/event-v2-bridge") -const { Config } = await import("../../src/config/config") -const { McpAuth } = await import("../../src/mcp/auth") -const { McpOAuthProvider } = await import("../../src/mcp/oauth-provider") -const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback") -const { FSUtil } = await import("@opencode-ai/core/fs-util") -const { CrossSpawnSpawner } = await import("@opencode-ai/core/cross-spawn-spawner") + await protocol.connect(transport) + const http = Bun.serve({ + port: 0, + async fetch(request) { + const url = new URL(request.url) + const origin = url.origin + const mcpUrl = `${origin}/mcp` + + if (url.pathname === "/.well-known/oauth-protected-resource/mcp") { + return Response.json({ + resource: mcpUrl, + authorization_servers: [origin], + scopes_supported: ["mcp"], + }) + } + if (url.pathname === "/.well-known/oauth-protected-resource") { + return Response.json({ + resource: mcpUrl, + authorization_servers: [origin], + scopes_supported: ["mcp"], + }) + } + if (url.pathname === "/.well-known/oauth-authorization-server") { + return Response.json({ + issuer: origin, + authorization_endpoint: `${origin}/authorize`, + token_endpoint: `${origin}/token`, + registration_endpoint: `${origin}/register`, + response_types_supported: ["code"], + grant_types_supported: ["authorization_code", "refresh_token"], + token_endpoint_auth_methods_supported: ["none"], + code_challenge_methods_supported: ["S256"], + scopes_supported: ["mcp"], + }) + } + if (url.pathname === "/register") { + const metadata = (await request.json()) as Record + return Response.json({ ...metadata, client_id: "replacement-client" }, { status: 201 }) + } + if (url.pathname === "/token") { + const body = new URLSearchParams(await request.text()) + if (body.get("code") !== "valid-code") { + return Response.json( + { error: "invalid_grant", error_description: "Token exchange failed" }, + { status: 400 }, + ) + } + return Response.json({ access_token: "replacement-token", token_type: "Bearer" }) + } + if (url.pathname !== "/mcp") return new Response("Not found", { status: 404 }) + + if (request.method === "GET") return new Response(null, { status: 405 }) + if (requiresAuth && request.headers.get("authorization") !== "Bearer replacement-token") { + return new Response("Unauthorized", { + status: 401, + headers: { + "WWW-Authenticate": `Bearer resource_metadata="${origin}/.well-known/oauth-protected-resource", scope="mcp"`, + }, + }) + } + return transport.handleRequest(request) + }, + }) -const mcpTest = testEffect( - LayerNode.compile( - LayerNode.group([MCP.node, McpAuth.node, EventV2Bridge.node, Config.node, CrossSpawnSpawner.node, FSUtil.node]), - ), -) + return { + url: new URL("/mcp", http.url).toString(), + allowAnonymous: () => { + requiresAuth = false + }, + listToolsCalls: () => listToolsCalls, + close: async () => { + await http.stop(true) + await protocol.close() + }, + } + }), + (server) => Effect.promise(server.close), + ) +} -const config = (name: string) => ({ - mcp: { - [name]: { - type: "remote" as const, - url: "https://example.com/mcp", - }, - }, +const remote = (url: string, enabled = true) => ({ + type: "remote" as const, + url, + enabled, }) -mcpTest.instance( - "first connect to OAuth server shows needs_auth instead of failed", - () => - MCP.Service.use((mcp) => - Effect.gen(function* () { - const result = yield* mcp.add("test-oauth", { - type: "remote", - url: "https://example.com/mcp", - }) +const stopOAuthCallback = Effect.addFinalizer(() => Effect.promise(() => McpOAuthCallback.stop()).pipe(Effect.ignore)) - const serverStatus = result.status as Record - - // The server should be detected as needing auth, NOT as failed. - // Before the fix, provider.state() would throw a plain Error - // ("No OAuth state saved for MCP server: test-oauth") which was - // not caught as UnauthorizedError, causing status to be "failed". - expect(serverStatus["test-oauth"]).toBeDefined() - expect(serverStatus["test-oauth"].status).toBe("needs_auth") - }), - ), - { config: config("test-oauth") }, +mcpTest.instance("first connect to OAuth server shows needs_auth instead of failed", () => + Effect.gen(function* () { + const server = yield* serveOAuthMcp() + const mcp = yield* MCP.Service + const result = yield* mcp.add("test-oauth", remote(server.url)) + + expect((result.status as Record)["test-oauth"]).toEqual({ status: "needs_auth" }) + }), ) -mcpTest.instance("state() generates a new state when none is saved", () => +mcpTest.instance("state() generates and persists a new state when none is saved", () => Effect.gen(function* () { const auth = yield* McpAuth.Service const provider = new McpOAuthProvider( @@ -204,17 +160,11 @@ mcpTest.instance("state() generates a new state when none is saved", () => auth, ) - const entryBefore = yield* McpAuth.use.get("test-state-gen") - expect(entryBefore?.oauthState).toBeUndefined() + expect((yield* auth.get("test-state-gen"))?.oauthState).toBeUndefined() - // state() should generate and return a new state, not throw const state = yield* Effect.promise(() => provider.state()) - expect(typeof state).toBe("string") - expect(state.length).toBe(64) // 32 bytes as hex - - // The generated state should be persisted - const entryAfter = yield* McpAuth.use.get("test-state-gen") - expect(entryAfter?.oauthState).toBe(state) + expect(state).toHaveLength(64) + expect((yield* auth.get("test-state-gen"))?.oauthState).toBe(state) }), ) @@ -229,139 +179,122 @@ mcpTest.instance("state() returns existing state when one is saved", () => auth, ) - // Pre-save a state - const existingState = "pre-saved-state-value" - yield* McpAuth.use.updateOAuthState("test-state-existing", existingState) + yield* auth.updateOAuthState("test-state-existing", "pre-saved-state-value") + expect(yield* Effect.promise(() => provider.state())).toBe("pre-saved-state-value") + }), +) - // state() should return the existing state - const state = yield* Effect.promise(() => provider.state()) - expect(state).toBe(existingState) +mcpTest.instance("pending provider does not expose or overwrite existing credentials before commit", () => + Effect.gen(function* () { + const auth = yield* McpAuth.Service + const name = "test-pending-credentials" + const url = "https://example.com/mcp" + const provider = new McpOAuthPendingProvider(name, url, {}, { onRedirect: async () => {} }, auth) + + yield* auth.updateClientInfo(name, { clientId: "old-client" }, url) + yield* auth.updateTokens(name, { accessToken: "old-token" }, url) + + expect(yield* Effect.promise(() => provider.clientInformation())).toBeUndefined() + expect(yield* Effect.promise(() => provider.tokens())).toBeUndefined() + expect((yield* auth.get(name))?.tokens?.accessToken).toBe("old-token") + expect((yield* auth.get(name))?.clientInfo?.clientId).toBe("old-client") }), ) -mcpTest.instance( - "failed reauthentication preserves existing credentials", - () => - Effect.gen(function* () { - yield* Effect.addFinalizer(() => Effect.promise(() => McpOAuthCallback.stop()).pipe(Effect.ignore)) - const mcp = yield* MCP.Service - const auth = yield* McpAuth.Service - const name = "test-reauth-failure" - const url = "https://example.com/mcp" - const clientInfo = { clientId: "dynamic-client", clientSecret: "dynamic-secret" } - - yield* auth.updateClientInfo(name, clientInfo, url) - yield* auth.updateTokens(name, { accessToken: "working-token" }, url) - expect((yield* mcp.startAuth(name)).authorizationUrl).toContain("https://auth.example.com/authorize") - finishAuthFails = true - - expect(yield* mcp.finishAuth(name, "invalid-code")).toEqual({ - status: "failed", - error: "OAuth completion failed: Token exchange failed", - }) - const entry = yield* auth.get(name) - expect(entry?.tokens?.accessToken).toBe("working-token") - expect(entry?.clientInfo).toEqual(clientInfo) - }), - { config: config("test-reauth-failure") }, +mcpTest.instance("failed reauthentication preserves existing credentials", () => + Effect.gen(function* () { + yield* stopOAuthCallback + const server = yield* serveOAuthMcp() + const mcp = yield* MCP.Service + const auth = yield* McpAuth.Service + const name = "test-reauth-failure" + + yield* auth.updateClientInfo(name, { clientId: "dynamic-client", clientSecret: "dynamic-secret" }, server.url) + yield* auth.updateTokens(name, { accessToken: "working-token" }, server.url) + yield* mcp.add(name, remote(server.url)) + expect((yield* mcp.startAuth(name)).authorizationUrl).toContain("/authorize") + + expect(yield* mcp.finishAuth(name, "invalid-code")).toEqual({ + status: "failed", + error: "OAuth completion failed: Token exchange failed", + }) + expect((yield* auth.get(name))?.tokens?.accessToken).toBe("working-token") + expect((yield* auth.get(name))?.clientInfo).toMatchObject({ + clientId: "dynamic-client", + clientSecret: "dynamic-secret", + }) + }), ) -mcpTest.instance( - "successful reauthentication commits replacement credentials", - () => - Effect.gen(function* () { - yield* Effect.addFinalizer(() => Effect.promise(() => McpOAuthCallback.stop()).pipe(Effect.ignore)) - const mcp = yield* MCP.Service - const auth = yield* McpAuth.Service - const name = "test-reauth-success" - const url = "https://example.com/mcp" - - yield* auth.updateClientInfo(name, { clientId: "old-client" }, url) - yield* auth.updateTokens(name, { accessToken: "old-token" }, url) - expect((yield* mcp.startAuth(name)).authorizationUrl).toContain("https://auth.example.com/authorize") - expect((yield* auth.get(name))?.tokens?.accessToken).toBe("old-token") - finishAuthStoresCredentials = true - connectSucceedsImmediately = true - - expect((yield* mcp.finishAuth(name, "valid-code")).status).toBe("connected") - const entry = yield* auth.get(name) - expect(entry?.tokens?.accessToken).toBe("replacement-token") - expect(entry?.clientInfo?.clientId).toBe("replacement-client") - expect(entry?.serverUrl).toBe(url) - }), - { config: config("test-reauth-success") }, +mcpTest.instance("successful reauthentication commits replacement credentials", () => + Effect.gen(function* () { + yield* stopOAuthCallback + const server = yield* serveOAuthMcp() + const mcp = yield* MCP.Service + const auth = yield* McpAuth.Service + const name = "test-reauth-success" + + yield* auth.updateClientInfo(name, { clientId: "old-client" }, server.url) + yield* auth.updateTokens(name, { accessToken: "old-token" }, server.url) + yield* mcp.add(name, remote(server.url)) + expect((yield* mcp.startAuth(name)).authorizationUrl).toContain("/authorize") + expect((yield* auth.get(name))?.tokens?.accessToken).toBe("old-token") + + expect((yield* mcp.finishAuth(name, "valid-code")).status).toBe("connected") + const entry = yield* auth.get(name) + expect(entry?.tokens?.accessToken).toBe("replacement-token") + expect(entry?.clientInfo?.clientId).toBe("replacement-client") + expect(entry?.serverUrl).toBe(server.url) + }), ) -mcpTest.instance( - "auth status only reports credentials stored for the configured server URL", - () => - Effect.gen(function* () { - const mcp = yield* MCP.Service - expect(transportCalls).toHaveLength(0) - yield* McpAuth.use.updateTokens("test-status-url", { accessToken: "old-token" }, "https://old.example.com/mcp") +mcpTest.instance("auth status only reports credentials stored for the configured server URL", () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + yield* mcp.add("test-status-url", remote("https://example.com/mcp", false)) + yield* McpAuth.use.updateTokens("test-status-url", { accessToken: "old-token" }, "https://old.example.com/mcp") - expect(yield* mcp.getAuthStatus("test-status-url")).toBe("not_authenticated") + expect(yield* mcp.getAuthStatus("test-status-url")).toBe("not_authenticated") - yield* McpAuth.use.updateTokens("test-status-url", { accessToken: "current-token" }, "https://example.com/mcp") - expect(yield* mcp.getAuthStatus("test-status-url")).toBe("authenticated") + yield* McpAuth.use.updateTokens("test-status-url", { accessToken: "current-token" }, "https://example.com/mcp") + expect(yield* mcp.getAuthStatus("test-status-url")).toBe("authenticated") - yield* McpAuth.use.updateTokens( - "test-status-url", - { accessToken: "expired-token", expiresAt: 1 }, - "https://example.com/mcp", - ) - expect(yield* mcp.getAuthStatus("test-status-url")).toBe("expired") - expect(transportCalls).toHaveLength(0) - }), - { config: config("test-status-url") }, + yield* McpAuth.use.updateTokens( + "test-status-url", + { accessToken: "expired-token", expiresAt: 1 }, + "https://example.com/mcp", + ) + expect(yield* mcp.getAuthStatus("test-status-url")).toBe("expired") + }), ) -mcpTest.instance( - "authenticate() stores a connected client when auth completes without redirect", - () => - MCP.Service.use((mcp) => - Effect.gen(function* () { - const added = yield* mcp.add("test-oauth-connect", { - type: "remote", - url: "https://example.com/mcp", - }) - const before = added.status as Record - expect(before["test-oauth-connect"]?.status).toBe("needs_auth") - - simulateAuthFlow = false - connectSucceedsImmediately = true - - const result = yield* mcp.authenticate("test-oauth-connect") - expect(result.status).toBe("connected") - - const after = yield* mcp.status() - expect(after["test-oauth-connect"]?.status).toBe("connected") - }), - ), - { config: config("test-oauth-connect") }, +mcpTest.instance("authenticate() stores a connected client when auth completes without redirect", () => + Effect.gen(function* () { + yield* stopOAuthCallback + const server = yield* serveOAuthMcp() + const mcp = yield* MCP.Service + const name = "test-oauth-connect" + const added = yield* mcp.add(name, remote(server.url)) + expect((added.status as Record)[name]?.status).toBe("needs_auth") + + server.allowAnonymous() + expect((yield* mcp.authenticate(name)).status).toBe("connected") + expect((yield* mcp.status())[name]?.status).toBe("connected") + }), ) -mcpTest.instance( - "authenticate() connects a resource-only server without listing tools", - () => - MCP.Service.use((mcp) => - Effect.gen(function* () { - const added = yield* mcp.add("test-oauth-resources", { - type: "remote", - url: "https://example.com/mcp", - }) - const before = added.status as Record - expect(before["test-oauth-resources"]?.status).toBe("needs_auth") - - simulateAuthFlow = false - connectSucceedsImmediately = true - serverCapabilities = { resources: {} } - - const result = yield* mcp.authenticate("test-oauth-resources") - expect(result.status).toBe("connected") - expect(listToolsCalls).toBe(0) - expect(Object.keys(yield* mcp.resources())).toEqual(["test-oauth-resources:docs://readme"]) - }), - ), - { config: config("test-oauth-resources") }, +mcpTest.instance("authenticate() connects a resource-only server without listing tools", () => + Effect.gen(function* () { + yield* stopOAuthCallback + const server = yield* serveOAuthMcp({ capabilities: "resources" }) + const mcp = yield* MCP.Service + const name = "test-oauth-resources" + const added = yield* mcp.add(name, remote(server.url)) + expect((added.status as Record)[name]?.status).toBe("needs_auth") + + server.allowAnonymous() + expect((yield* mcp.authenticate(name)).status).toBe("connected") + expect(server.listToolsCalls()).toBe(0) + expect(Object.keys(yield* mcp.resources())).toEqual([`${name}:docs://readme`]) + }), ) diff --git a/packages/opencode/test/mcp/oauth-browser.test.ts b/packages/opencode/test/mcp/oauth-browser.test.ts index 07b157ecc98b..9573805a9a14 100644 --- a/packages/opencode/test/mcp/oauth-browser.test.ts +++ b/packages/opencode/test/mcp/oauth-browser.test.ts @@ -1,152 +1,138 @@ -import { expect, mock, beforeEach } from "bun:test" -import { EventEmitter } from "events" +import { expect } from "bun:test" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" +import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Deferred, Effect, Layer, Option } from "effect" +import { Config } from "../../src/config/config" +import { EventV2Bridge } from "../../src/event-v2-bridge" +import { McpAuth } from "../../src/mcp/auth" +import { McpBrowser } from "../../src/mcp/browser" +import { MCP } from "../../src/mcp/index" +import { McpOAuthCallback } from "../../src/mcp/oauth-callback" import { awaitWithTimeout, testEffect } from "../lib/effect" -import type { MCP as MCPNS } from "../../src/mcp/index" - -// Track open() calls and control failure behavior -let openShouldFail = false -let openCalledWith: string | undefined -let openDeferred: Deferred.Deferred | undefined - -void mock.module("open", () => ({ - default: async (url: string) => { - openCalledWith = url - if (openDeferred) Effect.runSync(Deferred.succeed(openDeferred, url).pipe(Effect.ignore)) - - // Return a mock subprocess that emits an error if openShouldFail is true - const subprocess = new EventEmitter() - if (openShouldFail) { - // Emit error asynchronously like a real subprocess would - setTimeout(() => { - subprocess.emit("error", new Error("spawn xdg-open ENOENT")) - }, 10) - } - return subprocess - }, -})) - -// Mock UnauthorizedError -class MockUnauthorizedError extends Error { - constructor() { - super("Unauthorized") - this.name = "UnauthorizedError" - } -} - -// Track what options were passed to each transport constructor -const transportCalls: Array<{ - type: "streamable" | "sse" - url: string - options: { authProvider?: unknown; requestInit?: RequestInit } -}> = [] - -// Mock the transport constructors -void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ - StreamableHTTPClientTransport: class MockStreamableHTTP { - url: string - authProvider: { redirectToAuthorization?: (url: URL) => Promise } | undefined - constructor( - url: URL, - options?: { authProvider?: { redirectToAuthorization?: (url: URL) => Promise }; requestInit?: RequestInit }, - ) { - this.url = url.toString() - this.authProvider = options?.authProvider - transportCalls.push({ - type: "streamable", - url: url.toString(), - options: options ?? {}, - }) - } - async start() { - // Simulate OAuth redirect by calling the authProvider's redirectToAuthorization - if (this.authProvider?.redirectToAuthorization) { - await this.authProvider.redirectToAuthorization(new URL("https://auth.example.com/authorize?client_id=test")) - } - throw new MockUnauthorizedError() - } - async finishAuth(_code: string) { - // Mock successful auth completion - } - }, -})) - -void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ - SSEClientTransport: class MockSSE { - constructor(url: URL) { - transportCalls.push({ - type: "sse", - url: url.toString(), - options: {}, - }) - } - async start() { - throw new Error("Mock SSE transport cannot connect") - } - }, -})) -// Mock the MCP SDK Client to trigger OAuth flow -void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ - Client: class MockClient { - setRequestHandler() {} +const browsers = new Map; fail: boolean }>() + +const browserLayer = Layer.succeed( + McpBrowser.Service, + McpBrowser.Service.of({ + open: (url) => + Effect.gen(function* () { + const browser = browsers.get(new URL(url).origin) + if (!browser) return yield* Effect.fail(new Error(`Unexpected browser URL: ${url}`)) + Deferred.doneUnsafe(browser.opened, Effect.succeed(url)) + if (browser.fail) return yield* Effect.fail(new Error("spawn xdg-open ENOENT")) + yield* Effect.tryPromise({ + try: () => fetch(url).then((response) => response.body?.cancel()), + catch: (error) => (error instanceof Error ? error : new Error(String(error))), + }) + }), + }), +) - async connect(transport: { start: () => Promise }) { - await transport.start() - } +const mcpTest = testEffect( + LayerNode.compile(LayerNode.group([MCP.node, McpAuth.node, EventV2Bridge.node, Config.node]), [ + [McpBrowser.node, browserLayer], + ]), +) - getServerCapabilities() { - return { tools: {} } +const serveOAuthMcp = Effect.acquireRelease( + Effect.promise(async () => { + const requests: Array<{ pathname: string; headers: Headers }> = [] + const protocol = new Server({ name: "oauth-browser", version: "1.0.0" }, { capabilities: { tools: {} } }) + protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] })) + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + enableJsonResponse: true, + }) + await protocol.connect(transport) + + const http = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request) { + const url = new URL(request.url) + requests.push({ pathname: url.pathname, headers: new Headers(request.headers) }) + + if (url.pathname === "/mcp") { + if (request.headers.get("authorization") === "Bearer test-access-token") { + return transport.handleRequest(request) + } + return new Response("Unauthorized", { + status: 401, + headers: { + "WWW-Authenticate": `Bearer resource_metadata="${url.origin}/.well-known/oauth-protected-resource/mcp", scope="mcp"`, + }, + }) + } + + if (url.pathname === "/.well-known/oauth-protected-resource/mcp") { + return Response.json({ + resource: `${url.origin}/mcp`, + authorization_servers: [url.origin], + scopes_supported: ["mcp"], + }) + } + + if (url.pathname === "/.well-known/oauth-authorization-server") { + return Response.json({ + issuer: url.origin, + authorization_endpoint: `${url.origin}/authorize`, + token_endpoint: `${url.origin}/token`, + registration_endpoint: `${url.origin}/register`, + scopes_supported: ["mcp"], + response_types_supported: ["code"], + grant_types_supported: ["authorization_code"], + token_endpoint_auth_methods_supported: ["none"], + code_challenge_methods_supported: ["S256"], + }) + } + + if (url.pathname === "/register") { + const metadata = await request.json() + if (!metadata || typeof metadata !== "object") return new Response("Invalid metadata", { status: 400 }) + return Response.json({ ...metadata, client_id: "test-client" }, { status: 201 }) + } + + if (url.pathname === "/authorize") { + const redirect = new URL(url.searchParams.get("redirect_uri") ?? "") + redirect.searchParams.set("code", "test-code") + const state = url.searchParams.get("state") + if (state) redirect.searchParams.set("state", state) + return Response.redirect(redirect, 302) + } + + if (url.pathname === "/token") { + return Response.json({ access_token: "test-access-token", token_type: "Bearer", scope: "mcp" }) + } + + return new Response("Not found", { status: 404 }) + }, + }) + + return { + requests, + url: new URL("/mcp", http.url).toString(), + close: async () => { + await http.stop(true) + await protocol.close() + }, } - }, -})) - -// Mock UnauthorizedError in the auth module -void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ - UnauthorizedError: MockUnauthorizedError, -})) - -beforeEach(() => { - openShouldFail = false - openCalledWith = undefined - openDeferred = undefined - transportCalls.length = 0 -}) - -// Import modules after mocking -const { MCP } = await import("../../src/mcp/index") -const { EventV2Bridge } = await import("../../src/event-v2-bridge") -const { Config } = await import("../../src/config/config") -const { McpAuth } = await import("../../src/mcp/auth") -const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback") -const { FSUtil } = await import("@opencode-ai/core/fs-util") -const { CrossSpawnSpawner } = await import("@opencode-ai/core/cross-spawn-spawner") -const mcpTest = testEffect( - LayerNode.compile( - LayerNode.group([MCP.node, McpAuth.node, EventV2Bridge.node, Config.node, CrossSpawnSpawner.node, FSUtil.node]), - ), + }), + (server) => Effect.promise(server.close), ) -const service = MCP.Service as unknown as Effect.Effect - -const config = (name: string, headers?: Record) => ({ - mcp: { - [name]: { - type: "remote" as const, - url: "https://example.com/mcp", - headers, - }, - }, -}) const withCallbackStop = Effect.addFinalizer(() => Effect.promise(() => McpOAuthCallback.stop()).pipe(Effect.ignore)) -const trackBrowserOpen = Effect.gen(function* () { - const opened = yield* Deferred.make() - openDeferred = opened - yield* Effect.addFinalizer(() => Effect.sync(() => (openDeferred = undefined))) - return opened -}) +const trackBrowserOpen = (url: string, fail = false) => + Effect.gen(function* () { + const origin = new URL(url).origin + const opened = yield* Deferred.make() + browsers.set(origin, { opened, fail }) + yield* Effect.addFinalizer(() => Effect.sync(() => browsers.delete(origin))) + return opened + }) const trackBrowserOpenFailed = Effect.gen(function* () { const events = yield* EventV2Bridge.Service @@ -160,84 +146,80 @@ const trackBrowserOpenFailed = Effect.gen(function* () { return event }) -const authenticateScoped = (name: string, onAuthorization?: (authorizationUrl: string) => void) => +const addServer = Effect.fnUntraced(function* (name: string, url: string, headers?: Record) { + const mcp = yield* MCP.Service + const result = yield* mcp.add(name, { type: "remote", url, headers }) + expect(result.status).toMatchObject({ [name]: { status: "needs_auth" } }) + return mcp +}) + +mcpTest.instance("BrowserOpenFailed event is published when browser launch fails", () => Effect.gen(function* () { - const mcp = yield* service - yield* mcp.authenticate(name, onAuthorization).pipe( - Effect.ignore, - Effect.catchCause(() => Effect.void), - Effect.forkScoped, + yield* withCallbackStop + const server = yield* serveOAuthMcp + yield* trackBrowserOpen(server.url, true) + + const event = yield* trackBrowserOpenFailed + const mcp = yield* addServer("test-oauth-server", server.url) + yield* mcp.authenticate("test-oauth-server").pipe(Effect.ignore, Effect.forkScoped) + + const failure = yield* awaitWithTimeout( + Deferred.await(event), + "Timed out waiting for BrowserOpenFailed event", + "5 seconds", ) - }) -mcpTest.instance( - "BrowserOpenFailed event is published when open() throws", - () => - Effect.gen(function* () { - yield* withCallbackStop - openShouldFail = true - - const event = yield* trackBrowserOpenFailed - yield* authenticateScoped("test-oauth-server") - - const failure = yield* awaitWithTimeout( - Deferred.await(event), - "Timed out waiting for BrowserOpenFailed event", - "5 seconds", - ) - - expect(failure.mcpName).toBe("test-oauth-server") - expect(failure.url).toContain("https://") - }), - { config: config("test-oauth-server") }, + expect(failure.mcpName).toBe("test-oauth-server") + expect(failure.url).toStartWith(new URL("/authorize", server.url).toString()) + }), ) -mcpTest.instance( - "BrowserOpenFailed event is NOT published when open() succeeds", - () => - Effect.gen(function* () { - yield* withCallbackStop - openShouldFail = false - - const opened = yield* trackBrowserOpen - const event = yield* trackBrowserOpenFailed - yield* authenticateScoped("test-oauth-server-2") - - yield* awaitWithTimeout(Deferred.await(opened), "Timed out waiting for open()", "5 seconds") - const failure = yield* Deferred.await(event).pipe(Effect.timeoutOption("700 millis")) +mcpTest.instance("BrowserOpenFailed event is not published when browser launch succeeds", () => + Effect.gen(function* () { + yield* withCallbackStop + const server = yield* serveOAuthMcp + + const opened = yield* trackBrowserOpen(server.url) + const event = yield* trackBrowserOpenFailed + const mcp = yield* addServer("test-oauth-server-2", server.url) + const status = yield* awaitWithTimeout( + mcp.authenticate("test-oauth-server-2"), + "Timed out completing OAuth authentication", + "5 seconds", + ) + const url = yield* Deferred.await(opened) + const failure = yield* Deferred.await(event).pipe(Effect.timeoutOption("700 millis")) - expect(failure).toEqual(Option.none()) - expect(openCalledWith).toBeDefined() - }), - { config: config("test-oauth-server-2") }, + expect(status).toEqual({ status: "connected" }) + expect(failure).toEqual(Option.none()) + expect(new URL(url).origin).toBe(new URL(server.url).origin) + }), ) -mcpTest.instance( - "open() is called with the authorization URL", - () => - Effect.gen(function* () { - yield* withCallbackStop - openShouldFail = false - openCalledWith = undefined - - const opened = yield* trackBrowserOpen - const event = yield* trackBrowserOpenFailed - const authorization = yield* Deferred.make() - yield* authenticateScoped("test-oauth-server-3", (url) => Deferred.doneUnsafe(authorization, Effect.succeed(url))) - - const url = yield* awaitWithTimeout(Deferred.await(opened), "Timed out waiting for open()", "5 seconds") - const authorizationUrl = yield* awaitWithTimeout( - Deferred.await(authorization), - "Timed out waiting for authorization URL", - "5 seconds", - ) - const failure = yield* Deferred.await(event).pipe(Effect.timeoutOption("700 millis")) - - expect(failure).toEqual(Option.none()) - expect(authorizationUrl).toBe(url) - expect(typeof url).toBe("string") - expect(url).toContain("https://") - expect(transportCalls.at(-1)?.options.requestInit?.headers).toEqual({ "X-Custom-Header": "custom-value" }) - }), - { config: config("test-oauth-server-3", { "X-Custom-Header": "custom-value" }) }, +mcpTest.instance("browser launch receives the discovered authorization URL", () => + Effect.gen(function* () { + yield* withCallbackStop + const server = yield* serveOAuthMcp + + const opened = yield* trackBrowserOpen(server.url) + const authorization = yield* Deferred.make() + const mcp = yield* addServer("test-oauth-server-3", server.url, { "X-Custom-Header": "custom-value" }) + const status = yield* awaitWithTimeout( + mcp.authenticate("test-oauth-server-3", (url) => Deferred.doneUnsafe(authorization, Effect.succeed(url))), + "Timed out completing OAuth authentication", + "5 seconds", + ) + const url = yield* Deferred.await(opened) + const authorizationUrl = yield* Deferred.await(authorization) + + expect(status).toEqual({ status: "connected" }) + expect(authorizationUrl).toBe(url) + expect(new URL(url).pathname).toBe("/authorize") + expect(new URL(url).searchParams.get("client_id")).toBe("test-client") + expect( + server.requests.some( + (request) => request.pathname === "/mcp" && request.headers.get("x-custom-header") === "custom-value", + ), + ).toBe(true) + }), ) From e9f5d340963c5f9c500f644f4a92337f0fa93915 Mon Sep 17 00:00:00 2001 From: David Hill <1879069+iamdavidhill@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:44:10 +0100 Subject: [PATCH 15/34] fix(tui): shorten home tips (#31966) --- .../src/feature-plugins/home/tips-view.tsx | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/tui/src/feature-plugins/home/tips-view.tsx b/packages/tui/src/feature-plugins/home/tips-view.tsx index 16354a59ff6e..d9ef81e40cca 100644 --- a/packages/tui/src/feature-plugins/home/tips-view.tsx +++ b/packages/tui/src/feature-plugins/home/tips-view.tsx @@ -163,23 +163,23 @@ export function Tips(props: { api: TuiPluginApi; connected?: boolean }) { const TIPS: Tip[] = [ "Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files", - "Start a message with {highlight}!{/highlight} to run shell commands directly (e.g., {highlight}!ls -la{/highlight})", + "Start a message with {highlight}!{/highlight} to run shell commands (e.g., {highlight}!ls -la{/highlight})", (shortcuts) => press(shortcuts.agentCycle(), "to cycle between Build and Plan agents"), "Use {highlight}/undo{/highlight} to revert the last message and file changes", "Use {highlight}/redo{/highlight} to restore previously undone messages and file changes", - "Run {highlight}/share{/highlight} to create a public link to your conversation at opencode.ai", - "Drag and drop images or PDFs into the terminal to add them as context", + "Run {highlight}/share{/highlight} to create a public opencode.ai link", + "Drag and drop images or PDFs into the terminal as context", (shortcuts) => press(shortcuts.inputPaste(), "to paste images from your clipboard into the prompt"), (shortcuts) => `Use ${commandText("/editor", shortcuts.editorOpen())} to compose messages in your external editor`, "Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase", - (shortcuts) => `Use ${commandText("/models", shortcuts.modelList())} to see and switch between available AI models`, + (shortcuts) => `Use ${commandText("/models", shortcuts.modelList())} to switch between available AI models`, (shortcuts) => `Use ${commandText("/themes", shortcuts.themeList())} to switch between ${themeCount} built-in themes`, (shortcuts) => `Use ${commandText("/new", shortcuts.sessionNew())} to start a fresh conversation session`, (shortcuts) => `Use ${commandText("/sessions", shortcuts.sessionList())} to list, pin, and continue sessions`, - (shortcuts) => press(shortcuts.sessionPinToggle(), "in the session list to pin a session so it stays at the top"), + (shortcuts) => press(shortcuts.sessionPinToggle(), "in the session list to pin one at the top"), (shortcuts) => shortcuts.sessionQuickSwitch1() && shortcuts.sessionQuickSwitch9() - ? `Pinned sessions are assigned quick slots; use ${shortcutText(shortcuts.sessionQuickSwitch1())} through ${shortcutText(shortcuts.sessionQuickSwitch9())} to switch` + ? `Use ${shortcutText(shortcuts.sessionQuickSwitch1())} through ${shortcutText(shortcuts.sessionQuickSwitch9())} to switch pinned sessions` : undefined, "Run {highlight}/compact{/highlight} to summarize long sessions near context limits", (shortcuts) => `Use ${commandText("/export", shortcuts.sessionExport())} to save the conversation as Markdown`, @@ -198,7 +198,7 @@ const TIPS: Tip[] = [ (shortcuts) => press(shortcuts.inputNewline(), "to add newlines in your prompt"), (shortcuts) => press(shortcuts.inputClear(), "when typing to clear the input field"), (shortcuts) => press(shortcuts.sessionInterrupt(), "to stop the AI mid-response"), - "Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes", + "Switch to {highlight}Plan{/highlight} agent for suggestions without making changes", "Use {highlight}@agent-name{/highlight} in prompts to invoke specialized subagents", (shortcuts) => { const items = [ @@ -208,27 +208,27 @@ const TIPS: Tip[] = [ shortcuts.childNext(), ].filter(Boolean) if (!items.length) return undefined - return `Use ${items.map(shortcutText).join(" / ")} to move between parent and child sessions` + return `Use ${items.map(shortcutText).join(" / ")} for parent/child sessions` }, - "Create {highlight}opencode.json{/highlight} for server settings and {highlight}tui.json{/highlight} for TUI settings", + "Create {highlight}opencode.json{/highlight} for server settings, and {highlight}tui.json{/highlight} for TUI", "Place TUI settings in {highlight}~/.config/opencode/tui.json{/highlight} for global config", "Add {highlight}$schema{/highlight} to your config for autocomplete in your editor", "Configure {highlight}model{/highlight} in config to set your default model", "Override any keybind in {highlight}tui.json{/highlight} via the {highlight}keybinds{/highlight} section", "Set any keybind to {highlight}none{/highlight} to disable it completely", "Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section", - "Add {highlight}.md{/highlight} files to {highlight}.opencode/commands/{/highlight} to define reusable custom prompts", + "Add {highlight}.md{/highlight} files to {highlight}.opencode/commands/{/highlight} for reusable prompts", "Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input", - "Use backticks in commands to inject shell output (e.g., {highlight}`git status`{/highlight})", + "Use backticks to inject shell output (e.g., {highlight}`git status`{/highlight})", "Add {highlight}.md{/highlight} files to {highlight}.opencode/agents/{/highlight} for specialized AI personas", "Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools", 'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions', 'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands', 'Configure {highlight}"git push": "ask"{/highlight} to require approval before pushing', - 'Set {highlight}"formatter": true{/highlight} in config to enable built-in formatters like prettier, gofmt, and ruff', - 'Set {highlight}"formatter": false{/highlight} in config to disable formatters enabled by another config layer', + 'Set {highlight}"formatter": true{/highlight} to enable built-in formatters', + 'Set {highlight}"formatter": false{/highlight} to disable inherited formatters', "Define custom formatter commands with file extensions in config", - 'Set {highlight}"lsp": true{/highlight} in config to enable built-in LSP servers for code analysis', + 'Set {highlight}"lsp": true{/highlight} to enable built-in LSP code analysis', "Create {highlight}.ts{/highlight} files in {highlight}.opencode/tools/{/highlight} to define new LLM tools", "Tool definitions can invoke scripts written in Python, Go, etc", "Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugins/{/highlight} for event hooks", @@ -251,7 +251,7 @@ const TIPS: Tip[] = [ "Create JSON theme files in {highlight}.opencode/themes/{/highlight} directory", "Themes support dark/light variants for both modes", "Use numeric xterm color codes 0-255 in custom theme JSON", - "Use {highlight}{env:VAR_NAME}{/highlight} syntax to reference environment variables in config", + "Use {highlight}{env:VAR_NAME}{/highlight} for environment variables in config", "Use {highlight}{file:path}{/highlight} to include file contents in config values", "Use {highlight}instructions{/highlight} in config to load additional rules files", "Set agent {highlight}temperature{/highlight} from 0.0 (focused) to 1.0 (creative)", @@ -269,12 +269,12 @@ const TIPS: Tip[] = [ (shortcuts) => `Use ${commandText("/timeline", shortcuts.sessionTimeline())} to jump to specific messages`, (shortcuts) => press(shortcuts.messagesToggleConceal(), "to toggle code block visibility in messages"), (shortcuts) => `Use ${commandText("/status", shortcuts.statusView())} to see system status info`, - "Enable {highlight}scroll_acceleration{/highlight} in {highlight}tui.json{/highlight} for smooth macOS-style scrolling", + "Enable {highlight}scroll_acceleration{/highlight} in {highlight}tui.json{/highlight} for smooth scrolling", (shortcuts) => shortcuts.commandList() ? `Toggle username display in chat via the command palette (${shortcutText(shortcuts.commandList())})` : "Toggle username display in chat via the command palette", - "Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} for containerized use", + "Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} in a container", "Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models", "Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing", "Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs", From 68f225a11d119b7ee29b6c0110cb2f6543bf18cc Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:50:25 -0500 Subject: [PATCH 16/34] fix(provider): preserve OpenRouter small model effort (#35478) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 3 --- packages/opencode/test/provider/transform.test.ts | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 2a8c3cfb413c..f42e14139f1e 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1240,9 +1240,6 @@ export function smallOptions(model: Provider.Model) { return mergeDeep(base, small) } if (model.providerID === "openrouter" || model.providerID === "llmgateway") { - if (model.providerID === "openrouter" && small.reasoning?.effort === "low") { - return { reasoning: { effort: "none" } } - } if (Object.keys(small).length === 0 && model.api.id.includes("google")) { return { reasoning: { enabled: false } } } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 912dbb13cb2f..7307e3f60b7e 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4638,12 +4638,12 @@ describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => { } }) -test("ProviderTransform.smallOptions disables OpenRouter reasoning when the weakest effort is low", () => { +test("ProviderTransform.smallOptions preserves the weakest OpenRouter reasoning effort", () => { expect( ProviderTransform.smallOptions({ providerID: "openrouter", api: { - id: "anthropic/claude-sonnet-4.6", + id: "google/gemini-3.5-flash", npm: "@openrouter/ai-sdk-provider", }, variants: { @@ -4652,7 +4652,7 @@ test("ProviderTransform.smallOptions disables OpenRouter reasoning when the weak high: { reasoning: { effort: "high" } }, }, } as any), - ).toEqual({ reasoning: { effort: "none" } }) + ).toEqual({ reasoning: { effort: "low" } }) }) describe("ProviderTransform.smallOptions - google thinking controls", () => { From 2b34df94fa0faa7849959fc0a90328b542327812 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:28:28 -0500 Subject: [PATCH 17/34] fix(mcp): preserve metadata across tool pages (#35439) --- packages/opencode/test/mcp/catalog.test.ts | 62 ++++++++++++++++++- .../@modelcontextprotocol%2Fsdk@1.29.0.patch | 50 +++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/packages/opencode/test/mcp/catalog.test.ts b/packages/opencode/test/mcp/catalog.test.ts index 55cabaef7699..7b0d6403bb16 100644 --- a/packages/opencode/test/mcp/catalog.test.ts +++ b/packages/opencode/test/mcp/catalog.test.ts @@ -1,6 +1,10 @@ import { describe, expect, test } from "bun:test" -import type { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { McpCatalog } from "@/mcp/catalog" +import { Effect } from "effect" const options = { toolCallId: "call_mcp", abortSignal: new AbortController().signal } as any @@ -45,3 +49,59 @@ describe("McpCatalog.convertTool", () => { }) }) }) + +test("preserves output schema validation across paginated tool discovery", async () => { + const server = new Server({ name: "pagination", version: "1.0.0" }, { capabilities: { tools: {} } }) + server.setRequestHandler(ListToolsRequestSchema, ({ params }) => + Promise.resolve( + params?.cursor === "page-2" + ? { + tools: [ + { + name: "second", + inputSchema: { type: "object" }, + outputSchema: { + type: "object", + properties: { value: { type: "number" } }, + required: ["value"], + }, + }, + ], + } + : { + tools: [ + { + name: "first", + inputSchema: { type: "object" }, + outputSchema: { + type: "object", + properties: { value: { type: "string" } }, + required: ["value"], + }, + }, + ], + nextCursor: "page-2", + }, + ), + ) + server.setRequestHandler(CallToolRequestSchema, ({ params }) => + Promise.resolve({ + content: [], + structuredContent: { value: params.name === "first" ? 42 : 1 }, + }), + ) + + const client = new Client({ name: "pagination-test", version: "1.0.0" }) + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair() + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]) + + try { + const tools = await Effect.runPromise(McpCatalog.defs(client)) + expect(tools?.map((tool) => tool.name)).toEqual(["first", "second"]) + await expect(client.callTool({ name: "first", arguments: {} })).rejects.toThrow( + "Structured content does not match the tool's output schema", + ) + } finally { + await Promise.all([client.close(), server.close()]) + } +}) diff --git a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch b/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch index e38e68e75d49..13b8000a0139 100644 --- a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch +++ b/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch @@ -112,6 +112,31 @@ index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c1 /** * After initialization has completed, this will be populated with the server's reported capabilities. */ +@@ -541,9 +547,11 @@ class Client extends protocol_js_1.Protocol { + * Called after listTools() to pre-compile validators for better performance. + */ +- cacheToolMetadata(tools) { +- this._cachedToolOutputValidators.clear(); +- this._cachedKnownTaskTools.clear(); +- this._cachedRequiredTaskTools.clear(); ++ cacheToolMetadata(tools, reset = true) { ++ if (reset) { ++ this._cachedToolOutputValidators.clear(); ++ this._cachedKnownTaskTools.clear(); ++ this._cachedRequiredTaskTools.clear(); ++ } + for (const tool of tools) { + // If the tool has an outputSchema, create and cache the validator + if (tool.outputSchema) { +@@ -569,7 +577,7 @@ class Client extends protocol_js_1.Protocol { + async listTools(params, options) { + const result = await this.request({ method: 'tools/list', params }, types_js_1.ListToolsResultSchema, options); + // Cache the tools and their output schemas for future validation +- this.cacheToolMetadata(result.tools); ++ this.cacheToolMetadata(result.tools, params?.cursor === undefined); + return result; + } + /** diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644 --- a/dist/cjs/client/streamableHttp.js @@ -461,6 +486,31 @@ index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8e /** * After initialization has completed, this will be populated with the server's reported capabilities. */ +@@ -537,9 +543,11 @@ export class Client extends Protocol { + * Called after listTools() to pre-compile validators for better performance. + */ +- cacheToolMetadata(tools) { +- this._cachedToolOutputValidators.clear(); +- this._cachedKnownTaskTools.clear(); +- this._cachedRequiredTaskTools.clear(); ++ cacheToolMetadata(tools, reset = true) { ++ if (reset) { ++ this._cachedToolOutputValidators.clear(); ++ this._cachedKnownTaskTools.clear(); ++ this._cachedRequiredTaskTools.clear(); ++ } + for (const tool of tools) { + // If the tool has an outputSchema, create and cache the validator + if (tool.outputSchema) { +@@ -565,7 +573,7 @@ export class Client extends Protocol { + async listTools(params, options) { + const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options); + // Cache the tools and their output schemas for future validation +- this.cacheToolMetadata(result.tools); ++ this.cacheToolMetadata(result.tools, params?.cursor === undefined); + return result; + } + /** diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644 --- a/dist/esm/client/streamableHttp.js From e0ec9be238a1495454e46426665323af25273b63 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 6 Jul 2026 03:49:25 +0000 Subject: [PATCH 18/34] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index d17f7db33701..a86dbdec4f20 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-ovrz0pALxRek5VEqSYkVpzb9YeiiVQWxfAHZQ3kX0/0=", - "aarch64-linux": "sha256-+sqBt11Nl1fDrL1JvFAcN8JMWahspfbBPuMeM7YdagU=", - "aarch64-darwin": "sha256-PsZfcmMpz/Xzudjv4CQZu+wPfnyijrlVYeP2x9tRffk=", - "x86_64-darwin": "sha256-4TpIc3Gh9nDaA5Y1zkCq0KraMdWu+PWtYVY3p7/CsJ0=" + "x86_64-linux": "sha256-NRSHbXA5jYZ2VUQ+b7d/dwg5BWap54ej3Ys9MPwgLwc=", + "aarch64-linux": "sha256-ua9ZjsF0KDuLOPjEcxhD4u5cMfjPuqoAyUftrNPr2sI=", + "aarch64-darwin": "sha256-QH3as89//yrRpWK7f96rNjiwCO5JTDYhVrUHbqQMomw=", + "x86_64-darwin": "sha256-30cIeLqasI+FUBYBttb3+MezWoQmb+UpkFWhPy4B3Xk=" } } From d4f70399323131620e3bf486af17f36828c08904 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:52:37 -0500 Subject: [PATCH 19/34] fix(codemode): unify catalog signatures (#35452) --- packages/codemode/README.md | 27 +- packages/codemode/codemode.md | 102 +++--- packages/codemode/src/codemode.ts | 18 +- packages/codemode/src/tool-runtime.ts | 298 ++++++++---------- packages/codemode/src/tool.ts | 4 +- packages/codemode/test/codemode.test.ts | 195 ++++++++---- packages/codemode/test/enumeration.test.ts | 14 +- packages/codemode/test/signature.test.ts | 72 ++--- packages/opencode/src/tool/code-mode.ts | 11 +- .../test/tool/code-mode-integration.test.ts | 6 +- packages/opencode/test/tool/code-mode.test.ts | 42 ++- packages/opencode/test/tool/registry.test.ts | 2 +- 12 files changed, 430 insertions(+), 361 deletions(-) diff --git a/packages/codemode/README.md b/packages/codemode/README.md index 0be6d769c7cb..afd178a36cff 100644 --- a/packages/codemode/README.md +++ b/packages/codemode/README.md @@ -179,14 +179,14 @@ Supported bearer, basic, header, and query authentication follows OpenAPI `secur ## Discovery -The agent-tool instructions use a budgeted catalog. Every tool namespace is always listed with its tool count regardless of budget, and as many complete tool signatures (each with a one-line description) as fit an estimated-token budget are inlined. Selection is round-robin across namespaces for fairness: in each round (namespaces alphabetical), every namespace still holding un-inlined tools attempts to place its next-cheapest signature line against the shared budget, and a namespace whose next line does not fit drops out while the others keep going - so every namespace gets some representation before any namespace gets everything. The instructions state exactly how comprehensive the list is, both overall (`COMPLETE list` vs `PARTIAL - N of M shown`) and per namespace (`(3 tools)`, `(3 tools, 1 shown)`, `(3 tools, none shown)`). +The agent-tool instructions use a budgeted catalog. Every tool namespace is always listed with its tool count regardless of budget, and as many complete, JSDoc-annotated tool signatures (each with a one-line description) as fit an estimated-token budget are inlined. Schema field descriptions and tags are part of each signature's measured cost. Selection is round-robin across namespaces for fairness: in each round (namespaces alphabetical), every namespace still holding un-inlined tools attempts to place its next-cheapest signature against the shared budget, and a namespace whose next signature does not fit drops out while the others keep going - so every namespace gets some representation before any namespace gets everything. The instructions state exactly how comprehensive the list is, both overall (`COMPLETE list` vs `PARTIAL - N of M shown`) and per namespace (`(3 tools)`, `(3 tools, 1 shown)`, `(3 tools, none shown)`). -The default budget is 2,000 estimated tokens (characters / 4, the same heuristic OpenCode uses). Override it when constructing a runtime: +The catalog-entry budget defaults to 2,000 estimated tokens (characters / 4, the same heuristic OpenCode uses). It applies only to full tool entries shown in the catalog; fixed instructions and namespace summaries are not counted. Override it when constructing a runtime: ```ts const runtime = CodeMode.make({ tools, - discovery: { maxInlineCatalogTokens: 6_000 }, + discovery: { catalogBudget: 6_000 }, }) ``` @@ -199,30 +199,37 @@ const matches = await tools.$codemode.search({ query: "order status", namespace: "orders", // optional: scope to one top-level namespace limit: 10, + offset: 0, }) ``` -`search` performs deterministic, additive field-weighted matching. The query is tokenized (camelCase boundaries split; every non-alphanumeric character is a separator; empties and `*` are dropped), and each term scores every tool: exact path or path-segment match (20), path substring (8), description substring (4), and searchable-text substring (2). Each term also carries naive singular variants (trailing `s`/`es` stripped), and a field check passes when the term or any variant matches - so a plural query term (`issues`) still finds a tool whose text only says `issue`, without changing the weights. The searchable text also includes the input schema's property names and their description strings, so a query naming a parameter finds its tool, and substring matching means partial words match. Scores sum across terms; matches are sorted by score (ties broken alphabetically by path) and capped at `limit` results (default 10). +`search` performs deterministic, additive field-weighted matching. The query is tokenized (camelCase boundaries split; every non-alphanumeric character is a separator; empties and `*` are dropped), and each term scores every tool: exact path or path-segment match (20), path substring (8), description substring (4), and searchable-text substring (2). Each term also carries naive singular variants (trailing `s`/`es` stripped), and a field check passes when the term or any variant matches - so a plural query term (`issues`) still finds a tool whose text only says `issue`, without changing the weights. The searchable text also includes the input schema's property names and their description strings, so a query naming a parameter finds its tool, and substring matching means partial words match. Scores sum across terms; matches are sorted by score (ties broken alphabetically by path), then sliced from the zero-based `offset` (default 0) to the configured `limit` (default 10). `remaining` counts matches after the current page. `next` is `{ offset }` when another page exists and `null` on the final page; spread it into the original request to preserve its query, namespace, and limit. -Each result contains the path, description, and generated TypeScript signature, so no second lookup is needed. The result signature is the pretty, JSDoc-annotated multiline form: each described input/output field carries its schema `description` as a `/** ... */` comment, and constraints TypeScript cannot express ride along as tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`). The inline catalog in the instructions keeps the compact single-line form. +```ts +const request = { query: "order status", namespace: "orders", limit: 10 } +const page = await tools.$codemode.search(request) +const nextPage = page.next ? await tools.$codemode.search({ ...request, ...page.next }) : undefined +``` + +Each result contains the path, description, and the same generated TypeScript signature used by the inline catalog, so no second lookup is needed. Signatures use the JSDoc-annotated multiline form: each described input/output field carries its schema `description` as a `/** ... */` comment, and constraints TypeScript cannot express ride along as tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`). ```ts tools.github.list_issues(input: { /** Repository owner */ - owner: string + owner: string, /** Cursor from the previous response's pageInfo */ - after?: string + after?: string, /** * Results per page * @default 30 */ - perPage?: number + perPage?: number, }): Promise ``` Result paths are rendered as JavaScript expressions rooted at `tools` (`tools.orders.lookup`, or `tools.context7["resolve-library-id"]` for non-identifier segments), so each `path` is directly usable as the call site. An empty query browses the catalog alphabetically by path; combined with `namespace` (`{ query: "", namespace: "orders" }`) it lists everything in that namespace. A query that names one tool path exactly (canonical path, `tools.`-prefixed path, or rendered JavaScript expression) is treated as a lookup and returns that tool alone. -The instructions are structured markdown, ordered so the workflow sits at the top and the catalog at the bottom: a `## Workflow` section with numbered steps (find a tool via search when the catalog is partial, or pick from the inlined list when it is complete; call the exact path as-is; `JSON.parse` string results; return only the needed fields), a `## Rules` section holding only guidance the workflow does not already cover (only listed/search-result tools exist inside `tools`; filter and aggregate collections in code; treat `Promise` results as shapeless until verified; run independent calls through `Promise.all`; enumerate `tools` with `Object.keys`/`for...in`; browse a namespace via search when it is advertised), a short `## Syntax` section that assumes standard JavaScript and names only what is unusual (TypeScript annotations stripped; the data-boundary serialization of Date/Map/Set/RegExp) or missing (classes, generators, `for await...of`, `.then`/`.catch`/`.finally`), and the budgeted `## Available tools` catalog. Example call forms use explicit `.`/`` placeholders - never a real or fabricated tool name. +The instructions are structured markdown, ordered so the workflow sits at the top and the catalog at the bottom: a `## Workflow` section with numbered steps (find a tool via search when the catalog is partial, or pick from the inlined list when it is complete; call the exact path as-is; return only the needed fields), a `## Rules` section holding only guidance the workflow does not already cover (only listed/search-result Code Mode tools and internal runtime tools exist inside `tools`; filter and aggregate collections in code; narrow `Promise` results at runtime; run independent calls through `Promise.all`; enumerate `tools` with `Object.keys`/`for...in`; browse a namespace and paginate search results when search is advertised), a short `## Language` section that identifies the runtime as a restricted JavaScript orchestration language and names its major unavailable capabilities, and the budgeted `## Available tools` catalog. Example call forms use explicit `.`/`` placeholders - never a real or fabricated tool name. A host cannot define its own `$codemode` top-level namespace. @@ -234,7 +241,7 @@ CodeMode executes a deliberately bounded JavaScript subset. It supports: - `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references - anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`. - Arrow functions and function declarations with closures, defaults, rest parameters, and destructuring. - Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`. -- Common array, string, number, `Object`, `Math`, and `JSON` operations. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces and `Object.keys(tools.ns)` the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`. +- Common array, string, number, `Object`, `Math`, and `JSON` operations. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`. - `Date` - `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones). - Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout. Function replacers are not supported. - `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators). diff --git a/packages/codemode/codemode.md b/packages/codemode/codemode.md index 732c565b72dc..f42c160cad4e 100644 --- a/packages/codemode/codemode.md +++ b/packages/codemode/codemode.md @@ -65,17 +65,24 @@ From issue #34787 and design discussion. Do not relitigate these casually. ### Discovery / search - **Search only - no separate `describe`.** `tools.$codemode.search({ query?, namespace?, -limit? })` over the final tool tree, owned by this package. -- Search result item shape: `{ path, description, signature }` in an `{ items, total }` - wrapper. The `signature` string embeds the full input/output TypeScript types - in search - results it is the pretty, JSDoc-annotated multiline form (Fix 7), so per-field schema - `description`s and constraints (`@default`, `@format`, `@deprecated`, `@minItems`, - `@maxItems`) ride along as field comments. The original spec's separate `input`/`output` +limit?, offset? })` over the final tool tree, owned by this package. +- Search result item shape: `{ path, description, signature }` in an + `{ items, remaining, next }` + wrapper. The `signature` string embeds the full input/output TypeScript types and uses the + same pretty, JSDoc-annotated multiline form in inline catalogs and search results, so + per-field schema `description`s and constraints (`@default`, `@format`, `@deprecated`, + `@minItems`, `@maxItems`) ride along as field comments. The original spec's separate `input`/`output` raw-schema fields are deliberately NOT added: shapes are already fully expressed in the TypeScript signature and schema annotations now arrive as JSDoc - intent satisfied, letter deviated. Result `path`s render a JavaScript expression rooted at `tools` (for example `tools.github.list_issues` or `tools.context7["resolve-library-id"]`) so each is directly usable as the call site; the internal `ToolDescription.path` stays unprefixed. +- `offset` is zero-based and defaults to 0. `remaining` counts matches after the current page; + `next` is `{ offset }` when another page exists and `null` on the final page. +- Search is an internal `Tool.make` definition backed by Effect input/output schemas. Its + validation, output checking, call observation, and TypeScript signature use the same path as + host-provided schema tools. Host-only catalog preparation keeps internal tools out of their + own search index; only conditional advertisement remains special. - Default limit: **10** (done). Exact-path lookup goes through search too: a query equal to a canonical tool path, `tools.`-prefixed path, or rendered JavaScript expression returns that tool alone (done). @@ -251,7 +258,7 @@ output limit; return a smaller value]`; logs keep leading lines within the remai truncation is now the only result-size mechanism.) - **Search polish**: default limit 12 -> **10** (`defaultSearchLimit`); exact-path lookup - a trimmed query equal to one tool path (optionally `tools.`-prefixed) returns that tool alone - (`total: 1`), bypassing ranking. Tokenization/ranking/shape unchanged. + (`remaining: 0`, `next: null`), bypassing ranking. Tokenization/ranking/shape unchanged. ### Wave 3 - OpenCode MCP adapter (done) @@ -312,10 +319,10 @@ Instructions are now the budgeted-catalog + prompting-guidance form; verified e2 real MCP config. Package still 101 tests / 0 fail; opencode adapter suites still 34 + 16; both packages typecheck clean. -- **Budgeted catalog** (`discoveryPlan` in `tool-runtime.ts`): the all-or-nothing +- **Budgeted catalog** (`prepare` in `tool-runtime.ts`): the all-or-nothing inline/search modes are gone - `DiscoveryMode` deleted, `CodeMode.DiscoveryOptions` is just `{ maxInlineCatalogBytes? }` (default 16,000 UTF-8 bytes; later converted to - `maxInlineCatalogTokens`, default 4,000 estimated tokens - see Post-wave fixes). Port of + `catalogBudget`, default 4,000 estimated tokens - see Post-wave fixes). Port of the old opencode `describe()` `PREVIEW_BUDGET` algorithm, adapted to `ToolDescription`: every namespace is ALWAYS listed with its tool count; full signature lines @@ -439,9 +446,9 @@ adapter needed **no changes**. defeating discovery. Fixes, all in this package: - `ToolRuntime.make` now returns a `keys(path)` capability (`namespaceKeys` in `tool-runtime.ts`) threaded into the `Interpreter` alongside `invoke` - the interpreter - still never holds the host tool tree. `Object.keys(tools)` yields the top-level namespace - names (never `$codemode`, which is virtual - but `Object.keys(tools.$codemode)` yields - `["search"]`), `Object.keys(tools.ns)` the names at that node; a callable tool leaf + still never holds the callable tool tree. `Object.keys(tools)` yields the top-level namespace + names, including the internally registered `$codemode`; `Object.keys(tools.$codemode)` yields + `["search"]`, and `Object.keys(tools.ns)` yields names at that node; a callable tool leaf enumerates as `[]` (like `Object.keys` of a JS function); an unknown path throws an `UnknownTool` diagnostic suggesting `Object.keys(tools)` and `$codemode.search` (matching call-time unknown-tool behavior rather than silently returning `[]`). @@ -459,7 +466,7 @@ adapter needed **no changes**. - `supportedSyntaxMessage`, the instructions loops line, and README "Supported Programs" mention the new surface; tests in `test/enumeration.test.ts` (14, incl. the exact transcript program) plus one adapter-level assertion that `Object.keys(tools)` returns - MCP server names. + MCP server and CodeMode namespace names. - **Search ranking, namespace scoping, prefixed result paths (done).** Motivation: the Wave 4 e2e run showed a model retrying calls because search-result paths @@ -480,7 +487,7 @@ adapter needed **no changes**. An empty query now browses ALPHABETICALLY by path (was declaration order). Kept: `{ path, description, signature }` result items, default limit 10, exact-path instant lookup, input validation errors. - - **Namespace scoping**: `tools.$codemode.search({ query?, namespace?, limit? })` - + - **Namespace scoping**: `tools.$codemode.search({ query?, namespace?, limit?, offset? })` - `namespace` (validated as a string when provided) filters `SearchEntry`s to one top-level namespace before ranking; `{ query: "", namespace: "github" }` lists that namespace alphabetically. `searchSignature` updated. @@ -489,7 +496,7 @@ adapter needed **no changes**. segments), directly usable as the call site. Internal `ToolDescription.path` stays unprefixed; only the search RESULT items are rendered this way. Exact-path queries accept canonical paths and rendered expressions. - - **Instructions** (`discoveryPlan`): an explicit calling-convention line and a browse + - **Instructions** (`prepare`): an explicit calling-convention line and a browse hint on the search advertisement (both since absorbed into the `## Rules` section by the instructions restructure below). - **Tests**: package search/discovery tests updated (prefixed paths, alphabetical browse) @@ -499,28 +506,23 @@ adapter needed **no changes**. - **Instructions restructure: markdown sections, placeholder-only call forms (done).** The flat prose instructions (which mixed a real catalog tool with fabricated result - fields in the worked example) are replaced by structured markdown in `discoveryPlan`, + fields in the worked example) are replaced by structured markdown in `prepare`, ordered so the workflow sits at the top (the least likely part of a long description to be truncated or skimmed away) and the catalog at the bottom (the per-section content - described here was later condensed by Fix 8 - Workflow/Rules deduped, Syntax inverted): - - **Intro** (2 lines): "Write a CodeMode program... Return code only." + "Execute - JavaScript in a confined runtime with access to the tools listed below under - `tools.*`." (the second line drops the tools clause when the tree is empty). - - **`## Workflow`**: numbered steps - find a tool via `tools.$codemode.search` -> read - the `{ path, description, signature }` matches -> call by path -> `typeof res === -"string" ? JSON.parse(res) : res` -> return only the needed fields. When the catalog is - COMPLETE the search/read steps collapse into "Pick a tool from the list under - `## Available tools`" and the steps renumber (4 instead of 5). - - **`## Rules`**: call-by-exact-path; TEXT-is-JSON -> JSON.parse; return small (never raw - payloads); filter/aggregate large collections in code instead of per-item round-trips; + described here was later condensed by Fix 8 and the language-accuracy pass): + - **Intro**: identifies the language as restricted JavaScript for calling tools rather than + a general-purpose runtime. + - **`## Workflow`**: with a partial catalog, return search results from one execution, then + copy a selected path into the next execution. With a complete catalog, pick and call an + inlined signature, then return only the needed fields. + - **`## Rules`**: narrow unknown results at runtime; filter/aggregate large collections in code instead of per-item round-trips; console.log/warn/error/dir/table for intermediates; `Promise.all` parallelism (no .then/.catch - await + try/catch); `Object.keys(tools)`/`for...in` enumeration; browse-one-namespace via search (PARTIAL only); and host-side media handling (files/ images never enter the program; a media-only call yields a small text marker - wording verified against the adapter's `toSandboxResult`/`mediaMarker`). - - **`## Syntax`**: the dense syntax lines unchanged, minus the Promise.all and console - lines (moved into Rules) and the `for (const ns in tools)` fragment (redundant with - the enumeration rule). + - **`## Language`**: a concise positive capability summary plus the major unavailable + runtime capabilities and the data-boundary serialization note. - **`## Available tools`**: the budgeted catalog unchanged, with the COMPLETE/PARTIAL header merged into the section heading (no trailing colon); the search-signature advertisement follows when PARTIAL (its description-reading and browse clauses moved @@ -532,7 +534,7 @@ adapter needed **no changes**. appear anywhere in the instructions. Zero tools keep "No tools are currently available." under minimal sections (intro + Syntax + Available tools). - **Tests**: the package worked-example test replaced by section-structure/placeholder - assertions (section order; JSON.parse + return-small rules present; no + assertions (section order; unknown-result + return-small rules present; no `total_count`/`list_issues`/real-tool example lines; browse hint only when PARTIAL; zero-tool minimal sections) - 156 pass / 0 fail; adapter suites gain the same assertions on the built description (still 35 + 16, green). @@ -542,9 +544,9 @@ budget; namespaces must always be present): - `src/token.ts` added: copy of `@opencode-ai/core/util/token` (`round(chars / 4)`), so the package stays dependency-free; keep in sync if the core heuristic changes. -- `CodeMode.DiscoveryOptions.maxInlineCatalogBytes` -> `maxInlineCatalogTokens` (default 4,000 +- `CodeMode.DiscoveryOptions.maxInlineCatalogBytes` -> `catalogBudget` (default 4,000 estimated tokens ~ the old 16,000 bytes at 4 chars/token - behavior parity, not a size - reduction). `discoveryPlan` charges `estimate(catalogLine(tool))` per line; cheapest-first + reduction). `prepare` charges `estimate(catalogLine(tool))` per line; cheapest-first - stop-on-first-miss unchanged at the time (stop-on-first-miss replaced by round-robin in Fix 8). Namespace stub lines were and remain unbudgeted - every namespace always appears with its tool count, even at budget 0 (asserted in package and @@ -672,10 +674,8 @@ along). All in `tool-runtime.ts`; no interpreter changes. interfaces/type aliases are stripped and TS **enums actually work** (transpileModule compiles them to an IIFE the interpreter runs), hence enums deliberately unmentioned. `supportedSyntaxMessage` (the in-diagnostic text in `codemode.ts`) is untouched. -- **Workflow/Rules deduped**: the call-by-exact-path, JSON.parse-string-results, and - return-small content now lives ONLY in the numbered Workflow steps (with their - compliance-driving justifications inline: "most tools return JSON as a string", "raw - payloads get truncated and waste context"); Rules keeps only bullets adding new +- **Workflow/Rules deduped**: the call-by-exact-path and return-small content now lives ONLY + in the numbered Workflow steps; Rules keeps only bullets adding new content - filter/aggregate collections in code, console.\* intermediates (logs ride back), Promise.all parallelism, Object.keys/for...in enumeration, browse-namespace (PARTIAL only), and the media rule compressed to one line. The no-.then/.catch @@ -690,7 +690,7 @@ along). All in `tool-runtime.ts`; no interpreter changes. (332 -> 176), Syntax 453 -> 188 (419 -> 174); fixed prose total 1,005 -> 610 (927 -> 562), ~ 40% reduction with no behavioral content dropped. Workflow grew slightly because it absorbed the deduped parse/return-small justifications. -- **Round-robin namespace inlining** (`discoveryPlan`): the ported stop-on-first-miss +- **Round-robin namespace inlining** (`prepare`): the ported stop-on-first-miss behavior (alphabetically-late namespaces starved to "none shown" while an early namespace inlines everything) is replaced by round-robin fairness - in each round (namespaces alphabetical), every namespace still holding un-inlined tools attempts to @@ -708,9 +708,9 @@ along). All in `tool-runtime.ts`; no interpreter changes. the four field checks passes when ANY form matches. Weights, exact-path lookup, and namespace scoping untouched. A true plural path match still outranks a singular-only description match (path substring 8 + searchable 2 > description 4 + searchable 2). -- **Tests**: package instruction/structure assertions updated to the new text; new - syntax-section test (leads with "Standard modern JavaScript works", names the - verified not-supported list, keeps the data-boundary note); the budget-exhaustion +- **Tests**: package instruction/structure assertions updated to the new text; the + language-section test rejects full-runtime wording, names major unavailable capabilities, + and keeps the data-boundary note; the budget-exhaustion test rewritten to assert the new fairness (alpha.expensive not fitting must NOT prevent beta.cheap from showing: PARTIAL 2 of 3, `- beta (1 tool)` fully shown); new plural/singular test (query "issues" finds a singular-only tool; ranking still @@ -724,7 +724,7 @@ along). All in `tool-runtime.ts`; no interpreter changes. **Fix 9 - prompting trims per user review of Fix 8** (user reviewed the condensed instructions and directed further cuts): -- Default `maxInlineCatalogTokens` 4,000 -> **2,000** (user wants ~2k tokens of signatures +- Default `catalogBudget` 4,000 -> **2,000** (user wants ~2k tokens of signatures auto-inlined; round-robin fairness from Fix 8 spreads it across all namespaces). - Console rule and files/images rule DROPPED from `## Rules`. Replaced by a single `unknown`-treatment warning: "A result typed `Promise` has no guaranteed @@ -733,9 +733,9 @@ instructions and directed further cuts): return the same data; the prompt stays console-neutral, neither for nor against.) The media-stripping MECHANISM is unchanged and still tested; only the prose about it is gone - the `[N images attached]` marker is self-explanatory in context. -- Kept as-is per user: the JSON.parse workflow step (maps to the original motivating - transcript failure; NOT copied from prior art - see section 5 note), the browse-namespace rule - (undecided), no no-fetch/ambient-authority rule added (proposed, not approved). +- Later revised: unconditional JSON parsing was removed because text results are not + necessarily JSON. The browse-namespace rule remains; the language section now states that + ambient `fetch` is unavailable and external operations go through Code Mode tools. - Explicitly REJECTED for now: auto-parsing JSON-looking text results at the adapter boundary ("could get weird" - type flips, program-sees vs tool-sent divergence). Logged as a next-iteration follow-up below. @@ -1004,11 +1004,19 @@ focused interpreter-surface pass rather than picked off piecemeal. - [x] Sandbox values nested inside logged containers print `[CodeMode reference]` (`console.log({ m: map })`) - could deep-format instead. +### Next iteration: optional search input boundary + +- [ ] `SearchInput` uses Effect's exact `optionalKey`, so an omitted field is accepted but an + explicitly present `undefined` field is rejected. The previous handwritten validator + treated explicit `undefined` as omission. Decide whether search should preserve that + convenience locally or whether all tool arguments should adopt JSON-style undefined + normalization; do not broaden `copyOut` semantics solely to fix search. + ### Next iteration: text-result handling (deliberate follow-up, user-directed) - [ ] Revisit how MCP text results reach the program. Today: `structuredContent` when the - server sends it, else joined text as a plain string (the program JSON.parses it, - guided by a workflow step). Considered and deferred: (a) conservative boundary + server sends it, else joined text as a plain string. Programs narrow unknown results + before use; the prompt no longer recommends unconditional JSON parsing. Considered and deferred: (a) conservative boundary auto-parse (text starting with `{`/`[` that parses cleanly becomes an object) - rejected for now as potentially confusing (type flips; program sees something other than what the tool sent); (b) raw-envelope passthrough with the envelope shape diff --git a/packages/codemode/src/codemode.ts b/packages/codemode/src/codemode.ts index b207efe46a8c..083f62e28b54 100644 --- a/packages/codemode/src/codemode.ts +++ b/packages/codemode/src/codemode.ts @@ -38,12 +38,12 @@ export type ExecutionLimits = { /** Controls how much of the tool catalog is inlined in agent instructions. */ export type DiscoveryOptions = { /** - * Estimated-token budget (chars/4, default 2000) for inlined full tool signatures in agent - * instructions. Signatures that fit are inlined round-robin across namespaces; every - * namespace is always listed with its tool count regardless of budget, and + * Approximate budget, in estimated tokens (chars/4, default 2000), for full tool entries in + * the instruction catalog. Tool entries are selected round-robin across namespaces. Fixed + * instructions and namespace summaries do not count toward the budget, and * `tools.$codemode.search` is always registered. */ - readonly maxInlineCatalogTokens?: number + readonly catalogBudget?: number } type ToolTree = { @@ -3921,8 +3921,8 @@ const executeWithLimits = >( const tools = ToolRuntime.make( (options.tools ?? {}) as HostTools>, limits.maxToolCalls, - hooks, searchIndex, + hooks, ) const logs: Array = [] const logged = () => (logs.length > 0 ? { logs: [...logs] } : {}) @@ -4061,10 +4061,10 @@ export const make = = {}>( const tools = (options.tools ?? {}) as HostTools> ToolRuntime.assertValidTools(tools) const limits = resolveExecutionLimits(options.limits) - const discovery = ToolRuntime.discoveryPlan(tools, options.discovery?.maxInlineCatalogTokens) - const executeProgram = (code: string) => executeWithLimits({ ...options, code }, limits, discovery.searchIndex) - const catalog = discovery.catalog - const instructions = discovery.instructions + const prepared = ToolRuntime.prepare(tools, options.discovery?.catalogBudget) + const executeProgram = (code: string) => executeWithLimits({ ...options, code }, limits, prepared.searchIndex) + const catalog = prepared.catalog + const instructions = prepared.instructions return { catalog: () => catalog, diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index f19e7a9b4d55..29b27cb5c6ca 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -1,4 +1,4 @@ -import { Cause, Effect } from "effect" +import { Cause, Effect, Schema } from "effect" import { ToolError, toolError } from "./tool-error.js" import { decodeInput as decodeToolInput, @@ -8,6 +8,7 @@ import { inputTypeScript, isDefinition as isToolDefinition, outputTypeScript, + Tool, type Definition, } from "./tool.js" import { SandboxDate, SandboxMap, SandboxPromise, SandboxRegExp, SandboxSet } from "./values.js" @@ -76,10 +77,26 @@ export type ToolDescription = { export type SafeObject = Record const reservedNamespace = "$codemode" -const defaultMaxInlineCatalogTokens = 2_000 +const defaultCatalogBudget = 2_000 const defaultSearchLimit = 10 -const searchSignature = - "tools.$codemode.search({ query?: string, namespace?: string, limit?: number }): Promise<{ items: Array<{ path: string; description: string; signature: string }>; total: number }>" +const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0)) +const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) +const SearchInput = Schema.Struct({ + query: Schema.optionalKey(Schema.String), + namespace: Schema.optionalKey(Schema.String), + limit: Schema.optionalKey(PositiveInt), + offset: Schema.optionalKey(NonNegativeInt), +}) +const SearchItem = Schema.Struct({ + path: Schema.String, + description: Schema.String, + signature: Schema.String, +}) +const SearchOutput = Schema.Struct({ + items: Schema.Array(SearchItem), + remaining: NonNegativeInt, + next: Schema.NullOr(Schema.Struct({ offset: NonNegativeInt })), +}) const toolExpression = (path: string) => "tools" + path @@ -295,15 +312,17 @@ const definitions = ( return entries } +const describeDefinition = (path: string, definition: Definition): ToolDescription => ({ + path, + description: definition.description, + signature: `${toolExpression(path)}(input: ${inputTypeScript(definition, true)}): Promise<${outputTypeScript(definition, true)}>`, +}) + const visibleDefinitions = (tools: HostTools) => definitions(tools).map(({ path, definition }) => ({ path, definition, - description: { - path, - description: definition.description, - signature: `${toolExpression(path)}(input: ${inputTypeScript(definition)}): Promise<${outputTypeScript(definition)}>`, - }, + description: describeDefinition(path, definition), })) export const catalog = (tools: HostTools): ReadonlyArray => @@ -317,11 +336,6 @@ export type DiscoveryPlan = { export type SearchEntry = { readonly description: ToolDescription - /** - * JSDoc-annotated multiline signature shown on search-result items; the compact - * single-line form (inline catalog lines) stays in `description.signature`. - */ - readonly signature: string /** Top-level namespace (first path segment), matched by the search `namespace` option. */ readonly namespace: string /** Lowercased path + description + input property names/descriptions, for substring matching. */ @@ -355,8 +369,76 @@ const termForms = (term: string): Array => { return forms } +const makeSearchTool = (searchIndex: ReadonlyArray) => + Tool.make({ + description: "Search available Code Mode tools", + input: SearchInput, + output: SearchOutput, + run: (request) => + Effect.sync(() => { + const query = request.query ?? "" + const offset = request.offset ?? 0 + const scoped = + request.namespace === undefined + ? searchIndex + : searchIndex.filter((entry) => entry.namespace === request.namespace) + // A query that names one tool path exactly (canonical path or rendered JavaScript + // expression) is a lookup, not a search: return that tool alone. + const trimmed = query.trim() + const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed + const exact = + pathQuery === "" + ? undefined + : scoped.find( + (entry) => + entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed, + ) + const terms = tokenize(query).map(termForms) + // Additive field-weighted scoring, summed across terms: exact path or path segment + // (20) > path substring (8) > description substring (4) > any searchable text, + // including input parameter names and descriptions (2). + const ranked = + exact !== undefined + ? [exact] + : scoped + .map((entry) => { + const path = entry.description.path.toLowerCase() + const description = entry.description.description.toLowerCase() + const score = terms.reduce( + (total, forms) => + total + + (forms.some((form) => path === form || path.endsWith(`.${form}`)) ? 20 : 0) + + (forms.some((form) => path.includes(form)) ? 8 : 0) + + (forms.some((form) => description.includes(form)) ? 4 : 0) + + (forms.some((form) => entry.searchText.includes(form)) ? 2 : 0), + 0, + ) + return { entry, score } + }) + .filter(({ score }) => terms.length === 0 || score > 0) + .sort( + (left, right) => + right.score - left.score || + left.entry.description.path.localeCompare(right.entry.description.path), + ) + .map(({ entry }) => entry) + const items = ranked.slice(offset, offset + (request.limit ?? defaultSearchLimit)).map(({ description }) => ({ + ...description, + path: toolExpression(description.path), + })) + const remaining = Math.max(0, ranked.length - offset - items.length) + return { + items, + remaining, + next: remaining > 0 ? { offset: offset + items.length } : null, + } + }), + }) + +const searchDescription = describeDefinition(`${reservedNamespace}.search`, makeSearchTool([])) + const catalogLine = (tool: ToolDescription) => { - // Inline catalog lines use only a compact first line; full text stays in search results. + // Keep the tool description concise; the full schema documentation remains in the signature. const line = tool.description.split("\n", 1)[0]!.trim() const description = line.length > 120 ? line.slice(0, 119) + "..." : line return description === "" ? ` - ${tool.signature}` : ` - ${tool.signature} // ${description}` @@ -364,7 +446,6 @@ const catalogLine = (tool: ToolDescription) => { const toSearchEntry = (path: string, definition: Definition, description: ToolDescription): SearchEntry => ({ description, - signature: `${toolExpression(path)}(input: ${inputTypeScript(definition, true)}): Promise<${outputTypeScript(definition, true)}>`, namespace: path.split(".", 1)[0]!, searchText: [ path, @@ -389,7 +470,7 @@ export const assertValidTools = (tools: HostTools): void => { /** * Budgeted catalog: every namespace is always listed with its tool count; full call - * signatures are inlined against the `maxInlineCatalogTokens` budget (estimated tokens, + * signatures are inlined against the `catalogBudget` (estimated tokens, * chars/4) round-robin across namespaces - in each round (namespaces alphabetical), every * namespace still holding un-inlined tools attempts to place its next-cheapest line, and * a namespace whose next line does not fit is done while the others keep going - so every @@ -398,12 +479,12 @@ export const assertValidTools = (tools: HostTools): void => { * namespace. Namespace stub lines are never budgeted: every namespace appears with its * tool count even at budget 0. */ -export const discoveryPlan = ( +export const prepare = ( tools: HostTools, - maxInlineCatalogTokens = defaultMaxInlineCatalogTokens, + catalogBudget = defaultCatalogBudget, ): DiscoveryPlan => { - if (!Number.isSafeInteger(maxInlineCatalogTokens) || maxInlineCatalogTokens < 0) { - throw new RangeError("discovery.maxInlineCatalogTokens must be a non-negative safe integer") + if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) { + throw new RangeError("discovery.catalogBudget must be a non-negative safe integer") } const visible = visibleDefinitions(tools) const described = visible.map(({ description }) => description) @@ -438,7 +519,7 @@ export const discoveryPlan = ( for (const selection of active) { const tool = selection.queue[0]! const cost = estimateTokens(catalogLine(tool)) - if (used + cost > maxInlineCatalogTokens) continue + if (used + cost > catalogBudget) continue selection.queue.shift() selection.picked.add(tool) used += cost @@ -459,12 +540,11 @@ export const discoveryPlan = ( // catalog at the bottom. Example call forms use placeholders - never a real or fabricated // tool name - and show both dot and bracket notation so non-identifier names are not normalized. const intro = [ - "Write a CodeMode program to answer the request. Return code only.", empty - ? "Execute JavaScript in a confined runtime." + ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime." : complete - ? "Execute JavaScript in a confined runtime. Inside this program, `tools` contains only the host-provided tools listed below; surrounding agent tools are not available unless listed here." - : "Execute JavaScript in a confined runtime. Inside this program, `tools` contains only the host-provided tools listed or searchable below; surrounding agent tools are not available unless listed here.", + ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the Code Mode tools listed below and internal runtime tools; surrounding agent tools are not available." + : "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the Code Mode tools listed or searchable below and internal runtime tools; surrounding agent tools are not available.", ...(empty ? [] : ["Do not infer or normalize tool names; use only exact signatures shown below or returned by search."]), @@ -481,16 +561,12 @@ export const discoveryPlan = ( ...(complete ? [ "1. Pick a tool from the list under `## Available tools` - each line is the exact call signature; use it as-is rather than guessing segments.", - "2. Call it using the exact signature shown; bracket notation and quotes are part of the path.", - '3. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.', - "4. Return only the fields you need: `return { : data. }` - raw payloads get truncated and waste context.", + "2. Call it using the exact signature shown: `const result = await tools..(input)`; bracket notation and quotes are part of the path.", + "3. Return only the fields you need from structured results; narrow unknown results before reading fields, and avoid returning large raw payloads.", ] : [ - '1. If the exact signature is not listed below, first search: `const { items } = await tools.$codemode.search({ query: "" })`.', - "2. Read the matches: each item is `{ path, description, signature }` - read the description before using an unfamiliar tool.", - "3. Call the result's `path` as-is; bracket notation and quotes are part of the path.", - '4. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.', - "5. Return only the fields you need: `return { : data. }` - raw payloads get truncated and waste context.", + '1. If needed, discover tools: `return await tools.$codemode.search({ query: "" })`.', + "2. In the next execution, copy a returned path exactly, call it, and return only the needed fields.", ]), ] @@ -501,24 +577,26 @@ export const discoveryPlan = ( "## Rules", "", complete - ? "- Only tools listed here are available inside `tools`; tools from the surrounding agent/runtime are not implicitly exposed." - : "- Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`; tools from the surrounding agent/runtime are not implicitly exposed.", + ? "- Only Code Mode tools listed here and internal runtime tools are available; surrounding agent tools are not implicitly exposed." + : "- Only Code Mode tools listed here or returned by `tools.$codemode.search` and internal runtime tools are available; surrounding agent tools are not implicitly exposed.", "- Filter, aggregate, and transform collections in code - never return them raw or call a tool per item across messages.", - "- A result typed `Promise` has no guaranteed shape - verify what actually came back before relying on its fields.", + "- A result typed `Promise` may be structured data or text. Before reading fields, check that it is a non-null object and not an array; otherwise handle the returned text or primitive directly.", '- Run independent calls in parallel: `await Promise.all(items.map((item) => tools..(item)))`, or use `tools.["tool-name"](item)` when the listed signature uses bracket notation.', "- `Object.keys(tools)` lists namespaces; `Object.keys(tools.)` lists its tools; `for...in` works on both.", ...(complete ? [] - : ['- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "" })`.']), + : [ + '- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "" })`.', + "- If search returns `next`, repeat the same search with `offset: next.offset`.", + ]), ] - const syntax = [ + const language = [ "", - "## Syntax", + "## Language", "", - "Standard modern JavaScript works: functions/closures, destructuring, template literals, loops, try/catch, spread, optional chaining, the usual Array/String/Object/Math/JSON methods, plus Date, RegExp, Map, Set, and Promise.all/allSettled/race/resolve/reject.", - "TypeScript type annotations are allowed and stripped before execution (decorators are not supported).", - "Not supported (each fails with a message naming the alternative): classes, generators, for await...of, .then/.catch/.finally (use await with try/catch).", + "Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls.", + "Modules/imports, classes, generators, timers, fetch, eval, prototype access, arbitrary methods, and promise chaining are unavailable. Use Code Mode tools for external operations. Use await with try/catch.", "Dates serialize to ISO strings at data boundaries; Map/Set/RegExp serialize to `{}`.", ] @@ -547,11 +625,11 @@ export const discoveryPlan = ( for (const tool of group) if (picked.has(tool)) toolSection.push(catalogLine(tool)) } if (!complete) { - toolSection.push("", "Search returns complete callable signatures:", `- ${searchSignature}`) + toolSection.push("", "Search returns complete callable signatures:", `- ${searchDescription.signature}`) } } - const lines = [...intro, ...workflow, ...rules, ...syntax, ...toolSection] + const lines = [...intro, ...workflow, ...rules, ...language, ...toolSection] return { catalog: described, instructions: lines.join("\n"), @@ -560,7 +638,7 @@ export const discoveryPlan = ( } /** - * The enumerable names at one node of the host tool tree - namespace names at the root, + * The enumerable names at one node of the callable tool tree - namespace names at the root, * tool/namespace names below - powering `Object.keys(tools)` and `for...in` over tool * references. A callable tool is a leaf and enumerates as `[]` (like `Object.keys` of a * function in JS). An unknown path is an `UnknownTool` error pointing at the working @@ -569,11 +647,7 @@ export const discoveryPlan = ( const namespaceKeys = ( tools: HostTools, path: ReadonlyArray, - searchEnabled: boolean, ): ReadonlyArray => { - // The reserved discovery namespace is virtual (never present in the host tree); enumerate - // it explicitly so `Object.keys(tools.$codemode)` matches the callable surface. - if (searchEnabled && path.length === 1 && path[0] === reservedNamespace) return ["search"] let value: HostTool | Definition | HostTools = tools for (const segment of path) { if ( @@ -585,11 +659,9 @@ const namespaceKeys = ( throw new ToolRuntimeError( "UnknownTool", `Unknown tool namespace '${path.join(".")}'.`, - searchEnabled - ? [ - "Object.keys(tools) lists the available namespaces; tools.$codemode.search({ query }) finds described tools.", - ] - : ["Object.keys(tools) lists the available namespaces."], + [ + "Object.keys(tools) lists the available namespaces; tools.$codemode.search({ query }) finds described tools.", + ], ) } value = value[segment] as HostTool | Definition | HostTools @@ -601,7 +673,6 @@ const namespaceKeys = ( const resolve = ( tools: HostTools, path: ReadonlyArray, - searchEnabled: boolean, ): HostTool | Definition => { let value: HostTool | Definition | HostTools = tools @@ -615,7 +686,7 @@ const resolve = ( throw new ToolRuntimeError( "UnknownTool", `Unknown tool '${path.join(".")}'.`, - searchEnabled ? ["Use tools.$codemode.search({ query }) to find available described tools."] : [], + ["Use tools.$codemode.search({ query }) to find available described tools."], ) } value = value[segment] as HostTool | Definition | HostTools @@ -632,7 +703,7 @@ export type ToolRuntime = { readonly root: ToolReference readonly calls: Array readonly invoke: (path: ReadonlyArray, args: Array) => Effect.Effect - /** Enumerable namespace/tool names at one node of the host tool tree; see `namespaceKeys`. */ + /** Enumerable namespace/tool names at one node of the callable tool tree; see `namespaceKeys`. */ readonly keys: (path: ReadonlyArray) => ReadonlyArray } @@ -640,11 +711,14 @@ export const make = ( tools: HostTools, /** Undefined means unlimited tool calls. */ maxToolCalls: number | undefined, + searchIndex: ReadonlyArray, hooks?: ToolCallHooks, - searchIndex?: ReadonlyArray, ): ToolRuntime => { const calls: Array = [] - const searchEnabled = searchIndex !== undefined + const callableTools = { + ...tools, + [reservedNamespace]: { search: makeSearchTool(searchIndex) }, + } // Wraps the settling portion of a tool call so onToolCallEnd observes success and failure // symmetrically. Interruption (e.g. the execution timeout) fires neither outcome. @@ -683,7 +757,7 @@ export const make = ( return { root: new ToolReference([]), calls, - keys: (path) => namespaceKeys(tools, path, searchEnabled), + keys: (path) => namespaceKeys(callableTools, path), invoke: (path, args) => Effect.gen(function* () { const name = path.join(".") @@ -694,107 +768,7 @@ export const make = ( recordCall(call) return calls.length - 1 }).pipe(Effect.tap((index) => hooks?.onToolCallStart?.({ index, name, input }) ?? Effect.void)) - if (name === "$codemode.search") { - if (!searchEnabled) throw new ToolRuntimeError("UnknownTool", `Unknown tool '${name}'.`) - const input = externalArgs[0] - if (externalArgs.length !== 1 || input === null || typeof input !== "object" || Array.isArray(input)) { - throw new ToolRuntimeError( - "InvalidToolInput", - "tools.$codemode.search expects { query?: string; namespace?: string; limit?: number }.", - ) - } - const request = input as { query?: unknown; namespace?: unknown; limit?: unknown } - if (request.query !== undefined && typeof request.query !== "string") { - throw new ToolRuntimeError( - "InvalidToolInput", - "tools.$codemode.search query must be a string when provided.", - ) - } - if (request.namespace !== undefined && typeof request.namespace !== "string") { - throw new ToolRuntimeError( - "InvalidToolInput", - "tools.$codemode.search namespace must be a string when provided.", - ) - } - if ( - request.limit !== undefined && - (typeof request.limit !== "number" || !Number.isSafeInteger(request.limit) || request.limit <= 0) - ) { - throw new ToolRuntimeError( - "InvalidToolInput", - "tools.$codemode.search limit must be a positive safe integer when provided.", - ) - } - const query = typeof request.query === "string" ? request.query : "" - const namespace = typeof request.namespace === "string" ? request.namespace : undefined - const index = yield* recordAndObserve(request) - return yield* observeEnd( - Effect.try({ - try: () => { - const limit = typeof request.limit === "number" ? request.limit : defaultSearchLimit - const scoped = - namespace === undefined ? searchIndex : searchIndex.filter((entry) => entry.namespace === namespace) - // A query that names one tool path exactly (canonical path or rendered - // JavaScript expression) is a lookup, not a search: return that tool alone. - const trimmed = query.trim() - const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed - const exact = - pathQuery === "" - ? undefined - : scoped.find( - (entry) => - entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed, - ) - const terms = tokenize(query).map(termForms) - // Additive field-weighted scoring, summed across terms: exact path or path - // segment (20) > path substring (8) > description substring (4) > any - // searchable text, incl. input parameter names/descriptions (2). Each term - // matches a field when any of its forms (the term or a singular variant) - // does. An empty query browses everything, alphabetical by path. - const ranked = - exact !== undefined - ? [exact] - : scoped - .map((entry) => { - const path = entry.description.path.toLowerCase() - const description = entry.description.description.toLowerCase() - const score = terms.reduce( - (total, forms) => - total + - (forms.some((form) => path === form || path.endsWith(`.${form}`)) ? 20 : 0) + - (forms.some((form) => path.includes(form)) ? 8 : 0) + - (forms.some((form) => description.includes(form)) ? 4 : 0) + - (forms.some((form) => entry.searchText.includes(form)) ? 2 : 0), - 0, - ) - return { entry, score } - }) - .filter(({ score }) => terms.length === 0 || score > 0) - .sort( - (left, right) => - right.score - left.score || - left.entry.description.path.localeCompare(right.entry.description.path), - ) - .map(({ entry }) => entry) - // Result paths are rendered as JavaScript expressions so each `path` is - // directly usable as the call site (`await tools.github.list({ ... })` or - // `await tools.ns["dashed-name"]({ ... })`). The signature is the pretty, - // JSDoc-annotated form (schema descriptions and constraints ride along as - // field comments). - const items = ranked.slice(0, limit).map(({ description, signature }) => ({ - ...description, - path: toolExpression(description.path), - signature, - })) - return copyIn({ items, total: ranked.length }, "Result from tool '$codemode.search'") - }, - catch: (cause) => cause, - }), - { index, name, input: request }, - ) - } - - const tool = resolve(tools, path, searchEnabled) + const tool = resolve(callableTools, path) let describedInput: unknown if (isDefinition(tool)) { if (externalArgs.length !== 1) diff --git a/packages/codemode/src/tool.ts b/packages/codemode/src/tool.ts index e89f3d39d6c5..4ed2ecbcf29e 100644 --- a/packages/codemode/src/tool.ts +++ b/packages/codemode/src/tool.ts @@ -244,9 +244,9 @@ const renderSchema = ( if (properties.length === 0 && indexType === undefined) return "{}" const pad = " ".repeat(depth + 1) const lines = properties.map( - (entry) => `${jsdoc(entry[1].description, docTags(entry[1]), pad)}${pad}${field(entry)}`, + (entry) => `${jsdoc(entry[1].description, docTags(entry[1]), pad)}${pad}${field(entry)},`, ) - if (indexType !== undefined) lines.push(`${pad}[key: string]: ${indexType}`) + if (indexType !== undefined) lines.push(`${pad}[key: string]: ${indexType},`) return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}` } return "unknown" diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index 087678c778cb..168dc93d5e26 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -426,7 +426,7 @@ describe("CodeMode schema flexibility", () => { { path: "adapter.call", description: "Call an adapter-described tool", - signature: "tools.adapter.call(input: { id: string; count?: number }): Promise", + signature: "tools.adapter.call(input: {\n id: string,\n count?: number,\n}): Promise", }, ]) @@ -459,7 +459,7 @@ describe("CodeMode schema flexibility", () => { { path: "users.lookup", description: "Look up a user", - signature: "tools.users.lookup(input: { login: string }): Promise<{ login: string; id: number }>", + signature: "tools.users.lookup(input: {\n login: string,\n}): Promise<{\n login: string,\n id: number,\n}>", }, ]) @@ -475,7 +475,7 @@ describe("CodeMode schema flexibility", () => { run: () => Effect.succeed("pong"), }) const runtime = CodeMode.make({ tools: { net: { ping } } }) - expect(runtime.catalog()[0]?.signature).toBe("tools.net.ping(input: { host: string }): Promise") + expect(runtime.catalog()[0]?.signature).toBe("tools.net.ping(input: {\n host: string,\n}): Promise") const result = await Effect.runPromise(runtime.execute(`return await tools.net.ping({ host: "example.test" })`)) expect(result.ok).toBe(true) @@ -512,19 +512,19 @@ describe("CodeMode public contract", () => { { path: "orders.lookup", description: "Look up an order by ID", - signature: "tools.orders.lookup(input: { id: string }): Promise<{ id: string; status: string }>", + signature: "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>", }, ]) expect(runtime.instructions()).toContain("Available tools (COMPLETE list") expect(runtime.instructions()).toContain("- orders (1 tool)") expect(runtime.instructions()).toContain( - " - tools.orders.lookup(input: { id: string }): Promise<{ id: string; status: string }> // Look up an order by ID", + " - tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}> // Look up an order by ID", ) // A fully inlined catalog does not advertise search in the instructions... expect(runtime.instructions()).not.toMatch(/\$codemode/) - // ...but the search tool stays registered, so a speculative call still works. Search - // results carry the pretty multiline signature; the inline catalog stays compact. + // ...but the search tool stays registered, so a speculative call still works with the + // same signature as the inline catalog. const result = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: "order" })`)) expect(result.ok).toBe(true) if (result.ok) { @@ -533,10 +533,11 @@ describe("CodeMode public contract", () => { { path: "tools.orders.lookup", description: "Look up an order by ID", - signature: "tools.orders.lookup(input: {\n id: string\n}): Promise<{\n id: string\n status: string\n}>", + signature: "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>", }, ], - total: 1, + remaining: 0, + next: null, }) } }) @@ -554,11 +555,11 @@ describe("CodeMode public contract", () => { { path: "context7.resolve-library-id", description: "Resolve a library ID", - signature: 'tools.context7["resolve-library-id"](input: { libraryName: string }): Promise', + signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise', }, ]) expect(runtime.instructions()).toContain( - 'tools.context7["resolve-library-id"](input: { libraryName: string }): Promise', + 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise', ) const search = await Effect.runPromise( @@ -571,10 +572,11 @@ describe("CodeMode public contract", () => { { path: 'tools.context7["resolve-library-id"]', description: "Resolve a library ID", - signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string\n}): Promise', + signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise', }, ], - total: 1, + remaining: 0, + next: null, }) } @@ -588,7 +590,7 @@ describe("CodeMode public contract", () => { runtime.execute(`return await tools.$codemode.search({ query: 'tools.context7["resolve-library-id"]' })`), ) expect(exact.ok).toBe(true) - if (exact.ok) expect((exact.value as { total: number }).total).toBe(1) + if (exact.ok) expect(exact.value).toMatchObject({ remaining: 0, next: null }) }) test("instructions use markdown sections with placeholder-only call forms", () => { @@ -597,23 +599,26 @@ describe("CodeMode public contract", () => { // Sections in order: workflow at the top, catalog at the bottom. expect(instructions).toContain("## Workflow") expect(instructions).toContain("## Rules") - expect(instructions).toContain("## Syntax") + expect(instructions).toContain("## Language") expect(instructions.indexOf("## Workflow")).toBeLessThan(instructions.indexOf("## Rules")) - expect(instructions.indexOf("## Rules")).toBeLessThan(instructions.indexOf("## Syntax")) - expect(instructions.indexOf("## Syntax")).toBeLessThan(instructions.indexOf("\n## Available tools (COMPLETE list")) - // The workflow carries the result-shape guidance; Rules only add content beyond it. - expect(instructions).toContain( - '`const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string', + expect(instructions.indexOf("## Rules")).toBeLessThan(instructions.indexOf("## Language")) + expect(instructions.indexOf("## Language")).toBeLessThan( + instructions.indexOf("\n## Available tools (COMPLETE list"), ) + expect(instructions).not.toContain("JSON.parse(res)") expect(instructions).toContain("Return only the fields you need") - expect(instructions).toContain("raw payloads get truncated and waste context") + expect(instructions).toContain("avoid returning large raw payloads") expect(instructions).toContain("Do not infer or normalize tool names") expect(instructions).toContain("bracket notation and quotes are part of the path") - expect(instructions).toContain("surrounding agent tools are not available unless listed here") - expect(instructions).toContain("Only tools listed here are available inside `tools`") + expect(instructions).toContain("surrounding agent tools are not available") + expect(instructions).toContain("Only Code Mode tools listed here and internal runtime tools") // Placeholders use generic namespace/tool/field names only - no fabricated real tools // and no real catalog tools cherry-picked into example lines. - expect(instructions).toContain("`return { : data. }`") + expect(instructions).toContain("`const result = await tools..(input)`") + expect(instructions).toContain("Return only the fields you need from structured results") + expect(instructions).toContain("check that it is a non-null object and not an array") + expect(instructions).not.toContain("result.") + expect(instructions).not.toContain("data.") expect(instructions).not.toContain("total_count") expect(instructions).not.toContain("list_issues") expect(instructions).not.toContain("tools.orders.lookup({") @@ -621,36 +626,35 @@ describe("CodeMode public contract", () => { expect(instructions).toContain("1. Pick a tool from the list under `## Available tools`") expect(instructions).not.toContain("Browse one namespace") - const partial = CodeMode.make({ tools, discovery: { maxInlineCatalogTokens: 0 } }).instructions() + const partial = CodeMode.make({ tools, discovery: { catalogBudget: 0 } }).instructions() // PARTIAL: the workflow starts with search (with query-style guidance that is clearly // a query string, never a tool name) and the browse-namespace rule appears. expect(partial).toContain( - '1. If the exact signature is not listed below, first search: `const { items } = await tools.$codemode.search({ query: "" })`.', + '1. If needed, discover tools: `return await tools.$codemode.search({ query: "" })`.', ) + expect(partial).toContain("In the next execution, copy a returned path exactly") expect(partial).toContain( - "Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`", + "Only Code Mode tools listed here or returned by `tools.$codemode.search` and internal runtime tools", ) expect(partial).toContain( '- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "" })`.', ) + expect(partial).toContain("repeat the same search with `offset: next.offset`") + expect(partial).toContain(" limit?: number,\n offset?: number,") expect(partial).not.toContain("total_count") expect(partial).not.toContain("tools.orders.lookup({") }) - test("the syntax section names what is unusual or missing, not an allowlist", () => { + test("the language section describes the restricted runtime without overclaiming", () => { const instructions = CodeMode.make({ tools }).instructions() - // Models already know JavaScript; the section leads with that. - expect(instructions).toContain("Standard modern JavaScript works") - expect(instructions).toContain("TypeScript type annotations are allowed and stripped before execution") - // The not-supported list is derived from (and verified against) the interpreter. - expect(instructions).toContain("Not supported") - for (const missing of ["classes", "generators", "for await...of", ".then/.catch/.finally"]) { + expect(instructions).toContain("restricted JavaScript language for calling tools") + expect(instructions).toContain("not a general-purpose runtime") + expect(instructions).not.toContain("Standard modern JavaScript works") + expect(instructions).not.toContain("TypeScript type annotations") + for (const missing of ["Modules/imports", "classes", "generators", "fetch", "promise chaining"]) { expect(instructions).toContain(missing) } - // Implemented by the DSL-expansion pass, so no longer listed as missing. - expect(instructions).not.toContain("instanceof Error") - expect(instructions).not.toContain("splice") - // The data-boundary note survives. + expect(instructions).toContain("Use Code Mode tools for external operations") expect(instructions).toContain( "Dates serialize to ISO strings at data boundaries; Map/Set/RegExp serialize to `{}`.", ) @@ -660,7 +664,7 @@ describe("CodeMode public contract", () => { const runtime = CodeMode.make({}) const instructions = runtime.instructions() expect(instructions).toContain("No tools are currently available.") - expect(instructions).toContain("## Syntax") + expect(instructions).toContain("## Language") expect(instructions).toContain("## Available tools") expect(instructions).not.toContain("## Workflow") expect(instructions).not.toContain("## Rules") @@ -682,7 +686,7 @@ describe("CodeMode public contract", () => { }) const runtime = CodeMode.make({ tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } }, - discovery: { maxInlineCatalogTokens: 0 }, + discovery: { catalogBudget: 0 }, }) expect(runtime.instructions()).toContain( "Available tools (PARTIAL - 0 of 3 shown; find the rest with tools.$codemode.search)", @@ -707,15 +711,16 @@ describe("CodeMode public contract", () => { { path: "tools.thread.uploadFile", description: "Upload one readable local file to the current Discord thread", - signature: "tools.thread.uploadFile(input: {\n path: string\n}): Promise<{\n sent: boolean\n}>", + signature: "tools.thread.uploadFile(input: {\n path: string,\n}): Promise<{\n sent: boolean,\n}>", }, { path: "tools.thread.generateImage", description: "Generate an image and upload it to the current Discord thread", - signature: "tools.thread.generateImage(input: {\n prompt: string\n}): Promise<{\n sent: boolean\n}>", + signature: "tools.thread.generateImage(input: {\n prompt: string,\n}): Promise<{\n sent: boolean,\n}>", }, ], - total: 2, + remaining: 0, + next: null, }) expect(result.toolCalls).toStrictEqual([{ name: "$codemode.search" }]) @@ -761,9 +766,14 @@ describe("CodeMode public contract", () => { const browse = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({})`)) expect(browse.ok).toBe(true) if (browse.ok) { - const value = browse.value as { items: Array<{ path: string }>; total: number } + const value = browse.value as { + items: Array<{ path: string }> + remaining: number + next: { offset: number } | null + } expect(value.items).toHaveLength(10) - expect(value.total).toBe(14) + expect(value.remaining).toBe(4) + expect(value.next).toStrictEqual({ offset: 10 }) } for (const query of ["many.tool13", "tools.many.tool13"]) { @@ -777,10 +787,11 @@ describe("CodeMode public contract", () => { { path: "tools.many.tool13", description: "Numbered tool 13", - signature: "tools.many.tool13(input: {\n id: string\n}): Promise", + signature: "tools.many.tool13(input: {\n id: string,\n}): Promise", }, ], - total: 1, + remaining: 0, + next: null, }) } } @@ -807,8 +818,8 @@ describe("CodeMode public contract", () => { ) expect(browse.ok).toBe(true) if (browse.ok) { - const value = browse.value as { items: Array<{ path: string }>; total: number } - expect(value.total).toBe(2) + const value = browse.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) expect(value.items.map((item) => item.path)).toStrictEqual([ "tools.github.create_issue", "tools.github.list_issues", @@ -821,8 +832,8 @@ describe("CodeMode public contract", () => { ) expect(scoped.ok).toBe(true) if (scoped.ok) { - const value = scoped.value as { items: Array<{ path: string }>; total: number } - expect(value.total).toBe(1) + const value = scoped.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) expect(value.items[0]?.path).toBe("tools.linear.list_issues") } @@ -858,8 +869,8 @@ describe("CodeMode public contract", () => { ) expect(byParameter.ok).toBe(true) if (byParameter.ok) { - const value = byParameter.value as { items: Array<{ path: string }>; total: number } - expect(value.total).toBe(1) + const value = byParameter.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) expect(value.items[0]?.path).toBe("tools.files.upload") } @@ -869,8 +880,8 @@ describe("CodeMode public contract", () => { ) expect(bySubstring.ok).toBe(true) if (bySubstring.ok) { - const value = bySubstring.value as { items: Array<{ path: string }>; total: number } - expect(value.total).toBe(1) + const value = bySubstring.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) expect(value.items[0]?.path).toBe("tools.files.upload") } }) @@ -898,8 +909,8 @@ describe("CodeMode public contract", () => { ) expect(plural.ok).toBe(true) if (plural.ok) { - const value = plural.value as { items: Array<{ path: string }>; total: number } - expect(value.total).toBe(1) + const value = plural.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) expect(value.items[0]?.path).toBe("tools.tracker.fetch_all") } @@ -907,8 +918,8 @@ describe("CodeMode public contract", () => { const ranked = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: "issues" })`)) expect(ranked.ok).toBe(true) if (ranked.ok) { - const value = ranked.value as { items: Array<{ path: string }>; total: number } - expect(value.total).toBe(2) + const value = ranked.value as { items: Array<{ path: string }>; remaining: number } + expect(value.remaining).toBe(0) expect(value.items.map((item) => item.path)).toStrictEqual([ "tools.github.list_issues", "tools.tracker.fetch_all", @@ -934,13 +945,33 @@ describe("CodeMode public contract", () => { const browse = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({})`)) expect(browse.ok).toBe(true) if (browse.ok) { - const value = browse.value as { items: Array<{ path: string }>; total: number } + const value = browse.value as { items: Array<{ path: string }>; remaining: number; next: unknown } expect(value.items.map((item) => item.path)).toStrictEqual([ "tools.alpha.aardvark", "tools.alpha.beta", "tools.zeta.last", ]) + expect(value.remaining).toBe(0) + expect(value.next).toBeNull() + } + + const middle = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ limit: 1, offset: 1 })`), + ) + expect(middle.ok).toBe(true) + if (middle.ok) { + expect(middle.value).toMatchObject({ + items: [{ path: "tools.alpha.beta" }], + remaining: 1, + next: { offset: 2 }, + }) } + + const exhausted = await Effect.runPromise( + runtime.execute(`return await tools.$codemode.search({ limit: 1, offset: 3 })`), + ) + expect(exhausted.ok).toBe(true) + if (exhausted.ok) expect(exhausted.value).toStrictEqual({ items: [], remaining: 0, next: null }) }) test("inlines round-robin across namespaces so one expensive namespace cannot starve the rest", () => { @@ -965,7 +996,7 @@ describe("CodeMode public contract", () => { // other namespaces from inlining (beta already got its line in the same round). const runtime = CodeMode.make({ tools: { alpha: { cheap, expensive }, beta: { cheap } }, - discovery: { maxInlineCatalogTokens: 40 }, + discovery: { catalogBudget: 40 }, }) const instructions = runtime.instructions() @@ -973,14 +1004,38 @@ describe("CodeMode public contract", () => { "Available tools (PARTIAL - 2 of 3 shown; find the rest with tools.$codemode.search)", ) expect(instructions).toContain("- alpha (2 tools, 1 shown)") - expect(instructions).toContain(" - tools.alpha.cheap(input: { q: string }): Promise // Cheap") + expect(instructions).toContain(" - tools.alpha.cheap(input: {\n q: string,\n}): Promise // Cheap") expect(instructions).not.toContain("tools.alpha.expensive(") // Fully shown namespaces read cleanly (no "shown" annotation). expect(instructions).toContain("- beta (1 tool)") - expect(instructions).toContain(" - tools.beta.cheap(input: { q: string }): Promise // Cheap") + expect(instructions).toContain(" - tools.beta.cheap(input: {\n q: string,\n}): Promise // Cheap") expect(instructions).toMatch(/\$codemode\.search/) }) + test("charges inline JSDoc against the catalog token budget", () => { + const documented = Tool.make({ + description: "Look up a record", + input: { + type: "object", + properties: { + id: { type: "string", description: "A detailed identifier description. ".repeat(20) }, + }, + required: ["id"], + } as const, + run: () => Effect.succeed("ok"), + }) + const runtime = CodeMode.make({ + tools: { records: { lookup: documented } }, + discovery: { catalogBudget: 40 }, + }) + + expect(runtime.catalog()[0]?.signature).toContain("/** A detailed identifier description.") + expect(runtime.instructions()).toContain( + "Available tools (PARTIAL - 0 of 1 shown; find the rest with tools.$codemode.search)", + ) + expect(runtime.instructions()).not.toContain("tools.records.lookup(input:") + }) + test("decodes tool input and output before exposing either side", async () => { const observed: Array = [] const transformed = Tool.make({ @@ -1031,17 +1086,27 @@ describe("CodeMode public contract", () => { expect(() => CodeMode.execute({ code: "return 1", limits: { maxToolCalls: -1 } })).toThrow(RangeError) expect(() => CodeMode.execute({ code: "return 1", limits: { maxOutputBytes: -1 } })).toThrow(RangeError) - expect(() => CodeMode.make({ tools, discovery: { maxInlineCatalogTokens: -1 } })).toThrow(RangeError) + expect(() => CodeMode.make({ tools, discovery: { catalogBudget: -1 } })).toThrow(RangeError) const result = await Effect.runPromise( CodeMode.make({ tools, - discovery: { maxInlineCatalogTokens: 0 }, + discovery: { catalogBudget: 0 }, }).execute(`return await tools.$codemode.search({ query: "order", limit: 0.5 })`), ) expect(result.ok).toBe(false) if (result.ok) return expect(result.error.kind).toBe("InvalidToolInput") + + for (const offset of [-1, 0.5, Number.MAX_SAFE_INTEGER + 1, "1"]) { + const invalidOffset = await Effect.runPromise( + CodeMode.make({ tools }).execute( + `return await tools.$codemode.search({ query: "order", offset: ${JSON.stringify(offset)} })`, + ), + ) + expect(invalidOffset.ok).toBe(false) + if (!invalidOffset.ok) expect(invalidOffset.error.kind).toBe("InvalidToolInput") + } }) test("enforces the tool-call limit as a diagnostic", async () => { diff --git a/packages/codemode/test/enumeration.test.ts b/packages/codemode/test/enumeration.test.ts index 87c075a79282..f176fe2dc895 100644 --- a/packages/codemode/test/enumeration.test.ts +++ b/packages/codemode/test/enumeration.test.ts @@ -41,7 +41,7 @@ describe("Object.keys over tool references", () => { const namespaces = Object.keys(tools) return { namespaces, count: namespaces.length } `), - ).toEqual({ namespaces: ["github", "memory", "playwright"], count: 3 }) + ).toEqual({ namespaces: ["github", "memory", "playwright", "$codemode"], count: 4 }) }) test("enumerates tool names at a nested namespace", async () => { @@ -52,7 +52,7 @@ describe("Object.keys over tool references", () => { expect(await value(`return Object.keys(tools.github.list_issues)`)).toEqual([]) }) - test("the virtual discovery namespace enumerates its callable surface", async () => { + test("the internal discovery namespace enumerates its callable surface", async () => { expect(await value(`return Object.keys(tools.$codemode)`)).toEqual(["search"]) }) @@ -137,7 +137,7 @@ describe("for...in", () => { ).toBe("only") }) - test("enumerates namespaces and tools from the host tool tree", async () => { + test("enumerates namespaces and tools from the callable tool tree", async () => { expect( await value(` const names = [] @@ -146,7 +146,13 @@ describe("for...in", () => { } return names `), - ).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate"]) + ).toEqual([ + "github.list_issues", + "github.get_issue", + "memory.search", + "playwright.navigate", + "$codemode.search", + ]) }) test("unsupported values fail with a hint at for...of and Object.keys", async () => { diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts index 4f25645de22c..53a3b766d58a 100644 --- a/packages/codemode/test/signature.test.ts +++ b/packages/codemode/test/signature.test.ts @@ -40,21 +40,21 @@ describe("pretty signature rendering", () => { [ "{", " /** Repository owner */", - " owner: string", + " owner: string,", " /** Cursor from the previous response's pageInfo */", - " after?: string", + " after?: string,", " /**", " * Results per page", " * @default 30", " */", - " perPage?: number", + " perPage?: number,", " /**", " * Filter by labels", " * @minItems 1", " * @maxItems 10", " */", - " labels?: Array", - ' state?: "open" | "closed"', + " labels?: Array,", + ' state?: "open" | "closed",', "}", ].join("\n"), ) @@ -83,7 +83,7 @@ describe("pretty signature rendering", () => { true, ) expect(pretty).toBe( - ["{", " /** Search filter */", " filter?: {", " /** Issue state */", " state?: string", " }", "}"].join( + ["{", " /** Search filter */", " filter?: {", " /** Issue state */", " state?: string,", " },", "}"].join( "\n", ), ) @@ -91,10 +91,10 @@ describe("pretty signature rendering", () => { test("Effect Schema annotations become JSDoc on input and output fields", () => { expect(inputTypeScript(lookupOrder, true)).toBe( - ["{", " /** Order identifier */", " id: string", " verbose?: boolean", "}"].join("\n"), + ["{", " /** Order identifier */", " id: string,", " verbose?: boolean,", "}"].join("\n"), ) expect(outputTypeScript(lookupOrder, true)).toBe( - ["{", " /** Current order status */", " status: string", "}"].join("\n"), + ["{", " /** Current order status */", " status: string,", "}"].join("\n"), ) }) @@ -129,7 +129,7 @@ describe("pretty signature rendering", () => { { type: "object", properties: { size: { type: "number", default: 1n } } }, true, ) - expect(pretty).toBe(["{", " size?: number", "}"].join("\n")) + expect(pretty).toBe(["{", " size?: number,", "}"].join("\n")) }) test("neutralizes */ inside descriptions so nothing closes the comment early", () => { @@ -150,7 +150,7 @@ describe("pretty signature rendering", () => { true, ) expect(pretty).toBe( - ["{", " /**", " * First line", " *", " * Second line", " */", " query?: string", "}"].join("\n"), + ["{", " /**", " * First line", " *", " * Second line", " */", " query?: string,", "}"].join("\n"), ) }) @@ -235,12 +235,12 @@ describe("non-identifier property names render as quoted keys", () => { expect(jsonSchemaToTypeScript(rawSchema, true)).toBe( [ "{", - ' "123"?: number', - ' "foo-bar"?: string', - ' "@type": string', + ' "123"?: number,', + ' "foo-bar"?: string,', + ' "@type": string,', " /** Dotted name */", - ' "x.y"?: number', - " plain?: boolean", + ' "x.y"?: number,', + " plain?: boolean,", "}", ].join("\n"), ) @@ -259,7 +259,7 @@ describe("non-identifier property names render as quoted keys", () => { }) expect(inputTypeScript(tool)).toContain('"foo-bar"?: string') expect(outputTypeScript(tool)).toBe('{ "content-type": string }') - expect(outputTypeScript(tool, true)).toBe(["{", ' "content-type": string', "}"].join("\n")) + expect(outputTypeScript(tool, true)).toBe(["{", ' "content-type": string,', "}"].join("\n")) }) test("Effect Schema structs with non-identifier field names quote too", () => { @@ -269,7 +269,7 @@ describe("non-identifier property names render as quoted keys", () => { run: () => Effect.succeed(null), }) expect(inputTypeScript(tool)).toBe('{ "foo-bar": string; plain?: number }') - expect(inputTypeScript(tool, true)).toBe(["{", ' "foo-bar": string', " plain?: number", "}"].join("\n")) + expect(inputTypeScript(tool, true)).toBe(["{", ' "foo-bar": string,', " plain?: number,", "}"].join("\n")) }) }) @@ -332,7 +332,7 @@ describe("union schemas render every alternative", () => { }) }) -describe("pretty signatures in search results", () => { +describe("JSDoc signatures in catalogs and search results", () => { const runtime = CodeMode.make({ tools: { github: { list_issues: listIssues }, orders: { lookup: lookupOrder } } }) const search = async (query: string) => { @@ -341,7 +341,7 @@ describe("pretty signatures in search results", () => { ) expect(result.ok).toBe(true) if (!result.ok) throw new Error("search failed") - return result.value as { items: Array<{ path: string; signature: string }>; total: number } + return result.value as { items: Array<{ path: string; signature: string }>; remaining: number } } test("a raw JSON Schema (MCP-style) tool's result signature carries field JSDoc and tags", async () => { @@ -351,21 +351,21 @@ describe("pretty signatures in search results", () => { [ "tools.github.list_issues(input: {", " /** Repository owner */", - " owner: string", + " owner: string,", " /** Cursor from the previous response's pageInfo */", - " after?: string", + " after?: string,", " /**", " * Results per page", " * @default 30", " */", - " perPage?: number", + " perPage?: number,", " /**", " * Filter by labels", " * @minItems 1", " * @maxItems 10", " */", - " labels?: Array", - ' state?: "open" | "closed"', + " labels?: Array,", + ' state?: "open" | "closed",', "}): Promise", ].join("\n"), ) @@ -379,26 +379,26 @@ describe("pretty signatures in search results", () => { [ "tools.orders.lookup(input: {", " /** Order identifier */", - " id: string", - " verbose?: boolean", + " id: string,", + " verbose?: boolean,", "}): Promise<{", " /** Current order status */", - " status: string", + " status: string,", "}>", ].join("\n"), ) } }) - test("the inline catalog line for the same tool stays single-line compact", () => { + test("the inline catalog uses the same JSDoc signatures", async () => { const instructions = runtime.instructions() - expect(instructions).toContain( - ' - tools.github.list_issues(input: { owner: string; after?: string; perPage?: number; labels?: Array; state?: "open" | "closed" }): Promise // List issues in a repository', - ) - expect(instructions).toContain( - " - tools.orders.lookup(input: { id: string; verbose?: boolean }): Promise<{ status: string }> // Look up an order", - ) - expect(instructions).not.toContain("/**") + const github = (await search("list issues repository")).items.find( + ({ path }) => path === "tools.github.list_issues", + )! + const orders = (await search("look up order")).items.find(({ path }) => path === "tools.orders.lookup")! + expect(instructions).toContain(` - ${github.signature} // List issues in a repository`) + expect(instructions).toContain(` - ${orders.signature} // Look up an order`) + expect(instructions).toContain("/** Repository owner */") }) }) @@ -421,7 +421,7 @@ describe("non-identifier tool paths", () => { const instructions = runtime.instructions() expect(instructions).toContain( - 'tools.context7["resolve-library-id"](input: { query: string; libraryName: string }): Promise', + 'tools.context7["resolve-library-id"](input: {\n query: string,\n libraryName: string,\n}): Promise', ) expect(instructions).toContain("Do not infer or normalize tool names") expect(instructions).toContain("bracket notation and quotes are part of the path") diff --git a/packages/opencode/src/tool/code-mode.ts b/packages/opencode/src/tool/code-mode.ts index 0f566a49ade0..332d4b43f150 100644 --- a/packages/opencode/src/tool/code-mode.ts +++ b/packages/opencode/src/tool/code-mode.ts @@ -11,18 +11,11 @@ import { Plugin } from "@/plugin" export const CODE_MODE_TOOL = "execute" -const DESCRIPTION = [ - "Execute a JavaScript/TypeScript program that orchestrates the connected MCP tools inside a confined runtime.", - "The full usage guide and the catalog of available tools follow below.", -].join("\n") +const DESCRIPTION = "Run a confined orchestration script with access to connected MCP tools." export const Parameters = Schema.Struct({ code: Schema.String.annotate({ - description: [ - "JavaScript source to execute.", - "Inside CodeMode, `tools` contains only the MCP/CodeMode tools listed in this execute tool's description; top-level opencode tools like bash, read, or lsp are not available unless listed there.", - "Call available tools using the exact signatures shown in this execute tool's description, compose the results, and `return` the final value.", - ].join(" "), + description: "Script body executed by the confined interpreter.", }), }) diff --git a/packages/opencode/test/tool/code-mode-integration.test.ts b/packages/opencode/test/tool/code-mode-integration.test.ts index f36b6c866ef5..671acd896222 100644 --- a/packages/opencode/test/tool/code-mode-integration.test.ts +++ b/packages/opencode/test/tool/code-mode-integration.test.ts @@ -177,8 +177,10 @@ describe("code mode integration (real MCP server)", () => { test("the appended catalog inlines full signatures with real MCP schemas", () => { expect(description).toContain("Available tools (COMPLETE list") expect(description).toContain("- fixtures (4 tools)") - expect(description).toContain("tools.fixtures.add(input: { a: number; b: number }): Promise<{ sum: number }>") - expect(description).toContain("tools.fixtures.get_text(input: { name: string }): Promise") + expect(description).toContain( + "tools.fixtures.add(input: {\n a: number,\n b: number,\n}): Promise<{\n sum: number,\n}>", + ) + expect(description).toContain("tools.fixtures.get_text(input: {\n name: string,\n}): Promise") expect(description).toContain("// Add two numbers and return the structured sum") expect(description).not.toContain("$codemode") expect(description).toContain("## Workflow") diff --git a/packages/opencode/test/tool/code-mode.test.ts b/packages/opencode/test/tool/code-mode.test.ts index a220f14530fe..0a2b0b7fe424 100644 --- a/packages/opencode/test/tool/code-mode.test.ts +++ b/packages/opencode/test/tool/code-mode.test.ts @@ -98,6 +98,13 @@ describe("code mode execute", () => { const decode = Schema.decodeUnknownEffect(Parameters) await expect(Effect.runPromise(decode({ code: "return 1" }))).resolves.toEqual({ code: "return 1" }) await expect(Effect.runPromise(decode({}))).rejects.toThrow() + expect(Schema.toJsonSchemaDocument(Parameters).schema).toMatchObject({ + properties: { + code: { + description: "Script body executed by the confined interpreter.", + }, + }, + }) }) test("groups multi-underscore server names by longest matching prefix", () => { @@ -124,13 +131,13 @@ describe("code mode execute", () => { }, ["weather"], ) - expect(description).toContain("tools.weather.current(input: { city: string }): Promise<{ tempC: number }>") + expect(description).toContain("tools.weather.current(input: {\n city: string,\n}): Promise<{\n tempC: number,\n}>") }) test("the static base description carries no catalog; the registry appends it", async () => { const tool = await build({ github_list_issues: mcpTool("list_issues", () => "") }) expect(tool.id).toBe(CODE_MODE_TOOL) - expect(tool.description).toContain("confined runtime") + expect(tool.description).toBe("Run a confined orchestration script with access to connected MCP tools.") expect(tool.description).not.toContain("Available tools") expect(tool.description).not.toContain("list_issues") }) @@ -150,7 +157,7 @@ describe("code mode execute", () => { expect(description).toContain("- github (2 tools)") expect(description).toContain("- linear (1 tool)") expect(description).toContain( - "tools.github.create_issue(input: { title: string; body?: string }): Promise", + "tools.github.create_issue(input: {\n title: string,\n body?: string,\n}): Promise", ) expect(description).toContain("tools.github.list_issues(") expect(description).toContain("tools.linear.search(") @@ -159,9 +166,8 @@ describe("code mode execute", () => { expect(description).not.toContain("Browse one namespace") expect(description).toContain("## Workflow") expect(description).toContain("1. Pick a tool from the list under `## Available tools`") - expect(description).toContain( - '`const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string', - ) + expect(description).not.toContain("JSON.parse(res)") + expect(description).toContain("check that it is a non-null object and not an array") expect(description).toContain("Return only the fields you need") expect(description).not.toContain("total_count") }) @@ -180,7 +186,7 @@ describe("code mode execute", () => { ), }) expect(description).toContain( - "tools.weather.current(input: { city: string }): Promise<{ tempC: number; summary?: string }>", + "tools.weather.current(input: {\n city: string,\n}): Promise<{\n tempC: number,\n summary?: string,\n}>", ) }) @@ -207,9 +213,16 @@ describe("code mode execute", () => { expect(description).toContain("Available tools (PARTIAL - ") expect(description).toMatch(/- alpha \(150 tools, \d+ shown\)/) expect(description).toContain("- zeta (1 tool)\n") - expect(description).toContain("tools.zeta.only_tool(input: { topic: string }): Promise") + expect(description).toContain( + "tools.zeta.only_tool(input: {\n /** Subject to look up */\n topic: string,\n}): Promise", + ) expect(description).toContain("tools.$codemode.search(") - expect(description).toContain("1. If the exact signature is not listed below, first search:") + expect(description).toContain(" limit?: number,\n offset?: number,") + expect(description).toContain(" remaining: number,\n next: {") + expect(description).toContain(" offset: number,\n } | null,") + expect(description).toContain( + '1. If needed, discover tools: `return await tools.$codemode.search({ query: "" })`.', + ) expect(description).toContain( '- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "" })`.', ) @@ -219,17 +232,18 @@ describe("code mode execute", () => { const tool = await build(tools, ["alpha", "zeta"]) const out = await Effect.runPromise( - tool.execute({ code: "return await tools.$codemode.search({ query: 'only tool', limit: 3 })" }, ctx), + tool.execute({ code: "return await tools.$codemode.search({ query: 'only tool', limit: 3, offset: 0 })" }, ctx), ) const result = JSON.parse(out.output) expect(result.items.map((i: any) => i.path)).toContain("tools.zeta.only_tool") + expect(result).toMatchObject({ remaining: 0, next: null }) expect(result.items[0].signature).toContain("tools.") const signature = result.items.find((i: any) => i.path === "tools.zeta.only_tool").signature expect(signature).toContain("tools.zeta.only_tool(input: {\n") expect(signature).toContain(" /** Subject to look up */\n topic: string") - expect(description).not.toContain("/**") + expect(description).toContain("/** Subject to look up */") expect(out.metadata.toolCalls).toEqual([ - { tool: "$codemode.search", status: "completed", input: { query: "only tool", limit: 3 } }, + { tool: "$codemode.search", status: "completed", input: { query: "only tool", limit: 3, offset: 0 } }, ]) }) @@ -240,7 +254,7 @@ describe("code mode execute", () => { expect(output.metadata.toolCalls).toEqual([]) }) - test("Object.keys(tools) enumerates the MCP server namespaces", async () => { + test("Object.keys(tools) enumerates the MCP server and CodeMode namespaces", async () => { const tool = await build({ github_list_issues: mcpTool("list_issues", () => ""), linear_search: mcpTool("search", () => ""), @@ -251,7 +265,7 @@ describe("code mode execute", () => { ctx, ), ) - expect(JSON.parse(output.output)).toEqual({ namespaces: ["github", "linear"], count: 2 }) + expect(JSON.parse(output.output)).toEqual({ namespaces: ["github", "linear", "$codemode"], count: 3 }) }) test("calls a namespaced MCP tool and flows its text result back into the program", async () => { diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index f3ccd5997c19..c8c5fac59559 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -132,7 +132,7 @@ describe("tool.registry", () => { expect(ids).toContain("execute") expect(tools.map((tool) => tool.id)).toContain("execute") - expect(execute?.description).toContain("tools.weather.current(input: { city: string })") + expect(execute?.description).toContain("tools.weather.current(input: {\n city: string,\n})") }), ) From e12cb7fb6b22604cee6e97b3c4ef8fddc21d1bd9 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 6 Jul 2026 05:53:41 +0000 Subject: [PATCH 20/34] chore: generate --- packages/codemode/src/tool-runtime.ts | 39 ++++++------------- packages/codemode/test/codemode.test.ts | 3 +- packages/codemode/test/enumeration.test.ts | 8 +--- packages/codemode/test/signature.test.ts | 12 ++++-- packages/opencode/test/tool/code-mode.test.ts | 4 +- 5 files changed, 26 insertions(+), 40 deletions(-) diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index 29b27cb5c6ca..48d8209a48f4 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -390,8 +390,7 @@ const makeSearchTool = (searchIndex: ReadonlyArray) => pathQuery === "" ? undefined : scoped.find( - (entry) => - entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed, + (entry) => entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed, ) const terms = tokenize(query).map(termForms) // Additive field-weighted scoring, summed across terms: exact path or path segment @@ -418,8 +417,7 @@ const makeSearchTool = (searchIndex: ReadonlyArray) => .filter(({ score }) => terms.length === 0 || score > 0) .sort( (left, right) => - right.score - left.score || - left.entry.description.path.localeCompare(right.entry.description.path), + right.score - left.score || left.entry.description.path.localeCompare(right.entry.description.path), ) .map(({ entry }) => entry) const items = ranked.slice(offset, offset + (request.limit ?? defaultSearchLimit)).map(({ description }) => ({ @@ -479,10 +477,7 @@ export const assertValidTools = (tools: HostTools): void => { * namespace. Namespace stub lines are never budgeted: every namespace appears with its * tool count even at budget 0. */ -export const prepare = ( - tools: HostTools, - catalogBudget = defaultCatalogBudget, -): DiscoveryPlan => { +export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBudget): DiscoveryPlan => { if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) { throw new RangeError("discovery.catalogBudget must be a non-negative safe integer") } @@ -644,10 +639,7 @@ export const prepare = ( * function in JS). An unknown path is an `UnknownTool` error pointing at the working * discovery idioms, mirroring how calling an unknown tool fails. */ -const namespaceKeys = ( - tools: HostTools, - path: ReadonlyArray, -): ReadonlyArray => { +const namespaceKeys = (tools: HostTools, path: ReadonlyArray): ReadonlyArray => { let value: HostTool | Definition | HostTools = tools for (const segment of path) { if ( @@ -656,13 +648,9 @@ const namespaceKeys = ( isDefinition(value) || !Object.hasOwn(value, segment) ) { - throw new ToolRuntimeError( - "UnknownTool", - `Unknown tool namespace '${path.join(".")}'.`, - [ - "Object.keys(tools) lists the available namespaces; tools.$codemode.search({ query }) finds described tools.", - ], - ) + throw new ToolRuntimeError("UnknownTool", `Unknown tool namespace '${path.join(".")}'.`, [ + "Object.keys(tools) lists the available namespaces; tools.$codemode.search({ query }) finds described tools.", + ]) } value = value[segment] as HostTool | Definition | HostTools } @@ -670,10 +658,7 @@ const namespaceKeys = ( return Object.keys(value) } -const resolve = ( - tools: HostTools, - path: ReadonlyArray, -): HostTool | Definition => { +const resolve = (tools: HostTools, path: ReadonlyArray): HostTool | Definition => { let value: HostTool | Definition | HostTools = tools for (const segment of path) { @@ -683,11 +668,9 @@ const resolve = ( isDefinition(value) || !Object.hasOwn(value, segment) ) { - throw new ToolRuntimeError( - "UnknownTool", - `Unknown tool '${path.join(".")}'.`, - ["Use tools.$codemode.search({ query }) to find available described tools."], - ) + throw new ToolRuntimeError("UnknownTool", `Unknown tool '${path.join(".")}'.`, [ + "Use tools.$codemode.search({ query }) to find available described tools.", + ]) } value = value[segment] as HostTool | Definition | HostTools } diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index 168dc93d5e26..f5a7169cf553 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -533,7 +533,8 @@ describe("CodeMode public contract", () => { { path: "tools.orders.lookup", description: "Look up an order by ID", - signature: "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>", + signature: + "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>", }, ], remaining: 0, diff --git a/packages/codemode/test/enumeration.test.ts b/packages/codemode/test/enumeration.test.ts index f176fe2dc895..0de3dc3ea0dd 100644 --- a/packages/codemode/test/enumeration.test.ts +++ b/packages/codemode/test/enumeration.test.ts @@ -146,13 +146,7 @@ describe("for...in", () => { } return names `), - ).toEqual([ - "github.list_issues", - "github.get_issue", - "memory.search", - "playwright.navigate", - "$codemode.search", - ]) + ).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate", "$codemode.search"]) }) test("unsupported values fail with a hint at for...of and Object.keys", async () => { diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts index 53a3b766d58a..2d07d2f234e9 100644 --- a/packages/codemode/test/signature.test.ts +++ b/packages/codemode/test/signature.test.ts @@ -83,9 +83,15 @@ describe("pretty signature rendering", () => { true, ) expect(pretty).toBe( - ["{", " /** Search filter */", " filter?: {", " /** Issue state */", " state?: string,", " },", "}"].join( - "\n", - ), + [ + "{", + " /** Search filter */", + " filter?: {", + " /** Issue state */", + " state?: string,", + " },", + "}", + ].join("\n"), ) }) diff --git a/packages/opencode/test/tool/code-mode.test.ts b/packages/opencode/test/tool/code-mode.test.ts index 0a2b0b7fe424..34b3faa610d7 100644 --- a/packages/opencode/test/tool/code-mode.test.ts +++ b/packages/opencode/test/tool/code-mode.test.ts @@ -131,7 +131,9 @@ describe("code mode execute", () => { }, ["weather"], ) - expect(description).toContain("tools.weather.current(input: {\n city: string,\n}): Promise<{\n tempC: number,\n}>") + expect(description).toContain( + "tools.weather.current(input: {\n city: string,\n}): Promise<{\n tempC: number,\n}>", + ) }) test("the static base description carries no catalog; the registry appends it", async () => { From 14df88eab514e7e5d61e5a3f72279818fc64e948 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 06:08:59 +0000 Subject: [PATCH 21/34] fix(app): preserve provider dialog backdrop (#35370) Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Brendan Allan --- .../components/dialog-connect-provider.tsx | 26 ++-- .../src/components/dialog-custom-provider.tsx | 17 +-- .../components/dialog-select-model-unpaid.tsx | 2 +- .../src/components/dialog-select-provider.tsx | 128 ++++++++++-------- .../app/src/components/settings-providers.tsx | 9 +- .../src/components/settings-v2/providers.tsx | 9 +- .../pages/session/usage-exceeded-dialogs.tsx | 11 +- 7 files changed, 107 insertions(+), 95 deletions(-) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 3a57769b5a68..c94cca505351 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -17,19 +17,17 @@ import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useProviders } from "@/hooks/use-providers" -export function DialogConnectProvider(props: { provider: string; directory?: Accessor }) { +export function DialogConnectProvider(props: { + provider: string + directory?: Accessor + onBack: () => void +}) { const dialog = useDialog() const serverSync = useServerSync() const serverSDK = useServerSDK() const language = useLanguage() const providers = useProviders(props.directory) - const all = () => { - void import("./dialog-select-provider").then((x) => { - dialog.show(() => ) - }) - } - const alive = { value: true } const timer = { current: undefined as ReturnType | undefined } @@ -364,19 +362,11 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc } function goBack() { - if (methods().length === 1) { - all() - return - } - if (store.authorization) { - dispatch({ type: "method.reset" }) - return - } - if (store.methodIndex !== undefined) { + if (methods().length > 1 && store.methodIndex !== undefined) { dispatch({ type: "method.reset" }) return } - all() + props.onBack() } function MethodSelection() { @@ -600,6 +590,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc return ( } + transition >
diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index 647e5002a297..e8cae859e06a 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -6,18 +6,16 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { useMutation } from "@tanstack/solid-query" import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@/utils/toast" -import { type Accessor, batch, For } from "solid-js" +import { batch, For } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form" -import { DialogSelectProvider } from "./dialog-select-provider" type Props = { - back?: "providers" | "close" - directory?: Accessor + onBack: () => void } export function DialogCustomProvider(props: Props) { @@ -36,14 +34,6 @@ export function DialogCustomProvider(props: Props) { err: {}, }) - const goBack = () => { - if (props.back === "close") { - dialog.close() - return - } - dialog.show(() => ) - } - const addModel = () => { setForm( "models", @@ -164,12 +154,13 @@ export function DialogCustomProvider(props: Props) { return ( } diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index bcb4a2fbcbf4..0a32ad48b36d 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -24,7 +24,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props const connect = (provider: string) => { void import("./dialog-connect-provider").then((x) => { - dialog.show(() => ) + dialog.show(() => ) }) } diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx index ff0ab5d2bf0c..e1facf48219a 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -1,5 +1,5 @@ -import { type Accessor, Component, Show } from "solid-js" -import { useDialog } from "@opencode-ai/ui/context/dialog" +import { type Accessor, Component, Match, Show, Switch } from "solid-js" +import { createStore } from "solid-js/store" import { popularProviders, useProviders } from "@/hooks/use-providers" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" @@ -12,9 +12,13 @@ import { DialogCustomProvider } from "./dialog-custom-provider" const CUSTOM_ID = "_custom" export const DialogSelectProvider: Component<{ directory?: Accessor }> = (props) => { - const dialog = useDialog() const providers = useProviders(props.directory) const language = useLanguage() + const [store, setStore] = createStore({ selected: undefined as string | undefined }) + + function showPicker() { + setStore("selected", undefined) + } const popularGroup = () => language.t("dialog.provider.group.popular") const otherGroup = () => language.t("dialog.provider.group.other") @@ -27,61 +31,67 @@ export const DialogSelectProvider: Component<{ directory?: Accessor - x?.id} - items={() => { - language.locale() - return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()] - }} - filterKeys={["id", "name"]} - groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} - sortBy={(a, b) => { - if (a.id === CUSTOM_ID) return -1 - if (b.id === CUSTOM_ID) return 1 - if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) - return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) - return a.name.localeCompare(b.name) - }} - sortGroupsBy={(a, b) => { - const popular = popularGroup() - if (a.category === popular && b.category !== popular) return -1 - if (b.category === popular && a.category !== popular) return 1 - return 0 - }} - onSelect={(x) => { - if (!x) return - if (x.id === CUSTOM_ID) { - dialog.show(() => ) - return - } - dialog.show(() => ) - }} - > - {(i) => ( -
- - {i.name} - -
{language.t("dialog.provider.opencode.tagline")}
-
- - {language.t("settings.providers.tag.custom")} - - - {language.t("dialog.provider.tag.recommended")} - - {(value) =>
{value()}
}
- - {language.t("dialog.provider.tag.recommended")} - -
- )} -
-
+ + + + + + {(provider) => } + + + + x?.id} + items={() => { + language.locale() + return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()] + }} + filterKeys={["id", "name"]} + groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} + sortBy={(a, b) => { + if (a.id === CUSTOM_ID) return -1 + if (b.id === CUSTOM_ID) return 1 + if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) + return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) + return a.name.localeCompare(b.name) + }} + sortGroupsBy={(a, b) => { + const popular = popularGroup() + if (a.category === popular && b.category !== popular) return -1 + if (b.category === popular && a.category !== popular) return 1 + return 0 + }} + onSelect={(x) => { + if (!x) return + setStore("selected", x.id) + }} + > + {(i) => ( +
+ + {i.name} + +
{language.t("dialog.provider.opencode.tagline")}
+
+ + {language.t("settings.providers.tag.custom")} + + + {language.t("dialog.provider.tag.recommended")} + + {(value) =>
{value()}
}
+ + {language.t("dialog.provider.tag.recommended")} + +
+ )} +
+
+
+
) } diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 24e7a60104ae..9791fc21fcae 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -211,7 +211,12 @@ const SettingsProvidersContent: Component = () => { variant="secondary" icon="plus-small" onClick={() => { - dialog.show(() => ) + dialog.show(() => ( + dialog.show(() => )} + /> + )) }} > {language.t("common.connect")} @@ -239,7 +244,7 @@ const SettingsProvidersContent: Component = () => { variant="secondary" icon="plus-small" onClick={() => { - dialog.show(() => ) + dialog.show(() => ) }} > {language.t("common.connect")} diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index cd24bbd4558c..99a4101f9a35 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -211,7 +211,12 @@ export const SettingsProvidersV2: Component = () => { variant="neutral" icon="plus" onClick={() => { - dialog.show(() => ) + dialog.show(() => ( + dialog.show(() => )} + /> + )) }} > {language.t("common.connect")} @@ -241,7 +246,7 @@ export const SettingsProvidersV2: Component = () => { variant="neutral" icon="plus" onClick={() => { - dialog.show(() => ) + dialog.show(() => ) }} > {language.t("common.connect")} diff --git a/packages/app/src/pages/session/usage-exceeded-dialogs.tsx b/packages/app/src/pages/session/usage-exceeded-dialogs.tsx index 935eed7e6969..5c77e7bcecc0 100644 --- a/packages/app/src/pages/session/usage-exceeded-dialogs.tsx +++ b/packages/app/src/pages/session/usage-exceeded-dialogs.tsx @@ -77,7 +77,16 @@ export function useUsageExceededDialogs() { if (dontShowAgain) setGoUpsellState(keys.dontShow, Date.now()) else { void import("../../components/dialog-connect-provider").then((x) => - dialog.show(() => ), + dialog.show(() => ( + { + void import("../../components/dialog-select-provider").then((provider) => { + dialog.show(() => ) + }) + }} + /> + )), ) } }} From 3a149ba71ca7859e1b599b12fc34ef99dd797c4e Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:26:03 +1000 Subject: [PATCH 22/34] fix(app): optimize large review panes (#35375) --- .../review-pane-scaling-benchmark.spec.ts | 312 +++++++++++++ .../e2e/regression/review-tab-switch.spec.ts | 40 +- .../review-terminal-stacked.spec.ts | 212 +++++++++ .../src/components/file-tree-v2-model.test.ts | 46 ++ .../app/src/components/file-tree-v2-model.ts | 77 +++ packages/app/src/components/file-tree-v2.tsx | 441 ++++++------------ .../components/virtual-scroll-element.test.ts | 18 + .../src/components/virtual-scroll-element.ts | 4 + packages/app/src/pages/session.tsx | 18 +- .../src/pages/session/session-side-panel.tsx | 37 +- .../session/v2/review-diff-kinds.test.ts | 7 + .../src/pages/session/v2/review-diff-kinds.ts | 3 +- .../src/pages/session/v2/review-panel-v2.tsx | 1 - .../pages/session/v2/session-file-list-v2.tsx | 139 ++++-- .../app/test-browser/solid-virtual.test.ts | 15 + ...-review-file-preview-v2-virtualize.test.ts | 13 + ...ssion-review-file-preview-v2-virtualize.ts | 5 + .../session-review-file-preview-v2.tsx | 5 + .../src/v2/components/session-review-v2.tsx | 85 ++-- 19 files changed, 1074 insertions(+), 404 deletions(-) create mode 100644 packages/app/e2e/performance/timeline/review-pane-scaling-benchmark.spec.ts create mode 100644 packages/app/e2e/regression/review-terminal-stacked.spec.ts create mode 100644 packages/app/src/components/file-tree-v2-model.test.ts create mode 100644 packages/app/src/components/file-tree-v2-model.ts create mode 100644 packages/app/src/components/virtual-scroll-element.test.ts create mode 100644 packages/app/src/components/virtual-scroll-element.ts create mode 100644 packages/session-ui/src/v2/components/session-review-file-preview-v2-virtualize.test.ts create mode 100644 packages/session-ui/src/v2/components/session-review-file-preview-v2-virtualize.ts diff --git a/packages/app/e2e/performance/timeline/review-pane-scaling-benchmark.spec.ts b/packages/app/e2e/performance/timeline/review-pane-scaling-benchmark.spec.ts new file mode 100644 index 000000000000..81de0a055f90 --- /dev/null +++ b/packages/app/e2e/performance/timeline/review-pane-scaling-benchmark.spec.ts @@ -0,0 +1,312 @@ +import type { Page } from "@playwright/test" +import { benchmark, expect } from "../benchmark" +import { setupTimelineBenchmark } from "./session-timeline-benchmark.fixture" + +const changedLinesPerFile = 100 +const linesPerSide = changedLinesPerFile / 2 +const fileCounts = [1, 10, 100, 1_000, 10_000] +const filesPerDirectory = 100 +const readyFrames = 3 +const completionTimeoutMs = Number(process.env.REVIEW_PANE_COMPLETION_TIMEOUT_MS ?? 900_000) + +type ReviewPaneScalingSample = { + observedAtMs: number + logicalRows: number + treeRows: number + fileRows: number + diffLines: number + header: string + ready: boolean +} + +type ReviewPaneScalingProbe = { + startedAt?: number + firstTreeRowMs?: number + logicalTreeReadyMs?: number + firstDiffRenderMs?: number + stableReadyMs?: number + samples: ReviewPaneScalingSample[] + frameTimesMs: number[] + longTasks: { startTime: number; duration: number }[] + stop: () => void +} + +benchmark.describe("performance: review pane scaling", () => { + for (const fileCount of fileCounts) { + const changedLines = fileCount * changedLinesPerFile + + benchmark( + `${changedLines} changed lines across ${fileCount} ${fileCount === 1 ? "file" : "files"}`, + async ({ page, report }) => { + benchmark.setTimeout(1_200_000) + await page.emulateMedia({ reducedMotion: "reduce" }) + + const patchByteLimit = Number(process.env.REVIEW_PANE_PATCH_BYTE_LIMIT ?? Number.POSITIVE_INFINITY) + if (Number.isNaN(patchByteLimit) || patchByteLimit < 0) + throw new Error(`Invalid REVIEW_PANE_PATCH_BYTE_LIMIT: ${process.env.REVIEW_PANE_PATCH_BYTE_LIMIT}`) + const responseBody = JSON.stringify(createScalingDiffs(fileCount, patchByteLimit)) + await setupTimelineBenchmark(page, { + historyTurns: 0, + eventBatch: 1, + newLayoutDesigns: true, + }) + await page.route("**/vcs/diff**", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + headers: { "access-control-allow-origin": "*" }, + body: responseBody, + }), + ) + + const expectedRows = fileCount + 2 + Math.ceil(fileCount / filesPerDirectory) + const metrics = await measureReviewPaneLoad(page, { + expectedFile: reviewFile(0), + expectedRows, + }) + const search = await measureBroadReviewSearch(page, fileCount) + + expect(metrics.logicalRows).toBe(expectedRows) + expect(metrics.fileRows).toBeGreaterThan(0) + expect(metrics.treeRows).toBeGreaterThan(0) + expect(metrics.diffLines).toBeGreaterThan(0) + expect(search.logicalRows).toBe(fileCount) + expect(search.renderedRows).toBeGreaterThan(0) + report( + { ...metrics, search }, + { + fileCount, + changedLinesPerFile, + changedLines, + additions: changedLines / 2, + deletions: changedLines / 2, + patchLines: changedLines, + patchByteLimit: Number.isFinite(patchByteLimit) ? patchByteLimit : null, + payloadBytes: new TextEncoder().encode(responseBody).byteLength, + expectedRows, + }, + ) + }, + ) + } +}) + +async function measureBroadReviewSearch(page: Page, expectedRows: number) { + const filter = page.getByRole("searchbox", { name: "Filter files" }) + await filter.evaluate((element) => { + element.addEventListener( + "input", + () => { + ;(window as Window & { __reviewSearchStartedAt?: number }).__reviewSearchStartedAt = performance.now() + }, + { once: true, capture: true }, + ) + }) + await filter.fill("file-") + + return page.evaluate((expectedRows) => { + const startedAt = (window as Window & { __reviewSearchStartedAt?: number }).__reviewSearchStartedAt! + return new Promise<{ stableMs: number; logicalRows: number; renderedRows: number }>((resolve) => { + let previous = -1 + let streak = 0 + const sample = () => { + const tree = document.querySelector('#review-panel [data-component="file-tree-v2"]') + const rows = [...document.querySelectorAll('#review-panel [data-slot="file-tree-v2-row"]')] + const logicalRows = Number(tree?.dataset.totalRows ?? rows.length) + const ready = + logicalRows === expectedRows && rows.length > 0 && rows.every((row) => row.textContent?.includes("file-")) + streak = ready && rows.length === previous ? streak + 1 : ready ? 1 : 0 + previous = rows.length + if (streak >= 3) { + resolve({ stableMs: performance.now() - startedAt, logicalRows, renderedRows: rows.length }) + return + } + requestAnimationFrame(sample) + } + requestAnimationFrame(sample) + }) + }, expectedRows) +} + +function createScalingDiffs(fileCount: number, patchByteLimit: number) { + const changes = Array.from({ length: linesPerSide }, (_, index) => { + const line = String(index).padStart(3, "0") + return `-export const value_${line} = "before"\n+export const value_${line} = "after"` + }).join("\n") + let patchBytes = 0 + let capped = false + + return Array.from({ length: fileCount }, (_, index) => { + const file = reviewFile(index) + const fullPatch = [ + `diff --git a/${file} b/${file}`, + `--- a/${file}`, + `+++ b/${file}`, + `@@ -1,${linesPerSide} +1,${linesPerSide} @@`, + changes, + ].join("\n") + if (index === 0 && fullPatch.length > patchByteLimit) + throw new Error(`REVIEW_PANE_PATCH_BYTE_LIMIT must include the active patch (${fullPatch.length} bytes)`) + const patch = !capped && patchBytes + fullPatch.length <= patchByteLimit ? fullPatch : emptyReviewPatch(file) + if (patch === fullPatch) patchBytes += fullPatch.length + else capped = true + return { + file, + patch, + additions: linesPerSide, + deletions: linesPerSide, + status: "modified" as const, + } + }) +} + +function emptyReviewPatch(file: string) { + return [`diff --git a/${file} b/${file}`, `--- a/${file}`, `+++ b/${file}`].join("\n") +} + +function reviewFile(index: number) { + return `src/review/d${String(Math.floor(index / filesPerDirectory)).padStart(5, "0")}/file-${String(index).padStart(5, "0")}.ts` +} + +async function measureReviewPaneLoad(page: Page, input: { expectedFile: string; expectedRows: number }) { + const toggle = page.getByRole("button", { name: "Toggle review" }) + await expect(toggle).toBeVisible() + await toggle.evaluate((element) => element.setAttribute("data-review-pane-scaling-toggle", "")) + await installReviewPaneScalingProbe(page, input) + await toggle.click() + await page.waitForFunction( + () => + (window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe + ?.stableReadyMs !== undefined, + undefined, + { timeout: completionTimeoutMs }, + ) + + return page.evaluate(() => { + const probe = (window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe! + probe.stop() + const startedAt = probe.startedAt! + const final = probe.samples.at(-1)! + const resources = performance + .getEntriesByType("resource") + .filter((entry) => entry.name.includes("/vcs/diff")) as PerformanceResourceTiming[] + const resource = resources.at(-1) + const longTasks = probe.longTasks.filter( + (entry) => entry.startTime >= startedAt && entry.startTime <= startedAt + probe.stableReadyMs!, + ) + const frameGaps = probe.frameTimesMs.map((time, index) => time - (probe.frameTimesMs[index - 1] ?? 0)) + + return { + firstTreeRowMs: probe.firstTreeRowMs ?? null, + logicalTreeReadyMs: probe.logicalTreeReadyMs ?? null, + firstDiffRenderMs: probe.firstDiffRenderMs ?? null, + stableReadyMs: probe.stableReadyMs ?? null, + responseStartMs: resource ? resource.responseStart - startedAt : null, + responseEndMs: resource ? resource.responseEnd - startedAt : null, + responseToStableMs: resource ? probe.stableReadyMs! - (resource.responseEnd - startedAt) : null, + treeRows: final.treeRows, + logicalRows: final.logicalRows, + fileRows: final.fileRows, + diffLines: final.diffLines, + samples: probe.samples.length, + maxFrameGapMs: Math.max(0, ...frameGaps), + longTaskCount: longTasks.length, + longTaskTotalMs: longTasks.reduce((sum, entry) => sum + entry.duration, 0), + maxLongTaskMs: Math.max(0, ...longTasks.map((entry) => entry.duration)), + } + }) +} + +async function installReviewPaneScalingProbe(page: Page, input: { expectedFile: string; expectedRows: number }) { + await page.evaluate( + ({ expectedFile, expectedRows, stableFrames }) => { + let running = true + let readyStreak = 0 + const basename = expectedFile.split("/").at(-1)! + const longTaskObserver = PerformanceObserver.supportedEntryTypes.includes("longtask") + ? new PerformanceObserver((list) => { + probe.longTasks.push( + ...list.getEntries().map((entry) => ({ startTime: entry.startTime, duration: entry.duration })), + ) + }) + : undefined + const probe: ReviewPaneScalingProbe = { + samples: [], + frameTimesMs: [], + longTasks: [], + stop: () => { + running = false + longTaskObserver?.disconnect() + }, + } + + const sample = (time: number) => { + if (!running || probe.startedAt === undefined) return + const panel = document.querySelector("#review-panel") + const tree = panel?.querySelector('[data-component="file-tree-v2"]') + const rows = panel?.querySelectorAll('[data-slot="file-tree-v2-row"]') ?? [] + const fileRows = panel?.querySelectorAll('button[data-slot="file-tree-v2-row"]') ?? [] + const header = + panel?.querySelector('[data-slot="session-review-v2-file-header"]')?.textContent?.trim() ?? "" + const viewers = panel + ? [...panel.querySelectorAll('[data-component="file"][data-mode="diff"]')] + : [] + const diffLines = viewers.reduce( + (sum, viewer) => + sum + (viewer.querySelector("diffs-container")?.shadowRoot?.querySelectorAll("[data-line]").length ?? 0), + 0, + ) + const observedAtMs = time - probe.startedAt + const logicalRows = Number(tree?.dataset.totalRows ?? rows.length) + const ready = + logicalRows === expectedRows && + fileRows.length > 0 && + header.includes(basename) && + viewers.length === 1 && + diffLines > 0 + const previous = probe.samples.at(-1) + const stable = + ready && + previous?.ready === true && + previous.logicalRows === logicalRows && + previous.treeRows === rows.length && + previous.fileRows === fileRows.length && + previous.diffLines === diffLines && + previous.header === header + + probe.frameTimesMs.push(observedAtMs) + probe.samples.push({ + observedAtMs, + logicalRows, + treeRows: rows.length, + fileRows: fileRows.length, + diffLines, + header, + ready, + }) + if (probe.firstTreeRowMs === undefined && rows.length > 0) probe.firstTreeRowMs = observedAtMs + if (probe.logicalTreeReadyMs === undefined && logicalRows === expectedRows) + probe.logicalTreeReadyMs = observedAtMs + if (probe.firstDiffRenderMs === undefined && diffLines > 0) probe.firstDiffRenderMs = observedAtMs + readyStreak = !ready ? 0 : stable ? readyStreak + 1 : 1 + if (readyStreak === stableFrames) probe.stableReadyMs = observedAtMs + if (probe.stableReadyMs === undefined) requestAnimationFrame(sample) + } + + longTaskObserver?.observe({ type: "longtask", buffered: true }) + document.addEventListener( + "click", + (event) => { + const toggle = event.target instanceof Element ? event.target.closest("button") : undefined + if (!toggle?.hasAttribute("data-review-pane-scaling-toggle")) return + probe.startedAt = performance.now() + performance.mark("opencode.review-pane-scaling.click") + requestAnimationFrame(sample) + }, + { capture: true, once: true }, + ) + ;(window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe = probe + }, + { ...input, stableFrames: readyFrames }, + ) +} diff --git a/packages/app/e2e/regression/review-tab-switch.spec.ts b/packages/app/e2e/regression/review-tab-switch.spec.ts index 92bf12d51cf5..c2ea406c5ab3 100644 --- a/packages/app/e2e/regression/review-tab-switch.spec.ts +++ b/packages/app/e2e/regression/review-tab-switch.spec.ts @@ -10,6 +10,9 @@ const sessionB = "ses_review_tab_b" const titleA = "Alpha session" const titleB = "Beta session" const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` +const diffs = Array.from({ length: 2_740 }, (_, index) => + fileDiff(`src/generated-${String(index).padStart(4, "0")}.ts`), +) // Marks the review pane DOM node so a remount (fresh node) is detectable. const PROBE = "original" @@ -25,20 +28,34 @@ test("keeps the v2 review pane mounted when switching session tabs in a workspac await expectSessionTitle(page, titleA) await page.getByRole("button", { name: "Toggle review" }).click() + const reviewTab = page.getByRole("tab", { name: /Review/ }) + const reviewTabPanel = page.getByRole("tabpanel", { name: /Review/ }) + await expect(reviewTab).toHaveAttribute("aria-controls", "session-side-panel-review-tabpanel") + await expect(reviewTabPanel).toHaveAttribute("id", "session-side-panel-review-tabpanel") const review = page.locator('#review-panel [data-component="session-review-v2"]') await expectAppVisible(review) - await expectAppVisible(page.getByRole("button", { name: /example\.ts/ })) + await expectAppVisible(page.getByRole("button", { name: "generated-0000.ts" })) await writeProbe(page) await switchTab(page, titleB) await expectSessionTitle(page, titleB) await expectAppVisible(review) + await expectAppVisible(page.getByRole("button", { name: "generated-0000.ts" })) expect(await readProbe(page)).toBe(PROBE) await switchTab(page, titleA) await expectSessionTitle(page, titleA) await expectAppVisible(review) + await expectAppVisible(page.getByRole("button", { name: "generated-0000.ts" })) expect(await readProbe(page)).toBe(PROBE) + + const viewport = page.locator('#review-panel [data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport') + await viewport.hover() + await page.mouse.wheel(0, 100_000) + await expect + .poll(() => viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeLessThanOrEqual(1) + await expect(page.getByRole("button", { name: "generated-2739.ts" })).toBeVisible() }) type Probed = HTMLElement & { __e2eProbe?: string } @@ -80,16 +97,7 @@ async function setup(page: Page) { default: { providerID: "opencode", modelID: "test" }, }, sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)], - vcsDiff: [ - { - file: "src/example.ts", - additions: 1, - deletions: 1, - status: "modified", - patch: - "diff --git a/src/example.ts b/src/example.ts\n--- a/src/example.ts\n+++ b/src/example.ts\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n", - }, - ], + vcsDiff: diffs, pageMessages: () => ({ items: [] }), }) @@ -127,3 +135,13 @@ function session(id: string, title: string, created: number) { function sessionHref(sessionID: string) { return `/server/${base64Encode(server)}/session/${sessionID}` } + +function fileDiff(file: string) { + return { + file, + additions: 1, + deletions: 1, + status: "modified", + patch: `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n`, + } +} diff --git a/packages/app/e2e/regression/review-terminal-stacked.spec.ts b/packages/app/e2e/regression/review-terminal-stacked.spec.ts new file mode 100644 index 000000000000..1ba8ed9474cf --- /dev/null +++ b/packages/app/e2e/regression/review-terminal-stacked.spec.ts @@ -0,0 +1,212 @@ +import { expect, test, type Page } from "@playwright/test" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectSessionTitle } from "../utils/waits" + +const directory = "C:/OpenCode/ReviewTerminalStacked" +const projectID = "proj_review_terminal_stacked" +const sessionID = "ses_review_terminal_stacked" +const title = "Review terminal stacked" +const branchDiffs = [ + fileDiff(".github/actions/setup-bun/action.yml", 7), + ...Array.from({ length: 2_739 }, (_, index) => + fileDiff(`src/branch/generated-${String(index).padStart(4, "0")}.ts`, 100), + ), +] + +test("keeps the review tree and terminal sized when both panels are open", async ({ page }) => { + test.setTimeout(120_000) + await page.setViewportSize({ width: 1400, height: 900 }) + await mockOpenCodeServer(page, { + directory, + project: { + id: projectID, + worktree: directory, + vcs: "git", + name: "review-terminal-stacked", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + }, + provider: { + all: [ + { + id: "opencode", + name: "OpenCode", + models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } }, + }, + ], + connected: ["opencode"], + default: { providerID: "opencode", modelID: "test" }, + }, + sessions: [ + { + id: sessionID, + slug: "review-terminal-stacked", + projectID, + directory, + title, + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + }, + ], + pageMessages: () => ({ items: [] }), + }) + await page.route(/\/vcs(?:\?.*)?$/, (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ branch: "review-pane-performance", default_branch: "dev" }), + }), + ) + await page.route("**/vcs/diff**", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify( + new URL(route.request().url()).searchParams.get("mode") === "branch" + ? branchDiffs + : Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)), + ), + }), + ) + await page.route("**/pty", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ id: "pty_review_terminal", title: "Terminal 1" }), + }), + ) + await page.route("**/pty/pty_review_terminal", (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), + ) + await page.routeWebSocket("**/pty/pty_review_terminal/connect", () => undefined) + await page.addInitScript(() => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + localStorage.setItem( + "opencode.global.dat:layout", + JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }), + ) + }) + + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await expectSessionTitle(page, title) + await expect(page.locator("#review-panel")).toBeVisible() + await expectTree(page, 8, "git-0.ts") + + await selectMode(page, "Git changes", "Branch changes") + await expect(page.getByRole("tab", { name: "Review 2740" })).toBeVisible() + await page.keyboard.press("Control+Backquote") + await expect(page.locator("#terminal-panel")).toBeVisible() + await expectTree(page, 2_745, "action.yml") + await expectStackGeometry(page) + + const treeViewport = page.locator('#review-panel [data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport') + await treeViewport.hover() + await page.mouse.wheel(0, 100_000) + await expect + .poll(() => treeViewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeLessThanOrEqual(1) + const lastFile = page.getByRole("button", { name: "generated-2738.ts" }) + await expect(lastFile).toBeVisible() + const bottomGap = await lastFile.evaluate((element) => { + const viewport = element.closest(".scroll-view__viewport")!.getBoundingClientRect() + return viewport.bottom - element.getBoundingClientRect().bottom + }) + expect(bottomGap).toBeGreaterThanOrEqual(0) + expect(bottomGap).toBeLessThanOrEqual(16) + await selectMode(page, "Branch changes", "Git changes") + await expectTree(page, 8, "git-0.ts") + await selectMode(page, "Git changes", "Branch changes") + await expectTree(page, 2_745, "action.yml") + + const filter = page.getByRole("searchbox", { name: "Filter files" }) + await filter.fill("generated-2738") + await expectTree(page, 1, "generated-2738.ts") + await filter.fill("") + await expectTree(page, 2_745, "action.yml") + + await page.getByRole("button", { name: "Toggle file tree" }).click() + await expect(page.locator('[data-slot="session-review-v2-sidebar"]')).toHaveAttribute("aria-hidden", "true") + await expect(page.locator('#review-panel [data-component="file-tree-v2"]')).toHaveCount(1) + await page.getByRole("button", { name: "Toggle file tree" }).click() + await expectTree(page, 2_745, "action.yml") + + await page.keyboard.press("Control+Backquote") + await expect(page.locator("#terminal-panel")).toHaveCount(0) + await expectTree(page, 2_745, "action.yml") + await page.keyboard.press("Control+Backquote") + await expect(page.locator("#terminal-panel")).toBeVisible() + await expectTree(page, 2_745, "action.yml") + + await page.getByRole("button", { name: "Toggle review" }).click() + await expect(page.locator("#review-panel")).toHaveAttribute("aria-hidden", "true") + await expect(page.locator('#review-panel [data-component="file-tree-v2"]')).toHaveCount(1) + await page.getByRole("button", { name: "Toggle review" }).click() + await expectTree(page, 2_745, "action.yml") + await page.setViewportSize({ width: 1_000, height: 700 }) + await expectTree(page, 2_745, "action.yml") + await expectStackGeometry(page) + await page.setViewportSize({ width: 1_000, height: 120 }) + await page.setViewportSize({ width: 1_400, height: 900 }) + await expectTree(page, 2_745, "action.yml") + await expectStackGeometry(page) +}) + +async function selectMode(page: Page, current: string, next: string) { + await page.getByRole("button", { name: current }).click() + const option = page.getByRole("option", { name: next }) + await expect(option).toBeVisible() + await option.click() +} + +async function expectTree(page: Page, total: number, file: string) { + await expectMountedTree(page, total) + await expect(page.getByRole("button", { name: file })).toBeVisible() +} + +async function expectMountedTree(page: Page, total: number) { + const tree = page.locator('#review-panel [data-component="file-tree-v2"]') + await expect(tree).toHaveAttribute("data-total-rows", String(total)) + await expect + .poll(() => tree.evaluate((element) => element.querySelectorAll('[data-slot="file-tree-v2-row"]').length)) + .toBeGreaterThan(0) + const state = await tree.evaluate((element) => ({ + root: element.getBoundingClientRect().height, + viewport: element.closest(".scroll-view__viewport")!.getBoundingClientRect().height, + rows: element.querySelectorAll('[data-slot="file-tree-v2-row"]').length, + })) + expect(state.viewport).toBeGreaterThan(0) + expect(state.root).toBeGreaterThan(0) + expect(state.rows).toBeGreaterThan(0) + expect(state.rows).toBeLessThanOrEqual(60) +} + +async function expectStackGeometry(page: Page) { + const geometry = await page.evaluate(() => { + const review = document.querySelector("#review-panel")! + const terminal = document.querySelector("#terminal-panel")! + const reviewParent = review.parentElement!.getBoundingClientRect() + const terminalParent = terminal.parentElement!.getBoundingClientRect() + return { + review: review.getBoundingClientRect().height, + reviewParent: reviewParent.height, + terminal: terminal.getBoundingClientRect().height, + terminalParent: terminalParent.height, + } + }) + expect(Math.abs(geometry.review - geometry.reviewParent)).toBeLessThanOrEqual(1) + expect(Math.abs(geometry.terminal - geometry.terminalParent)).toBeLessThanOrEqual(1) +} + +function base64Encode(value: string) { + return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") +} + +function fileDiff(file: string, additions: number) { + return { + file, + additions, + deletions: 0, + status: "modified", + patch: `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n`, + } +} diff --git a/packages/app/src/components/file-tree-v2-model.test.ts b/packages/app/src/components/file-tree-v2-model.test.ts new file mode 100644 index 000000000000..288f7112e929 --- /dev/null +++ b/packages/app/src/components/file-tree-v2-model.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "bun:test" +import { buildFileTreeV2Model, flattenFileTreeV2 } from "./file-tree-v2-model" + +describe("file tree v2 model", () => { + test("builds sorted depth-first rows", () => { + const model = buildFileTreeV2Model(["src/z.ts", "src/lib/b.ts", "src/lib/a.ts", "README.md", "docs/guide.md"]) + + expect(model.total).toBe(8) + expect(flattenFileTreeV2(model, () => true).map((row) => [row.node.path, row.node.type, row.level])).toEqual([ + ["docs", "directory", 0], + ["docs/guide.md", "file", 1], + ["src", "directory", 0], + ["src/lib", "directory", 1], + ["src/lib/a.ts", "file", 2], + ["src/lib/b.ts", "file", 2], + ["src/z.ts", "file", 1], + ["README.md", "file", 0], + ]) + }) + + test("omits descendants of collapsed directories", () => { + const model = buildFileTreeV2Model(["src/lib/a.ts", "src/z.ts"]) + + expect(flattenFileTreeV2(model, (path) => path !== "src/lib").map((row) => row.node.path)).toEqual([ + "src", + "src/lib", + "src/z.ts", + ]) + }) + + test("normalizes separators and duplicate paths", () => { + const model = buildFileTreeV2Model(["src\\lib\\a.ts", "src/lib/a.ts", "/src//lib/b.ts/"]) + const rows = flattenFileTreeV2(model, () => true) + + expect(model.total).toBe(4) + expect(rows.map((row) => row.node.path)).toEqual(["src", "src/lib", "src/lib/a.ts", "src/lib/b.ts"]) + expect(rows.find((row) => row.node.path === "src/lib/a.ts")?.node.originalPath).toBe("src\\lib\\a.ts") + }) + + test("supports paths deeper than the legacy recursion limit", () => { + const file = `${Array.from({ length: 130 }, (_, index) => `dir-${index}`).join("/")}/file.ts` + const model = buildFileTreeV2Model([file]) + + expect(flattenFileTreeV2(model, () => true)).toHaveLength(131) + }) +}) diff --git a/packages/app/src/components/file-tree-v2-model.ts b/packages/app/src/components/file-tree-v2-model.ts new file mode 100644 index 000000000000..2127b6800f86 --- /dev/null +++ b/packages/app/src/components/file-tree-v2-model.ts @@ -0,0 +1,77 @@ +import type { FileNode } from "@opencode-ai/sdk/v2" + +export type FileTreeV2Model = { + children: ReadonlyMap + total: number +} + +export type FileTreeV2Node = FileNode & { originalPath: string } + +export type FileTreeV2Row = { + node: FileTreeV2Node + level: number +} + +export function normalizeFileTreeV2Path(value: string) { + return value + .replaceAll("\\", "/") + .replace(/^\/+|\/+$/g, "") + .replace(/\/{2,}/g, "/") +} + +export function buildFileTreeV2Model(paths: readonly string[]): FileTreeV2Model { + const nodes = new Map() + + paths.forEach((value) => { + const file = normalizeFileTreeV2Path(value) + if (!file) return + + const parts = file.split("/") + parts.forEach((name, index) => { + const path = parts.slice(0, index + 1).join("/") + if (nodes.has(path)) return + nodes.set(path, { + name, + path, + absolute: path, + type: index === parts.length - 1 ? "file" : "directory", + ignored: false, + originalPath: index === parts.length - 1 ? value : path, + }) + }) + }) + + const children = new Map() + nodes.forEach((node) => { + const index = node.path.lastIndexOf("/") + const parent = index === -1 ? "" : node.path.slice(0, index) + const list = children.get(parent) + if (list) list.push(node) + else children.set(parent, [node]) + }) + children.forEach((nodes) => + nodes.sort((a, b) => { + if (a.type !== b.type) return a.type === "directory" ? -1 : 1 + return a.name.localeCompare(b.name) + }), + ) + + return { children, total: nodes.size } +} + +export function flattenFileTreeV2(model: FileTreeV2Model, expanded: (path: string) => boolean) { + const rows: FileTreeV2Row[] = [] + const stack = (model.children.get("") ?? []).toReversed().map((node) => ({ node, level: 0 })) + + while (stack.length > 0) { + const row = stack.pop()! + rows.push(row) + if (row.node.type !== "directory" || !expanded(row.node.path)) continue + const children = model.children.get(row.node.path) ?? [] + for (let index = children.length - 1; index >= 0; index--) { + stack.push({ node: children[index]!, level: row.level + 1 }) + } + } + + return rows +} diff --git a/packages/app/src/components/file-tree-v2.tsx b/packages/app/src/components/file-tree-v2.tsx index 302d9b58a353..c712a8044360 100644 --- a/packages/app/src/components/file-tree-v2.tsx +++ b/packages/app/src/components/file-tree-v2.tsx @@ -1,95 +1,26 @@ import { useFile } from "@/context/file" -import { Collapsible } from "@opencode-ai/ui/collapsible" import { FileIcon } from "@opencode-ai/ui/file-icon" import "@opencode-ai/ui/v2/file-tree-v2.css" import { createEffect, createMemo, + createSignal, For, - Match, - on, Show, splitProps, - Switch, - untrack, type ComponentProps, type ParentProps, } from "solid-js" import { Dynamic } from "solid-js/web" import type { FileNode } from "@opencode-ai/sdk/v2" import { Icon } from "@opencode-ai/ui/v2/icon" -import { - dirsToExpand, - pathToFileUrl, - shouldListRoot, - visibleKind, - withFileDragImage, - type Filter, - type Kind, -} from "@/components/file-tree" +import { pathToFileUrl, withFileDragImage, type Kind } from "@/components/file-tree" +import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual" +import { buildFileTreeV2Model, flattenFileTreeV2, normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" +import { virtualScrollElement } from "@/components/virtual-scroll-element" export type { Kind } from "@/components/file-tree" -const MAX_DEPTH = 128 - -function visibleNodesForPath(path: string, children: (dir: string) => FileNode[], current: Filter | undefined) { - const nodes = children(path) - if (!current) return nodes - - const parent = (item: string) => { - const idx = item.lastIndexOf("/") - if (idx === -1) return "" - return item.slice(0, idx) - } - - const leaf = (item: string) => { - const idx = item.lastIndexOf("/") - return idx === -1 ? item : item.slice(idx + 1) - } - - const out = nodes.filter((node) => { - if (node.type === "file") return current.files.has(node.path) - return current.dirs.has(node.path) - }) - - const seen = new Set(out.map((node) => node.path)) - - for (const dir of current.dirs) { - if (parent(dir) !== path) continue - if (seen.has(dir)) continue - out.push({ - name: leaf(dir), - path: dir, - absolute: dir, - type: "directory", - ignored: false, - }) - seen.add(dir) - } - - for (const item of current.files) { - if (parent(item) !== path) continue - if (seen.has(item)) continue - out.push({ - name: leaf(item), - path: item, - absolute: item, - type: "file", - ignored: false, - }) - seen.add(item) - } - - out.sort((a, b) => { - if (a.type !== b.type) { - return a.type === "directory" ? -1 : 1 - } - return a.name.localeCompare(b.name) - }) - - return out -} - const INDENT_STEP = 16 function rowPaddingLeft(level: number, type: FileNode["type"]) { @@ -123,7 +54,6 @@ const FileTreeNodeV2 = ( active?: string draggable: boolean kinds?: ReadonlyMap - marks?: Set as?: "div" | "button" }, ) => { @@ -133,18 +63,18 @@ const FileTreeNodeV2 = ( "active", "draggable", "kinds", - "marks", "as", "children", "class", "classList", ]) - const kind = () => visibleKind(local.node, local.kinds, local.marks) + const kind = () => local.kinds?.get(local.node.path) return ( + {(_, index) => ( +
+ )} + + ) +} + export default function FileTreeV2(props: { - path: string active?: string - level?: number allowed?: readonly string[] kinds?: ReadonlyMap draggable?: boolean onFileClick?: (file: FileNode) => void - - _filter?: Filter - _marks?: Set - _deeps?: Map - _kinds?: ReadonlyMap - _chain?: readonly string[] }) { const file = useFile() - const level = props.level ?? 0 const draggable = () => props.draggable ?? true - - const key = (p: string) => - file - .normalize(p) - .replace(/[\\/]+$/, "") - .replaceAll("\\", "/") - const chain = props._chain ? [...props._chain, key(props.path)] : [key(props.path)] - - const filter = createMemo(() => { - if (props._filter) return props._filter - - const allowed = props.allowed - if (!allowed) return - - const files = new Set(allowed) - const dirs = new Set() - - for (const item of allowed) { - const parts = item.split("/") - const parents = parts.slice(0, -1) - for (const [idx] of parents.entries()) { - const dir = parents.slice(0, idx + 1).join("/") - if (dir) dirs.add(dir) - } - } - - return { files, dirs } - }) - - const marks = createMemo(() => { - if (props._marks) return props._marks - - const out = new Set(props.kinds?.keys() ?? []) - if (out.size === 0) return - return out - }) - - const kinds = createMemo(() => { - if (props._kinds) return props._kinds - return props.kinds - }) - - const deeps = createMemo(() => { - if (props._deeps) return props._deeps - - const out = new Map() - - const root = props.path - if (!(file.tree.state(root)?.expanded ?? false)) return out - - const seen = new Set() - const stack: { dir: string; lvl: number; i: number; kids: string[]; max: number }[] = [] - - const push = (dir: string, lvl: number) => { - const id = key(dir) - if (seen.has(id)) return - seen.add(id) - - const kids = file.tree - .children(dir) - .filter((node) => node.type === "directory" && (file.tree.state(node.path)?.expanded ?? false)) - .map((node) => node.path) - - stack.push({ dir, lvl, i: 0, kids, max: lvl }) - } - - push(root, level - 1) - - while (stack.length > 0) { - const top = stack[stack.length - 1]! - - if (top.i < top.kids.length) { - const next = top.kids[top.i]! - top.i++ - push(next, top.lvl + 1) - continue - } - - out.set(top.dir, top.max) - stack.pop() - - const parent = stack[stack.length - 1] - if (!parent) continue - parent.max = Math.max(parent.max, top.max) - } - - return out + const active = () => normalizeFileTreeV2Path(props.active ?? "") + const model = createMemo(() => buildFileTreeV2Model(props.allowed ?? [])) + const rows = createMemo(() => flattenFileTreeV2(model(), (path) => file.tree.state(path)?.expanded ?? true)) + const [root, setRoot] = createSignal() + const [focused, setFocused] = createSignal() + const virtualizer = createVirtualizer({ + get count() { + return rows().length + }, + getScrollElement: () => virtualScrollElement(root()), + initialRect: { width: 0, height: 600 }, + estimateSize: () => 28, + gap: 2, + overscan: 10, + get getItemKey() { + const current = rows() + return (index: number) => current[index]?.node.path ?? index + }, + rangeExtractor: (range) => { + const indexes = defaultRangeExtractor(range) + const path = focused() + const index = path ? rows().findIndex((row) => row.node.path === path) : -1 + if (index < 0 || indexes.includes(index)) return indexes + return [...indexes, index].sort((a, b) => a - b) + }, }) - createEffect(() => { - const current = filter() - const dirs = dirsToExpand({ - level, - filter: current, - expanded: (dir) => untrack(() => file.tree.state(dir)?.expanded) ?? false, + const path = active() + if (!path) return + const index = rows().findIndex((row) => row.node.path === path) + if (index < 0) return + queueMicrotask(() => { + if (virtualizer.range && index >= virtualizer.range.startIndex && index <= virtualizer.range.endIndex) return + virtualizer.scrollToIndex(index, { align: "auto" }) }) - // Nodes come from the `allowed` filter; skip listing so directories that only - // exist on the diff's base branch do not each fail with an error toast. - for (const dir of dirs) file.tree.expand(dir, { list: false }) }) - - createEffect( - on( - () => props.path, - (path) => { - const dir = untrack(() => file.tree.state(path)) - if (!shouldListRoot({ level, dir })) return - void file.tree.list(path) - }, - { defer: false }, - ), + const rowByKey = createMemo(() => new Map(rows().map((row) => [row.node.path, row] as const))) + const virtualItemByKey = createMemo( + () => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)), ) - - const nodes = createMemo(() => visibleNodesForPath(props.path, file.tree.children, filter())) + const virtualRowKeys = createMemo(() => virtualizer.getVirtualItems().map((item) => item.key)) return ( - // group/file-tree-v2 scopes the group-hover guide lines below; hosts may add - // an outer group with the same name to widen the hover area. -
- - {(node) => { - const expanded = () => file.tree.state(node.path)?.expanded ?? false - const deep = () => deeps().get(node.path) ?? -1 - const hasChildren = () => visibleNodesForPath(node.path, file.tree.children, filter()).length > 0 - return ( - - - - open ? file.tree.expand(node.path, { list: false }) : file.tree.collapse(node.path) - } - > - - + + {(key) => ( + + {(item) => ( +
+ + {(row) => ( + setFocused(row().node.path)} + onBlur={() => setFocused(undefined)} + onClick={() => + props.onFileClick?.({ + ...row().node, + path: row().node.originalPath, + absolute: row().node.originalPath, + }) + } + > + + 0}> +
+ + + + + + + } > -
- -
- - - - -
- ...
} + setFocused(row().node.path)} + onBlur={() => setFocused(undefined)} + aria-expanded={file.tree.state(row().node.path)?.expanded ?? true} + onClick={() => + file.tree.state(row().node.path)?.expanded === false + ? file.tree.expand(row().node.path, { list: false }) + : file.tree.collapse(row().node.path) + } > - -
- - - - - - props.onFileClick?.(node)} - > - 0}> -
- - } - > - - - - - - - - - ) - }} + +
+ +
+ + + )} + +
+ )} +
+ )}
) diff --git a/packages/app/src/components/virtual-scroll-element.test.ts b/packages/app/src/components/virtual-scroll-element.test.ts new file mode 100644 index 000000000000..20c25a8561a0 --- /dev/null +++ b/packages/app/src/components/virtual-scroll-element.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from "bun:test" +import { virtualScrollElement } from "./virtual-scroll-element" + +test("resolves the connected viewport that owns the virtual root", () => { + const stale = document.createElement("div") + stale.className = "scroll-view__viewport" + const viewport = document.createElement("div") + viewport.className = "scroll-view__viewport" + const root = document.createElement("div") + viewport.append(root) + document.body.append(viewport) + + expect(virtualScrollElement(root)).toBe(viewport) + expect(virtualScrollElement(root)).not.toBe(stale) + + viewport.remove() + expect(virtualScrollElement(root)).toBeNull() +}) diff --git a/packages/app/src/components/virtual-scroll-element.ts b/packages/app/src/components/virtual-scroll-element.ts new file mode 100644 index 000000000000..8708781d86a7 --- /dev/null +++ b/packages/app/src/components/virtual-scroll-element.ts @@ -0,0 +1,4 @@ +export function virtualScrollElement(root: HTMLElement | undefined) { + if (!root?.isConnected) return null + return root.closest(".scroll-view__viewport") +} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 8b422f713edc..46bd907167a4 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -2157,6 +2157,7 @@ export default function Page() { diffsReady={reviewReady} empty={reviewEmptyText} hasReview={hasReview} + reviewHasFocusableContent={hasReview} reviewCount={reviewCount} reviewPanel={reviewPanel} activeDiff={tree.activeDiff} @@ -2168,14 +2169,20 @@ export default function Page() {
- -
+ +
hasReview() || reviewV2State.sidebarOpened()} reviewCount={reviewCount} reviewPanel={reviewPanelV2} activeDiff={tree.activeDiff} @@ -2204,7 +2211,12 @@ export default function Page() {
-
+
diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 690262102039..3f44aba48832 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -14,6 +14,9 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import FileTree from "@/components/file-tree" import { SessionContextUsage } from "@/components/session-context-usage" + +const reviewTabID = "session-side-panel-review-tab" +const reviewTabPanelID = "session-side-panel-review-tabpanel" import { SessionContextTab, SortableTab, FileVisual } from "@/components/session" import { useCommand } from "@/context/command" import { useFile, type SelectedLineRange } from "@/context/file" @@ -45,6 +48,7 @@ export function SessionSidePanel(props: { diffsReady: () => boolean empty: () => string hasReview: () => boolean + reviewHasFocusableContent: () => boolean reviewCount: () => number reviewPanel: () => JSX.Element activeDiff?: string @@ -75,6 +79,7 @@ export function SessionSidePanel(props: { }), ) const open = createMemo(() => reviewOpen() || fileOpen()) + const rendered = createMemo((previous) => previous || open(), false) const reviewTab = createMemo(() => isDesktop()) const panelWidth = createMemo(() => { if (!open()) return "0px" @@ -155,6 +160,10 @@ export function SessionSidePanel(props: { const openedTabs = tabState.openedTabs const activeTab = tabState.activeTab const activeFileTab = tabState.activeFileTab + const reviewContentRendered = createMemo( + (previous) => previous || (reviewOpen() && activeTab() === "review"), + false, + ) const fileTreeTab = () => layout.fileTree.tab() @@ -223,7 +232,7 @@ export function SessionSidePanel(props: { class="relative min-w-0 flex overflow-hidden bg-background-base" classList={{ "h-full shrink-0": !props.stacked, - "min-h-0 flex-1": props.stacked, + "h-full min-h-0": props.stacked, "pointer-events-none": !open(), "transition-[width] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none": !props.size.active() && !props.reviewSnap, @@ -232,7 +241,7 @@ export function SessionSidePanel(props: { }} style={{ width: panelWidth() }} > - +
- +
{language.t("session.tab.review")}
@@ -328,10 +341,20 @@ export function SessionSidePanel(props: {
- - - {props.reviewPanel()} - + +
+ {props.reviewPanel()} +
diff --git a/packages/app/src/pages/session/v2/review-diff-kinds.test.ts b/packages/app/src/pages/session/v2/review-diff-kinds.test.ts index b5a26cb32147..ee37a2c8b4c9 100644 --- a/packages/app/src/pages/session/v2/review-diff-kinds.test.ts +++ b/packages/app/src/pages/session/v2/review-diff-kinds.test.ts @@ -12,6 +12,13 @@ describe("reviewDiffKinds", () => { expect(kinds.get("src/b.ts")).toBe("del") expect(kinds.get("src")).toBe("mix") }) + + test("normalizes file and directory paths", () => { + const kinds = reviewDiffKinds([{ file: "\\src//lib/a.ts/", additions: 1, deletions: 1, status: "modified" }]) + + expect(kinds.get("src/lib/a.ts")).toBe("mix") + expect(kinds.get("src/lib")).toBe("mix") + }) }) describe("filterReviewFiles", () => { diff --git a/packages/app/src/pages/session/v2/review-diff-kinds.ts b/packages/app/src/pages/session/v2/review-diff-kinds.ts index 4d22d9a67a5c..c288c357068d 100644 --- a/packages/app/src/pages/session/v2/review-diff-kinds.ts +++ b/packages/app/src/pages/session/v2/review-diff-kinds.ts @@ -1,10 +1,11 @@ import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import type { Kind } from "@/components/file-tree-v2" +import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" export type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff export function normalizePath(p: string) { - return p.replaceAll("\\", "/").replace(/\/+$/, "") + return normalizeFileTreeV2Path(p) } export function filterRenderableDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff { diff --git a/packages/app/src/pages/session/v2/review-panel-v2.tsx b/packages/app/src/pages/session/v2/review-panel-v2.tsx index 4b8e6359c8c6..fcd52756ab08 100644 --- a/packages/app/src/pages/session/v2/review-panel-v2.tsx +++ b/packages/app/src/pages/session/v2/review-panel-v2.tsx @@ -203,7 +203,6 @@ function ReviewPanelV2Sidebar(props: { when={props.searching()} fallback={ normalizePath(props.active ?? "") const highlighted = () => normalizePath(props.highlighted ?? "") - let rootRef: HTMLDivElement | undefined + const normalized = createMemo(() => props.files.map(normalizePath)) + const [root, setRoot] = createSignal() + const [focused, setFocused] = createSignal() + const virtualizer = createVirtualizer({ + get count() { + return props.files.length + }, + getScrollElement: () => virtualScrollElement(root()), + initialRect: { width: 0, height: 600 }, + estimateSize: () => 28, + gap: 2, + overscan: 10, + get getItemKey() { + const files = props.files + return (index: number) => files[index] ?? index + }, + rangeExtractor: (range) => { + const indexes = defaultRangeExtractor(range) + const path = focused() + const index = path ? props.files.indexOf(path) : -1 + if (index < 0 || indexes.includes(index)) return indexes + return [...indexes, index].sort((a, b) => a - b) + }, + }) createEffect(() => { - highlighted() - if (!rootRef) return + const index = normalized().indexOf(highlighted()) + if (index < 0) return queueMicrotask(() => { - const row = rootRef?.querySelector('[data-slot="file-tree-v2-row"][data-highlighted]') - row?.scrollIntoView({ block: "nearest" }) + if (virtualizer.range && index >= virtualizer.range.startIndex && index <= virtualizer.range.endIndex) return + virtualizer.scrollToIndex(index, { align: "auto" }) }) }) + const virtualItemByKey = createMemo( + () => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)), + ) + const virtualRowKeys = createMemo(() => virtualizer.getVirtualItems().map((item) => item.key)) return (
{ - rootRef = el - }} + ref={setRoot} data-component="file-tree-v2" + data-total-rows={props.files.length} + style={{ position: "relative", height: `${virtualizer.getTotalSize()}px` }} > - - {(path) => { - const normalized = normalizePath(path) - const selected = () => { - if (highlighted()) return highlighted() === normalized - return active() === normalized - } - const highlightedRow = () => highlighted() === normalized - const kind = () => props.kinds?.get(normalized) - const directory = () => (normalized.includes("/") ? getDirectory(normalized) : undefined) - const filename = () => getFilename(normalized) + + {(key) => { + const path = key as string + const value = normalizePath(path) + const selected = () => (highlighted() ? highlighted() === value : active() === value) + const highlightedRow = () => highlighted() === value + const kind = () => props.kinds?.get(value) + const directory = () => (value.includes("/") ? getDirectory(value) : undefined) + const filename = () => getFilename(value) return ( - + + {(item) => ( +
+ +
+ )} +
) }}
diff --git a/packages/app/test-browser/solid-virtual.test.ts b/packages/app/test-browser/solid-virtual.test.ts index 727248d606e8..716fa7fa9ea7 100644 --- a/packages/app/test-browser/solid-virtual.test.ts +++ b/packages/app/test-browser/solid-virtual.test.ts @@ -27,6 +27,21 @@ test("reactive count updates preserve measured row sizes", () => { }) }) +test("initial rect projects rows before a scroll element connects", () => { + createRoot((dispose) => { + const virtualizer = createVirtualizer({ + count: 100, + getScrollElement: () => null, + estimateSize: () => 28, + initialRect: { width: 0, height: 600 }, + overscan: 10, + }) + + expect(virtualizer.getVirtualItems().length).toBeGreaterThan(0) + dispose() + }) +}) + test("logical scroll offset includes pending measurement adjustments", () => { createRoot((dispose) => { const virtualizer = createVirtualizer({ diff --git a/packages/session-ui/src/v2/components/session-review-file-preview-v2-virtualize.test.ts b/packages/session-ui/src/v2/components/session-review-file-preview-v2-virtualize.test.ts new file mode 100644 index 000000000000..3bbf173c8bbd --- /dev/null +++ b/packages/session-ui/src/v2/components/session-review-file-preview-v2-virtualize.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "bun:test" +import { shouldVirtualizeReviewDiff } from "./session-review-file-preview-v2-virtualize" + +describe("shouldVirtualizeReviewDiff", () => { + test("renders small diffs directly", () => { + expect(shouldVirtualizeReviewDiff({ additionLines: 500, deletionLines: 500 })).toBe(false) + }) + + test("virtualizes large diffs", () => { + expect(shouldVirtualizeReviewDiff({ additionLines: 501, deletionLines: 1 })).toBe(true) + expect(shouldVirtualizeReviewDiff({ additionLines: 1, deletionLines: 501 })).toBe(true) + }) +}) diff --git a/packages/session-ui/src/v2/components/session-review-file-preview-v2-virtualize.ts b/packages/session-ui/src/v2/components/session-review-file-preview-v2-virtualize.ts new file mode 100644 index 000000000000..7795a859619f --- /dev/null +++ b/packages/session-ui/src/v2/components/session-review-file-preview-v2-virtualize.ts @@ -0,0 +1,5 @@ +const lineThreshold = 500 + +export function shouldVirtualizeReviewDiff(input: { additionLines: number; deletionLines: number }) { + return Math.max(input.additionLines, input.deletionLines) > lineThreshold +} diff --git a/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx b/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx index c2607971dff1..3e7bd830519e 100644 --- a/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx +++ b/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx @@ -22,6 +22,7 @@ import type { } from "../../components/session-review" import type { SessionReviewExpandMode } from "./session-review-v2" import { createLineCommentControllerV2 } from "./line-comment-annotations-v2" +import { shouldVirtualizeReviewDiff } from "./session-review-file-preview-v2-virtualize" import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import "./session-review-v2.css" @@ -219,6 +220,10 @@ export function SessionReviewFilePreviewV2(props: SessionReviewFilePreviewV2Prop preloadedDiff={view().preloaded} diffStyle={props.diffStyle} expandUnchanged={expandUnchanged()} + virtualize={shouldVirtualizeReviewDiff({ + additionLines: view().fileDiff.additionLines.length, + deletionLines: view().fileDiff.deletionLines.length, + })} hunkSeparators={view().fileDiff.isPartial ? "simple" : "line-info-basic"} enableLineSelection={lineCommentsEnabled()} enableGutterUtility={lineCommentsEnabled()} diff --git a/packages/session-ui/src/v2/components/session-review-v2.tsx b/packages/session-ui/src/v2/components/session-review-v2.tsx index 34ee36532ab5..176dce4fc983 100644 --- a/packages/session-ui/src/v2/components/session-review-v2.tsx +++ b/packages/session-ui/src/v2/components/session-review-v2.tsx @@ -11,6 +11,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { ScrollView } from "@opencode-ai/ui/scroll-view" import { makeEventListener } from "@solid-primitives/event-listener" import { Show, createEffect, createMemo, createSignal, type JSX } from "solid-js" +import { getWorkerPool } from "../../pierre/worker" import "./session-review-v2.css" export const SESSION_REVIEW_V2_SIDEBAR_WIDTH_DEFAULT = 240 @@ -48,6 +49,7 @@ export type SessionReviewV2SidebarProps = { onWidthChange?: (width: number) => void minWidth?: number maxWidth?: number + viewportRef?: (element: HTMLDivElement) => void children?: JSX.Element } @@ -74,44 +76,47 @@ export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) { inert={!props.open} style={{ width: props.open ? `${width()}px` : "0px" }} > - -
-
{props.title}
- {props.stats} -
-
- props.onFilterChange(event.currentTarget.value)} - onKeyDown={props.onFilterKeyDown} - showClearButton={props.filter.length > 0} - clearLabel={i18n.t("ui.list.clearFilter")} - onClearClick={() => props.onFilterChange("")} - placeholder={i18n.t("ui.sessionReviewV2.filterFiles")} - aria-label={i18n.t("ui.sessionReviewV2.filterFiles")} - leadingIcon={ - - } - /> -
- - {props.children} - -
+
+
{props.title}
+ {props.stats} +
+
+ props.onFilterChange(event.currentTarget.value)} + onKeyDown={props.onFilterKeyDown} + showClearButton={props.filter.length > 0} + clearLabel={i18n.t("ui.list.clearFilter")} + onClearClick={() => props.onFilterChange("")} + placeholder={i18n.t("ui.sessionReviewV2.filterFiles")} + aria-label={i18n.t("ui.sessionReviewV2.filterFiles")} + leadingIcon={ + + } + /> +
+ + {props.children} +
setResizing(true)}> @@ -131,6 +136,10 @@ export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) { export function SessionReviewV2(props: SessionReviewV2Props) { const i18n = useI18n() + createEffect(() => { + getWorkerPool(props.diffStyle) + }) + const fileIndex = () => { const files = props.files if (files.length === 0) return -1 From 38bb38ecb2b50f81c9dd8e943288a5eaebb180df Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:50:08 +0800 Subject: [PATCH 23/34] refactor(app): unify provider connect dialog (#35518) --- .../components/dialog-connect-provider.tsx | 281 ++++++++++++---- .../src/components/dialog-custom-provider.tsx | 318 +++++++++--------- .../src/components/dialog-manage-models.tsx | 6 +- .../components/dialog-select-model-unpaid.tsx | 13 +- .../src/components/dialog-select-model.tsx | 8 +- .../src/components/dialog-select-provider.tsx | 97 ------ .../app/src/components/dialog-settings.tsx | 17 +- .../app/src/components/settings-providers.tsx | 33 +- .../settings-v2/dialog-settings-v2.tsx | 16 +- .../src/components/settings-v2/providers.tsx | 33 +- packages/app/src/pages/layout.tsx | 4 +- .../pages/session/usage-exceeded-dialogs.tsx | 17 +- 12 files changed, 441 insertions(+), 402 deletions(-) delete mode 100644 packages/app/src/components/dialog-select-provider.tsx diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index c94cca505351..6499642b3896 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -7,20 +7,167 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { List, type ListRef } from "@opencode-ai/ui/list" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Spinner } from "@opencode-ai/ui/spinner" +import { Tag } from "@opencode-ai/ui/tag" import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@/utils/toast" -import { type Accessor, createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js" +import { + type Accessor, + type Component, + createEffect, + createMemo, + createResource, + Match, + onCleanup, + onMount, + Show, + Switch, +} from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" -import { useProviders } from "@/hooks/use-providers" +import { popularProviders, useProviders } from "@/hooks/use-providers" +import { CustomProviderForm } from "./dialog-custom-provider" -export function DialogConnectProvider(props: { +const CUSTOM_ID = "_custom" + +export function useProviderConnectController(options: { onBack?: () => void } = {}) { + const [store, setStore] = createStore({ selected: undefined as string | undefined }) + const reset = () => setStore("selected", undefined) + + return { + selected: () => store.selected, + select: (provider?: string) => setStore("selected", provider), + back: options.onBack ?? reset, + } +} + +export const DialogConnectProvider: Component<{ + directory?: Accessor + controller?: ReturnType +}> = (props) => { + const fallback = useProviderConnectController() + const controller = props.controller ?? fallback + const language = useLanguage() + const reset = controller.back + const back = { current: reset } + const select = (provider?: string) => { + back.current = reset + controller.select(provider) + } + + return ( + + back.current()} + aria-label={language.t("common.goBack")} + /> + + } + > + + + + + + {(provider) => ( + (back.current = handler)} + /> + )} + + + + + + + ) +} + +function ProviderPicker(props: { directory?: Accessor; onSelect: (provider: string) => void }) { + const providers = useProviders(props.directory) + const language = useLanguage() + const popularGroup = () => language.t("dialog.provider.group.popular") + const otherGroup = () => language.t("dialog.provider.group.other") + const customLabel = () => language.t("settings.providers.tag.custom") + const note = (id: string) => { + if (id === "anthropic") return language.t("dialog.provider.anthropic.note") + if (id === "openai") return language.t("dialog.provider.openai.note") + if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note") + if (id === "opencode-go") return language.t("dialog.provider.opencodeGo.tagline") + return undefined + } + + return ( + x?.id} + items={() => { + language.locale() + return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()] + }} + filterKeys={["id", "name"]} + groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} + sortBy={(a, b) => { + if (a.id === CUSTOM_ID) return -1 + if (b.id === CUSTOM_ID) return 1 + if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) + return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) + return a.name.localeCompare(b.name) + }} + sortGroupsBy={(a, b) => { + const popular = popularGroup() + if (a.category === popular && b.category !== popular) return -1 + if (b.category === popular && a.category !== popular) return 1 + return 0 + }} + onSelect={(x) => { + if (!x) return + props.onSelect(x.id) + }} + > + {(i) => ( +
+ + {i.name} + +
{language.t("dialog.provider.opencode.tagline")}
+
+ + {language.t("settings.providers.tag.custom")} + + + {language.t("dialog.provider.tag.recommended")} + + {(value) =>
{value()}
}
+ + {language.t("dialog.provider.tag.recommended")} + +
+ )} +
+ ) +} + +function ProviderConnection(props: { provider: string directory?: Accessor onBack: () => void + setBack: (handler: () => void) => void }) { const dialog = useDialog() const serverSync = useServerSync() @@ -369,6 +516,8 @@ export function DialogConnectProvider(props: { props.onBack() } + props.setBack(goBack) + function MethodSelection() { return ( <> @@ -589,81 +738,67 @@ export function DialogConnectProvider(props: { } return ( - - } - transition - > -
-
- -
- - - {language.t("provider.connect.title.anthropicProMax")} - - {language.t("provider.connect.title", { provider: provider().name })} - -
+
+
+ +
+ + + {language.t("provider.connect.title.anthropicProMax")} + + {language.t("provider.connect.title", { provider: provider().name })} +
-
-
- - -
-
- - {language.t("provider.connect.status.inProgress")} -
+
+
+
+ + +
+
+ + {language.t("provider.connect.status.inProgress")}
- - - - - -
-
- - {language.t("provider.connect.status.inProgress")} -
+
+
+ + + + +
+
+ + {language.t("provider.connect.status.inProgress")}
- - - - - -
-
- - {language.t("provider.connect.status.failed", { error: store.error ?? "" })} -
+
+
+ + + + +
+
+ + {language.t("provider.connect.status.failed", { error: store.error ?? "" })}
- - - - - - - - - - - - - - - -
+
+
+ + + + + + + + + + + + + +
-
+
) } diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index e8cae859e06a..363a2e390a49 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -19,6 +19,28 @@ type Props = { } export function DialogCustomProvider(props: Props) { + const language = useLanguage() + + return ( + + } + transition + > + + + ) +} + +export function CustomProviderForm() { const dialog = useDialog() const serverSync = useServerSync() const serverSDK = useServerSDK() @@ -153,169 +175,155 @@ export function DialogCustomProvider(props: Props) { } return ( - - } - transition - > -
-
- -
{language.t("provider.custom.title")}
-
+
+
+ +
{language.t("provider.custom.title")}
+
-
-

- {language.t("provider.custom.description.prefix")} - - {language.t("provider.custom.description.link")} - - {language.t("provider.custom.description.suffix")} -

+ +

+ {language.t("provider.custom.description.prefix")} + + {language.t("provider.custom.description.link")} + + {language.t("provider.custom.description.suffix")} +

-
- setField("providerID", v)} - validationState={form.err.providerID ? "invalid" : undefined} - error={form.err.providerID} - /> - setField("name", v)} - validationState={form.err.name ? "invalid" : undefined} - error={form.err.name} - /> - setField("baseURL", v)} - validationState={form.err.baseURL ? "invalid" : undefined} - error={form.err.baseURL} - /> - setField("apiKey", v)} - /> -
+
+ setField("providerID", v)} + validationState={form.err.providerID ? "invalid" : undefined} + error={form.err.providerID} + /> + setField("name", v)} + validationState={form.err.name ? "invalid" : undefined} + error={form.err.name} + /> + setField("baseURL", v)} + validationState={form.err.baseURL ? "invalid" : undefined} + error={form.err.baseURL} + /> + setField("apiKey", v)} + /> +
-
- - - {(m, i) => ( -
-
- setModel(i(), "id", v)} - validationState={m.err.id ? "invalid" : undefined} - error={m.err.id} - /> -
-
- setModel(i(), "name", v)} - validationState={m.err.name ? "invalid" : undefined} - error={m.err.name} - /> -
- removeModel(i())} - disabled={form.models.length <= 1} - aria-label={language.t("provider.custom.models.remove")} +
+ + + {(m, i) => ( +
+
+ setModel(i(), "id", v)} + validationState={m.err.id ? "invalid" : undefined} + error={m.err.id} />
- )} - - -
- -
- - - {(h, i) => ( -
-
- setHeader(i(), "key", v)} - validationState={h.err.key ? "invalid" : undefined} - error={h.err.key} - /> -
-
- setHeader(i(), "value", v)} - validationState={h.err.value ? "invalid" : undefined} - error={h.err.value} - /> -
- removeHeader(i())} - disabled={form.headers.length <= 1} - aria-label={language.t("provider.custom.headers.remove")} +
+ setModel(i(), "name", v)} + validationState={m.err.name ? "invalid" : undefined} + error={m.err.name} />
- )} - - -
+ removeModel(i())} + disabled={form.models.length <= 1} + aria-label={language.t("provider.custom.models.remove")} + /> +
+ )} +
+ +
- - -
-
+
+ + + +
) } diff --git a/packages/app/src/components/dialog-manage-models.tsx b/packages/app/src/components/dialog-manage-models.tsx index 0238f543c41f..d6c5e9918670 100644 --- a/packages/app/src/components/dialog-manage-models.tsx +++ b/packages/app/src/components/dialog-manage-models.tsx @@ -16,7 +16,7 @@ import { useLocal } from "@/context/local" import { popularProviders } from "@/hooks/use-providers" import { useLanguage } from "@/context/language" import { useDialog } from "@opencode-ai/ui/context/dialog" -import { DialogSelectProvider } from "./dialog-select-provider" +import { DialogConnectProvider } from "./dialog-connect-provider" import { decode64 } from "@/utils/base64" import { SettingsListV2 } from "./settings-v2/parts/list" import { SettingsRowV2 } from "./settings-v2/parts/row" @@ -31,7 +31,7 @@ export const DialogManageModels: Component = () => { const directory = () => decode64(local.slug()) const handleConnectProvider = () => { - dialog.show(() => ) + void dialog.show(() => ) } const providerRank = (id: string) => popularProviders.indexOf(id) const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID) @@ -123,7 +123,7 @@ export const DialogManageModelsV2: Component = () => { const directory = () => decode64(local.slug()) const handleConnectProvider = () => { - dialog.show(() => ) + void dialog.show(() => ) } const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID) const providerVisible = (providerID: string) => diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index 0a32ad48b36d..4611a36c950e 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -22,17 +22,16 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props const providers = useProviders(directory) const language = useLanguage() - const connect = (provider: string) => { + const openProviders = (provider?: string) => { void import("./dialog-connect-provider").then((x) => { - dialog.show(() => ) + const controller = x.useProviderConnectController() + controller.select(provider) + void dialog.show(() => ) }) } - const all = () => { - void import("./dialog-select-provider").then((x) => { - dialog.show(() => ) - }) - } + const connect = (provider: string) => openProviders(provider) + const all = () => openProviders() let listRef: ListRef | undefined const handleKeyDown = (e: KeyboardEvent) => { diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index f026f371eac0..cea5f34d0d9c 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -155,8 +155,8 @@ export function ModelSelectorPopover(props: { const handleConnectProvider = () => { close("provider") - void import("./dialog-select-provider").then((x) => { - dialog.show(() => ) + void import("./dialog-connect-provider").then((x) => { + void dialog.show(() => ) }) } const language = useLanguage() @@ -503,8 +503,8 @@ export const DialogSelectModel: Component<{ provider?: string; model?: ModelStat const directory = () => decode64(local.slug()) const provider = () => { - void import("./dialog-select-provider").then((x) => { - dialog.show(() => ) + void import("./dialog-connect-provider").then((x) => { + void dialog.show(() => ) }) } diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx deleted file mode 100644 index e1facf48219a..000000000000 --- a/packages/app/src/components/dialog-select-provider.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { type Accessor, Component, Match, Show, Switch } from "solid-js" -import { createStore } from "solid-js/store" -import { popularProviders, useProviders } from "@/hooks/use-providers" -import { Dialog } from "@opencode-ai/ui/dialog" -import { List } from "@opencode-ai/ui/list" -import { Tag } from "@opencode-ai/ui/tag" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { DialogConnectProvider } from "./dialog-connect-provider" -import { useLanguage } from "@/context/language" -import { DialogCustomProvider } from "./dialog-custom-provider" - -const CUSTOM_ID = "_custom" - -export const DialogSelectProvider: Component<{ directory?: Accessor }> = (props) => { - const providers = useProviders(props.directory) - const language = useLanguage() - const [store, setStore] = createStore({ selected: undefined as string | undefined }) - - function showPicker() { - setStore("selected", undefined) - } - - const popularGroup = () => language.t("dialog.provider.group.popular") - const otherGroup = () => language.t("dialog.provider.group.other") - const customLabel = () => language.t("settings.providers.tag.custom") - const note = (id: string) => { - if (id === "anthropic") return language.t("dialog.provider.anthropic.note") - if (id === "openai") return language.t("dialog.provider.openai.note") - if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note") - if (id === "opencode-go") return language.t("dialog.provider.opencodeGo.tagline") - } - - return ( - - - - - - {(provider) => } - - - - x?.id} - items={() => { - language.locale() - return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()] - }} - filterKeys={["id", "name"]} - groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} - sortBy={(a, b) => { - if (a.id === CUSTOM_ID) return -1 - if (b.id === CUSTOM_ID) return 1 - if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) - return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) - return a.name.localeCompare(b.name) - }} - sortGroupsBy={(a, b) => { - const popular = popularGroup() - if (a.category === popular && b.category !== popular) return -1 - if (b.category === popular && a.category !== popular) return 1 - return 0 - }} - onSelect={(x) => { - if (!x) return - setStore("selected", x.id) - }} - > - {(i) => ( -
- - {i.name} - -
{language.t("dialog.provider.opencode.tagline")}
-
- - {language.t("settings.providers.tag.custom")} - - - {language.t("dialog.provider.tag.recommended")} - - {(value) =>
{value()}
}
- - {language.t("dialog.provider.tag.recommended")} - -
- )} -
-
-
-
- ) -} diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index 20d71f4bfd44..231237eb8b91 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -4,19 +4,30 @@ import { Tabs } from "@opencode-ai/ui/tabs" import { Icon } from "@opencode-ai/ui/icon" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" +import { useDialog } from "@opencode-ai/ui/context/dialog" import { SettingsGeneral } from "./settings-general" import { SettingsKeybinds } from "./settings-keybinds" import { SettingsProviders } from "./settings-providers" import { SettingsModels } from "./settings-models" import { SettingsServers } from "./settings-servers" -export const DialogSettings: Component = () => { +export const DialogSettings: Component<{ defaultValue?: string }> = (props) => { const language = useLanguage() const platform = usePlatform() + const dialog = useDialog() + + const showProviders = () => { + void dialog.show(() => ) + } return ( - +
@@ -70,7 +81,7 @@ export const DialogSettings: Component = () => { - + diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 9791fc21fcae..bcd30edbc7de 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -8,8 +8,7 @@ import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" -import { DialogConnectProvider } from "./dialog-connect-provider" -import { DialogSelectProvider } from "./dialog-select-provider" +import { DialogConnectProvider, useProviderConnectController } from "./dialog-connect-provider" import { DialogCustomProvider } from "./dialog-custom-provider" import { SettingsList } from "./settings-list" import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker" @@ -28,20 +27,26 @@ const PROVIDER_NOTES = [ { match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" }, ] as const -export const SettingsProviders: Component = () => { +export const SettingsProviders: Component<{ onBack?: () => void }> = (props) => { return ( - + ) } -const SettingsProvidersContent: Component = () => { +const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => { const dialog = useDialog() const language = useLanguage() const serverSDK = useServerSDK() const serverSync = useServerSync() const providers = useProviders() + const providerConnect = useProviderConnectController({ onBack: props.onBack }) + + const connect = (provider?: string) => { + providerConnect.select(provider) + void dialog.show(() => ) + } const connected = createMemo(() => { return providers @@ -206,19 +211,7 @@ const SettingsProvidersContent: Component = () => { {(key) => {language.t(key())}}
-
@@ -255,9 +248,7 @@ const SettingsProvidersContent: Component = () => { diff --git a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx index a93a10dcb2f6..aee4fc9896ff 100644 --- a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx +++ b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx @@ -10,16 +10,28 @@ import { SettingsProvidersV2 } from "./providers" import { SettingsModelsV2 } from "./models" import "./settings-v2.css" import { SettingsServersV2 } from "./servers" +import { useDialog } from "@opencode-ai/ui/context/dialog" export const DialogSettings: Component<{ sessionID?: string + defaultValue?: string }> = (props) => { const language = useLanguage() const platform = usePlatform() + const dialog = useDialog() + + const showProviders = () => { + void dialog.show(() => ) + } return ( - +
@@ -73,7 +85,7 @@ export const DialogSettings: Component<{ - + diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index 99a4101f9a35..f945fa33c643 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -8,8 +8,7 @@ import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" -import { DialogConnectProvider } from "../dialog-connect-provider" -import { DialogSelectProvider } from "../dialog-select-provider" +import { DialogConnectProvider, useProviderConnectController } from "../dialog-connect-provider" import { DialogCustomProvider } from "../dialog-custom-provider" import { SettingsListV2 } from "./parts/list" import "./settings-v2.css" @@ -30,12 +29,18 @@ const PROVIDER_NOTES = [ const PROVIDER_ICON_SIZE = 16 -export const SettingsProvidersV2: Component = () => { +export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) => { const dialog = useDialog() const language = useLanguage() const serverSdk = useServerSDK() const serverSync = useServerSync() const providers = useProviders() + const providerConnect = useProviderConnectController({ onBack: props.onBack }) + + const connect = (provider?: string) => { + providerConnect.select(provider) + void dialog.show(() => ) + } const connected = createMemo(() => { return providers @@ -206,19 +211,7 @@ export const SettingsProvidersV2: Component = () => {
- { - dialog.show(() => ( - dialog.show(() => )} - /> - )) - }} - > + connect(item.id)}> {language.t("common.connect")}
@@ -254,13 +247,7 @@ export const SettingsProvidersV2: Component = () => {
-
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 690460d3af6e..fd9d16b90ad6 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1095,9 +1095,9 @@ export default function LegacyLayout(props: ParentProps) { function connectProvider() { const run = ++dialogRun - void import("@/components/dialog-select-provider").then((x) => { + void import("@/components/dialog-connect-provider").then((x) => { if (dialogDead || dialogRun !== run) return - dialog.show(() => ) + void dialog.show(() => ) }) } diff --git a/packages/app/src/pages/session/usage-exceeded-dialogs.tsx b/packages/app/src/pages/session/usage-exceeded-dialogs.tsx index 5c77e7bcecc0..d56fa3d1f48c 100644 --- a/packages/app/src/pages/session/usage-exceeded-dialogs.tsx +++ b/packages/app/src/pages/session/usage-exceeded-dialogs.tsx @@ -76,18 +76,11 @@ export function useUsageExceededDialogs() { setGoUpsellState(keys.lastSeenAt, Date.now()) if (dontShowAgain) setGoUpsellState(keys.dontShow, Date.now()) else { - void import("../../components/dialog-connect-provider").then((x) => - dialog.show(() => ( - { - void import("../../components/dialog-select-provider").then((provider) => { - dialog.show(() => ) - }) - }} - /> - )), - ) + void import("../../components/dialog-connect-provider").then((x) => { + const controller = x.useProviderConnectController() + controller.select("opencode-go") + void dialog.show(() => ) + }) } }} /> From 377d5d22872cf18bcf98caabd04db5778399f27b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:12:30 +0800 Subject: [PATCH 24/34] fix(app): avoid shortcut settings flash (#35349) Co-authored-by: Jay <53023+jayair@users.noreply.github.com> --- packages/app/src/components/dialog-settings.tsx | 6 ++++-- packages/app/src/components/settings-keybinds.tsx | 14 ++++---------- .../components/settings-v2/dialog-settings-v2.tsx | 6 ++++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index 231237eb8b91..b554fa79a822 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -1,4 +1,4 @@ -import { Component } from "solid-js" +import { Component, createSignal, startTransition } from "solid-js" import { Dialog } from "@opencode-ai/ui/dialog" import { Tabs } from "@opencode-ai/ui/tabs" import { Icon } from "@opencode-ai/ui/icon" @@ -15,6 +15,7 @@ export const DialogSettings: Component<{ defaultValue?: string }> = (props) => { const language = useLanguage() const platform = usePlatform() const dialog = useDialog() + const [tab, setTab] = createSignal(props.defaultValue ?? "general") const showProviders = () => { void dialog.show(() => ) @@ -25,7 +26,8 @@ export const DialogSettings: Component<{ defaultValue?: string }> = (props) => { void startTransition(() => setTab(value))} class="h-full settings-dialog" > diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index 98f6c9ffa04c..3e3db45bcf5f 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -5,24 +5,18 @@ import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { TextField } from "@opencode-ai/ui/text-field" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" import { showToast } from "@/utils/toast" import fuzzysort from "fuzzysort" import { formatKeybind, parseKeybind, useCommand } from "@/context/command" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { SettingsList } from "./settings-list" +import { SettingsListV2 } from "./settings-v2/parts/list" -const ButtonV2 = lazy(() => import("@opencode-ai/ui/v2/button-v2").then((module) => ({ default: module.ButtonV2 }))) const IconV2 = lazy(() => import("@opencode-ai/ui/v2/icon").then((module) => ({ default: module.Icon }))) -const IconButtonV2 = lazy(() => - import("@opencode-ai/ui/v2/icon-button-v2").then((module) => ({ default: module.IconButtonV2 })), -) -const TextInputV2 = lazy(() => - import("@opencode-ai/ui/v2/text-input-v2").then((module) => ({ default: module.TextInputV2 })), -) -const SettingsListV2 = lazy(() => - import("./settings-v2/parts/list").then((module) => ({ default: module.SettingsListV2 })), -) const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) const PALETTE_ID = "command.palette" diff --git a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx index aee4fc9896ff..af24a47274fa 100644 --- a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx +++ b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx @@ -1,4 +1,4 @@ -import { Component } from "solid-js" +import { Component, createSignal, startTransition } from "solid-js" import { Dialog } from "@opencode-ai/ui/v2/dialog-v2" import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2" import { Icon } from "@opencode-ai/ui/icon" @@ -19,6 +19,7 @@ export const DialogSettings: Component<{ const language = useLanguage() const platform = usePlatform() const dialog = useDialog() + const [tab, setTab] = createSignal(props.defaultValue ?? "general") const showProviders = () => { void dialog.show(() => ) @@ -29,7 +30,8 @@ export const DialogSettings: Component<{ void startTransition(() => setTab(value))} class="settings-v2" > From 7f57d2a9acabc5a0425c93e4e217515076037599 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:12:53 +0800 Subject: [PATCH 25/34] feat(app): show draft server status in titlebar (#35521) --- .../src/components/session/session-header.tsx | 4 ++-- packages/app/src/components/titlebar.tsx | 8 +++++++- packages/app/src/pages/new-session.tsx | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 4ec225c94225..500cce7def13 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -31,6 +31,7 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { reviewTooltipKeybind } from "../command-tooltip-keybind" +import { useTitlebarRightMount } from "../titlebar" const OPEN_APPS = [ "vscode", @@ -284,10 +285,9 @@ export function SessionHeader() { } const [centerMount, setCenterMount] = createSignal(null) - const [rightMount, setRightMount] = createSignal(null) + const rightMount = useTitlebarRightMount() onMount(() => { setCenterMount(document.getElementById("opencode-titlebar-center")) - setRightMount(document.getElementById("opencode-titlebar-right")) }) return ( diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 3592bbe6227f..8706fd93c904 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, createResource, createSignal, Match, Show, Switch, untrack } from "solid-js" +import { createEffect, createMemo, createResource, createSignal, Match, onMount, Show, Switch, untrack } from "solid-js" import { createStore } from "solid-js/store" import { useLocation, useNavigate, useParams } from "@solidjs/router" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -60,6 +60,12 @@ export type TitlebarUpdate = { install: () => void } +export function useTitlebarRightMount() { + const [mount, setMount] = createSignal(null) + onMount(() => setMount(document.getElementById("opencode-titlebar-right"))) + return mount +} + export function Titlebar(props: { update?: TitlebarUpdate }) { const layout = useLayout() const platform = usePlatform() diff --git a/packages/app/src/pages/new-session.tsx b/packages/app/src/pages/new-session.tsx index af53ca3d4c23..fdb9929eca58 100644 --- a/packages/app/src/pages/new-session.tsx +++ b/packages/app/src/pages/new-session.tsx @@ -1,8 +1,11 @@ import { Show, createEffect, createMemo, createResource, untrack } from "solid-js" import { createStore } from "solid-js/store" +import { Portal } from "solid-js/web" import { useSearchParams } from "@solidjs/router" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { NewSessionDesignView } from "@/components/session" import { PromptInput } from "@/components/prompt-input" +import { StatusPopoverV2 } from "@/components/status-popover" import { useSettingsCommand } from "@/components/settings-dialog" import { PromptProjectAddButton, @@ -15,11 +18,13 @@ import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" +import { useSettings } from "@/context/settings" import { createPromptInputController, createPromptProjectControls } from "@/pages/session/composer" import { useSessionKey } from "@/pages/session/session-layout" import { useComposerCommands } from "@/pages/session/use-composer-commands" import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" import { PromptWorkspaceSelector } from "@/components/prompt-workspace-selector" +import { useTitlebarRightMount } from "@/components/titlebar" const showWorkspaceBar = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" @@ -35,6 +40,7 @@ export default function NewSessionPage() { const serverSync = useServerSync() const comments = useComments() const language = useLanguage() + const settings = useSettings() const route = useSessionKey() const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>() @@ -55,6 +61,7 @@ export default function NewSessionPage() { }) const [store, setStore] = createStore<{ worktree?: string }>({}) + const rightMount = useTitlebarRightMount() const newSessionWorktree = createMemo(() => { if (store.worktree) return store.worktree @@ -92,6 +99,17 @@ export default function NewSessionPage() { return (
+ + {(mount) => ( + + + + + + + + )} +
From dffecb6478d4cc20caa9ca0a1b245cce880b3b0c Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:20:15 +0800 Subject: [PATCH 26/34] fix(desktop): gate first launch onboarding (#34930) --- packages/app/src/app.tsx | 87 ++++++++++++-------- packages/app/src/context/server.tsx | 2 +- packages/app/src/index.ts | 6 ++ packages/app/src/pages/new-session.tsx | 14 ++-- packages/desktop/src/main/index.ts | 3 + packages/desktop/src/main/ipc.ts | 6 ++ packages/desktop/src/main/onboarding.ts | 28 +++++++ packages/desktop/src/main/store-keys.ts | 1 + packages/desktop/src/preload/index.ts | 3 + packages/desktop/src/preload/types.ts | 2 + packages/desktop/src/renderer/index.tsx | 16 +++- packages/desktop/src/renderer/onboarding.tsx | 79 ++++++++++++++++++ 12 files changed, 204 insertions(+), 43 deletions(-) create mode 100644 packages/desktop/src/main/onboarding.ts create mode 100644 packages/desktop/src/renderer/onboarding.tsx diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index c7cdc73f9b3d..9c2e7d6a1f0e 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -126,10 +126,10 @@ function SelectedServerProviders(props: ParentProps) { ) } -function LegacyServerLayout(props: ParentProps) { +function LegacyServerLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) { return ( - {props.children} + {props.children} ) } @@ -263,12 +263,14 @@ function DesktopCommands() { type ServerScopedShellProps = ParentProps<{ directory?: () => string | undefined sessionID?: () => string | undefined + serverScoped?: JSX.Element }> function ServerScopedProviders(props: ServerScopedShellProps) { return ( + {props.serverScoped} {props.children} @@ -277,16 +279,16 @@ function ServerScopedProviders(props: ServerScopedShellProps) { function LegacyServerScopedShell(props: ServerScopedShellProps) { return ( - + {props.children} ) } -function NewAppLayout(props: ParentProps) { +function NewAppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) { return ( - + {props.children} @@ -347,7 +349,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) { ) } -function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) { +function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; startup?: Promise }>) { const server = useServer() const checkServerHealth = useCheckServerHealth() @@ -376,34 +378,45 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) { const checking = createMemo( () => checkMode() === "blocking" && ["unresolved", "pending"].includes(startupHealthCheck.state), ) + const [startup] = createResource(async () => { + if (!props.startup) return true + await props.startup.catch((error) => { + console.error("[startup] startup gate failed", error) + }) + return true + }) + const startupChecking = createMemo( + () => startupHealthCheck.latest === true && ["unresolved", "pending"].includes(startup.state), + ) + const loading = createMemo(() => checking() || startupChecking()) return ( - + <> + + { + if (checkMode() === "background") void healthCheckActions.refetch() + }} + onServerSelected={(key) => { + setCheckMode("blocking") + server.setActive(key) + void healthCheckActions.refetch() + }} + /> + } + > + {props.children} + + + +
- } - > - { - if (checkMode() === "background") void healthCheckActions.refetch() - }} - onServerSelected={(key) => { - setCheckMode("blocking") - server.setActive(key) - void healthCheckActions.refetch() - }} - /> - } - > - {props.children} -
+ ) } @@ -470,6 +483,8 @@ export function AppInterface(props: { servers?: Array router?: Component disableHealthCheck?: boolean + startup?: Promise + serverScoped?: JSX.Element }) { // The visual new layout lives in the router root so it remains mounted across // route changes. Draft and session routes override only their server-bound data @@ -491,7 +506,7 @@ export function AppInterface(props: { > - + - {routerProps.children} + {routerProps.children} )} > - + @@ -517,12 +532,16 @@ export function AppInterface(props: { ) } -function Routes() { +function Routes(props: { serverScoped?: JSX.Element }) { const settings = useSettings() return ( <> - + ( + {routeProps.children} + )} + > {} } /> diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index 95c6940d70c5..960617f443aa 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -312,7 +312,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( }) } - const isReady = createMemo(() => ready() && !!state.active) + const isReady = Object.assign(createMemo(() => ready() && !!state.active), { promise: ready.promise }) const scope = (key = state.active) => ServerScope.fromServerKey(key, props.canonicalLocalServer) const projects = createServerProjects({ scope, store, setStore }) diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index 2bc9262fde63..ced001ceccf9 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -1,4 +1,10 @@ export { AppBaseProviders, AppInterface } from "./app" +export { useLayout } from "./context/layout" +export { useServerSDK } from "./context/server-sdk" +export { useServerSync } from "./context/server-sync" +export { useServer } from "./context/server" +export { useTabs } from "./context/tabs" +export { useProviders } from "./hooks/use-providers" export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker" export { useCommand } from "./context/command" export { loadLocaleDict, normalizeLocale, type Locale } from "./context/language" diff --git a/packages/app/src/pages/new-session.tsx b/packages/app/src/pages/new-session.tsx index fdb9929eca58..461be60450b4 100644 --- a/packages/app/src/pages/new-session.tsx +++ b/packages/app/src/pages/new-session.tsx @@ -26,7 +26,7 @@ import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" import { PromptWorkspaceSelector } from "@/components/prompt-workspace-selector" import { useTitlebarRightMount } from "@/components/titlebar" -const showWorkspaceBar = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" +const workspaceBarEnabled = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" /** * The `/new-session` draft page. Unlike `session.tsx`, this only renders the prompt @@ -63,7 +63,9 @@ export default function NewSessionPage() { const [store, setStore] = createStore<{ worktree?: string }>({}) const rightMount = useTitlebarRightMount() + const showWorkspaceBar = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git") const newSessionWorktree = createMemo(() => { + if (!showWorkspaceBar()) return "main" if (store.worktree) return store.worktree const project = sync().project if (project && sdk().directory !== project.worktree) return sdk().directory @@ -123,7 +125,7 @@ export default function NewSessionPage() {
} > -
+
- + pendingDeepLinks.splice(0), getDefaultServerUrl: () => getDefaultServerUrl(), setDefaultServerUrl: (url) => setDefaultServerUrl(url), + isFirstLaunchOnboardingPending, + finishFirstLaunchOnboarding, getDisplayBackend: async () => null, setDisplayBackend: async () => undefined, parseMarkdown: async (markdown) => parseMarkdown(markdown), diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 22560ff7da3a..073d288e5cf0 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -27,6 +27,8 @@ type Deps = { consumeInitialDeepLinks: () => Promise | string[] getDefaultServerUrl: () => Promise | string | null setDefaultServerUrl: (url: string | null) => Promise | void + isFirstLaunchOnboardingPending: () => Promise | boolean + finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise | string | null getDisplayBackend: () => Promise setDisplayBackend: (backend: string | null) => Promise | void parseMarkdown: (markdown: string) => Promise | string @@ -50,6 +52,10 @@ export function registerIpcHandlers(deps: Deps) { ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) => deps.setDefaultServerUrl(url), ) + ipcMain.handle("is-first-launch-onboarding-pending", () => deps.isFirstLaunchOnboardingPending()) + ipcMain.handle("finish-first-launch-onboarding", (_event: IpcMainInvokeEvent, createDefaultProject: boolean) => + deps.finishFirstLaunchOnboarding(createDefaultProject), + ) ipcMain.handle("get-display-backend", () => deps.getDisplayBackend()) ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) => deps.setDisplayBackend(backend), diff --git a/packages/desktop/src/main/onboarding.ts b/packages/desktop/src/main/onboarding.ts new file mode 100644 index 000000000000..926506e7b9ba --- /dev/null +++ b/packages/desktop/src/main/onboarding.ts @@ -0,0 +1,28 @@ +import { mkdir } from "node:fs/promises" +import { join } from "node:path" +import { app } from "electron" +import { getStore } from "./store" +import { FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY } from "./store-keys" +import { write as writeLog } from "./logging" + +const DEFAULT_PROJECT_DIR = "New OpenCode Project" + +export function isFirstLaunchOnboardingPending() { + const pending = getStore().get(FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY) !== true + writeLog("onboarding", "first launch onboarding pending checked", { pending }) + return pending +} + +export async function finishFirstLaunchOnboarding(createDefaultProject: boolean) { + if (!isFirstLaunchOnboardingPending()) { + writeLog("onboarding", "first launch onboarding already completed") + return null + } + + const defaultProject = createDefaultProject ? join(app.getPath("documents"), DEFAULT_PROJECT_DIR) : null + if (defaultProject) await mkdir(defaultProject, { recursive: true }) + + getStore().set(FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY, true) + writeLog("onboarding", "first launch onboarding completed", { createDefaultProject, defaultProject }) + return defaultProject +} diff --git a/packages/desktop/src/main/store-keys.ts b/packages/desktop/src/main/store-keys.ts index 270ffe7504e0..506924b74a66 100644 --- a/packages/desktop/src/main/store-keys.ts +++ b/packages/desktop/src/main/store-keys.ts @@ -1,5 +1,6 @@ export const SETTINGS_STORE = "opencode.settings" export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl" +export const FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY = "firstLaunchOnboardingComplete" export const WSL_SERVERS_KEY = "wslServers" export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled" export const WINDOW_IDS_KEY = "windowIds" diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts index 2cb8a9ee073c..47a757a557d3 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -59,6 +59,9 @@ const api: ElectronAPI = { consumeInitialDeepLinks: () => ipcRenderer.invoke("consume-initial-deep-links"), getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"), setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url), + isFirstLaunchOnboardingPending: () => ipcRenderer.invoke("is-first-launch-onboarding-pending"), + finishFirstLaunchOnboarding: (createDefaultProject) => + ipcRenderer.invoke("finish-first-launch-onboarding", createDefaultProject), getDisplayBackend: () => ipcRenderer.invoke("get-display-backend"), setDisplayBackend: (backend) => ipcRenderer.invoke("set-display-backend", backend), parseMarkdownCommand: (markdown) => ipcRenderer.invoke("parse-markdown", markdown), diff --git a/packages/desktop/src/preload/types.ts b/packages/desktop/src/preload/types.ts index 5401c2070d7a..d7509580ad05 100644 --- a/packages/desktop/src/preload/types.ts +++ b/packages/desktop/src/preload/types.ts @@ -49,6 +49,8 @@ export type ElectronAPI = { consumeInitialDeepLinks: () => Promise getDefaultServerUrl: () => Promise setDefaultServerUrl: (url: string | null) => Promise + isFirstLaunchOnboardingPending: () => Promise + finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise getDisplayBackend: () => Promise setDisplayBackend: (backend: LinuxDisplayBackend | null) => Promise parseMarkdownCommand: (markdown: string) => Promise diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index 5369ab21e310..966fb0c0bf3a 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -23,6 +23,7 @@ import { render } from "solid-js/web" import pkg from "../../package.json" import { initI18n, t } from "./i18n" import { initializationData, initializationReady } from "./initialization" +import { DesktopFirstLaunchOnboarding } from "./onboarding" import { resetZoom, setPinchZoomEnabled, webviewZoom, zoomIn, zoomOut } from "./webview-zoom" import { availableStartupServer, readyWslConnections } from "./wsl/connections" import "./styles.css" @@ -347,6 +348,7 @@ function DesktopRoot(props: { windowState: DesktopWindowState }) { const router = (props: BaseRouterProps) => ( ) + const onboarding = Promise.withResolvers() function handleClick(e: MouseEvent) { const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null @@ -400,12 +402,22 @@ function DesktopRoot(props: { windowState: DesktopWindowState }) { const effectiveDefaultServer = createMemo(() => ServerConnection.Key.make(availableStartupServer(defaultServer.latest, wslServers.data)), ) - return ( }> {(key) => ( - + + } + > )} diff --git a/packages/desktop/src/renderer/onboarding.tsx b/packages/desktop/src/renderer/onboarding.tsx new file mode 100644 index 000000000000..47758bc78869 --- /dev/null +++ b/packages/desktop/src/renderer/onboarding.tsx @@ -0,0 +1,79 @@ +import { + ServerConnection, + useLayout, + useProviders, + useServer, + useServerSDK, + useServerSync, + useTabs, +} from "@opencode-ai/app" +import { onMount, startTransition } from "solid-js" + +export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoaded: () => void }) { + const server = useServer() + const serverSDK = useServerSDK() + const serverSync = useServerSync() + const layout = useLayout() + const providers = useProviders() + const tabs = useTabs() + + onMount(() => { + void runFirstLaunchOnboarding().finally(props.onLoaded) + }) + + async function runFirstLaunchOnboarding() { + try { + await Promise.all( + [server.ready.promise, layout.ready.promise, tabs.ready.promise, tabs.recentReady.promise].map( + (p) => p ?? Promise.resolve(), + ), + ) + if (!server.isLocal()) return + + const pending = await window.api.isFirstLaunchOnboardingPending() + if (!pending) return + + const sessions = await serverSDK() + .client.session.list() + .then((x) => x.data ?? []) + .catch(() => undefined) + const connectedProviders = providers.connected() + const paidProviders = providers.paid() + const persistedProjects = layout.projects.list() + const shouldTrigger = + props.initialUrl === "/" && + sessions?.length === 0 && + paidProviders.length === 0 && + persistedProjects.length === 0 && + tabs.store.length === 0 && + server.list.every(ServerConnection.builtin) + + console.info("[desktop-onboarding] first launch onboarding evaluated", { + pending, + shouldTrigger, + initialUrl: props.initialUrl, + sessions: sessions?.length, + connectedProviders: connectedProviders.length, + paidProviders: paidProviders.length, + serverProjects: serverSync().data.project.length, + persistedProjects: persistedProjects.length, + tabs: tabs.store.length, + servers: server.list.map(ServerConnection.key), + }) + + const directory = await window.api.finishFirstLaunchOnboarding(shouldTrigger) + if (!shouldTrigger || !directory) return + + console.info("[desktop-onboarding] starting first launch draft", { directory }) + server.projects.open(directory) + server.projects.touch(directory) + await startTransition(() => { + tabs.newDraft({ server: server.key, directory }) + }) + } catch (error) { + console.error("[desktop-onboarding] first launch onboarding failed", error) + } + } + + return null +} From 977a40af686ced65d14e86d52ca41c9137b7e515 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 6 Jul 2026 08:21:25 +0000 Subject: [PATCH 27/34] chore: generate --- packages/app/src/context/server.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index 960617f443aa..450129f45884 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -312,7 +312,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( }) } - const isReady = Object.assign(createMemo(() => ready() && !!state.active), { promise: ready.promise }) + const isReady = Object.assign( + createMemo(() => ready() && !!state.active), + { promise: ready.promise }, + ) const scope = (key = state.active) => ServerScope.fromServerKey(key, props.canonicalLocalServer) const projects = createServerProjects({ scope, store, setStore }) From b0e41ff2c4069bafdaac9c11327659b1bf911f87 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:37:50 +0800 Subject: [PATCH 28/34] fix(app): use selected home project for new sessions (#35530) --- packages/app/src/components/titlebar.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 8706fd93c904..2a818d565da1 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -334,6 +334,21 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { return } + if (route.type === "home") { + const selection = layout.home.selection() + const conn = global.servers.list().find((item) => ServerConnection.key(item) === selection.server) + const project = conn + ? global + .ensureServerCtx(conn) + .projects.list() + .find((item) => item.worktree === selection.directory) + : undefined + if (conn && project) { + tabs.newDraft({ server: ServerConnection.key(conn), directory: project.worktree }, "") + return + } + } + const current = layout.projects.list()[0] if (current) { tabs.newDraft({ server: server.key, directory: current.worktree }, "") From 561070fbc24eaf4c691ef69e8a1fa07c5dab0f87 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Mon, 6 Jul 2026 13:01:08 +0200 Subject: [PATCH 29/34] deps: upgrade OpenTUI to v0.4.3 (#35226) --- bun.lock | 50 ++++++++++++------- package.json | 6 +-- packages/plugin/package.json | 6 +-- .../tui/src/component/dialog-provider.tsx | 6 +-- packages/tui/src/routes/session/index.tsx | 2 +- packages/tui/src/ui/dialog-prompt.tsx | 2 +- 6 files changed, 43 insertions(+), 29 deletions(-) diff --git a/bun.lock b/bun.lock index 742578ad2b25..49f4ff3d7a96 100644 --- a/bun.lock +++ b/bun.lock @@ -704,9 +704,9 @@ "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.3.4", - "@opentui/keymap": ">=0.3.4", - "@opentui/solid": ">=0.3.4", + "@opentui/core": ">=0.4.3", + "@opentui/keymap": ">=0.4.3", + "@opentui/solid": ">=0.4.3", }, "optionalPeers": [ "@opentui/core", @@ -1096,9 +1096,9 @@ "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@opentui/core": "0.3.4", - "@opentui/keymap": "0.3.4", - "@opentui/solid": "0.3.4", + "@opentui/core": "0.4.3", + "@opentui/keymap": "0.4.3", + "@opentui/solid": "0.4.3", "@pierre/diffs": "1.2.10", "@playwright/test": "1.59.1", "@sentry/solid": "10.36.0", @@ -2031,27 +2031,27 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], - "@opentui/core": ["@opentui/core@0.3.4", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.3.4", "@opentui/core-darwin-x64": "0.3.4", "@opentui/core-linux-arm64": "0.3.4", "@opentui/core-linux-arm64-musl": "0.3.4", "@opentui/core-linux-x64": "0.3.4", "@opentui/core-linux-x64-musl": "0.3.4", "@opentui/core-win32-arm64": "0.3.4", "@opentui/core-win32-x64": "0.3.4" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-y0DlrChP9lcJ4jC5z/1wMS34+ygfSTW7gD5OJHwJaAScfmlFvuJOZbwmCGrJURZ+5wFBxuOi9LatZsmeAUIKAA=="], + "@opentui/core": ["@opentui/core@0.4.3", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.3", "@opentui/core-darwin-x64": "0.4.3", "@opentui/core-linux-arm64": "0.4.3", "@opentui/core-linux-arm64-musl": "0.4.3", "@opentui/core-linux-x64": "0.4.3", "@opentui/core-linux-x64-musl": "0.4.3", "@opentui/core-win32-arm64": "0.4.3", "@opentui/core-win32-x64": "0.4.3" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-rrJfAk13tALDqldYjhc78eWQ+aKq1iknJgffIOg3OwyZoqQo+p6gtuqyhmWvXIfQzlNUbpgpCPcxbXlhMnlaHQ=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4A7JYXUsZqhu9PPCe07E30ourSJYkitkwMujUyNKjM5e/dHNDVnz+5r5cO3M5snofLafc1DN7+9jEPn4UQzchQ=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jvm9E8n2sPhKEyKSXn9GlmJcj8WoJXJTooXb3djwjVaiimjihIj0XxHzCWhdqbDtQp+VxDFyCKoQagOOz20qhA=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+fh0vEUE0lwVC7RW5ijYLRlTLp5NfvCRj8SzxDVd7IL2j2ssB6YXcfIbXq2EW7UGnrejwPRXf1tgUrIXW9KmOw=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-0uPuHCeZxm/O7+L+iNQl8zRAfehiwYstKkT9J0uTZO64/byBCLvy5lvn1DiE/72s/nTJ5nwpLN+pQs2/WYVKLQ=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gl6qA5QJy6u8Cbt7gOtHbhhfMZ4qQDb0kEwFXHcMGmbnKzz4OHoq74D6tNjyvSQB9saoC7C6C0tvn2DcJOuNog=="], - "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-sJYUzYcSOb5PCXRlhwsse/fdsMiVomNvIwq/2TDhAANef+YPO3Br+OH9kQRbuj0bjVDmUS36SGYWSTFu2lUO+A=="], + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-8p8g8/AEq/xFGpQ7XcIFKcAqjc0QwsZcv+Ll9RbCDpUA56FGH6jfLDir0KYTNTgYXJTIrBIENI9K46VuxMUMQA=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-btYIQeNdPbN4JCrCjVB/RwMGrnRY7qWB2piNEfALSByuULKNjPKQ33PYIj38Yd01zCvCV7FotIeXEGSHx3tgCA=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-dXpJitiZdYE3hq2Pvx6e9I0uPQSOcnaLLp1pDgWAHv+3kvKSHEX//9Yr/pV/Ua6qqT7p+2D/K4vXNap/NKVo2w=="], - "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-fhmUey4oJJ2+N62xlIgAPxAl36Fa7wYffqDOT4QLpm0jfyD5xzo+wL/hr2zUqaEI439R8Iq6jHNxf/Nsx1WuuQ=="], + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/QiFpCrpU2O7vy8QYmLIQYbvAtKDgmqcVjR7dGtqSzkiQk3ktNJoo5RozG7ueXnjung1Wp0nKldKxo2Csg/OrA=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.3.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-sh432vPU+eLp8eA4I0KWKKn7D0VHbk01YTg6mA9/ihCNYHntc6LZ8/sLvsPv8CvKscMotfIkh3M5YhdS36BuXw=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mx2zuOjrhm/z2SDS6RExIyjP/SnN/8QhhagxURUw0jQi/NssGSeAllu1cBAFFnhobJL5QLTE4FU4CRhUK9svgg=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-dw8FcjUZaLAjw25P3/7BarobCh/QOHn3srYaWYQdysoqyvSlPkQumpI8kV/KgpJtdITU1GW02MQC4EeLIFFalA=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NuoqvWKGXaYnmlqvu7Gg2lLI6yVMnS9OfWBvxp+7Q+McSgHFSTQmYBXaPpvQ8HikpQXE1nCeMPtuSG4PdZHe2w=="], - "@opentui/keymap": ["@opentui/keymap@0.3.4", "", { "dependencies": { "@opentui/core": "0.3.4" }, "peerDependencies": { "@opentui/react": "0.3.4", "@opentui/solid": "0.3.4", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-8fo6BZWQgCjANfbKkzPo0ghAzS1E7TlHjDDS+SUhrX01qEUO1clFTRssKluHbXd2UJY1Ehle01TV5bFmY78f8w=="], + "@opentui/keymap": ["@opentui/keymap@0.4.3", "", { "dependencies": { "@opentui/core": "0.4.3" }, "peerDependencies": { "@opentui/react": "0.4.3", "@opentui/solid": "0.4.3", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-sinX0pyQBRrEvo89PSSUbSUDIYpL3xWo81VEfec58VFoVRB5FG48/deAtvRTQfJ8w1kgbzN8hzdOXdSm61zBmw=="], - "@opentui/solid": ["@opentui/solid@0.3.4", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.3.4", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-gin1VnsVBahX0nrU3mpgh5U1qvyJBIZu4NE5mc0YnObWOEf9HVNxKY4/BpUvQPh91kT6zeOzTBvAvYK4R7g9MQ=="], + "@opentui/solid": ["@opentui/solid@0.4.3", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.3", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-RcV0+S8HMdXOASyr7HmJUBuTUIaFPzAxMDa44VftS5C2JUgrmAuWo0Njv1q3TWRB1owjHnyKhEfWGKq7A82wxw=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -3243,7 +3243,7 @@ "builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="], - "bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="], + "bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="], "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], @@ -6383,6 +6383,8 @@ "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], @@ -7091,6 +7093,17 @@ "opencode/@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="], + + + + + + + + + + + "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], @@ -7425,6 +7438,7 @@ "opencode/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], diff --git a/package.json b/package.json index fd86ad3decd2..e5526b22ffb1 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,9 @@ "@octokit/rest": "22.0.0", "@hono/standard-validator": "0.2.0", "@hono/zod-validator": "0.4.2", - "@opentui/core": "0.3.4", - "@opentui/keymap": "0.3.4", - "@opentui/solid": "0.3.4", + "@opentui/core": "0.4.3", + "@opentui/keymap": "0.4.3", + "@opentui/solid": "0.4.3", "@tanstack/solid-virtual": "3.13.28", "@shikijs/stream": "4.2.0", "ulid": "3.0.1", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 50aeffb8e10a..1b16375c032e 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -27,9 +27,9 @@ "zod": "catalog:" }, "peerDependencies": { - "@opentui/core": ">=0.3.4", - "@opentui/keymap": ">=0.3.4", - "@opentui/solid": ">=0.3.4" + "@opentui/core": ">=0.4.3", + "@opentui/keymap": ">=0.4.3", + "@opentui/solid": ">=0.4.3" }, "peerDependenciesMeta": { "@opentui/core": { diff --git a/packages/tui/src/component/dialog-provider.tsx b/packages/tui/src/component/dialog-provider.tsx index 2767508eb2d2..0fd51e3c1c71 100644 --- a/packages/tui/src/component/dialog-provider.tsx +++ b/packages/tui/src/component/dialog-provider.tsx @@ -366,8 +366,8 @@ function ApiMethod(props: ApiMethodProps) { + ({ opencode: ( @@ -390,7 +390,7 @@ function ApiMethod(props: ApiMethodProps) { ), - }[props.providerID] ?? undefined + })[props.providerID] ?? undefined } onConfirm={async (value) => { if (!value) return diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 6df6b00d2d11..6d77b0ea58fd 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1528,7 +1528,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las customBorderChars={SplitBorder.customBorderChars} borderColor={theme.error} > - {props.message.error?.data.message} + {errorMessage(props.message.error)} diff --git a/packages/tui/src/ui/dialog-prompt.tsx b/packages/tui/src/ui/dialog-prompt.tsx index d627ea2970d9..f518fb2950b7 100644 --- a/packages/tui/src/ui/dialog-prompt.tsx +++ b/packages/tui/src/ui/dialog-prompt.tsx @@ -83,7 +83,7 @@ export function DialogPrompt(props: DialogPromptProps) { - {props.description} + {props.description?.()}