From e8ded21d05019122f3a42564d1b76e9dc4bfbf8b Mon Sep 17 00:00:00 2001 From: Sami Jawhar Date: Tue, 28 Apr 2026 21:59:28 +0000 Subject: [PATCH] test(session): cover custom session id round-trip and duplicate detection --- .../server/routes/instance/httpapi/errors.ts | 17 + .../routes/instance/httpapi/groups/session.ts | 4 +- .../httpapi/handlers/session-errors.ts | 6 + .../instance/httpapi/handlers/session.ts | 2 +- packages/opencode/src/session/projectors.ts | 16 +- packages/opencode/src/session/session.ts | 9 + .../opencode/test/session/session.test.ts | 46 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 2 + packages/sdk/js/src/v2/gen/types.gen.ts | 298 +++--- packages/sdk/openapi.json | 929 +++++++++--------- 10 files changed, 730 insertions(+), 599 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/errors.ts b/packages/opencode/src/server/routes/instance/httpapi/errors.ts index 5e35d6a79a3c..e84836a08e96 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/errors.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/errors.ts @@ -185,9 +185,26 @@ export class ApiNotFoundError extends Schema.ErrorClass("NotFo { httpApiStatus: 404 }, ) {} +export class ApiDuplicateIDError extends Schema.ErrorClass("DuplicateIDError")( + { + name: Schema.Literal("DuplicateIDError"), + data: Schema.Struct({ + id: Schema.String, + }), + }, + { httpApiStatus: 409 }, +) {} + export function notFound(message: string) { return new ApiNotFoundError({ name: "NotFoundError", data: { message }, }) } + +export function duplicateID(id: string) { + return new ApiDuplicateIDError({ + name: "DuplicateIDError", + data: { id }, + }) +} diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts index 29b98d16d4d2..cf944233f1c0 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts @@ -19,7 +19,7 @@ import { WorkspaceRoutingQuery, WorkspaceRoutingQueryFields, } from "../middleware/workspace-routing" -import { ApiNotFoundError, PermissionNotFoundError, SessionBusyError } from "../errors" +import { ApiDuplicateIDError, ApiNotFoundError, PermissionNotFoundError, SessionBusyError } from "../errors" import { described } from "./metadata" import { QueryBoolean } from "./query" @@ -200,7 +200,7 @@ export const SessionApi = HttpApi.make("session") query: WorkspaceRoutingQuery, payload: [HttpApiSchema.NoContent, Session.CreateInput], success: described(Session.Info, "Successfully created session"), - error: HttpApiError.BadRequest, + error: [HttpApiError.BadRequest, ApiDuplicateIDError], }).annotateMerge( OpenApi.annotations({ identifier: "session.create", diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts index e297bdd0a843..11390c62d682 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts @@ -3,6 +3,8 @@ import type { Session } from "@/session/session" import { Effect } from "effect" import * as ApiError from "../errors" +type DuplicateID = InstanceType + export function mapStorageNotFound(self: Effect.Effect) { return self.pipe(Effect.mapError((error) => ApiError.notFound(error.message))) } @@ -19,3 +21,7 @@ export function mapBusy(self: Effect.Effect) { ), ) } + +export function mapDuplicateID(self: Effect.Effect) { + return self.pipe(Effect.mapError((error) => ApiError.duplicateID(error.data.id))) +} diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts index 4d4cce367b41..8b15ba8bdc9d 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts @@ -150,7 +150,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", }) const create = Effect.fn("SessionHttpApi.create")(function* (ctx: { payload?: Session.CreateInput }) { - return yield* shareSvc.create(ctx.payload) + return yield* SessionError.mapDuplicateID(shareSvc.create(ctx.payload)) }) const createRaw = Effect.fn("SessionHttpApi.createRaw")(function* (ctx: { diff --git a/packages/opencode/src/session/projectors.ts b/packages/opencode/src/session/projectors.ts index 3dd848c5bc05..dde2d50d3f08 100644 --- a/packages/opencode/src/session/projectors.ts +++ b/packages/opencode/src/session/projectors.ts @@ -18,6 +18,11 @@ function foreign(err: unknown) { if ("code" in err && err.code === "SQLITE_CONSTRAINT_FOREIGNKEY") return true return "message" in err && typeof err.message === "string" && err.message.includes("FOREIGN KEY constraint failed") } +function duplicate(err: unknown, table: string) { + if (typeof err !== "object" || err === null) return false + if (!("message" in err) || typeof err.message !== "string") return false + return err.message.includes(`UNIQUE constraint failed: ${table}.id`) +} export type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial | null } : T @@ -98,9 +103,14 @@ export function toPartialRow(info: DeepPartial) { export default [ SyncEvent.project(Session.Event.Created, (db, data) => { - db.insert(SessionTable) - .values(Session.toRow(data.info as Session.Info)) - .run() + try { + db.insert(SessionTable) + .values(Session.toRow(data.info as Session.Info)) + .run() + } catch (err) { + if (duplicate(err, "session")) throw new Session.DuplicateIDError({ id: data.info.id }) + throw err + } if (data.info.workspaceID) { db.update(WorkspaceTable).set({ time_used: Date.now() }).where(eq(WorkspaceTable.id, data.info.workspaceID)).run() diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index f75ac910d40a..519777533aa6 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -25,6 +25,7 @@ import { PartTable, SessionTable } from "./session.sql" import { ProjectTable } from "../project/project.sql" import { Storage } from "@/storage/storage" import * as Log from "@opencode-ai/core/util/log" +import { NamedError } from "@opencode-ai/core/util/error" import { MessageV2 } from "./message-v2" import type { InstanceContext } from "../project/instance-context" import { InstanceState } from "@/effect/instance-state" @@ -242,6 +243,7 @@ export type GlobalInfo = Types.DeepMutable export const CreateInput = Schema.optional( Schema.Struct({ + id: Schema.optional(SessionID), parentID: Schema.optional(SessionID), title: Schema.optional(Schema.String), agent: Schema.optional(Schema.String), @@ -448,9 +450,14 @@ export class BusyError extends Schema.TaggedErrorClass()("SessionBusy export type NotFound = NotFoundError +export const DuplicateIDError = NamedError.create("DuplicateIDError", { + id: Schema.String, +}) + export interface Interface { readonly list: (input?: ListInput) => Effect.Effect readonly create: (input?: { + id?: SessionID parentID?: SessionID title?: string agent?: string @@ -655,6 +662,7 @@ export const layer: Layer.Layer< }) const create = Effect.fn("Session.create")(function* (input?: { + id?: SessionID parentID?: SessionID title?: string agent?: string @@ -665,6 +673,7 @@ export const layer: Layer.Layer< const ctx = yield* InstanceState.context const workspace = yield* InstanceState.workspaceID return yield* createNext({ + id: input?.id, parentID: input?.parentID, directory: ctx.directory, path: sessionPath(ctx.worktree, ctx.directory), diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index 9a2b15578178..aff3ca41cfec 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -1,10 +1,10 @@ import { describe, expect } from "bun:test" -import { Deferred, Effect, Exit, Layer } from "effect" +import { Cause, Deferred, Effect, Exit, Layer } from "effect" import { Session as SessionNs } from "@/session/session" import { GlobalBus, type GlobalEvent } from "../../src/bus/global" import * as Log from "@opencode-ai/core/util/log" import { MessageV2 } from "../../src/session/message-v2" -import { MessageID, PartID, type SessionID } from "../../src/session/schema" +import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from "../../src/session/schema" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" @@ -35,7 +35,7 @@ const awaitDeferred = (deferred: Deferred.Deferred, message: string) => Effect.sleep("2 seconds").pipe(Effect.flatMap(() => Effect.fail(new Error(message)))), ) -const remove = (id: SessionID) => SessionNs.use.remove(id) +const remove = (id: SessionIDType) => SessionNs.use.remove(id) const subscribeGlobal = (type: string, callback: (event: NonNullable) => void) => { const listener = (event: GlobalEvent) => { @@ -185,3 +185,43 @@ describe("Session", () => { }), ) }) + +describe("custom session ID", () => { + it.instance("round-trip: create with custom id returns same id and is retrievable", () => + Effect.gen(function* () { + const session = yield* SessionNs.Service + const customID = SessionID.descending() + const created = yield* session.create({ id: customID }) + const fetched = yield* session.get(customID) + + expect(created.id).toBe(customID) + expect(fetched.id).toBe(customID) + }), + ) + + it.instance("creating with duplicate id throws DuplicateIDError", () => + Effect.gen(function* () { + const session = yield* SessionNs.Service + const customID = SessionID.descending() + yield* session.create({ id: customID }) + + const exit = yield* session.create({ id: customID }).pipe(Effect.exit) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) { + expect(Cause.squash(exit.cause)).toMatchObject({ name: "DuplicateIDError", data: { id: customID } }) + } + }), + ) + + it.instance("creating with malformed id (wrong prefix) throws", () => + Effect.gen(function* () { + const session = yield* SessionNs.Service + const exit = yield* session.create({ id: "not-a-session-id" as never }).pipe(Effect.exit) + + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) { + expect(Cause.pretty(exit.cause)).toContain('Expected a string starting with "ses"') + } + }), + ) +}) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index a6b327fab46f..7c4f2a8f19b3 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -3092,6 +3092,7 @@ export class Session2 extends HeyApiClient { parameters?: { directory?: string workspace?: string + id?: string parentID?: string title?: string agent?: string @@ -3112,6 +3113,7 @@ export class Session2 extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "id" }, { in: "body", key: "parentID" }, { in: "body", key: "title" }, { in: "body", key: "agent" }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index ebcb4271c431..3c5354a9ed09 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -5,12 +5,6 @@ export type ClientOptions = { } export type Event = - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow1 - | EventTuiSessionSelect - | EventServerConnected - | EventGlobalDisposed | EventServerInstanceDisposed | EventFileEdited | EventFileWatcherUpdated @@ -27,6 +21,10 @@ export type Event = | EventTodoUpdated | EventSessionStatus | EventSessionIdle + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow1 + | EventTuiSessionSelect | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted @@ -77,11 +75,13 @@ export type Event = | EventSessionNextCompactionStarted | EventSessionNextCompactionDelta | EventSessionNextCompactionEnded - | EventCatalogModelUpdated + | EventServerConnected + | EventGlobalDisposed | EventModelsDevRefreshed | EventAccountAdded | EventAccountRemoved | EventAccountSwitched + | EventCatalogModelUpdated export type OAuth = { type: "oauth" @@ -119,61 +119,6 @@ export type InvalidRequestError = { field?: string } -export type EventTuiPromptAppend = { - id: string - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute = { - id: string - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "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" - | string - } -} - -export type EventTuiToastShow = { - id: string - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - duration?: number - } -} - -export type EventTuiSessionSelect = { - id: string - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - export type PermissionRequest = { id: string sessionID: string @@ -352,6 +297,61 @@ export type SessionStatus = type: "busy" } +export type EventTuiPromptAppend = { + id: string + type: "tui.prompt.append" + properties: { + text: string + } +} + +export type EventTuiCommandExecute = { + id: string + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "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" + | string + } +} + +export type EventTuiToastShow = { + id: string + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + duration?: number + } +} + +export type EventTuiSessionSelect = { + id: string + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + export type Project = { id: string worktree: string @@ -806,12 +806,6 @@ export type GlobalEvent = { project?: string workspace?: string payload: - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventServerConnected - | EventGlobalDisposed | EventServerInstanceDisposed | EventFileEdited | EventFileWatcherUpdated @@ -828,6 +822,10 @@ export type GlobalEvent = { | EventTodoUpdated | EventSessionStatus | EventSessionIdle + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted @@ -878,11 +876,13 @@ export type GlobalEvent = { | EventSessionNextCompactionStarted | EventSessionNextCompactionDelta | EventSessionNextCompactionEnded - | EventCatalogModelUpdated + | EventServerConnected + | EventGlobalDisposed | EventModelsDevRefreshed | EventAccountAdded | EventAccountRemoved | EventAccountSwitched + | EventCatalogModelUpdated | SyncEventMessageUpdated | SyncEventMessageRemoved | SyncEventMessagePartUpdated @@ -1794,6 +1794,13 @@ export type NotFoundError = { } } +export type DuplicateIdError = { + name: "DuplicateIDError" + data: { + id: string + } +} + export type TextPartInput = { id?: string type: "text" @@ -2503,22 +2510,6 @@ export type SyncEventSessionNextCompactionEnded = { } } -export type EventServerConnected = { - id: string - type: "server.connected" - properties: { - [key: string]: unknown - } -} - -export type EventGlobalDisposed = { - id: string - type: "global.disposed" - properties: { - [key: string]: unknown - } -} - export type EventServerInstanceDisposed = { id: string type: "server.instance.disposed" @@ -3245,6 +3236,80 @@ export type EventSessionNextCompactionEnded = { } } +export type EventServerConnected = { + id: string + type: "server.connected" + properties: { + [key: string]: unknown + } +} + +export type EventGlobalDisposed = { + id: string + type: "global.disposed" + properties: { + [key: string]: unknown + } +} + +export type EventModelsDevRefreshed = { + id: string + type: "models-dev.refreshed" + properties: { + [key: string]: unknown + } +} + +export type AccountV2oAuthCredential = { + type: "oauth" + refresh: string + access: string + expires: number +} + +export type AccountV2ApiKeyCredential = { + type: "api" + key: string + metadata?: { + [key: string]: string + } +} + +export type AccountV2Credential = AccountV2oAuthCredential | AccountV2ApiKeyCredential + +export type AccountV2Info = { + id: string + serviceID: string + description: string + credential: AccountV2Credential +} + +export type EventAccountAdded = { + id: string + type: "account.added" + properties: { + account: AccountV2Info + } +} + +export type EventAccountRemoved = { + id: string + type: "account.removed" + properties: { + account: AccountV2Info + } +} + +export type EventAccountSwitched = { + id: string + type: "account.switched" + properties: { + serviceID: string + from?: string + to?: string + } +} + export type ModelV2Info = { id: string apiID: string @@ -3351,64 +3416,6 @@ export type EventCatalogModelUpdated = { } } -export type EventModelsDevRefreshed = { - id: string - type: "models-dev.refreshed" - properties: { - [key: string]: unknown - } -} - -export type AccountV2oAuthCredential = { - type: "oauth" - refresh: string - access: string - expires: number -} - -export type AccountV2ApiKeyCredential = { - type: "api" - key: string - metadata?: { - [key: string]: string - } -} - -export type AccountV2Credential = AccountV2oAuthCredential | AccountV2ApiKeyCredential - -export type AccountV2Info = { - id: string - serviceID: string - description: string - credential: AccountV2Credential -} - -export type EventAccountAdded = { - id: string - type: "account.added" - properties: { - account: AccountV2Info - } -} - -export type EventAccountRemoved = { - id: string - type: "account.removed" - properties: { - account: AccountV2Info - } -} - -export type EventAccountSwitched = { - id: string - type: "account.switched" - properties: { - serviceID: string - from?: string - to?: string - } -} - export type SessionInfo = { id: string parentID?: string @@ -6059,6 +6066,7 @@ export type SessionListResponse = SessionListResponses[keyof SessionListResponse export type SessionCreateData = { body?: { + id?: string parentID?: string title?: string agent?: string @@ -6083,6 +6091,10 @@ export type SessionCreateErrors = { * BadRequest | InvalidRequestError */ 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * DuplicateIDError + */ + 409: DuplicateIdError } export type SessionCreateError = SessionCreateErrors[keyof SessionCreateErrors] diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 0965e77d57f8..eec3688a2a19 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -5149,6 +5149,16 @@ } } } + }, + "409": { + "description": "DuplicateIDError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DuplicateIDError" + } + } + } } }, "description": "Create a new OpenCode session for interacting with AI assistants and managing conversations.", @@ -5159,6 +5169,10 @@ "schema": { "type": "object", "properties": { + "id": { + "type": "string", + "pattern": "^ses" + }, "parentID": { "type": "string", "pattern": "^ses" @@ -10479,24 +10493,6 @@ "schemas": { "Event": { "anyOf": [ - { - "$ref": "#/components/schemas/Event.tui.prompt.append" - }, - { - "$ref": "#/components/schemas/Event.tui.command.execute" - }, - { - "$ref": "#/components/schemas/EventTuiToastShow1" - }, - { - "$ref": "#/components/schemas/Event.tui.session.select" - }, - { - "$ref": "#/components/schemas/EventServerConnected" - }, - { - "$ref": "#/components/schemas/EventGlobalDisposed" - }, { "$ref": "#/components/schemas/EventServerInstanceDisposed" }, @@ -10545,6 +10541,18 @@ { "$ref": "#/components/schemas/EventSessionIdle" }, + { + "$ref": "#/components/schemas/Event.tui.prompt.append" + }, + { + "$ref": "#/components/schemas/Event.tui.command.execute" + }, + { + "$ref": "#/components/schemas/EventTuiToastShow1" + }, + { + "$ref": "#/components/schemas/Event.tui.session.select" + }, { "$ref": "#/components/schemas/EventMcpToolsChanged" }, @@ -10695,6 +10703,24 @@ { "$ref": "#/components/schemas/EventSessionNextCompactionEnded" }, + { + "$ref": "#/components/schemas/EventServerConnected" + }, + { + "$ref": "#/components/schemas/EventGlobalDisposed" + }, + { + "$ref": "#/components/schemas/EventModels-devRefreshed" + }, + { + "$ref": "#/components/schemas/EventAccountAdded" + }, + { + "$ref": "#/components/schemas/EventAccountRemoved" + }, + { + "$ref": "#/components/schemas/EventAccountSwitched" + }, { "$ref": "#/components/schemas/EventCatalogModelUpdated" }, @@ -10775,18 +10801,6 @@ }, { "$ref": "#/components/schemas/EventSessionNextCompactionEnded" - }, - { - "$ref": "#/components/schemas/EventModels-devRefreshed" - }, - { - "$ref": "#/components/schemas/EventAccountAdded" - }, - { - "$ref": "#/components/schemas/EventAccountRemoved" - }, - { - "$ref": "#/components/schemas/EventAccountSwitched" } ] }, @@ -10898,140 +10912,6 @@ "required": ["_tag", "message"], "additionalProperties": false }, - "Event.tui.prompt.append": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.prompt.append"] - }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.command.execute": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.command.execute"] - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "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", "properties"], - "additionalProperties": false - }, - "Event.tui.toast.show": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.toast.show"] - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.session.select": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.session.select"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "PermissionRequest": { "type": "object", "properties": { @@ -11494,53 +11374,187 @@ } ] }, - "Project": { + "Event.tui.prompt.append": { "type": "object", "properties": { "id": { "type": "string" }, - "worktree": { - "type": "string" - }, - "vcs": { + "type": { "type": "string", - "enum": ["git"] - }, - "name": { - "type": "string" + "enum": ["tui.prompt.append"] }, - "icon": { + "properties": { "type": "object", "properties": { - "url": { - "type": "string" - }, - "override": { - "type": "string" - }, - "color": { + "text": { "type": "string" } }, + "required": ["text"], "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "Event.tui.command.execute": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "commands": { - "type": "object", - "properties": { - "start": { - "type": "string", - "description": "Startup script to run when creating a new workspace (worktree)" - } - }, - "additionalProperties": false + "type": { + "type": "string", + "enum": ["tui.command.execute"] }, - "time": { + "properties": { "type": "object", "properties": { - "created": { - "type": "integer", - "minimum": 0 + "command": { + "anyOf": [ + { + "type": "string", + "enum": [ + "session.list", + "session.new", + "session.share", + "session.interrupt", + "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", "properties"], + "additionalProperties": false + }, + "Event.tui.toast.show": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["tui.toast.show"] + }, + "properties": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "variant": { + "type": "string", + "enum": ["info", "success", "warning", "error"] + }, + "duration": { + "type": "integer", + "exclusiveMinimum": 0 + } + }, + "required": ["message", "variant"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "Event.tui.session.select": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["tui.session.select"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses", + "description": "Session ID to navigate to" + } + }, + "required": ["sessionID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "Project": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "worktree": { + "type": "string" + }, + "vcs": { + "type": "string", + "enum": ["git"] + }, + "name": { + "type": "string" + }, + "icon": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "override": { + "type": "string" + }, + "color": { + "type": "string" + } + }, + "additionalProperties": false + }, + "commands": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "Startup script to run when creating a new workspace (worktree)" + } + }, + "additionalProperties": false + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "integer", + "minimum": 0 }, "updated": { "type": "integer", @@ -12905,24 +12919,6 @@ }, "payload": { "anyOf": [ - { - "$ref": "#/components/schemas/Event.tui.prompt.append" - }, - { - "$ref": "#/components/schemas/Event.tui.command.execute" - }, - { - "$ref": "#/components/schemas/Event.tui.toast.show" - }, - { - "$ref": "#/components/schemas/Event.tui.session.select" - }, - { - "$ref": "#/components/schemas/EventServerConnected" - }, - { - "$ref": "#/components/schemas/EventGlobalDisposed" - }, { "$ref": "#/components/schemas/EventServerInstanceDisposed" }, @@ -12971,6 +12967,18 @@ { "$ref": "#/components/schemas/EventSessionIdle" }, + { + "$ref": "#/components/schemas/Event.tui.prompt.append" + }, + { + "$ref": "#/components/schemas/Event.tui.command.execute" + }, + { + "$ref": "#/components/schemas/Event.tui.toast.show" + }, + { + "$ref": "#/components/schemas/Event.tui.session.select" + }, { "$ref": "#/components/schemas/EventMcpToolsChanged" }, @@ -13121,6 +13129,24 @@ { "$ref": "#/components/schemas/EventSessionNextCompactionEnded" }, + { + "$ref": "#/components/schemas/EventServerConnected" + }, + { + "$ref": "#/components/schemas/EventGlobalDisposed" + }, + { + "$ref": "#/components/schemas/EventModels-devRefreshed" + }, + { + "$ref": "#/components/schemas/EventAccountAdded" + }, + { + "$ref": "#/components/schemas/EventAccountRemoved" + }, + { + "$ref": "#/components/schemas/EventAccountSwitched" + }, { "$ref": "#/components/schemas/EventCatalogModelUpdated" }, @@ -13202,18 +13228,6 @@ { "$ref": "#/components/schemas/EventSessionNextCompactionEnded" }, - { - "$ref": "#/components/schemas/EventModels-devRefreshed" - }, - { - "$ref": "#/components/schemas/EventAccountAdded" - }, - { - "$ref": "#/components/schemas/EventAccountRemoved" - }, - { - "$ref": "#/components/schemas/EventAccountSwitched" - }, { "$ref": "#/components/schemas/SyncEventMessageUpdated" }, @@ -15736,6 +15750,27 @@ } } }, + "DuplicateIDError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["DuplicateIDError"] + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, "TextPartInput": { "type": "object", "properties": { @@ -18123,7 +18158,7 @@ "required": ["type", "name", "id", "seq", "aggregateID", "data"], "additionalProperties": false }, - "EventServerConnected": { + "EventServerInstanceDisposed": { "type": "object", "properties": { "id": { @@ -18131,17 +18166,23 @@ }, "type": { "type": "string", - "enum": ["server.connected"] + "enum": ["server.instance.disposed"] }, "properties": { "type": "object", - "properties": {} + "properties": { + "directory": { + "type": "string" + } + }, + "required": ["directory"], + "additionalProperties": false } }, "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventGlobalDisposed": { + "EventFileEdited": { "type": "object", "properties": { "id": { @@ -18149,17 +18190,23 @@ }, "type": { "type": "string", - "enum": ["global.disposed"] + "enum": ["file.edited"] }, "properties": { "type": "object", - "properties": {} + "properties": { + "file": { + "type": "string" + } + }, + "required": ["file"], + "additionalProperties": false } }, "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventServerInstanceDisposed": { + "EventFileWatcherUpdated": { "type": "object", "properties": { "id": { @@ -18167,60 +18214,12 @@ }, "type": { "type": "string", - "enum": ["server.instance.disposed"] + "enum": ["file.watcher.updated"] }, "properties": { "type": "object", "properties": { - "directory": { - "type": "string" - } - }, - "required": ["directory"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventFileEdited": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["file.edited"] - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": ["file"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventFileWatcherUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["file.watcher.updated"] - }, - "properties": { - "type": "object", - "properties": { - "file": { + "file": { "type": "string" }, "event": { @@ -20362,6 +20361,208 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventServerConnected": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["server.connected"] + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventGlobalDisposed": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["global.disposed"] + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventModels-devRefreshed": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["models-dev.refreshed"] + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "AccountV2OAuthCredential": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["oauth"] + }, + "refresh": { + "type": "string" + }, + "access": { + "type": "string" + }, + "expires": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["type", "refresh", "access", "expires"], + "additionalProperties": false + }, + "AccountV2ApiKeyCredential": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["api"] + }, + "key": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["type", "key"], + "additionalProperties": false + }, + "AccountV2Credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/AccountV2OAuthCredential" + }, + { + "$ref": "#/components/schemas/AccountV2ApiKeyCredential" + } + ] + }, + "AccountV2Info": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "serviceID": { + "type": "string" + }, + "description": { + "type": "string" + }, + "credential": { + "$ref": "#/components/schemas/AccountV2Credential" + } + }, + "required": ["id", "serviceID", "description", "credential"], + "additionalProperties": false + }, + "EventAccountAdded": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["account.added"] + }, + "properties": { + "type": "object", + "properties": { + "account": { + "$ref": "#/components/schemas/AccountV2Info" + } + }, + "required": ["account"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventAccountRemoved": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["account.removed"] + }, + "properties": { + "type": "object", + "properties": { + "account": { + "$ref": "#/components/schemas/AccountV2Info" + } + }, + "required": ["account"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventAccountSwitched": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["account.switched"] + }, + "properties": { + "type": "object", + "properties": { + "serviceID": { + "type": "string" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + } + }, + "required": ["serviceID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "ModelV2Info": { "type": "object", "properties": { @@ -20710,172 +20911,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventModels-devRefreshed": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["models-dev.refreshed"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "AccountV2OAuthCredential": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["oauth"] - }, - "refresh": { - "type": "string" - }, - "access": { - "type": "string" - }, - "expires": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["type", "refresh", "access", "expires"], - "additionalProperties": false - }, - "AccountV2ApiKeyCredential": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["api"] - }, - "key": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["type", "key"], - "additionalProperties": false - }, - "AccountV2Credential": { - "anyOf": [ - { - "$ref": "#/components/schemas/AccountV2OAuthCredential" - }, - { - "$ref": "#/components/schemas/AccountV2ApiKeyCredential" - } - ] - }, - "AccountV2Info": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "serviceID": { - "type": "string" - }, - "description": { - "type": "string" - }, - "credential": { - "$ref": "#/components/schemas/AccountV2Credential" - } - }, - "required": ["id", "serviceID", "description", "credential"], - "additionalProperties": false - }, - "EventAccountAdded": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["account.added"] - }, - "properties": { - "type": "object", - "properties": { - "account": { - "$ref": "#/components/schemas/AccountV2Info" - } - }, - "required": ["account"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventAccountRemoved": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["account.removed"] - }, - "properties": { - "type": "object", - "properties": { - "account": { - "$ref": "#/components/schemas/AccountV2Info" - } - }, - "required": ["account"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventAccountSwitched": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["account.switched"] - }, - "properties": { - "type": "object", - "properties": { - "serviceID": { - "type": "string" - }, - "from": { - "type": "string" - }, - "to": { - "type": "string" - } - }, - "required": ["serviceID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "SessionInfo": { "type": "object", "properties": {