From baf2e1a21749c710c667ac24ed6ed5c14a39dd52 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 30 Apr 2026 16:29:29 -0400 Subject: [PATCH 01/24] refactor(session): define v2 session event schemas --- packages/core/src/util/log.ts | 2 + .../migration.sql | 17 + .../snapshot.json | 1481 +++++++++++++++++ packages/opencode/src/cli/cmd/tui/app.tsx | 45 +- .../src/cli/cmd/tui/context/sync-v2.tsx | 273 +++ .../tui/feature-plugins/system/session-v2.tsx | 970 +++++++++++ .../src/cli/cmd/tui/plugin/internal.ts | 2 + .../src/server/routes/instance/httpapi/api.ts | 2 + .../routes/instance/httpapi/handlers/v2.ts | 87 + .../server/routes/instance/httpapi/server.ts | 2 + .../src/server/routes/instance/httpapi/v2.ts | 110 ++ .../src/server/routes/instance/index.ts | 128 +- .../opencode/src/server/routes/instance/v2.ts | 43 + packages/opencode/src/session/compaction.ts | 25 +- packages/opencode/src/session/processor.ts | 158 +- .../opencode/src/session/projectors-next.ts | 153 ++ packages/opencode/src/session/projectors.ts | 5 +- packages/opencode/src/session/prompt.ts | 69 + packages/opencode/src/session/session.sql.ts | 19 +- packages/opencode/src/util/effect-zod.ts | 2 +- packages/opencode/src/v2/event.ts | 42 + .../opencode/src/v2/session-entry-stepper.ts | 261 --- packages/opencode/src/v2/session-entry.ts | 220 --- packages/opencode/src/v2/session-event.ts | 634 +++---- .../src/v2/session-message-updater.ts | 307 ++++ packages/opencode/src/v2/session-message.ts | 196 +++ packages/opencode/src/v2/session-prompt.ts | 36 + packages/opencode/src/v2/session.ts | 158 +- packages/opencode/src/v2/tool-output.ts | 18 + packages/opencode/test/preload.ts | 2 +- .../test/server/httpapi-session.test.ts | 43 +- .../opencode/test/session/compaction.test.ts | 10 + packages/opencode/test/session/prompt.test.ts | 41 + .../session/session-entry-stepper.test.ts | 916 ---------- .../test/v2/session-message-updater.test.ts | 193 +++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 48 + packages/sdk/js/src/v2/gen/types.gen.ts | 1000 ++++++++++- specs/v2/session-concepts-gap.md | 131 ++ 38 files changed, 5939 insertions(+), 1910 deletions(-) create mode 100644 packages/opencode/migration/20260427172553_slow_nightmare/migration.sql create mode 100644 packages/opencode/migration/20260427172553_slow_nightmare/snapshot.json create mode 100644 packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx create mode 100644 packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx create mode 100644 packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts create mode 100644 packages/opencode/src/server/routes/instance/httpapi/v2.ts create mode 100644 packages/opencode/src/server/routes/instance/v2.ts create mode 100644 packages/opencode/src/session/projectors-next.ts create mode 100644 packages/opencode/src/v2/event.ts delete mode 100644 packages/opencode/src/v2/session-entry-stepper.ts delete mode 100644 packages/opencode/src/v2/session-entry.ts create mode 100644 packages/opencode/src/v2/session-message-updater.ts create mode 100644 packages/opencode/src/v2/session-message.ts create mode 100644 packages/opencode/src/v2/session-prompt.ts create mode 100644 packages/opencode/src/v2/tool-output.ts delete mode 100644 packages/opencode/test/session/session-entry-stepper.test.ts create mode 100644 packages/opencode/test/v2/session-message-updater.test.ts create mode 100644 specs/v2/session-concepts-gap.md diff --git a/packages/core/src/util/log.ts b/packages/core/src/util/log.ts index a61c15f7a7a4..e1962aed4ca9 100644 --- a/packages/core/src/util/log.ts +++ b/packages/core/src/util/log.ts @@ -1,3 +1,5 @@ +export * as Log from "./log" + import path from "path" import fs from "fs/promises" import { createWriteStream } from "fs" diff --git a/packages/opencode/migration/20260427172553_slow_nightmare/migration.sql b/packages/opencode/migration/20260427172553_slow_nightmare/migration.sql new file mode 100644 index 000000000000..d5efe5f9e8b3 --- /dev/null +++ b/packages/opencode/migration/20260427172553_slow_nightmare/migration.sql @@ -0,0 +1,17 @@ +CREATE TABLE `session_message` ( + `id` text PRIMARY KEY, + `session_id` text NOT NULL, + `type` text NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + `data` text NOT NULL, + CONSTRAINT `fk_session_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE +); +--> statement-breakpoint +DROP INDEX IF EXISTS `session_entry_session_idx`;--> statement-breakpoint +DROP INDEX IF EXISTS `session_entry_session_type_idx`;--> statement-breakpoint +DROP INDEX IF EXISTS `session_entry_time_created_idx`;--> statement-breakpoint +CREATE INDEX `session_message_session_idx` ON `session_message` (`session_id`);--> statement-breakpoint +CREATE INDEX `session_message_session_type_idx` ON `session_message` (`session_id`,`type`);--> statement-breakpoint +CREATE INDEX `session_message_time_created_idx` ON `session_message` (`time_created`);--> statement-breakpoint +DROP TABLE `session_entry`; \ No newline at end of file diff --git a/packages/opencode/migration/20260427172553_slow_nightmare/snapshot.json b/packages/opencode/migration/20260427172553_slow_nightmare/snapshot.json new file mode 100644 index 000000000000..bb6d06237e41 --- /dev/null +++ b/packages/opencode/migration/20260427172553_slow_nightmare/snapshot.json @@ -0,0 +1,1481 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "61f807f9-6398-4067-be05-804acc2561bc", + "prevIds": [ + "66cbe0d7-def0-451b-b88a-7608513a9b44" + ], + "ddl": [ + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "columns": [ + "active_account_id" + ], + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": [ + "email", + "url" + ], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "project_id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 7117ae7d1bfd..ea742f699708 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -28,6 +28,7 @@ import { useEvent } from "@tui/context/event" import { SDKProvider, useSDK } from "@tui/context/sdk" import { StartupLoading } from "@tui/component/startup-loading" import { SyncProvider, useSync } from "@tui/context/sync" +import { SyncProviderV2 } from "@tui/context/sync-v2" import { LocalProvider, useLocal } from "@tui/context/local" import { DialogModel } from "@tui/component/dialog-model" import { useConnected } from "@tui/component/use-connected" @@ -168,27 +169,29 @@ export function tui(input: { > - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx new file mode 100644 index 000000000000..d9df4a1ab2ce --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx @@ -0,0 +1,273 @@ +import { useEvent } from "@tui/context/event" +import type { + SessionMessage, + SessionMessageAssistant, + SessionMessageAssistantReasoning, + SessionMessageAssistantText, + SessionMessageAssistantTool, +} from "@opencode-ai/sdk/v2" +import { createStore, produce, reconcile } from "solid-js/store" +import { createSimpleContext } from "./helper" +import { useSDK } from "./sdk" + +function activeAssistant(messages: SessionMessage[]) { + const index = messages.findLastIndex((message) => message.type === "assistant" && !message.time.completed) + if (index < 0) return + const assistant = messages[index] + return assistant?.type === "assistant" ? assistant : undefined +} + +function activeCompaction(messages: SessionMessage[]) { + const index = messages.findLastIndex((message) => message.type === "compaction") + if (index < 0) return + const compaction = messages[index] + return compaction?.type === "compaction" ? compaction : undefined +} + +function latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) { + return assistant?.content.findLast( + (item): item is SessionMessageAssistantTool => item.type === "tool" && (callID === undefined || item.id === callID), + ) +} + +function latestText(assistant: SessionMessageAssistant | undefined) { + return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text") +} + +function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) { + return assistant?.content.findLast( + (item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && item.id === reasoningID, + ) +} + +export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext({ + name: "SyncV2", + init: () => { + const [store, setStore] = createStore<{ + messages: { + [sessionID: string]: SessionMessage[] + } + }>({ + messages: {}, + }) + + const event = useEvent() + const sdk = useSDK() + + function update(sessionID: string, fn: (messages: SessionMessage[]) => void) { + setStore( + "messages", + produce((draft) => { + fn((draft[sessionID] ??= [])) + }), + ) + } + + event.subscribe((event) => { + switch (event.type) { + case "session.next.prompted": { + update(event.properties.sessionID, (draft) => { + draft.push({ + id: event.properties.id, + type: "user", + text: event.properties.prompt.text, + files: event.properties.prompt.files, + agents: event.properties.prompt.agents, + time: { created: event.properties.timestamp }, + }) + }) + break + } + case "session.next.synthetic": + update(event.properties.sessionID, (draft) => { + draft.push({ + id: event.properties.id, + type: "synthetic", + sessionID: event.properties.sessionID, + text: event.properties.text, + time: { created: event.properties.timestamp }, + }) + }) + break + case "session.next.step.started": + update(event.properties.sessionID, (draft) => { + const currentAssistant = activeAssistant(draft) + if (currentAssistant) currentAssistant.time.completed = event.properties.timestamp + draft.push({ + id: event.properties.id, + type: "assistant", + agent: event.properties.agent, + model: event.properties.model, + content: [], + snapshot: event.properties.snapshot ? { start: event.properties.snapshot } : undefined, + time: { created: event.properties.timestamp }, + }) + }) + break + case "session.next.step.ended": + update(event.properties.sessionID, (draft) => { + const currentAssistant = activeAssistant(draft) + if (!currentAssistant) return + currentAssistant.time.completed = event.properties.timestamp + currentAssistant.finish = event.properties.finish + currentAssistant.cost = event.properties.cost + currentAssistant.tokens = event.properties.tokens + if (event.properties.snapshot) + currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.properties.snapshot } + }) + break + case "session.next.text.started": + update(event.properties.sessionID, (draft) => { + activeAssistant(draft)?.content.push({ type: "text", text: "" }) + }) + break + case "session.next.text.delta": + update(event.properties.sessionID, (draft) => { + const match = latestText(activeAssistant(draft)) + if (match) match.text += event.properties.delta + }) + break + case "session.next.text.ended": + update(event.properties.sessionID, (draft) => { + const match = latestText(activeAssistant(draft)) + if (match) match.text = event.properties.text + }) + break + case "session.next.tool.input.started": + update(event.properties.sessionID, (draft) => { + activeAssistant(draft)?.content.push({ + type: "tool", + id: event.properties.callID, + name: event.properties.name, + time: { created: event.properties.timestamp }, + state: { status: "pending", input: "" }, + }) + }) + break + case "session.next.tool.input.delta": + update(event.properties.sessionID, (draft) => { + const match = latestTool(activeAssistant(draft), event.properties.callID) + if (match?.state.status === "pending") match.state.input += event.properties.delta + }) + break + case "session.next.tool.input.ended": + break + case "session.next.tool.called": + update(event.properties.sessionID, (draft) => { + const match = latestTool(activeAssistant(draft), event.properties.callID) + if (!match) return + match.time.ran = event.properties.timestamp + match.provider = event.properties.provider + match.state = { status: "running", input: event.properties.input, structured: {}, content: [] } + }) + break + case "session.next.tool.progress": + update(event.properties.sessionID, (draft) => { + const match = latestTool(activeAssistant(draft), event.properties.callID) + if (match?.state.status !== "running") return + match.state.structured = event.properties.structured + match.state.content = [...event.properties.content] + }) + break + case "session.next.tool.success": + update(event.properties.sessionID, (draft) => { + const match = latestTool(activeAssistant(draft), event.properties.callID) + if (match?.state.status !== "running") return + match.state = { + status: "completed", + input: match.state.input, + structured: event.properties.structured, + content: [...event.properties.content], + } + match.provider = event.properties.provider + match.time.completed = event.properties.timestamp + }) + break + case "session.next.tool.error": + update(event.properties.sessionID, (draft) => { + const match = latestTool(activeAssistant(draft), event.properties.callID) + if (match?.state.status !== "running") return + match.state = { + status: "error", + error: event.properties.error, + input: match.state.input, + structured: match.state.structured, + content: match.state.content, + } + match.provider = event.properties.provider + match.time.completed = event.properties.timestamp + }) + break + case "session.next.reasoning.started": + update(event.properties.sessionID, (draft) => { + activeAssistant(draft)?.content.push({ + type: "reasoning", + id: event.properties.reasoningID, + text: "", + }) + }) + break + case "session.next.reasoning.delta": + update(event.properties.sessionID, (draft) => { + const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID) + if (match) match.text += event.properties.delta + }) + break + case "session.next.reasoning.ended": + update(event.properties.sessionID, (draft) => { + const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID) + if (match) match.text = event.properties.text + }) + break + case "session.next.retried": + break + case "session.next.compaction.started": + update(event.properties.sessionID, (draft) => { + draft.push({ + id: event.properties.id, + type: "compaction", + reason: event.properties.reason, + summary: "", + time: { created: event.properties.timestamp }, + }) + }) + break + case "session.next.compaction.delta": + update(event.properties.sessionID, (draft) => { + const match = activeCompaction(draft) + if (match) match.summary += event.properties.text + }) + break + case "session.next.compaction.ended": + update(event.properties.sessionID, (draft) => { + const match = activeCompaction(draft) + if (!match) return + match.summary = event.properties.text + match.include = event.properties.include + }) + break + } + }) + + const result = { + data: store, + session: { + message: { + async sync(sessionID: string) { + const response = await sdk.client.v2.session.messages({ + sessionID, + }) + setStore("messages", sessionID, reconcile(response.data ?? [])) + }, + fromSession(sessionID: string) { + const messages = store.messages[sessionID] + if (!messages) return [] + return messages + }, + }, + }, + } + + return result + }, +}) diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx new file mode 100644 index 000000000000..7d2814b1c624 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx @@ -0,0 +1,970 @@ +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" +import { useSyncV2 } from "@tui/context/sync-v2" +import { SplitBorder } from "@tui/component/border" +import { Spinner } from "@tui/component/spinner" +import { useTheme } from "@tui/context/theme" +import { useLocal } from "@tui/context/local" +import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid" +import type { SyntaxStyle } from "@opentui/core" +import { Locale } from "@/util/locale" +import { LANGUAGE_EXTENSIONS } from "@/lsp/language" +import path from "path" +import stripAnsi from "strip-ansi" +import type { + SessionMessage, + SessionMessageAssistant, + SessionMessageAssistantReasoning, + SessionMessageAssistantText, + SessionMessageAssistantTool, + SessionMessageCompaction, + SessionMessageSynthetic, + SessionMessageUser, + ToolFileContent, + ToolTextContent, +} from "@opencode-ai/sdk/v2" +import { createEffect, createMemo, For, Match, Show, Switch } from "solid-js" + +const id = "internal:session-v2-debug" +const route = "session.v2.messages" + +function currentSessionID(api: TuiPluginApi) { + const current = api.route.current + if (current.name !== "session") return + const sessionID = current.params?.sessionID + return typeof sessionID === "string" ? sessionID : undefined +} + +function View(props: { api: TuiPluginApi; sessionID: string }) { + const sync = useSyncV2() + const dimensions = useTerminalDimensions() + const { theme, syntax, subtleSyntax } = useTheme() + const messages = createMemo(() => sync.data.messages[props.sessionID] ?? []) + const lastAssistant = createMemo(() => messages().findLast((message) => message.type === "assistant")) + + createEffect(() => { + void sync.session.message.sync(props.sessionID) + }) + + useKeyboard((event) => { + if (event.name !== "escape") return + event.preventDefault() + event.stopPropagation() + props.api.route.navigate("session", { sessionID: props.sessionID }) + }) + + return ( + + + + + + + + + + {(message, index) => ( + + + + + + + + + + + + + + + + + + )} + + + + + + + ) +} + +function MissingData(props: { label: string; detail: string }) { + const { theme } = useTheme() + return ( + + + MISSING DATA {props.label} + + {props.detail} + + ) +} + +function UserMessage(props: { message: SessionMessageUser; index: number }) { + const { theme } = useTheme() + const attachments = createMemo(() => [...(props.message.files ?? []), ...(props.message.agents ?? [])]) + return ( + + + + } + > + {props.message.text} + + + + + {(file) => ( + + {file.mime} + {file.name ?? file.uri} + + )} + + + {(agent) => ( + + agent + {agent.name} + + )} + + + + {Locale.todayTimeOrDateTime(props.message.time.created)} + + + ) +} + +function SyntheticMessage(props: { message: SessionMessageSynthetic; index: number }) { + const { theme } = useTheme() + return ( + + Synthetic + {props.message.text} + + ) +} + +function CompactionMessage(props: { message: SessionMessageCompaction }) { + const { theme } = useTheme() + return ( + + + {props.message.summary} + + + ) +} + +function UnknownMessage(props: { message: SessionMessage }) { + return +} + +function AssistantMessage(props: { + message: SessionMessageAssistant + last: boolean + syntax: SyntaxStyle + subtleSyntax: SyntaxStyle +}) { + const { theme } = useTheme() + const local = useLocal() + const duration = createMemo(() => { + if (!props.message.time.completed) return 0 + return props.message.time.completed - props.message.time.created + }) + const model = createMemo(() => { + const variant = props.message.model.variant ? `/${props.message.model.variant}` : "" + return `${props.message.model.providerID}/${props.message.model.id}${variant}` + }) + const final = createMemo(() => props.message.finish && !["tool-calls", "unknown"].includes(props.message.finish)) + return ( + <> + + {(part) => ( + + + + + + + + + + + + )} + + + + + + + {props.message.error} + + + + + + + {Locale.titlecase(props.message.agent)} + · {model()} + + · {Locale.duration(duration())} + + + + + + ) +} + +function AssistantText(props: { part: SessionMessageAssistantText; syntax: SyntaxStyle }) { + const { theme } = useTheme() + return ( + + + + + + ) +} + +function AssistantReasoning(props: { part: SessionMessageAssistantReasoning; subtleSyntax: SyntaxStyle }) { + const { theme } = useTheme() + const content = createMemo(() => props.part.text.replace("[REDACTED]", "").trim()) + return ( + + + + + + ) +} + +function AssistantTool(props: { part: SessionMessageAssistantTool }) { + const input = createMemo(() => toolInputRecord(props.part.state.input)) + const toolprops = { + get input() { + return input() + }, + get metadata() { + return props.part.provider?.metadata ?? {} + }, + get output() { + return props.part.state.status === "pending" ? undefined : toolOutput(props.part.state.content) + }, + part: props.part, + } + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +type ToolProps = { + input: Record + metadata: Record + output?: string + part: SessionMessageAssistantTool +} + +function GenericTool(props: ToolProps) { + return ( + + {props.part.name} {input(props.input)} + + } + > + + {props.output} + + + ) +} + +function InlineTool(props: { + icon: string + complete: unknown + pending: string + spinner?: boolean + children: JSX.Element + part: SessionMessageAssistantTool +}) { + const { theme } = useTheme() + const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error.message : undefined)) + const denied = createMemo(() => { + const message = error() + if (!message) return false + return ( + message.includes("QuestionRejectedError") || + message.includes("rejected permission") || + message.includes("user dismissed") + ) + }) + return ( + + + + {props.children} + + + + ~ {props.pending}} when={props.complete}> + {props.icon} {props.children} + + + + + + {error()} + + + ) +} + +function BlockTool(props: { + title: string + children: JSX.Element + part: SessionMessageAssistantTool + spinner?: boolean +}) { + const { theme } = useTheme() + const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error.message : undefined)) + return ( + + + {props.title} + + } + > + {props.title.replace(/^# /, "")} + + {props.children} + + {error()} + + + ) +} + +function Bash(props: ToolProps) { + const { theme } = useTheme() + const output = createMemo(() => stripAnsi((stringValue(props.metadata.output) ?? props.output ?? "").trim())) + const command = createMemo(() => stringValue(props.input.command) ?? pendingInput(props.part)) + const title = createMemo(() => `# ${stringValue(props.input.description) ?? "Shell"}`) + return ( + + + + + $ {command()} + {output()} + + + + + + {command()} + + + + ) +} + +function Glob(props: ToolProps) { + return ( + + Glob "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "} + in {normalizePath(stringValue(props.input.path))} + + {(count) => ( + <> + ({count()} {count() === 1 ? "match" : "matches"}) + + )} + + + ) +} + +function Read(props: ToolProps) { + const { theme } = useTheme() + const loaded = createMemo(() => + arrayValue(props.metadata.loaded).filter((item): item is string => typeof item === "string"), + ) + return ( + <> + + Read {normalizePath(stringValue(props.input.filePath) ?? pendingInput(props.part))}{" "} + {input(props.input, ["filePath"])} + + + {(filepath) => ( + + + ↳ Loaded {normalizePath(filepath)} + + + )} + + + ) +} + +function Grep(props: ToolProps) { + return ( + + Grep "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "} + in {normalizePath(stringValue(props.input.path))} + + {(matches) => ( + <> + ({matches()} {matches() === 1 ? "match" : "matches"}) + + )} + + + ) +} + +function WebFetch(props: ToolProps) { + return ( + + WebFetch {stringValue(props.input.url) ?? pendingInput(props.part)} + + ) +} + +function CodeSearch(props: ToolProps) { + return ( + + Exa Code Search "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "} + {(results) => <>({results()} results)} + + ) +} + +function WebSearch(props: ToolProps) { + return ( + + Exa Web Search "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "} + {(results) => <>({results()} results)} + + ) +} + +function Write(props: ToolProps) { + const { theme, syntax } = useTheme() + const filePath = createMemo(() => stringValue(props.input.filePath) ?? "") + const content = createMemo(() => stringValue(props.input.content) ?? "") + return ( + + + + + + + + + + + + Write {normalizePath(filePath())} + + + + ) +} + +function Edit(props: ToolProps) { + const { theme, syntax } = useTheme() + const dimensions = useTerminalDimensions() + const filePath = createMemo(() => stringValue(props.input.filePath) ?? "") + const diff = createMemo(() => stringValue(props.metadata.diff)) + return ( + + + {(diff) => ( + + + 120 ? "split" : "unified"} + filetype={filetype(filePath())} + syntaxStyle={syntax()} + showLineNumbers={true} + width="100%" + wrapMode="word" + fg={theme.text} + addedBg={theme.diffAddedBg} + removedBg={theme.diffRemovedBg} + contextBg={theme.diffContextBg} + addedSignColor={theme.diffHighlightAdded} + removedSignColor={theme.diffHighlightRemoved} + lineNumberFg={theme.diffLineNumber} + lineNumberBg={theme.diffContextBg} + addedLineNumberBg={theme.diffAddedLineNumberBg} + removedLineNumberBg={theme.diffRemovedLineNumberBg} + /> + + + + )} + + + + Edit {normalizePath(filePath())} {input({ replaceAll: props.input.replaceAll })} + + + + ) +} + +function ApplyPatch(props: ToolProps) { + const { theme, syntax } = useTheme() + const dimensions = useTerminalDimensions() + const files = createMemo(() => arrayValue(props.metadata.files).flatMap((item) => (isRecord(item) ? [item] : []))) + const fileTitle = (file: Record) => { + const type = stringValue(file.type) + const relativePath = stringValue(file.relativePath) ?? stringValue(file.filePath) ?? "patch" + if (type === "delete") return "# Deleted " + relativePath + if (type === "add") return "# Created " + relativePath + if (type === "move") return "# Moved " + normalizePath(stringValue(file.filePath)) + " → " + relativePath + return "← Patched " + relativePath + } + return ( + + 0}> + + {(file) => ( + + + -{numberValue(file.deletions) ?? 0} line{numberValue(file.deletions) === 1 ? "" : "s"} + + } + > + {(patch) => ( + + 120 ? "split" : "unified"} + filetype={filetype(stringValue(file.filePath) ?? stringValue(file.relativePath))} + syntaxStyle={syntax()} + showLineNumbers={true} + width="100%" + wrapMode="word" + fg={theme.text} + addedBg={theme.diffAddedBg} + removedBg={theme.diffRemovedBg} + contextBg={theme.diffContextBg} + addedSignColor={theme.diffHighlightAdded} + removedSignColor={theme.diffHighlightRemoved} + lineNumberFg={theme.diffLineNumber} + lineNumberBg={theme.diffContextBg} + addedLineNumberBg={theme.diffAddedLineNumberBg} + removedLineNumberBg={theme.diffRemovedLineNumberBg} + /> + + )} + + + )} + + + + + Patch + + + + ) +} + +function TodoWrite(props: ToolProps) { + const { theme } = useTheme() + const todos = createMemo(() => arrayValue(props.input.todos).flatMap((item) => (isRecord(item) ? [item] : []))) + return ( + + 0 && props.part.state.status === "completed"}> + + + + {(todo) => ( + + {todoIcon(stringValue(todo.status))} {stringValue(todo.content)} + + )} + + + + + + + Updating todos... + + + + ) +} + +function Question(props: ToolProps) { + const { theme } = useTheme() + const questions = createMemo(() => + arrayValue(props.input.questions).flatMap((item) => (isRecord(item) ? [item] : [])), + ) + const answers = createMemo(() => arrayValue(props.metadata.answers)) + return ( + + 0}> + + + + {(question, index) => ( + + {stringValue(question.question)} + {formatAnswer(answers()[index()])} + + )} + + + + + + + Asked {questions().length} question{questions().length === 1 ? "" : "s"} + + + + ) +} + +function Skill(props: ToolProps) { + return ( + + Skill "{stringValue(props.input.name) ?? pendingInput(props.part)}" + + ) +} + +function Task(props: ToolProps) { + const content = createMemo(() => { + const description = stringValue(props.input.description) + if (!description) return pendingInput(props.part) + return `${Locale.titlecase(stringValue(props.input.subagent_type) ?? "General")} Task — ${description}` + }) + return ( + + {content()} + + ) +} + +function Diagnostics(props: { diagnostics: unknown; filePath: string }) { + const { theme } = useTheme() + const errors = createMemo(() => { + if (!isRecord(props.diagnostics)) return [] + const value = props.diagnostics[normalizePath(props.filePath)] ?? props.diagnostics[props.filePath] + return arrayValue(value) + .flatMap((item) => (isRecord(item) ? [item] : [])) + .filter((diagnostic) => diagnostic.severity === 1) + .slice(0, 3) + }) + return ( + + + + {(diagnostic) => Error {stringValue(diagnostic.message)}} + + + + ) +} + +function toolOutput(content?: Array) { + return (content ?? []) + .map((item) => { + if (item.type === "text") return item.text.trim() + return `[file ${item.name ?? item.uri}]` + }) + .filter(Boolean) + .join("\n") +} + +function toolInputRecord(input: string | Record) { + if (typeof input === "string") return {} + return input +} + +function pendingInput(part: SessionMessageAssistantTool) { + if (part.state.status !== "pending") return "" + return part.state.input.trim() +} + +function toolComplete(part: SessionMessageAssistantTool) { + if (part.state.status === "pending") return pendingInput(part) + return part.state.status === "completed" || part.state.status === "error" || part.state.status === "running" +} + +function stringValue(value: unknown) { + return typeof value === "string" ? value : undefined +} + +function numberValue(value: unknown) { + return typeof value === "number" ? value : undefined +} + +function arrayValue(value: unknown): unknown[] { + return Array.isArray(value) ? value : [] +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value) +} + +function input(input: Record, omit?: string[]) { + const primitives = Object.entries(input).filter(([key, value]) => { + if (omit?.includes(key)) return false + return typeof value === "string" || typeof value === "number" || typeof value === "boolean" + }) + if (primitives.length === 0) return "" + return `[${primitives.map(([key, value]) => `${key}=${value}`).join(", ")}]` +} + +function normalizePath(input?: string) { + if (!input) return "" + const absolute = path.isAbsolute(input) ? input : path.resolve(process.cwd(), input) + const relative = path.relative(process.cwd(), absolute) + if (!relative) return "." + if (!relative.startsWith("..")) return relative + return absolute +} + +function filetype(input?: string) { + if (!input) return "none" + const language = LANGUAGE_EXTENSIONS[path.extname(input)] + if (["typescriptreact", "javascriptreact", "javascript"].includes(language)) return "typescript" + return language +} + +function todoIcon(status?: string) { + if (status === "completed") return "✓" + if (status === "in_progress") return "~" + if (status === "cancelled") return "✕" + return "☐" +} + +function formatAnswer(answer: unknown) { + if (!Array.isArray(answer)) return "(no answer)" + if (answer.length === 0) return "(no answer)" + return answer.filter((item): item is string => typeof item === "string").join(", ") +} + +const tui: TuiPlugin = async (api) => { + api.route.register([ + { + name: route, + render(input) { + const sessionID = input.params?.sessionID + if (typeof sessionID !== "string") { + return Missing sessionID + } + return + }, + }, + ]) + + api.command.register(() => [ + { + title: "View v2 session messages", + value: route, + category: "Debug", + suggested: api.route.current.name === "session", + enabled: api.route.current.name === "session", + onSelect() { + const sessionID = currentSessionID(api) + if (!sessionID) return + api.route.navigate(route, { sessionID }) + }, + }, + ]) +} + +const plugin: TuiPluginModule & { id: string } = { + id, + tui, +} + +export default plugin diff --git a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts index 856ee0ebb156..156d66ba9808 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts @@ -7,6 +7,7 @@ import SidebarTodo from "../feature-plugins/sidebar/todo" import SidebarFiles from "../feature-plugins/sidebar/files" import SidebarFooter from "../feature-plugins/sidebar/footer" import PluginManager from "../feature-plugins/system/plugins" +import SessionV2Debug from "../feature-plugins/system/session-v2" import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui" export type InternalTuiPlugin = TuiPluginModule & { @@ -24,4 +25,5 @@ export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [ SidebarFiles, SidebarFooter, PluginManager, + SessionV2Debug, ] diff --git a/packages/opencode/src/server/routes/instance/httpapi/api.ts b/packages/opencode/src/server/routes/instance/httpapi/api.ts index 81ea2394c061..722be3e66748 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/api.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/api.ts @@ -19,6 +19,7 @@ import { SessionApi } from "./groups/session" import { SyncApi } from "./groups/sync" import { TuiApi } from "./groups/tui" import { WorkspaceApi } from "./groups/workspace" +import { V2Api } from "./v2" // SSE event schemas built from the same BusEvent/SyncEvent registries that // the Hono spec uses, so both specs emit identical Event/SyncEvent components. @@ -40,6 +41,7 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance") .addHttpApi(ProviderApi) .addHttpApi(SessionApi) .addHttpApi(SyncApi) + .addHttpApi(V2Api) .addHttpApi(TuiApi) .addHttpApi(WorkspaceApi) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts new file mode 100644 index 000000000000..dac1b0336966 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts @@ -0,0 +1,87 @@ +import { SessionMessage } from "@/v2/session-message" +import { SessionV2 } from "@/v2/session" +import { Effect, Layer, Schema } from "effect" +import * as DateTime from "effect/DateTime" +import { HttpApiBuilder, HttpApiError, HttpApiSchema } from "effect/unstable/httpapi" +import { InstanceHttpApi } from "../api" + +const DefaultMessagesLimit = 50 + +const Cursor = Schema.Struct({ + id: SessionMessage.ID, + time: Schema.Number, +}) + +const decodeCursor = Schema.decodeUnknownSync(Cursor) + +const cursor = { + encode(message: SessionMessage.Message) { + return Buffer.from( + JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created) }), + ).toString("base64url") + }, + decode(input: string) { + return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) + }, +} + +export const v2Handlers = HttpApiBuilder.group(InstanceHttpApi, "v2", (handlers) => + Effect.gen(function* () { + const session = yield* SessionV2.Service + + return handlers + .handle( + "messages", + Effect.fn(function* (ctx) { + if (ctx.query.before && ctx.query.after) return yield* new HttpApiError.BadRequest({}) + if (ctx.query.from && (ctx.query.before || ctx.query.after)) return yield* new HttpApiError.BadRequest({}) + const decoded = yield* Effect.try({ + try: () => { + return { + before: ctx.query.before ? cursor.decode(ctx.query.before) : undefined, + after: ctx.query.after ? cursor.decode(ctx.query.after) : undefined, + } + }, + catch: () => new HttpApiError.BadRequest({}), + }) + const messages = yield* session.messages({ + sessionID: ctx.params.sessionID, + limit: ctx.query.limit ?? DefaultMessagesLimit, + cursor: decoded.before ?? decoded.after, + direction: decoded.after ? "after" : "before", + }) + const oldest = messages[0] + const newest = messages.at(-1) + return { + items: messages, + before: oldest ? cursor.encode(oldest) : undefined, + after: newest ? cursor.encode(newest) : undefined, + } + }), + ) + .handle( + "prompt", + Effect.fn(function* (ctx) { + return yield* session.prompt({ + sessionID: ctx.params.sessionID, + prompt: ctx.payload.prompt, + delivery: ctx.payload.delivery ?? SessionV2.DefaultDelivery, + }) + }), + ) + .handle( + "compact", + Effect.fn(function* (ctx) { + yield* session.compact(ctx.params.sessionID) + return HttpApiSchema.NoContent.make() + }), + ) + .handle( + "wait", + Effect.fn(function* (ctx) { + yield* session.wait(ctx.params.sessionID) + return HttpApiSchema.NoContent.make() + }), + ) + }), +).pipe(Layer.provide(SessionV2.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 0b4bc252c3d1..e53eca3effa0 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -64,6 +64,7 @@ import { questionHandlers } from "./handlers/question" import { sessionHandlers } from "./handlers/session" import { syncHandlers } from "./handlers/sync" import { tuiHandlers } from "./handlers/tui" +import { v2Handlers } from "./handlers/v2" import { workspaceHandlers } from "./handlers/workspace" import { instanceContextLayer, instanceRouterMiddleware } from "./middleware/instance-context" import { workspaceRouterMiddleware, workspaceRoutingLayer } from "./middleware/workspace-routing" @@ -115,6 +116,7 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe( providerHandlers, sessionHandlers, syncHandlers, + v2Handlers, tuiHandlers, workspaceHandlers, ]), diff --git a/packages/opencode/src/server/routes/instance/httpapi/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/v2.ts new file mode 100644 index 000000000000..4d54cad32fe3 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/v2.ts @@ -0,0 +1,110 @@ +import { SessionID } from "@/session/schema" +import { SessionMessage } from "@/v2/session-message" +import { Prompt } from "@/v2/session-prompt" +import { SessionV2 } from "@/v2/session" +import { Schema } from "effect" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./middleware/authorization" + +export const V2Api = HttpApi.make("v2") + .add( + HttpApiGroup.make("v2") + .add( + HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", { + params: { sessionID: SessionID }, + query: Schema.Struct({ + limit: Schema.optional( + Schema.NumberFromString.check( + Schema.isInt(), + Schema.isGreaterThanOrEqualTo(1), + Schema.isLessThanOrEqualTo(200), + ), + ).annotate({ + description: + "Maximum number of messages to return. When omitted, the endpoint returns its default page size. Use limit without a cursor to fetch the newest page for chat history.", + }), + before: Schema.optional(Schema.String).annotate({ + description: + "Opaque pagination cursor for the item at the start of the current window. Returns messages older than this cursor. Mutually exclusive with after.", + }), + after: Schema.optional(Schema.String).annotate({ + description: + "Opaque pagination cursor for the item at the end of the current window. Returns messages newer than this cursor. Mutually exclusive with before.", + }), + from: Schema.optional(Schema.Literal("start")).annotate({ + description: + "Start from the beginning of session history instead of the newest messages. Mutually exclusive with before and after.", + }), + }).annotate({ identifier: "V2SessionMessagesQuery" }), + success: Schema.Struct({ + items: Schema.Array(SessionMessage.Message), + before: Schema.String.pipe(Schema.optional), + after: Schema.String.pipe(Schema.optional), + }).annotate({ identifier: "V2SessionMessagesResponse" }), + error: HttpApiError.BadRequest, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.messages", + summary: "Get v2 session messages", + description: + "Retrieve projected v2 messages for a session. For chat clients, request the latest page with limit, page backward through older history with before, and catch up with newer messages using after.", + }), + ), + ) + .add( + HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", { + params: { sessionID: SessionID }, + payload: Schema.Struct({ + prompt: Prompt, + delivery: SessionV2.Delivery.pipe(Schema.optional), + }), + success: SessionMessage.Message, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.prompt", + summary: "Send v2 message", + description: "Create a v2 session message and queue it for the agent loop.", + }), + ), + ) + .add( + HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", { + params: { sessionID: SessionID }, + success: HttpApiSchema.NoContent, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.compact", + summary: "Compact v2 session", + description: "Compact a v2 session conversation.", + }), + ), + ) + .add( + HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", { + params: { sessionID: SessionID }, + success: HttpApiSchema.NoContent, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.wait", + summary: "Wait for v2 session", + description: "Wait for a v2 session agent loop to become idle.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "v2", + description: "Experimental v2 routes.", + }), + ) + .middleware(Authorization), + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + +export * as V2HttpApi from "./v2" diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index f0da2f3d856a..95fc0d7347c7 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -1,7 +1,8 @@ import { describeRoute, resolver, validator } from "hono-openapi" import { Hono } from "hono" import type { UpgradeWebSocket } from "hono/ws" -import { Effect } from "effect" +import { Context, Effect } from "effect" +import { Flag } from "@opencode-ai/core/flag/flag" import z from "zod" import { Format } from "@/format" import { TuiRoutes } from "./tui" @@ -25,11 +26,135 @@ import { ExperimentalRoutes } from "./experimental" import { ProviderRoutes } from "./provider" import { EventRoutes } from "./event" import { SyncRoutes } from "./sync" +import { InstanceMiddleware } from "./middleware" import { jsonRequest } from "./trace" +import { ExperimentalHttpApiServer } from "./httpapi/server" +import { EventPaths } from "./httpapi/event" +import { ExperimentalPaths } from "./httpapi/groups/experimental" +import { FilePaths } from "./httpapi/groups/file" +import { InstancePaths } from "./httpapi/groups/instance" +import { McpPaths } from "./httpapi/groups/mcp" +import { PtyPaths } from "./httpapi/groups/pty" +import { SessionPaths } from "./httpapi/groups/session" +import { SyncPaths } from "./httpapi/groups/sync" +import { TuiPaths } from "./httpapi/groups/tui" +import { WorkspacePaths } from "./httpapi/groups/workspace" export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { const app = new Hono() + if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { + const handler = ExperimentalHttpApiServer.webHandler().handler + const context = Context.empty() as Context.Context + app.all("/api/*", (c) => handler(c.req.raw, context)) + app.get(EventPaths.event, (c) => handler(c.req.raw, context)) + app.get("/question", (c) => handler(c.req.raw, context)) + app.post("/question/:requestID/reply", (c) => handler(c.req.raw, context)) + app.post("/question/:requestID/reject", (c) => handler(c.req.raw, context)) + app.get("/permission", (c) => handler(c.req.raw, context)) + app.post("/permission/:requestID/reply", (c) => handler(c.req.raw, context)) + app.get("/config", (c) => handler(c.req.raw, context)) + app.patch("/config", (c) => handler(c.req.raw, context)) + app.get("/config/providers", (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.console, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context)) + app.post(ExperimentalPaths.consoleSwitch, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.tool, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.toolIDs, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context)) + app.post(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context)) + app.delete(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context)) + app.post(ExperimentalPaths.worktreeReset, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.session, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.resource, (c) => handler(c.req.raw, context)) + app.get("/provider", (c) => handler(c.req.raw, context)) + app.get("/provider/auth", (c) => handler(c.req.raw, context)) + app.post("/provider/:providerID/oauth/authorize", (c) => handler(c.req.raw, context)) + app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context)) + app.get("/project", (c) => handler(c.req.raw, context)) + app.get("/project/current", (c) => handler(c.req.raw, context)) + app.post("/project/git/init", (c) => handler(c.req.raw, context)) + app.patch("/project/:projectID", (c) => handler(c.req.raw, context)) + app.get(FilePaths.findText, (c) => handler(c.req.raw, context)) + app.get(FilePaths.findFile, (c) => handler(c.req.raw, context)) + app.get(FilePaths.findSymbol, (c) => handler(c.req.raw, context)) + app.get(FilePaths.list, (c) => handler(c.req.raw, context)) + app.get(FilePaths.content, (c) => handler(c.req.raw, context)) + app.get(FilePaths.status, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.path, (c) => handler(c.req.raw, context)) + app.post(InstancePaths.dispose, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.vcs, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.vcsDiff, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.command, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.agent, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.skill, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.lsp, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.formatter, (c) => handler(c.req.raw, context)) + app.get(McpPaths.status, (c) => handler(c.req.raw, context)) + app.post(McpPaths.status, (c) => handler(c.req.raw, context)) + app.post(McpPaths.auth, (c) => handler(c.req.raw, context)) + app.post(McpPaths.authCallback, (c) => handler(c.req.raw, context)) + app.post(McpPaths.authAuthenticate, (c) => handler(c.req.raw, context)) + app.delete(McpPaths.auth, (c) => handler(c.req.raw, context)) + app.post(McpPaths.connect, (c) => handler(c.req.raw, context)) + app.post(McpPaths.disconnect, (c) => handler(c.req.raw, context)) + app.post(SyncPaths.start, (c) => handler(c.req.raw, context)) + app.post(SyncPaths.replay, (c) => handler(c.req.raw, context)) + app.post(SyncPaths.history, (c) => handler(c.req.raw, context)) + app.get(PtyPaths.list, (c) => handler(c.req.raw, context)) + app.post(PtyPaths.create, (c) => handler(c.req.raw, context)) + app.get(PtyPaths.get, (c) => handler(c.req.raw, context)) + app.put(PtyPaths.update, (c) => handler(c.req.raw, context)) + app.delete(PtyPaths.remove, (c) => handler(c.req.raw, context)) + app.get(PtyPaths.connect, (c) => handler(c.req.raw, context)) + app.get(SessionPaths.list, (c) => handler(c.req.raw, context)) + app.get(SessionPaths.status, (c) => handler(c.req.raw, context)) + app.get(SessionPaths.get, (c) => handler(c.req.raw, context)) + app.get(SessionPaths.children, (c) => handler(c.req.raw, context)) + app.get(SessionPaths.todo, (c) => handler(c.req.raw, context)) + app.get(SessionPaths.diff, (c) => handler(c.req.raw, context)) + app.get(SessionPaths.messages, (c) => handler(c.req.raw, context)) + app.get(SessionPaths.message, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.create, (c) => handler(c.req.raw, context)) + app.delete(SessionPaths.remove, (c) => handler(c.req.raw, context)) + app.patch(SessionPaths.update, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.init, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.fork, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.abort, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.share, (c) => handler(c.req.raw, context)) + app.delete(SessionPaths.share, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.summarize, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.prompt, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.promptAsync, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.command, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.shell, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.revert, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.unrevert, (c) => handler(c.req.raw, context)) + app.post(SessionPaths.permissions, (c) => handler(c.req.raw, context)) + app.delete(SessionPaths.deleteMessage, (c) => handler(c.req.raw, context)) + app.delete(SessionPaths.deletePart, (c) => handler(c.req.raw, context)) + app.patch(SessionPaths.updatePart, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.appendPrompt, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.openHelp, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.openSessions, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.openThemes, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.openModels, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.submitPrompt, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.clearPrompt, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.executeCommand, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.showToast, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.publish, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.selectSession, (c) => handler(c.req.raw, context)) + app.get(TuiPaths.controlNext, (c) => handler(c.req.raw, context)) + app.post(TuiPaths.controlResponse, (c) => handler(c.req.raw, context)) + app.get(WorkspacePaths.adaptors, (c) => handler(c.req.raw, context)) + app.post(WorkspacePaths.list, (c) => handler(c.req.raw, context)) + app.get(WorkspacePaths.list, (c) => handler(c.req.raw, context)) + app.get(WorkspacePaths.status, (c) => handler(c.req.raw, context)) + app.delete(WorkspacePaths.remove, (c) => handler(c.req.raw, context)) + app.post(WorkspacePaths.sessionRestore, (c) => handler(c.req.raw, context)) + } + return app .route("/project", ProjectRoutes()) .route("/pty", PtyRoutes(upgrade)) @@ -40,6 +165,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { .route("/question", QuestionRoutes()) .route("/provider", ProviderRoutes()) .route("/sync", SyncRoutes()) + .route("/api", V2Routes()) .route("/", FileRoutes()) .route("/", EventRoutes()) .route("/mcp", McpRoutes()) diff --git a/packages/opencode/src/server/routes/instance/v2.ts b/packages/opencode/src/server/routes/instance/v2.ts new file mode 100644 index 000000000000..6338eceb409e --- /dev/null +++ b/packages/opencode/src/server/routes/instance/v2.ts @@ -0,0 +1,43 @@ +import { SessionID } from "@/session/schema" +import { SessionMessage } from "@/v2/session-message" +import { SessionV2 } from "@/v2/session" +import { zod } from "@/util/effect-zod" +import { lazy } from "@/util/lazy" +import { Effect, Schema } from "effect" +import { Hono } from "hono" +import { describeRoute, resolver, validator } from "hono-openapi" +import z from "zod" +import { errors } from "../../error" +import { jsonRequest } from "./trace" + +export const V2Routes = lazy(() => + new Hono().get( + "/session/:sessionID/message", + describeRoute({ + summary: "Get v2 session messages", + description: "Retrieve projected v2 messages for a session directly from the message database.", + operationId: "v2.session.messages", + responses: { + 200: { + description: "List of v2 session messages", + content: { + "application/json": { + schema: resolver(zod(Schema.Array(SessionMessage.Message))), + }, + }, + }, + ...errors(400, 404), + }, + }), + validator("param", z.object({ sessionID: SessionID.zod })), + async (c) => { + const sessionID = c.req.valid("param").sessionID + return jsonRequest("V2Routes.messages", c, function* () { + return yield* Effect.gen(function* () { + const session = yield* SessionV2.Service + return yield* session.messages({ sessionID }) + }).pipe(Effect.provide(SessionV2.defaultLayer)) + }) + }, + ), +) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index aaee2be2feba..e194e783da1d 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -14,10 +14,13 @@ import { Config } from "@/config/config" import { NotFoundError } from "@/storage/storage" import { ModelID, ProviderID } from "@/provider/schema" import { Effect, Layer, Context, Schema } from "effect" +import * as DateTime from "effect/DateTime" import { InstanceState } from "@/effect/instance-state" import { isOverflow as overflow, usable } from "./overflow" import { makeRuntime } from "@/effect/run-service" import { fn } from "@/util/fn" +import { SyncEvent } from "@/sync" +import { SessionEvent } from "@/v2/session-event" const log = Log.create({ service: "session.compaction" }) @@ -556,7 +559,21 @@ export const layer: Layer.Layer< } if (processor.message.error) return "stop" - if (result === "continue") yield* bus.publish(Event.Compacted, { sessionID: input.sessionID }) + if (result === "continue") { + const summary = summaryText( + (yield* session.messages({ sessionID: input.sessionID })).find((item) => item.info.id === msg.id) ?? { + info: msg, + parts: [], + }, + ) + SyncEvent.run(SessionEvent.Compaction.Ended.Sync, { + sessionID: input.sessionID, + timestamp: DateTime.makeUnsafe(Date.now()), + text: summary ?? "", + include: selected.tail_start_id, + }) + yield* bus.publish(Event.Compacted, { sessionID: input.sessionID }) + } return result }) @@ -583,6 +600,12 @@ export const layer: Layer.Layer< auto: input.auto, overflow: input.overflow, }) + SyncEvent.run(SessionEvent.Compaction.Started.Sync, { + id: SessionEvent.ID.create(), + sessionID: input.sessionID, + timestamp: DateTime.makeUnsafe(Date.now()), + reason: input.auto ? "auto" : "manual", + }) }) return Service.of({ diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index b475ec1c5997..6da1013ebc42 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -20,6 +20,9 @@ import { Question } from "@/question" import { errorMessage } from "@/util/error" import * as Log from "@opencode-ai/core/util/log" import { isRecord } from "@/util/record" +import { SyncEvent } from "@/sync" +import { SessionEvent } from "@/v2/session-event" +import * as DateTime from "effect/DateTime" const DOOM_LOOP_THRESHOLD = 3 const log = Log.create({ service: "session.processor" }) @@ -221,6 +224,12 @@ export const layer: Layer.Layer< case "reasoning-start": if (value.id in ctx.reasoningMap) return + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Reasoning.Started.Sync, { + sessionID: ctx.sessionID, + reasoningID: value.id, + timestamp: DateTime.makeUnsafe(Date.now()), + }) ctx.reasoningMap[value.id] = { id: PartID.ascending(), messageID: ctx.assistantMessage.id, @@ -235,6 +244,13 @@ export const layer: Layer.Layer< case "reasoning-delta": if (!(value.id in ctx.reasoningMap)) return + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Reasoning.Delta.Sync, { + sessionID: ctx.sessionID, + reasoningID: value.id, + delta: value.text, + timestamp: DateTime.makeUnsafe(Date.now()), + }) ctx.reasoningMap[value.id].text += value.text if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata yield* session.updatePartDelta({ @@ -248,6 +264,13 @@ export const layer: Layer.Layer< case "reasoning-end": if (!(value.id in ctx.reasoningMap)) return + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Reasoning.Ended.Sync, { + sessionID: ctx.sessionID, + reasoningID: value.id, + text: ctx.reasoningMap[value.id].text, + timestamp: DateTime.makeUnsafe(Date.now()), + }) // oxlint-disable-next-line no-self-assign -- reactivity trigger ctx.reasoningMap[value.id].text = ctx.reasoningMap[value.id].text ctx.reasoningMap[value.id].time = { ...ctx.reasoningMap[value.id].time, end: Date.now() } @@ -260,6 +283,13 @@ export const layer: Layer.Layer< if (ctx.assistantMessage.summary) { throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`) } + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Tool.Input.Started.Sync, { + sessionID: ctx.sessionID, + callID: value.id, + name: value.toolName, + timestamp: DateTime.makeUnsafe(Date.now()), + }) const part = yield* session.updatePart({ id: ctx.toolcalls[value.id]?.partID ?? PartID.ascending(), messageID: ctx.assistantMessage.id, @@ -281,13 +311,34 @@ export const layer: Layer.Layer< case "tool-input-delta": return - case "tool-input-end": + case "tool-input-end": { + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Tool.Input.Ended.Sync, { + sessionID: ctx.sessionID, + callID: value.id, + text: "", + timestamp: DateTime.makeUnsafe(Date.now()), + }) return + } case "tool-call": { if (ctx.assistantMessage.summary) { throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`) } + const toolCall = yield* readToolCall(value.toolCallId) + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Tool.Called.Sync, { + sessionID: ctx.sessionID, + callID: value.toolCallId, + tool: value.toolName, + input: value.input, + provider: { + executed: toolCall?.part.metadata?.providerExecuted === true, + ...(value.providerMetadata ? { metadata: value.providerMetadata } : {}), + }, + timestamp: DateTime.makeUnsafe(Date.now()), + }) yield* updateToolCall(value.toolCallId, (match) => ({ ...match, tool: value.toolName, @@ -331,11 +382,48 @@ export const layer: Layer.Layer< } case "tool-result": { + const toolCall = yield* readToolCall(value.toolCallId) + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Tool.Success.Sync, { + sessionID: ctx.sessionID, + callID: value.toolCallId, + structured: value.output.metadata, + content: [ + { + type: "text", + text: value.output.output, + }, + ...(value.output.attachments?.map((item: MessageV2.FilePart) => ({ + type: "file", + uri: item.url, + mime: item.mime, + name: item.filename, + })) ?? []), + ], + provider: { + executed: toolCall?.part.metadata?.providerExecuted === true, + }, + timestamp: DateTime.makeUnsafe(Date.now()), + }) yield* completeToolCall(value.toolCallId, value.output) return } case "tool-error": { + const toolCall = yield* readToolCall(value.toolCallId) + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Tool.Error.Sync, { + sessionID: ctx.sessionID, + callID: value.toolCallId, + error: { + type: "unknown", + message: errorMessage(value.error), + }, + provider: { + executed: toolCall?.part.metadata?.providerExecuted === true, + }, + timestamp: DateTime.makeUnsafe(Date.now()), + }) yield* failToolCall(value.toolCallId, value.error) return } @@ -345,6 +433,21 @@ export const layer: Layer.Layer< case "start-step": if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track() + if (!ctx.assistantMessage.summary) { + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Step.Started.Sync, { + id: SessionEvent.ID.create(), + sessionID: ctx.sessionID, + agent: input.assistantMessage.agent, + model: { + id: ctx.model.id, + providerID: ctx.model.providerID, + variant: input.assistantMessage.variant, + }, + snapshot: ctx.snapshot, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + } yield* session.updatePart({ id: PartID.ascending(), messageID: ctx.assistantMessage.id, @@ -355,18 +458,30 @@ export const layer: Layer.Layer< return case "finish-step": { + const completedSnapshot = yield* snapshot.track() const usage = Session.getUsage({ model: ctx.model, usage: value.usage, metadata: value.providerMetadata, }) + if (!ctx.assistantMessage.summary) { + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Step.Ended.Sync, { + sessionID: ctx.sessionID, + finish: value.finishReason, + cost: usage.cost, + tokens: usage.tokens, + snapshot: completedSnapshot, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + } ctx.assistantMessage.finish = value.finishReason ctx.assistantMessage.cost += usage.cost ctx.assistantMessage.tokens = usage.tokens yield* session.updatePart({ id: PartID.ascending(), reason: value.finishReason, - snapshot: yield* snapshot.track(), + snapshot: completedSnapshot, messageID: ctx.assistantMessage.id, sessionID: ctx.assistantMessage.sessionID, type: "step-finish", @@ -404,6 +519,13 @@ export const layer: Layer.Layer< } case "text-start": + if (!ctx.assistantMessage.summary) { + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Text.Started.Sync, { + sessionID: ctx.sessionID, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + } ctx.currentText = { id: PartID.ascending(), messageID: ctx.assistantMessage.id, @@ -418,6 +540,13 @@ export const layer: Layer.Layer< case "text-delta": if (!ctx.currentText) return + if (ctx.assistantMessage.summary) { + SyncEvent.run(SessionEvent.Compaction.Delta.Sync, { + sessionID: ctx.sessionID, + text: value.text, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + } ctx.currentText.text += value.text if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata yield* session.updatePartDelta({ @@ -442,6 +571,14 @@ export const layer: Layer.Layer< }, { text: ctx.currentText.text }, )).text + if (!ctx.assistantMessage.summary) { + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Text.Ended.Sync, { + sessionID: ctx.sessionID, + text: ctx.currentText.text, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + } { const end = Date.now() ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end } @@ -568,13 +705,24 @@ export const layer: Layer.Layer< Effect.retry( SessionRetry.policy({ parse, - set: (info) => - status.set(ctx.sessionID, { + set: (info) => { + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Retried.Sync, { + sessionID: ctx.sessionID, + attempt: info.attempt, + error: { + message: info.message, + isRetryable: true, + }, + timestamp: DateTime.makeUnsafe(Date.now()), + }) + return status.set(ctx.sessionID, { type: "retry", attempt: info.attempt, message: info.message, next: info.next, - }), + }) + }, }), ), Effect.catch(halt), diff --git a/packages/opencode/src/session/projectors-next.ts b/packages/opencode/src/session/projectors-next.ts new file mode 100644 index 000000000000..32f5ae83ea54 --- /dev/null +++ b/packages/opencode/src/session/projectors-next.ts @@ -0,0 +1,153 @@ +import { and, desc, eq } from "@/storage/db" +import type { Database } from "@/storage/db" +import { SessionMessage } from "@/v2/session-message" +import { SessionMessageUpdater } from "@/v2/session-message-updater" +import { SessionEvent } from "@/v2/session-event" +import * as DateTime from "effect/DateTime" +import { SyncEvent } from "@/sync" +import { SessionMessageTable } from "./session.sql" +import type { SessionID } from "./schema" +import { Schema } from "effect" + +const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message) +type SessionMessageData = NonNullable + +function encodeDateTimes(value: unknown): unknown { + if (DateTime.isDateTime(value)) return DateTime.toEpochMillis(value) + if (Array.isArray(value)) return value.map(encodeDateTimes) + if (typeof value === "object" && value !== null) { + return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, encodeDateTimes(item)])) + } + return value +} + +function encodeMessageData(value: unknown): SessionMessageData { + return encodeDateTimes(value) as SessionMessageData +} + +function sqlite(db: Database.TxOrDb, sessionID: SessionID): SessionMessageUpdater.Adapter { + return { + getCurrentAssistant() { + return db + .select() + .from(SessionMessageTable) + .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "assistant"))) + .orderBy(desc(SessionMessageTable.id)) + .all() + .map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type })) + .find((message): message is SessionMessage.Assistant => message.type === "assistant" && !message.time.completed) + }, + getCurrentCompaction() { + return db + .select() + .from(SessionMessageTable) + .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction"))) + .orderBy(desc(SessionMessageTable.id)) + .all() + .map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type })) + .find((message): message is SessionMessage.Compaction => message.type === "compaction") + }, + updateAssistant(assistant) { + const { id, type, ...data } = assistant + db.update(SessionMessageTable) + .set({ data: encodeMessageData(data) }) + .where( + and( + eq(SessionMessageTable.id, id), + eq(SessionMessageTable.session_id, sessionID), + eq(SessionMessageTable.type, type), + ), + ) + .run() + }, + updateCompaction(compaction) { + const { id, type, ...data } = compaction + db.update(SessionMessageTable) + .set({ data: encodeMessageData(data) }) + .where( + and( + eq(SessionMessageTable.id, id), + eq(SessionMessageTable.session_id, sessionID), + eq(SessionMessageTable.type, type), + ), + ) + .run() + }, + appendMessage(message) { + const { id, type, ...data } = message + db.insert(SessionMessageTable) + .values([ + { + id, + session_id: sessionID, + type, + time_created: DateTime.toEpochMillis(message.time.created), + data: encodeMessageData(data), + }, + ]) + .run() + }, + finish() {}, + } +} + +function update(db: Database.TxOrDb, event: SessionEvent.Event) { + SessionMessageUpdater.update(sqlite(db, event.data.sessionID), event) +} + +export default [ + SyncEvent.project(SessionEvent.Prompted.Sync, (db, data) => { + update(db, { type: "session.next.prompted", data }) + }), + SyncEvent.project(SessionEvent.Synthetic.Sync, (db, data) => { + update(db, { type: "session.next.synthetic", data }) + }), + SyncEvent.project(SessionEvent.Step.Started.Sync, (db, data) => { + update(db, { type: "session.next.step.started", data }) + }), + SyncEvent.project(SessionEvent.Step.Ended.Sync, (db, data) => { + update(db, { type: "session.next.step.ended", data }) + }), + SyncEvent.project(SessionEvent.Text.Started.Sync, (db, data) => { + update(db, { type: "session.next.text.started", data }) + }), + SyncEvent.project(SessionEvent.Text.Delta.Sync, () => {}), + SyncEvent.project(SessionEvent.Text.Ended.Sync, (db, data) => { + update(db, { type: "session.next.text.ended", data }) + }), + SyncEvent.project(SessionEvent.Tool.Input.Started.Sync, (db, data) => { + update(db, { type: "session.next.tool.input.started", data }) + }), + SyncEvent.project(SessionEvent.Tool.Input.Delta.Sync, () => {}), + SyncEvent.project(SessionEvent.Tool.Input.Ended.Sync, (db, data) => { + update(db, { type: "session.next.tool.input.ended", data }) + }), + SyncEvent.project(SessionEvent.Tool.Called.Sync, (db, data) => { + update(db, { type: "session.next.tool.called", data }) + }), + SyncEvent.project(SessionEvent.Tool.Success.Sync, (db, data) => { + update(db, { type: "session.next.tool.success", data }) + }), + SyncEvent.project(SessionEvent.Tool.Error.Sync, (db, data) => { + update(db, { type: "session.next.tool.error", data }) + }), + SyncEvent.project(SessionEvent.Reasoning.Started.Sync, (db, data) => { + update(db, { type: "session.next.reasoning.started", data }) + }), + SyncEvent.project(SessionEvent.Reasoning.Delta.Sync, () => {}), + SyncEvent.project(SessionEvent.Reasoning.Ended.Sync, (db, data) => { + update(db, { type: "session.next.reasoning.ended", data }) + }), + SyncEvent.project(SessionEvent.Retried.Sync, (db, data) => { + update(db, { type: "session.next.retried", data }) + }), + SyncEvent.project(SessionEvent.Compaction.Started.Sync, (db, data) => { + update(db, { type: "session.next.compaction.started", data }) + }), + SyncEvent.project(SessionEvent.Compaction.Delta.Sync, (db, data) => { + update(db, { type: "session.next.compaction.delta", data }) + }), + SyncEvent.project(SessionEvent.Compaction.Ended.Sync, (db, data) => { + update(db, { type: "session.next.compaction.ended", data }) + }), +] diff --git a/packages/opencode/src/session/projectors.ts b/packages/opencode/src/session/projectors.ts index a3832ebe655c..9819ad810fcd 100644 --- a/packages/opencode/src/session/projectors.ts +++ b/packages/opencode/src/session/projectors.ts @@ -5,7 +5,8 @@ import { SyncEvent } from "@/sync" import * as Session from "./session" import { MessageV2 } from "./message-v2" import { SessionTable, MessageTable, PartTable } from "./session.sql" -import * as Log from "@opencode-ai/core/util/log" +import { Log } from "@opencode-ai/core/util/log" +import nextProjectors from "./projectors-next" const log = Log.create({ service: "session.projector" }) @@ -136,4 +137,6 @@ export default [ log.warn("ignored late part update", { partID: id, messageID, sessionID }) } }), + + ...nextProjectors, ] diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 9f1420388e2e..1bdcc8066e5f 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -54,6 +54,10 @@ import { InstanceState } from "@/effect/instance-state" import { TaskTool, type TaskPromptOps } from "@/tool/task" import { SessionRunState } from "./run-state" import { EffectBridge } from "@/effect/bridge" +import { SessionEvent } from "@/v2/session-event" +import { AgentAttachment, FileAttachment, Source } from "@/v2/session-prompt" +import { SyncEvent } from "@/sync" +import * as DateTime from "effect/DateTime" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -1250,6 +1254,71 @@ NOTE: At any point in time through this workflow you should feel free to ask the yield* sessions.updateMessage(info) for (const part of parts) yield* sessions.updatePart(part) + const nextPrompt = parts.reduce( + (result, part) => { + if (part.type === "text") { + if (part.synthetic) result.synthetic.push(part.text) + else result.text.push(part.text) + } + if (part.type === "file") { + result.files.push( + new FileAttachment({ + uri: part.url, + mime: part.mime, + name: part.filename, + source: part.source + ? new Source({ + start: part.source.text.start, + end: part.source.text.end, + text: part.source.text.value, + }) + : undefined, + }), + ) + } + if (part.type === "agent") { + result.agents.push( + new AgentAttachment({ + name: part.name, + source: part.source + ? new Source({ + start: part.source.start, + end: part.source.end, + text: part.source.value, + }) + : undefined, + }), + ) + } + return result + }, + { + text: [] as string[], + files: [] as FileAttachment[], + agents: [] as AgentAttachment[], + synthetic: [] as string[], + }, + ) + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Prompted.Sync, { + id: SessionEvent.ID.create(), + sessionID: input.sessionID, + timestamp: DateTime.makeUnsafe(info.time.created), + prompt: { + text: nextPrompt.text.join("\n"), + files: nextPrompt.files, + agents: nextPrompt.agents, + }, + }) + for (const text of nextPrompt.synthetic) { + // TODO(v2): Temporary dual-write while migrating session messages to v2 events. + SyncEvent.run(SessionEvent.Synthetic.Sync, { + id: SessionEvent.ID.create(), + sessionID: input.sessionID, + timestamp: DateTime.makeUnsafe(info.time.created), + text, + }) + } return { info, parts } }, Effect.scoped) diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index 863fb21d65c7..ae6351d58843 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -1,7 +1,7 @@ import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core" import { ProjectTable } from "../project/project.sql" import type { MessageV2 } from "./message-v2" -import type { SessionEntry } from "../v2/session-entry" +import type { SessionMessage } from "../v2/session-message" import type { Snapshot } from "../snapshot" import type { Permission } from "../permission" import type { ProjectID } from "../project/schema" @@ -11,6 +11,7 @@ import { Timestamps } from "../storage/schema.sql" type PartData = Omit type InfoData = Omit +type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id"> export const SessionTable = sqliteTable( "session", @@ -96,22 +97,22 @@ export const TodoTable = sqliteTable( ], ) -export const SessionEntryTable = sqliteTable( - "session_entry", +export const SessionMessageTable = sqliteTable( + "session_message", { - id: text().$type().primaryKey(), + id: text().$type().primaryKey(), session_id: text() .$type() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), - type: text().$type().notNull(), + type: text().$type().notNull(), ...Timestamps, - data: text({ mode: "json" }).notNull().$type>(), + data: text({ mode: "json" }).notNull().$type(), }, (table) => [ - index("session_entry_session_idx").on(table.session_id), - index("session_entry_session_type_idx").on(table.session_id, table.type), - index("session_entry_time_created_idx").on(table.time_created), + index("session_message_session_idx").on(table.session_id), + index("session_message_session_type_idx").on(table.session_id, table.type), + index("session_message_time_created_idx").on(table.time_created), ], ) diff --git a/packages/opencode/src/util/effect-zod.ts b/packages/opencode/src/util/effect-zod.ts index 332a5c76ebf4..1c88712d7d1e 100644 --- a/packages/opencode/src/util/effect-zod.ts +++ b/packages/opencode/src/util/effect-zod.ts @@ -90,7 +90,7 @@ function bodyWithChecks(ast: SchemaAST.AST): z.ZodTypeAny { // Schema.withDecodingDefault also attaches encoding, but we want `.default(v)` // on the inner Zod rather than a transform wrapper — so optional ASTs whose // encoding resolves a default from Option.none() route through body()/opt(). - const hasEncoding = ast.encoding?.length && ast._tag !== "Declaration" + const hasEncoding = ast.encoding?.length && (ast._tag !== "Declaration" || ast.typeParameters.length === 0) const hasTransform = hasEncoding && !(SchemaAST.isOptional(ast) && extractDefault(ast) !== undefined) const base = hasTransform ? encoded(ast) : body(ast) return ast.checks?.length ? applyChecks(base, ast.checks, ast) : base diff --git a/packages/opencode/src/v2/event.ts b/packages/opencode/src/v2/event.ts new file mode 100644 index 000000000000..a5c1c4182439 --- /dev/null +++ b/packages/opencode/src/v2/event.ts @@ -0,0 +1,42 @@ +import { Identifier } from "@/id/id" +import { SyncEvent } from "@/sync" +import { withStatics } from "@/util/schema" +import * as Schema from "effect/Schema" + +export const ID = Schema.String.pipe( + Schema.brand("Event.ID"), + withStatics((s) => ({ + create: () => s.make(Identifier.create("evt", "ascending")), + })), +) +export type ID = Schema.Schema.Type + +export function define(input: { + type: Type + schema: Fields + aggregate: string + version?: number +}) { + const Payload = Schema.Struct({ + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + type: Schema.Literal(input.type), + data: Schema.Struct(input.schema), + }).annotate({ + identifier: input.type, + }) + + const Sync = SyncEvent.define({ + type: input.type, + version: input.version ?? 1, + aggregate: input.aggregate, + schema: Payload.fields.data, + }) + + return Object.assign(Payload, { + Sync, + version: input.version, + aggregate: input.aggregate, + }) +} + +export * as Event from "./event" diff --git a/packages/opencode/src/v2/session-entry-stepper.ts b/packages/opencode/src/v2/session-entry-stepper.ts deleted file mode 100644 index 3fe4266c04cb..000000000000 --- a/packages/opencode/src/v2/session-entry-stepper.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { produce, type WritableDraft } from "immer" -import { SessionEvent } from "./session-event" -import { SessionEntry } from "./session-entry" - -export type MemoryState = { - entries: SessionEntry.Entry[] - pending: SessionEntry.Entry[] -} - -export interface Adapter { - readonly getCurrentAssistant: () => SessionEntry.Assistant | undefined - readonly updateAssistant: (assistant: SessionEntry.Assistant) => void - readonly appendEntry: (entry: SessionEntry.Entry) => void - readonly appendPending: (entry: SessionEntry.Entry) => void - readonly finish: () => Result -} - -export function memory(state: MemoryState): Adapter { - const activeAssistantIndex = () => - state.entries.findLastIndex((entry) => entry.type === "assistant" && !entry.time.completed) - - return { - getCurrentAssistant() { - const index = activeAssistantIndex() - if (index < 0) return - const assistant = state.entries[index] - return assistant?.type === "assistant" ? assistant : undefined - }, - updateAssistant(assistant) { - const index = activeAssistantIndex() - if (index < 0) return - const current = state.entries[index] - if (current?.type !== "assistant") return - state.entries[index] = assistant - }, - appendEntry(entry) { - state.entries.push(entry) - }, - appendPending(entry) { - state.pending.push(entry) - }, - finish() { - return state - }, - } -} - -export function stepWith(adapter: Adapter, event: SessionEvent.Event): Result { - const currentAssistant = adapter.getCurrentAssistant() - type DraftAssistant = WritableDraft - type DraftTool = WritableDraft - type DraftText = WritableDraft - type DraftReasoning = WritableDraft - - const latestTool = (assistant: DraftAssistant | undefined, callID?: string) => - assistant?.content.findLast( - (item): item is DraftTool => item.type === "tool" && (callID === undefined || item.callID === callID), - ) - - const latestText = (assistant: DraftAssistant | undefined) => - assistant?.content.findLast((item): item is DraftText => item.type === "text") - - const latestReasoning = (assistant: DraftAssistant | undefined) => - assistant?.content.findLast((item): item is DraftReasoning => item.type === "reasoning") - - SessionEvent.Event.match(event, { - prompt: (event) => { - const entry = SessionEntry.User.fromEvent(event) - if (currentAssistant) { - adapter.appendPending(entry) - return - } - adapter.appendEntry(entry) - }, - synthetic: (event) => { - adapter.appendEntry(SessionEntry.Synthetic.fromEvent(event)) - }, - "step.started": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.time.completed = event.timestamp - }), - ) - } - adapter.appendEntry(SessionEntry.Assistant.fromEvent(event)) - }, - "step.ended": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.time.completed = event.timestamp - draft.cost = event.cost - draft.tokens = event.tokens - }), - ) - } - }, - "text.started": () => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.content.push({ - type: "text", - text: "", - }) - }), - ) - } - }, - "text.delta": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestText(draft) - if (match) match.text += event.delta - }), - ) - } - }, - "text.ended": () => {}, - "tool.input.started": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.content.push({ - type: "tool", - callID: event.callID, - name: event.name, - time: { - created: event.timestamp, - }, - state: { - status: "pending", - input: "", - }, - }) - }), - ) - } - }, - "tool.input.delta": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.callID) - // oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string) - if (match && match.state.status === "pending") match.state.input += event.delta - }), - ) - } - }, - "tool.input.ended": () => {}, - "tool.called": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.callID) - if (match) { - match.time.ran = event.timestamp - match.state = { - status: "running", - input: event.input, - } - } - }), - ) - } - }, - "tool.success": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.callID) - if (match && match.state.status === "running") { - match.state = { - status: "completed", - input: match.state.input, - output: event.output ?? "", - title: event.title, - metadata: event.metadata ?? {}, - attachments: [...(event.attachments ?? [])], - } - } - }), - ) - } - }, - "tool.error": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestTool(draft, event.callID) - if (match && match.state.status === "running") { - match.state = { - status: "error", - error: event.error, - input: match.state.input, - metadata: event.metadata ?? {}, - } - } - }), - ) - } - }, - "reasoning.started": () => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.content.push({ - type: "reasoning", - text: "", - }) - }), - ) - } - }, - "reasoning.delta": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestReasoning(draft) - if (match) match.text += event.delta - }), - ) - } - }, - "reasoning.ended": (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - const match = latestReasoning(draft) - if (match) match.text = event.text - }), - ) - } - }, - retried: (event) => { - if (currentAssistant) { - adapter.updateAssistant( - produce(currentAssistant, (draft) => { - draft.retries = [...(draft.retries ?? []), SessionEntry.AssistantRetry.fromEvent(event)] - }), - ) - } - }, - compacted: (event) => { - adapter.appendEntry(SessionEntry.Compaction.fromEvent(event)) - }, - }) - - return adapter.finish() -} - -export function step(old: MemoryState, event: SessionEvent.Event): MemoryState { - return produce(old, (draft) => { - stepWith(memory(draft as MemoryState), event) - }) -} - -export * as SessionEntryStepper from "./session-entry-stepper" diff --git a/packages/opencode/src/v2/session-entry.ts b/packages/opencode/src/v2/session-entry.ts deleted file mode 100644 index 66576a688e7e..000000000000 --- a/packages/opencode/src/v2/session-entry.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { Schema } from "effect" -import { NonNegativeInt } from "@/util/schema" -import { SessionEvent } from "./session-event" - -export const ID = SessionEvent.ID -export type ID = Schema.Schema.Type - -const Base = { - id: SessionEvent.ID, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - time: Schema.Struct({ - created: Schema.DateTimeUtc, - }), -} - -export class User extends Schema.Class("Session.Entry.User")({ - ...Base, - text: SessionEvent.Prompt.fields.text, - files: SessionEvent.Prompt.fields.files, - agents: SessionEvent.Prompt.fields.agents, - type: Schema.Literal("user"), - time: Schema.Struct({ - created: Schema.DateTimeUtc, - }), -}) { - static fromEvent(event: SessionEvent.Prompt) { - return new User({ - id: event.id, - type: "user", - metadata: event.metadata, - text: event.text, - files: event.files, - agents: event.agents, - time: { created: event.timestamp }, - }) - } -} - -export class Synthetic extends Schema.Class("Session.Entry.Synthetic")({ - ...SessionEvent.Synthetic.fields, - ...Base, - type: Schema.Literal("synthetic"), -}) { - static fromEvent(event: SessionEvent.Synthetic) { - return new Synthetic({ - ...event, - time: { created: event.timestamp }, - }) - } -} - -export class ToolStatePending extends Schema.Class("Session.Entry.ToolState.Pending")({ - status: Schema.Literal("pending"), - input: Schema.String, -}) {} - -export class ToolStateRunning extends Schema.Class("Session.Entry.ToolState.Running")({ - status: Schema.Literal("running"), - input: Schema.Record(Schema.String, Schema.Unknown), - title: Schema.String.pipe(Schema.optional), - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), -}) {} - -export class ToolStateCompleted extends Schema.Class("Session.Entry.ToolState.Completed")({ - status: Schema.Literal("completed"), - input: Schema.Record(Schema.String, Schema.Unknown), - output: Schema.String, - title: Schema.String, - metadata: Schema.Record(Schema.String, Schema.Unknown), - attachments: SessionEvent.FileAttachment.pipe(Schema.Array, Schema.optional), -}) {} - -export class ToolStateError extends Schema.Class("Session.Entry.ToolState.Error")({ - status: Schema.Literal("error"), - input: Schema.Record(Schema.String, Schema.Unknown), - error: Schema.String, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), -}) {} - -export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( - Schema.toTaggedUnion("status"), -) -export type ToolState = Schema.Schema.Type - -export class AssistantTool extends Schema.Class("Session.Entry.Assistant.Tool")({ - type: Schema.Literal("tool"), - callID: Schema.String, - name: Schema.String, - state: ToolState, - time: Schema.Struct({ - created: Schema.DateTimeUtc, - ran: Schema.DateTimeUtc.pipe(Schema.optional), - completed: Schema.DateTimeUtc.pipe(Schema.optional), - pruned: Schema.DateTimeUtc.pipe(Schema.optional), - }), -}) {} - -export class AssistantText extends Schema.Class("Session.Entry.Assistant.Text")({ - type: Schema.Literal("text"), - text: Schema.String, -}) {} - -export class AssistantReasoning extends Schema.Class("Session.Entry.Assistant.Reasoning")({ - type: Schema.Literal("reasoning"), - text: Schema.String, -}) {} - -export class AssistantRetry extends Schema.Class("Session.Entry.Assistant.Retry")({ - attempt: NonNegativeInt, - error: SessionEvent.RetryError, - time: Schema.Struct({ - created: Schema.DateTimeUtc, - }), -}) { - static fromEvent(event: SessionEvent.Retried) { - return new AssistantRetry({ - attempt: event.attempt, - error: event.error, - time: { - created: event.timestamp, - }, - }) - } -} - -export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe( - Schema.toTaggedUnion("type"), -) -export type AssistantContent = Schema.Schema.Type - -export class Assistant extends Schema.Class("Session.Entry.Assistant")({ - ...Base, - type: Schema.Literal("assistant"), - content: AssistantContent.pipe(Schema.Array), - retries: AssistantRetry.pipe(Schema.Array, Schema.optional), - cost: Schema.Finite.pipe(Schema.optional), - tokens: Schema.Struct({ - input: NonNegativeInt, - output: NonNegativeInt, - reasoning: NonNegativeInt, - cache: Schema.Struct({ - read: NonNegativeInt, - write: NonNegativeInt, - }), - }).pipe(Schema.optional), - error: Schema.String.pipe(Schema.optional), - time: Schema.Struct({ - created: Schema.DateTimeUtc, - completed: Schema.DateTimeUtc.pipe(Schema.optional), - }), -}) { - static fromEvent(event: SessionEvent.Step.Started) { - return new Assistant({ - id: event.id, - type: "assistant", - time: { - created: event.timestamp, - }, - content: [], - retries: [], - }) - } -} - -export class Compaction extends Schema.Class("Session.Entry.Compaction")({ - ...SessionEvent.Compacted.fields, - type: Schema.Literal("compaction"), - ...Base, -}) { - static fromEvent(event: SessionEvent.Compacted) { - return new Compaction({ - ...event, - type: "compaction", - time: { created: event.timestamp }, - }) - } -} - -export const Entry = Schema.Union([User, Synthetic, Assistant, Compaction]).pipe(Schema.toTaggedUnion("type")) - -export type Entry = Schema.Schema.Type - -export type Type = Entry["type"] - -/* -export interface Interface { - readonly decode: (row: typeof SessionEntryTable.$inferSelect) => Entry - readonly fromSession: (sessionID: SessionID) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/SessionEntry") {} - -export const layer: Layer.Layer = Layer.effect( - Service, - Effect.gen(function* () { - const decodeEntry = Schema.decodeUnknownSync(Entry) - - const decode: (typeof Service.Service)["decode"] = (row) => decodeEntry({ ...row, id: row.id, type: row.type }) - - const fromSession = Effect.fn("SessionEntry.fromSession")(function* (sessionID: SessionID) { - return Database.use((db) => - db - .select() - .from(SessionEntryTable) - .where(eq(SessionEntryTable.session_id, sessionID)) - .orderBy(SessionEntryTable.id) - .all() - .map((row) => decode(row)), - ) - }) - - return Service.of({ - decode, - fromSession, - }) - }), -) -*/ - -export * as SessionEntry from "./session-entry" diff --git a/packages/opencode/src/v2/session-event.ts b/packages/opencode/src/v2/session-event.ts index aaf71c8dccdb..99de3cff2510 100644 --- a/packages/opencode/src/v2/session-event.ts +++ b/packages/opencode/src/v2/session-event.ts @@ -1,128 +1,75 @@ -import { Identifier } from "@/id/id" -import { NonNegativeInt, withStatics } from "@/util/schema" -import * as DateTime from "effect/DateTime" +import { SessionID } from "@/session/schema" +import { NonNegativeInt } from "@/util/schema" +import { Event } from "./event" +import { FileAttachment, Prompt } from "./session-prompt" import { Schema } from "effect" +export { FileAttachment } +import { ToolOutput } from "./tool-output" -export namespace SessionEvent { - export const ID = Schema.String.pipe( - Schema.brand("Session.Event.ID"), - withStatics((s) => ({ - create: () => s.make(Identifier.create("evt", "ascending")), - })), - ) - export type ID = Schema.Schema.Type - type Stamp = Schema.Schema.Type - type BaseInput = { - id?: ID - metadata?: Record - timestamp?: Stamp - } - - const Base = { - id: ID, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - timestamp: Schema.DateTimeUtc, - } - - export class Source extends Schema.Class("Session.Event.Source")({ - start: NonNegativeInt, - end: NonNegativeInt, - text: Schema.String, - }) {} - - export class FileAttachment extends Schema.Class("Session.Event.FileAttachment")({ - uri: Schema.String, - mime: Schema.String, - name: Schema.String.pipe(Schema.optional), - description: Schema.String.pipe(Schema.optional), - source: Source.pipe(Schema.optional), - }) { - static create(input: FileAttachment) { - return new FileAttachment({ - uri: input.uri, - mime: input.mime, - name: input.name, - description: input.description, - source: input.source, - }) - } - } +export const ID = Event.ID +export type ID = Schema.Schema.Type - export class AgentAttachment extends Schema.Class("Session.Event.AgentAttachment")({ - name: Schema.String, - source: Source.pipe(Schema.optional), - }) {} +export const Source = Schema.Struct({ + start: NonNegativeInt, + end: NonNegativeInt, + text: Schema.String, +}).annotate({ + identifier: "session.next.event.source", +}) +export type Source = Schema.Schema.Type - export class RetryError extends Schema.Class("Session.Event.Retry.Error")({ - message: Schema.String, - statusCode: NonNegativeInt.pipe(Schema.optional), - isRetryable: Schema.Boolean, - responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), - responseBody: Schema.String.pipe(Schema.optional), - metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), - }) {} +const Base = { + timestamp: Schema.DateTimeUtcFromMillis, + sessionID: SessionID, +} - export class Prompt extends Schema.Class("Session.Event.Prompt")({ +export const Prompted = Event.define({ + type: "session.next.prompted", + aggregate: "sessionID", + version: 1, + schema: { ...Base, - type: Schema.Literal("prompt"), - text: Schema.String, - files: Schema.Array(FileAttachment).pipe(Schema.optional), - agents: Schema.Array(AgentAttachment).pipe(Schema.optional), - }) { - static create(input: BaseInput & { text: string; files?: FileAttachment[]; agents?: AgentAttachment[] }) { - return new Prompt({ - id: input.id ?? ID.create(), - type: "prompt", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - text: input.text, - files: input.files, - agents: input.agents, - }) - } - } + id: ID, + prompt: Prompt, + }, +}) +export type Prompted = Schema.Schema.Type - export class Synthetic extends Schema.Class("Session.Event.Synthetic")({ +export const Synthetic = Event.define({ + type: "session.next.synthetic", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("synthetic"), + id: ID, text: Schema.String, - }) { - static create(input: BaseInput & { text: string }) { - return new Synthetic({ - id: input.id ?? ID.create(), - type: "synthetic", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - text: input.text, - }) - } - } + }, +}) +export type Synthetic = Schema.Schema.Type - export namespace Step { - export class Started extends Schema.Class("Session.Event.Step.Started")({ +export namespace Step { + export const Started = Event.define({ + type: "session.next.step.started", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("step.started"), + id: ID, + agent: Schema.String, model: Schema.Struct({ id: Schema.String, providerID: Schema.String, variant: Schema.String.pipe(Schema.optional), }), - }) { - static create(input: BaseInput & { model: { id: string; providerID: string; variant?: string } }) { - return new Started({ - id: input.id ?? ID.create(), - type: "step.started", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - model: input.model, - }) - } - } + snapshot: Schema.String.pipe(Schema.optional), + }, + }) + export type Started = Schema.Schema.Type - export class Ended extends Schema.Class("Session.Event.Step.Ended")({ + export const Ended = Event.define({ + type: "session.next.step.ended", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("step.ended"), - reason: Schema.String, + finish: Schema.String, cost: Schema.Finite, tokens: Schema.Struct({ input: NonNegativeInt, @@ -133,177 +80,118 @@ export namespace SessionEvent { write: NonNegativeInt, }), }), - }) { - static create(input: BaseInput & { reason: string; cost: number; tokens: Ended["tokens"] }) { - return new Ended({ - id: input.id ?? ID.create(), - type: "step.ended", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - reason: input.reason, - cost: input.cost, - tokens: input.tokens, - }) - } - } - } + snapshot: Schema.String.pipe(Schema.optional), + }, + }) + export type Ended = Schema.Schema.Type +} - export namespace Text { - export class Started extends Schema.Class("Session.Event.Text.Started")({ +export namespace Text { + export const Started = Event.define({ + type: "session.next.text.started", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("text.started"), - }) { - static create(input: BaseInput = {}) { - return new Started({ - id: input.id ?? ID.create(), - type: "text.started", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - }) - } - } + }, + }) + export type Started = Schema.Schema.Type - export class Delta extends Schema.Class("Session.Event.Text.Delta")({ + export const Delta = Event.define({ + type: "session.next.text.delta", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("text.delta"), delta: Schema.String, - }) { - static create(input: BaseInput & { delta: string }) { - return new Delta({ - id: input.id ?? ID.create(), - type: "text.delta", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - delta: input.delta, - }) - } - } + }, + }) + export type Delta = Schema.Schema.Type - export class Ended extends Schema.Class("Session.Event.Text.Ended")({ + export const Ended = Event.define({ + type: "session.next.text.ended", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("text.ended"), text: Schema.String, - }) { - static create(input: BaseInput & { text: string }) { - return new Ended({ - id: input.id ?? ID.create(), - type: "text.ended", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - text: input.text, - }) - } - } - } + }, + }) + export type Ended = Schema.Schema.Type +} - export namespace Reasoning { - export class Started extends Schema.Class("Session.Event.Reasoning.Started")({ +export namespace Reasoning { + export const Started = Event.define({ + type: "session.next.reasoning.started", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("reasoning.started"), - }) { - static create(input: BaseInput = {}) { - return new Started({ - id: input.id ?? ID.create(), - type: "reasoning.started", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - }) - } - } + reasoningID: Schema.String, + }, + }) + export type Started = Schema.Schema.Type - export class Delta extends Schema.Class("Session.Event.Reasoning.Delta")({ + export const Delta = Event.define({ + type: "session.next.reasoning.delta", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("reasoning.delta"), + reasoningID: Schema.String, delta: Schema.String, - }) { - static create(input: BaseInput & { delta: string }) { - return new Delta({ - id: input.id ?? ID.create(), - type: "reasoning.delta", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - delta: input.delta, - }) - } - } + }, + }) + export type Delta = Schema.Schema.Type - export class Ended extends Schema.Class("Session.Event.Reasoning.Ended")({ + export const Ended = Event.define({ + type: "session.next.reasoning.ended", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("reasoning.ended"), + reasoningID: Schema.String, text: Schema.String, - }) { - static create(input: BaseInput & { text: string }) { - return new Ended({ - id: input.id ?? ID.create(), - type: "reasoning.ended", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - text: input.text, - }) - } - } - } + }, + }) + export type Ended = Schema.Schema.Type +} - export namespace Tool { - export namespace Input { - export class Started extends Schema.Class("Session.Event.Tool.Input.Started")({ +export namespace Tool { + export namespace Input { + export const Started = Event.define({ + type: "session.next.tool.input.started", + aggregate: "sessionID", + schema: { ...Base, callID: Schema.String, name: Schema.String, - type: Schema.Literal("tool.input.started"), - }) { - static create(input: BaseInput & { callID: string; name: string }) { - return new Started({ - id: input.id ?? ID.create(), - type: "tool.input.started", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - name: input.name, - }) - } - } + }, + }) + export type Started = Schema.Schema.Type - export class Delta extends Schema.Class("Session.Event.Tool.Input.Delta")({ + export const Delta = Event.define({ + type: "session.next.tool.input.delta", + aggregate: "sessionID", + schema: { ...Base, callID: Schema.String, - type: Schema.Literal("tool.input.delta"), delta: Schema.String, - }) { - static create(input: BaseInput & { callID: string; delta: string }) { - return new Delta({ - id: input.id ?? ID.create(), - type: "tool.input.delta", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - delta: input.delta, - }) - } - } + }, + }) + export type Delta = Schema.Schema.Type - export class Ended extends Schema.Class("Session.Event.Tool.Input.Ended")({ + export const Ended = Event.define({ + type: "session.next.tool.input.ended", + aggregate: "sessionID", + schema: { ...Base, callID: Schema.String, - type: Schema.Literal("tool.input.ended"), text: Schema.String, - }) { - static create(input: BaseInput & { callID: string; text: string }) { - return new Ended({ - id: input.id ?? ID.create(), - type: "tool.input.ended", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - text: input.text, - }) - } - } - } + }, + }) + export type Ended = Schema.Schema.Type + } - export class Called extends Schema.Class("Session.Event.Tool.Called")({ + export const Called = Event.define({ + type: "session.next.tool.called", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("tool.called"), callID: Schema.String, tool: Schema.String, input: Schema.Record(Schema.String, Schema.Unknown), @@ -311,148 +199,152 @@ export namespace SessionEvent { executed: Schema.Boolean, metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), }), - }) { - static create( - input: BaseInput & { - callID: string - tool: string - input: Record - provider: Called["provider"] - }, - ) { - return new Called({ - id: input.id ?? ID.create(), - type: "tool.called", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - tool: input.tool, - input: input.input, - provider: input.provider, - }) - } - } + }, + }) + export type Called = Schema.Schema.Type - export class Success extends Schema.Class("Session.Event.Tool.Success")({ + export const Progress = Event.define({ + type: "session.next.tool.progress", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("tool.success"), callID: Schema.String, - title: Schema.String, - output: Schema.String.pipe(Schema.optional), - attachments: Schema.Array(FileAttachment).pipe(Schema.optional), + structured: ToolOutput.Structured, + content: Schema.Array(ToolOutput.Content), + }, + }) + export type Progress = Schema.Schema.Type + + export const Success = Event.define({ + type: "session.next.tool.success", + aggregate: "sessionID", + schema: { + ...Base, + callID: Schema.String, + structured: ToolOutput.Structured, + content: Schema.Array(ToolOutput.Content), provider: Schema.Struct({ executed: Schema.Boolean, metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), }), - }) { - static create( - input: BaseInput & { - callID: string - title: string - output?: string - attachments?: FileAttachment[] - provider: Success["provider"] - }, - ) { - return new Success({ - id: input.id ?? ID.create(), - type: "tool.success", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - title: input.title, - output: input.output, - attachments: input.attachments, - provider: input.provider, - }) - } - } + }, + }) + export type Success = Schema.Schema.Type - export class Error extends Schema.Class("Session.Event.Tool.Error")({ + export const Error = Event.define({ + type: "session.next.tool.error", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("tool.error"), callID: Schema.String, - error: Schema.String, + error: Schema.Struct({ + type: Schema.String, + message: Schema.String, + }), provider: Schema.Struct({ executed: Schema.Boolean, metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), }), - }) { - static create(input: BaseInput & { callID: string; error: string; provider: Error["provider"] }) { - return new Error({ - id: input.id ?? ID.create(), - type: "tool.error", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - error: input.error, - provider: input.provider, - }) - } - } - } + }, + }) + export type Error = Schema.Schema.Type +} - export class Retried extends Schema.Class("Session.Event.Retried")({ +export const RetryError = Schema.Struct({ + message: Schema.String, + statusCode: NonNegativeInt.pipe(Schema.optional), + isRetryable: Schema.Boolean, + responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + responseBody: Schema.String.pipe(Schema.optional), + metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), +}).annotate({ + identifier: "session.next.retry_error", +}) +export type RetryError = Schema.Schema.Type + +export const Retried = Event.define({ + type: "session.next.retried", + aggregate: "sessionID", + schema: { ...Base, - type: Schema.Literal("retried"), attempt: NonNegativeInt, error: RetryError, - }) { - static create(input: BaseInput & { attempt: number; error: RetryError }) { - return new Retried({ - id: input.id ?? ID.create(), - type: "retried", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - attempt: input.attempt, - error: input.error, - }) - } - } + }, +}) +export type Retried = Schema.Schema.Type - export class Compacted extends Schema.Class("Session.Event.Compated")({ - ...Base, - type: Schema.Literal("compacted"), - auto: Schema.Boolean, - overflow: Schema.Boolean.pipe(Schema.optional), - }) { - static create(input: BaseInput & { auto: boolean; overflow?: boolean }) { - return new Compacted({ - id: input.id ?? ID.create(), - type: "compacted", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - auto: input.auto, - overflow: input.overflow, - }) - } - } +export namespace Compaction { + export const Started = Event.define({ + type: "session.next.compaction.started", + aggregate: "sessionID", + schema: { + ...Base, + id: ID, + reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]), + }, + }) + export type Started = Schema.Schema.Type - export const Event = Schema.Union( - [ - Prompt, - Synthetic, - Step.Started, - Step.Ended, - Text.Started, - Text.Delta, - Text.Ended, - Tool.Input.Started, - Tool.Input.Delta, - Tool.Input.Ended, - Tool.Called, - Tool.Success, - Tool.Error, - Reasoning.Started, - Reasoning.Delta, - Reasoning.Ended, - Retried, - Compacted, - ], - { - mode: "oneOf", + export const Delta = Event.define({ + type: "session.next.compaction.delta", + aggregate: "sessionID", + schema: { + ...Base, + text: Schema.String, }, - ).pipe(Schema.toTaggedUnion("type")) - export type Event = Schema.Schema.Type - export type Type = Event["type"] + }) + + export const Ended = Event.define({ + type: "session.next.compaction.ended", + aggregate: "sessionID", + schema: { + ...Base, + text: Schema.String, + include: Schema.String.pipe(Schema.optional), + }, + }) + export type Ended = Schema.Schema.Type } + +export const All = Schema.Union( + [ + Prompted, + Synthetic, + Step.Started, + Step.Ended, + Text.Started, + Text.Delta, + Text.Ended, + Tool.Input.Started, + Tool.Input.Delta, + Tool.Input.Ended, + Tool.Called, + Tool.Progress, + Tool.Success, + Tool.Error, + Reasoning.Started, + Reasoning.Delta, + Reasoning.Ended, + Retried, + Compaction.Started, + Compaction.Delta, + Compaction.Ended, + ], + { + mode: "oneOf", + }, +).pipe(Schema.toTaggedUnion("type")) + +// user +// assistant +// assistant +// assistant +// user +// compaction marker +// -> text +// assistant + +export type Event = Schema.Schema.Type +export type Type = Event["type"] + +export * as SessionEvent from "./session-event" diff --git a/packages/opencode/src/v2/session-message-updater.ts b/packages/opencode/src/v2/session-message-updater.ts new file mode 100644 index 000000000000..bba605910f24 --- /dev/null +++ b/packages/opencode/src/v2/session-message-updater.ts @@ -0,0 +1,307 @@ +import { produce, type WritableDraft } from "immer" +import { SessionEvent } from "./session-event" +import { SessionMessage } from "./session-message" + +export type MemoryState = { + messages: SessionMessage.Message[] +} + +export interface Adapter { + readonly getCurrentAssistant: () => SessionMessage.Assistant | undefined + readonly getCurrentCompaction: () => SessionMessage.Compaction | undefined + readonly updateAssistant: (assistant: SessionMessage.Assistant) => void + readonly updateCompaction: (compaction: SessionMessage.Compaction) => void + readonly appendMessage: (message: SessionMessage.Message) => void + readonly finish: () => Result +} + +export function memory(state: MemoryState): Adapter { + const activeAssistantIndex = () => + state.messages.findLastIndex((message) => message.type === "assistant" && !message.time.completed) + const activeCompactionIndex = () => state.messages.findLastIndex((message) => message.type === "compaction") + + return { + getCurrentAssistant() { + const index = activeAssistantIndex() + if (index < 0) return + const assistant = state.messages[index] + return assistant?.type === "assistant" ? assistant : undefined + }, + getCurrentCompaction() { + const index = activeCompactionIndex() + if (index < 0) return + const compaction = state.messages[index] + return compaction?.type === "compaction" ? compaction : undefined + }, + updateAssistant(assistant) { + const index = activeAssistantIndex() + if (index < 0) return + const current = state.messages[index] + if (current?.type !== "assistant") return + state.messages[index] = assistant + }, + updateCompaction(compaction) { + const index = activeCompactionIndex() + if (index < 0) return + const current = state.messages[index] + if (current?.type !== "compaction") return + state.messages[index] = compaction + }, + appendMessage(message) { + state.messages.push(message) + }, + finish() { + return state + }, + } +} + +export function update(adapter: Adapter, event: SessionEvent.Event): Result { + const currentAssistant = adapter.getCurrentAssistant() + type DraftAssistant = WritableDraft + type DraftTool = WritableDraft + type DraftText = WritableDraft + type DraftReasoning = WritableDraft + + const latestTool = (assistant: DraftAssistant | undefined, callID?: string) => + assistant?.content.findLast( + (item): item is DraftTool => item.type === "tool" && (callID === undefined || item.id === callID), + ) + + const latestText = (assistant: DraftAssistant | undefined) => + assistant?.content.findLast((item): item is DraftText => item.type === "text") + + const latestReasoning = (assistant: DraftAssistant | undefined, reasoningID: string) => + assistant?.content.findLast( + (item): item is DraftReasoning => item.type === "reasoning" && item.id === reasoningID, + ) + + SessionEvent.All.match(event, { + "session.next.prompted": (event) => { + adapter.appendMessage(SessionMessage.User.fromEvent(event)) + }, + "session.next.synthetic": (event) => { + adapter.appendMessage(SessionMessage.Synthetic.fromEvent(event)) + }, + "session.next.step.started": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + draft.time.completed = event.data.timestamp + }), + ) + } + adapter.appendMessage(SessionMessage.Assistant.fromEvent(event)) + }, + "session.next.step.ended": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + draft.time.completed = event.data.timestamp + draft.finish = event.data.finish + draft.cost = event.data.cost + draft.tokens = event.data.tokens + if (event.data.snapshot) draft.snapshot = { ...draft.snapshot, end: event.data.snapshot } + }), + ) + } + }, + "session.next.text.started": () => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + draft.content.push({ + type: "text", + text: "", + }) + }), + ) + } + }, + "session.next.text.delta": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestText(draft) + if (match) match.text += event.data.delta + }), + ) + } + }, + "session.next.text.ended": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestText(draft) + if (match) match.text = event.data.text + }), + ) + } + }, + "session.next.tool.input.started": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + draft.content.push({ + type: "tool", + id: event.data.callID, + name: event.data.name, + time: { + created: event.data.timestamp, + }, + state: { + status: "pending", + input: "", + }, + }) + }), + ) + } + }, + "session.next.tool.input.delta": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestTool(draft, event.data.callID) + // oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string) + if (match && match.state.status === "pending") match.state.input += event.data.delta + }), + ) + } + }, + "session.next.tool.input.ended": () => {}, + "session.next.tool.called": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestTool(draft, event.data.callID) + if (match) { + match.provider = event.data.provider + match.time.ran = event.data.timestamp + match.state = { + status: "running", + input: event.data.input, + structured: {}, + content: [], + } + } + }), + ) + } + }, + "session.next.tool.progress": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestTool(draft, event.data.callID) + if (match && match.state.status === "running") { + match.state.structured = event.data.structured + match.state.content = [...event.data.content] + } + }), + ) + } + }, + "session.next.tool.success": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestTool(draft, event.data.callID) + if (match && match.state.status === "running") { + match.provider = event.data.provider + match.time.completed = event.data.timestamp + match.state = { + status: "completed", + input: match.state.input, + structured: event.data.structured, + content: [...event.data.content], + } + } + }), + ) + } + }, + "session.next.tool.error": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestTool(draft, event.data.callID) + if (match && match.state.status === "running") { + match.provider = event.data.provider + match.time.completed = event.data.timestamp + match.state = { + status: "error", + error: event.data.error, + input: match.state.input, + structured: match.state.structured, + content: match.state.content, + } + } + }), + ) + } + }, + "session.next.reasoning.started": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + draft.content.push({ + type: "reasoning", + id: event.data.reasoningID, + text: "", + }) + }), + ) + } + }, + "session.next.reasoning.delta": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestReasoning(draft, event.data.reasoningID) + if (match) match.text += event.data.delta + }), + ) + } + }, + "session.next.reasoning.ended": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestReasoning(draft, event.data.reasoningID) + if (match) match.text = event.data.text + }), + ) + } + }, + "session.next.retried": () => {}, + "session.next.compaction.started": (event) => { + adapter.appendMessage(SessionMessage.Compaction.fromEvent(event)) + }, + "session.next.compaction.delta": (event) => { + const currentCompaction = adapter.getCurrentCompaction() + if (currentCompaction) { + adapter.updateCompaction( + produce(currentCompaction, (draft) => { + draft.summary += event.data.text + }), + ) + } + }, + "session.next.compaction.ended": (event) => { + const currentCompaction = adapter.getCurrentCompaction() + if (currentCompaction) { + adapter.updateCompaction( + produce(currentCompaction, (draft) => { + draft.summary = event.data.text + draft.include = event.data.include + }), + ) + } + }, + }) + + return adapter.finish() +} + +export * as SessionMessageUpdater from "./session-message-updater" diff --git a/packages/opencode/src/v2/session-message.ts b/packages/opencode/src/v2/session-message.ts new file mode 100644 index 000000000000..a805bd62ce24 --- /dev/null +++ b/packages/opencode/src/v2/session-message.ts @@ -0,0 +1,196 @@ +import { Schema } from "effect" +import { Prompt } from "./session-prompt" +import { SessionEvent } from "./session-event" +import { Event } from "./event" +import { ToolOutput } from "./tool-output" + +export const ID = Event.ID +export type ID = Schema.Schema.Type + +const Base = { + id: ID, + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + time: Schema.Struct({ + created: Schema.DateTimeUtcFromMillis, + }), +} + +export class User extends Schema.Class("Session.Message.User")({ + ...Base, + text: Prompt.fields.text, + files: Prompt.fields.files, + agents: Prompt.fields.agents, + type: Schema.Literal("user"), + time: Schema.Struct({ + created: Schema.DateTimeUtcFromMillis, + }), +}) { + static fromEvent(event: SessionEvent.Prompted) { + return new User({ + id: event.data.id, + type: "user", + metadata: event.metadata, + text: event.data.prompt.text, + files: event.data.prompt.files, + agents: event.data.prompt.agents, + time: { created: event.data.timestamp }, + }) + } +} + +export class Synthetic extends Schema.Class("Session.Message.Synthetic")({ + ...Base, + sessionID: SessionEvent.Synthetic.fields.data.fields.sessionID, + text: SessionEvent.Synthetic.fields.data.fields.text, + type: Schema.Literal("synthetic"), +}) { + static fromEvent(event: SessionEvent.Synthetic) { + return new Synthetic({ + sessionID: event.data.sessionID, + text: event.data.text, + id: event.data.id, + type: "synthetic", + time: { created: event.data.timestamp }, + }) + } +} + +export class ToolStatePending extends Schema.Class("Session.Message.ToolState.Pending")({ + status: Schema.Literal("pending"), + input: Schema.String, +}) {} + +export class ToolStateRunning extends Schema.Class("Session.Message.ToolState.Running")({ + status: Schema.Literal("running"), + input: Schema.Record(Schema.String, Schema.Unknown), + structured: ToolOutput.Structured, + content: ToolOutput.Content.pipe(Schema.Array), +}) {} + +export class ToolStateCompleted extends Schema.Class("Session.Message.ToolState.Completed")({ + status: Schema.Literal("completed"), + input: Schema.Record(Schema.String, Schema.Unknown), + attachments: SessionEvent.FileAttachment.pipe(Schema.Array, Schema.optional), + content: ToolOutput.Content.pipe(Schema.Array), + structured: ToolOutput.Structured, +}) {} + +export class ToolStateError extends Schema.Class("Session.Message.ToolState.Error")({ + status: Schema.Literal("error"), + input: Schema.Record(Schema.String, Schema.Unknown), + content: ToolOutput.Content.pipe(Schema.Array), + structured: ToolOutput.Structured, + error: Schema.Struct({ + type: Schema.String, + message: Schema.String, + }), +}) {} + +export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( + Schema.toTaggedUnion("status"), +) +export type ToolState = Schema.Schema.Type + +export class AssistantTool extends Schema.Class("Session.Message.Assistant.Tool")({ + type: Schema.Literal("tool"), + id: Schema.String, + name: Schema.String, + provider: Schema.Struct({ + executed: Schema.Boolean, + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + }).pipe(Schema.optional), + state: ToolState, + time: Schema.Struct({ + created: Schema.DateTimeUtcFromMillis, + ran: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + completed: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + pruned: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + }), +}) {} + +export class AssistantText extends Schema.Class("Session.Message.Assistant.Text")({ + type: Schema.Literal("text"), + text: Schema.String, +}) {} + +export class AssistantReasoning extends Schema.Class("Session.Message.Assistant.Reasoning")({ + type: Schema.Literal("reasoning"), + id: Schema.String, + text: Schema.String, +}) {} + +export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe( + Schema.toTaggedUnion("type"), +) +export type AssistantContent = Schema.Schema.Type + +export class Assistant extends Schema.Class("Session.Message.Assistant")({ + ...Base, + type: Schema.Literal("assistant"), + agent: Schema.String, + model: SessionEvent.Step.Started.fields.data.fields.model, + content: AssistantContent.pipe(Schema.Array), + snapshot: Schema.Struct({ + start: Schema.String.pipe(Schema.optional), + end: Schema.String.pipe(Schema.optional), + }).pipe(Schema.optional), + finish: Schema.String.pipe(Schema.optional), + cost: Schema.Number.pipe(Schema.optional), + tokens: Schema.Struct({ + input: Schema.Number, + output: Schema.Number, + reasoning: Schema.Number, + cache: Schema.Struct({ + read: Schema.Number, + write: Schema.Number, + }), + }).pipe(Schema.optional), + error: Schema.String.pipe(Schema.optional), + time: Schema.Struct({ + created: Schema.DateTimeUtcFromMillis, + completed: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + }), +}) { + static fromEvent(event: SessionEvent.Step.Started) { + return new Assistant({ + id: event.data.id, + type: "assistant", + agent: event.data.agent, + model: event.data.model, + time: { + created: event.data.timestamp, + }, + content: [], + snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined, + }) + } +} + +export class Compaction extends Schema.Class("Session.Message.Compaction")({ + type: Schema.Literal("compaction"), + reason: SessionEvent.Compaction.Started.fields.data.fields.reason, + summary: Schema.String, + include: Schema.String.pipe(Schema.optional), + ...Base, +}) { + static fromEvent(event: SessionEvent.Compaction.Started) { + return new Compaction({ + id: event.data.id, + type: "compaction", + metadata: event.metadata, + reason: event.data.reason, + summary: "", + time: { created: event.data.timestamp }, + }) + } +} + +export const Message = Schema.Union([User, Synthetic, Assistant, Compaction]) + .pipe(Schema.toTaggedUnion("type")) + .annotate({ identifier: "Session.Message" }) + +export type Message = Schema.Schema.Type + +export type Type = Message["type"] + +export * as SessionMessage from "./session-message" diff --git a/packages/opencode/src/v2/session-prompt.ts b/packages/opencode/src/v2/session-prompt.ts new file mode 100644 index 000000000000..e7068e409233 --- /dev/null +++ b/packages/opencode/src/v2/session-prompt.ts @@ -0,0 +1,36 @@ +import * as Schema from "effect/Schema" + +export class Source extends Schema.Class("Prompt.Source")({ + start: Schema.Number, + end: Schema.Number, + text: Schema.String, +}) {} + +export class FileAttachment extends Schema.Class("Prompt.FileAttachment")({ + uri: Schema.String, + mime: Schema.String, + name: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + source: Source.pipe(Schema.optional), +}) { + static create(input: FileAttachment) { + return new FileAttachment({ + uri: input.uri, + mime: input.mime, + name: input.name, + description: input.description, + source: input.source, + }) + } +} + +export class AgentAttachment extends Schema.Class("Prompt.AgentAttachment")({ + name: Schema.String, + source: Source.pipe(Schema.optional), +}) {} + +export class Prompt extends Schema.Class("Prompt")({ + text: Schema.String, + files: Schema.Array(FileAttachment).pipe(Schema.optional), + agents: Schema.Array(AgentAttachment).pipe(Schema.optional), +}) {} diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index 2bac11f4fe3a..7d134cc29feb 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -1,69 +1,129 @@ -import { Context, Layer, Schema, Effect } from "effect" -import { SessionEntry } from "./session-entry" -import { Struct } from "effect" +import { SessionMessageTable } from "@/session/session.sql" +import type { SessionID } from "@/session/schema" +import { and, asc, desc, eq, gt, inArray, lt, or } from "@/storage/db" +import * as Database from "@/storage/db" +import { Context, Effect, Layer, Schema } from "effect" +import { SessionMessage } from "./session-message" +import type { Prompt } from "./session-prompt" import { Session } from "@/session/session" -import { SessionID } from "@/session/schema" +import { SessionPrompt } from "@/session/prompt" +import type { Event } from "./event" -export const ID = SessionID +export const Delivery = Schema.Union([Schema.Literal("immediate"), Schema.Literal("deferred")]).annotate({ + identifier: "Session.Delivery", +}) +export type Delivery = Schema.Schema.Type -export type ID = Schema.Schema.Type +export const DefaultDelivery = "immediate" satisfies Delivery -export class PromptInput extends Schema.Class("Session.PromptInput")({ - ...Struct.omit(SessionEntry.User.fields, ["time", "type"]), - id: Schema.optionalKey(SessionEntry.ID), - sessionID: ID, -}) {} +export type MessagesCursor = { + id: SessionMessage.ID + time: number +} + +export type MessagesInput = { + sessionID: SessionID + limit?: number + cursor?: MessagesCursor + direction?: "before" | "after" +} -export class CreateInput extends Schema.Class("Session.CreateInput")({ - id: Schema.optionalKey(ID), -}) {} +const older = (item: MessagesCursor) => + or( + lt(SessionMessageTable.time_created, item.time), + and(eq(SessionMessageTable.time_created, item.time), lt(SessionMessageTable.id, item.id)), + ) -export class Info extends Schema.Class("Session.Info")({ - id: ID, - model: Schema.Struct({ - id: Schema.String, - providerID: Schema.String, - modelID: Schema.String, - }).pipe(Schema.optional), -}) {} +const newer = (item: MessagesCursor) => + or( + gt(SessionMessageTable.time_created, item.time), + and(eq(SessionMessageTable.time_created, item.time), gt(SessionMessageTable.id, item.id)), + ) export interface Interface { - fromID: (id: ID) => Effect.Effect - create: (input: CreateInput) => Effect.Effect - prompt: (input: PromptInput) => Effect.Effect + readonly messages: (input: MessagesInput) => Effect.Effect + readonly prompt: (input: { + id?: Event.ID + sessionID: SessionID + prompt: Prompt + delivery?: Delivery + }) => Effect.Effect + readonly compact: (sessionID: SessionID) => Effect.Effect + readonly wait: (sessionID: SessionID) => Effect.Effect } -export class Service extends Context.Service()("Session.Service") {} +export class Service extends Context.Service()("@opencode/v2/Session") {} -export const layer = Layer.effect(Service)( +export const layer = Layer.effect( + Service, Effect.gen(function* () { - const session = yield* Session.Service + const prompt = yield* SessionPrompt.Service + const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message) + const decode = (row: typeof SessionMessageTable.$inferSelect) => + decodeMessage({ ...row.data, id: row.id, type: row.type }) - const create: Interface["create"] = Effect.fn("Session.create")(function* (_input) { - throw new Error("Not implemented") - }) + const result: Interface = { + messages: Effect.fn("V2Session.messages")(function* (input) { + if (input.limit === undefined) { + const rows = Database.use((db) => + db + .select() + .from(SessionMessageTable) + .where(eq(SessionMessageTable.session_id, input.sessionID)) + .orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id)) + .all(), + ) + return rows.map((row) => decode(row)) + } - const prompt: Interface["prompt"] = Effect.fn("Session.prompt")(function* (_input) { - throw new Error("Not implemented") - }) + const limit = input.limit + const direction = input.direction ?? "before" + const where = input.cursor + ? and( + eq(SessionMessageTable.session_id, input.sessionID), + direction === "after" ? newer(input.cursor) : older(input.cursor), + ) + : eq(SessionMessageTable.session_id, input.sessionID) + const rows = Database.use((db) => { + if (direction === "after") { + return db + .select() + .from(SessionMessageTable) + .where(where) + .orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id)) + .limit(limit) + .all() + } + const ids = db + .select({ id: SessionMessageTable.id }) + .from(SessionMessageTable) + .where(where) + .orderBy(desc(SessionMessageTable.time_created), desc(SessionMessageTable.id)) + .limit(limit) + .all() + .map((row) => row.id) + if (ids.length === 0) return [] + return db + .select() + .from(SessionMessageTable) + .where(inArray(SessionMessageTable.id, ids)) + .orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id)) + .all() + }) + return rows.map((row) => decode(row)) + }), + prompt: Effect.fn("V2Session.prompt")(function* (input) { + const delivery = input.delivery ?? DefaultDelivery + return {} as any + }), + compact: Effect.fn("V2Session.compact")(function* (sessionID) {}), + wait: Effect.fn("V2Session.wait")(function* (sessionID) {}), + } - const fromID: Interface["fromID"] = Effect.fn("Session.fromID")(function* (id) { - const match = yield* session.get(id) - return fromV1(match) - }) - - return Service.of({ - create, - prompt, - fromID, - }) + return Service.of(result) }), ) -function fromV1(input: Session.Info): Info { - return new Info({ - id: ID.make(input.id), - }) -} +export const defaultLayer = layer.pipe(Layer.provide(SessionPrompt.defaultLayer)) export * as SessionV2 from "./session" diff --git a/packages/opencode/src/v2/tool-output.ts b/packages/opencode/src/v2/tool-output.ts new file mode 100644 index 000000000000..dee2bb11ed83 --- /dev/null +++ b/packages/opencode/src/v2/tool-output.ts @@ -0,0 +1,18 @@ +export * as ToolOutput from "./tool-output" +import { Schema } from "effect" + +export class TextContent extends Schema.Class("Tool.TextContent")({ + type: Schema.Literal("text"), + text: Schema.String, +}) {} + +export class FileContent extends Schema.Class("Tool.FileContent")({ + type: Schema.Literal("file"), + uri: Schema.String, + mime: Schema.String, + name: Schema.String.pipe(Schema.optional), +}) {} + +export const Content = Schema.Union([TextContent, FileContent]).pipe(Schema.toTaggedUnion("type")) + +export const Structured = Schema.Record(Schema.String, Schema.Any) diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index 479da7f518a6..aca0170bd8ec 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -79,7 +79,7 @@ delete process.env["OPENCODE_SERVER_USERNAME"] process.env["OPENCODE_DB"] = ":memory:" // Now safe to import from src/ -const Log = await import("@opencode-ai/core/util/log") +const { Log } = await import("@opencode-ai/core/util/log") const { initProjectors } = await import("../src/server/projectors") void Log.init({ diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index 70fe2d81b350..5af93deb02cf 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -17,7 +17,9 @@ import { Session } from "@/session/session" import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { MessageV2 } from "../../src/session/message-v2" import { Database } from "@/storage/db" -import { SessionTable } from "@/session/session.sql" +import { SessionMessageTable, SessionTable } from "@/session/session.sql" +import { SessionMessage } from "../../src/v2/session-message" +import * as DateTime from "effect/DateTime" import * as Log from "@opencode-ai/core/util/log" import { eq } from "drizzle-orm" import { resetDatabase } from "../fixture/db" @@ -203,6 +205,45 @@ describe("session HttpApi", () => { { headers }, ), ).toMatchObject({ info: { id: message.info.id } }) + + yield* Effect.promise(() => + Instance.provide({ + directory: tmp.path, + fn: async () => { + const message = new SessionMessage.Assistant({ + id: SessionMessage.ID.create(), + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + time: { created: DateTime.makeUnsafe(1) }, + content: [], + }) + Database.use((db) => + db + .insert(SessionMessageTable) + .values([ + { + id: message.id, + session_id: parent.id, + type: message.type, + time_created: 1, + data: { + time: { created: 1 }, + agent: message.agent, + model: message.model, + content: message.content, + } as NonNullable, + }, + ]) + .run(), + ) + }, + }), + ) + + expect(yield* requestJson(`/api/session/${parent.id}/message`, { headers })).toMatchObject([ + { type: "assistant" }, + ]) }), ), ) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index df83adb8d40e..0d02d9918a29 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -20,6 +20,7 @@ import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" +import { SessionV2 } from "../../src/v2/session" import { ModelID, ProviderID } from "../../src/provider/schema" import type { Provider } from "@/provider/provider" import * as SessionProcessorModule from "../../src/session/processor" @@ -597,6 +598,15 @@ describe("session.compaction.create", () => { auto: true, overflow: true, }) + + const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe( + Effect.provide(SessionV2.defaultLayer), + ) + expect(v2.at(-1)).toMatchObject({ + type: "compaction", + reason: "auto", + summary: "", + }) }), ), ) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 53305694018c..b49d6c9d9d92 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -19,6 +19,7 @@ import { ModelID, ProviderID } from "../../src/provider/schema" import { Question } from "../../src/question" import { Todo } from "../../src/session/todo" import { Session } from "@/session/session" +import { SessionMessageTable } from "../../src/session/session.sql" import { LLM } from "../../src/session/llm" import { MessageV2 } from "../../src/session/message-v2" import { AppFileSystem } from "@opencode-ai/core/filesystem" @@ -31,6 +32,7 @@ import { SessionRevert } from "../../src/session/revert" import { SessionRunState } from "../../src/session/run-state" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" +import { SessionV2 } from "../../src/v2/session" import { Skill } from "../../src/skill" import { SystemPrompt } from "../../src/session/system" import { Shell } from "../../src/shell/shell" @@ -39,6 +41,7 @@ import { ToolRegistry } from "@/tool/registry" import { Truncate } from "@/tool/truncate" import * as Log from "@opencode-ai/core/util/log" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import * as Database from "../../src/storage/db" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" @@ -371,6 +374,44 @@ it.live("loop calls LLM and returns assistant message", () => ), ) +it.live("prompt emits v2 prompted and synthetic events", () => + provideTmpdirServer( + Effect.fnUntraced(function* () { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ title: "Pinned" }) + + yield* prompt.prompt({ + sessionID: chat.id, + agent: "build", + noReply: true, + parts: [ + { type: "text", text: "hello v2" }, + { + type: "file", + mime: "text/plain", + filename: "note.txt", + url: "data:text/plain;base64,bm90ZSBjb250ZW50", + }, + ], + }) + + const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe( + Effect.provide(SessionV2.layer), + ) + const row = Database.use((db) => + db.select().from(SessionMessageTable).where(Database.eq(SessionMessageTable.session_id, chat.id)).get(), + ) + expect(messages).toHaveLength(3) + expect(messages[0]).toMatchObject({ type: "user", text: "hello v2" }) + expect(typeof row?.data.time.created).toBe("number") + expect(messages[1]).toMatchObject({ type: "synthetic", text: expect.stringContaining("Called the Read tool") }) + expect(messages[2]).toMatchObject({ type: "synthetic", text: "note content" }) + }), + { git: true, config: providerCfg }, + ), +) + it.live("static loop returns assistant text through local provider", () => provideTmpdirServer( Effect.fnUntraced(function* ({ llm }) { diff --git a/packages/opencode/test/session/session-entry-stepper.test.ts b/packages/opencode/test/session/session-entry-stepper.test.ts deleted file mode 100644 index defce40c14f3..000000000000 --- a/packages/opencode/test/session/session-entry-stepper.test.ts +++ /dev/null @@ -1,916 +0,0 @@ -import { describe, expect, test } from "bun:test" -import * as DateTime from "effect/DateTime" -import * as FastCheck from "effect/testing/FastCheck" -import { SessionEntry } from "../../src/v2/session-entry" -import { SessionEntryStepper } from "../../src/v2/session-entry-stepper" -import { SessionEvent } from "../../src/v2/session-event" - -const time = (n: number) => DateTime.makeUnsafe(n) - -const word = FastCheck.string({ minLength: 1, maxLength: 8 }) -const text = FastCheck.string({ maxLength: 16 }) -const texts = FastCheck.array(text, { maxLength: 8 }) -const val = FastCheck.oneof(FastCheck.boolean(), FastCheck.integer(), FastCheck.string({ maxLength: 12 })) -const dict = FastCheck.dictionary(word, val, { maxKeys: 4 }) -const files = FastCheck.array( - word.map((x) => SessionEvent.FileAttachment.create({ uri: `file://${encodeURIComponent(x)}`, mime: "text/plain" })), - { maxLength: 2 }, -) - -function maybe(arb: FastCheck.Arbitrary) { - return FastCheck.oneof(FastCheck.constant(undefined), arb) -} - -function assistant() { - return new SessionEntry.Assistant({ - id: SessionEvent.ID.create(), - type: "assistant", - time: { created: time(0) }, - content: [], - retries: [], - }) -} - -function retryError(message: string) { - return new SessionEvent.RetryError({ - message, - isRetryable: true, - }) -} - -function retry(attempt: number, message: string, created: number) { - return new SessionEntry.AssistantRetry({ - attempt, - error: retryError(message), - time: { - created: time(created), - }, - }) -} - -function memoryState() { - const state: SessionEntryStepper.MemoryState = { - entries: [], - pending: [], - } - return state -} - -function active() { - const state: SessionEntryStepper.MemoryState = { - entries: [assistant()], - pending: [], - } - return state -} - -function run(events: SessionEvent.Event[], state = memoryState()) { - return events.reduce((state, event) => SessionEntryStepper.step(state, event), state) -} - -function last(state: SessionEntryStepper.MemoryState) { - const entry = [...state.pending, ...state.entries].reverse().find((x) => x.type === "assistant") - expect(entry?.type).toBe("assistant") - return entry?.type === "assistant" ? entry : undefined -} - -function texts_of(state: SessionEntryStepper.MemoryState) { - const entry = last(state) - if (!entry) return [] - return entry.content.filter((x): x is SessionEntry.AssistantText => x.type === "text") -} - -function reasons(state: SessionEntryStepper.MemoryState) { - const entry = last(state) - if (!entry) return [] - return entry.content.filter((x): x is SessionEntry.AssistantReasoning => x.type === "reasoning") -} - -function tools(state: SessionEntryStepper.MemoryState) { - const entry = last(state) - if (!entry) return [] - return entry.content.filter((x): x is SessionEntry.AssistantTool => x.type === "tool") -} - -function tool(state: SessionEntryStepper.MemoryState, callID: string) { - return tools(state).find((x) => x.callID === callID) -} - -function retriesOf(state: SessionEntryStepper.MemoryState) { - const entry = last(state) - if (!entry) return [] - return entry.retries ?? [] -} - -function adapterStore() { - return { - committed: [] as SessionEntry.Entry[], - deferred: [] as SessionEntry.Entry[], - } -} - -function adapterFor(store: ReturnType): SessionEntryStepper.Adapter { - const activeAssistantIndex = () => - store.committed.findLastIndex((entry) => entry.type === "assistant" && !entry.time.completed) - - const getCurrentAssistant = () => { - const index = activeAssistantIndex() - if (index < 0) return - const assistant = store.committed[index] - return assistant?.type === "assistant" ? assistant : undefined - } - - return { - getCurrentAssistant, - updateAssistant(assistant) { - const index = activeAssistantIndex() - if (index < 0) return - const current = store.committed[index] - if (current?.type !== "assistant") return - store.committed[index] = assistant - }, - appendEntry(entry) { - store.committed.push(entry) - }, - appendPending(entry) { - store.deferred.push(entry) - }, - finish() { - return store - }, - } -} - -describe("session-entry-stepper", () => { - describe("stepWith", () => { - test("reduces through a custom adapter", () => { - const store = adapterStore() - store.committed.push(assistant()) - - SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Prompt.create({ text: "hello", timestamp: time(1) })) - SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Reasoning.Started.create({ timestamp: time(2) })) - SessionEntryStepper.stepWith( - adapterFor(store), - SessionEvent.Reasoning.Delta.create({ delta: "thinking", timestamp: time(3) }), - ) - SessionEntryStepper.stepWith( - adapterFor(store), - SessionEvent.Reasoning.Ended.create({ text: "thought", timestamp: time(4) }), - ) - SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Text.Started.create({ timestamp: time(5) })) - SessionEntryStepper.stepWith( - adapterFor(store), - SessionEvent.Text.Delta.create({ delta: "world", timestamp: time(6) }), - ) - SessionEntryStepper.stepWith( - adapterFor(store), - SessionEvent.Step.Ended.create({ - reason: "stop", - cost: 1, - tokens: { - input: 1, - output: 2, - reasoning: 3, - cache: { - read: 4, - write: 5, - }, - }, - timestamp: time(7), - }), - ) - - expect(store.deferred).toHaveLength(1) - expect(store.deferred[0]?.type).toBe("user") - expect(store.committed).toHaveLength(1) - expect(store.committed[0]?.type).toBe("assistant") - if (store.committed[0]?.type !== "assistant") return - - expect(store.committed[0].content).toEqual([ - { type: "reasoning", text: "thought" }, - { type: "text", text: "world" }, - ]) - expect(store.committed[0].time.completed).toEqual(time(7)) - }) - - test("aggregates retry events onto the current assistant", () => { - const store = adapterStore() - store.committed.push(assistant()) - - SessionEntryStepper.stepWith( - adapterFor(store), - SessionEvent.Retried.create({ - attempt: 1, - error: retryError("rate limited"), - timestamp: time(1), - }), - ) - SessionEntryStepper.stepWith( - adapterFor(store), - SessionEvent.Retried.create({ - attempt: 2, - error: retryError("provider overloaded"), - timestamp: time(2), - }), - ) - - expect(store.committed[0]?.type).toBe("assistant") - if (store.committed[0]?.type !== "assistant") return - - expect(store.committed[0].retries).toEqual([retry(1, "rate limited", 1), retry(2, "provider overloaded", 2)]) - }) - }) - - describe("memory", () => { - test("tracks and replaces the current assistant", () => { - const state = active() - const adapter = SessionEntryStepper.memory(state) - const current = adapter.getCurrentAssistant() - - expect(current?.type).toBe("assistant") - if (!current) return - - adapter.updateAssistant( - new SessionEntry.Assistant({ - ...current, - content: [new SessionEntry.AssistantText({ type: "text", text: "done" })], - time: { - ...current.time, - completed: time(1), - }, - }), - ) - - expect(adapter.getCurrentAssistant()).toBeUndefined() - expect(state.entries[0]?.type).toBe("assistant") - if (state.entries[0]?.type !== "assistant") return - - expect(state.entries[0].content).toEqual([{ type: "text", text: "done" }]) - expect(state.entries[0].time.completed).toEqual(time(1)) - }) - - test("appends committed and pending entries", () => { - const state = memoryState() - const adapter = SessionEntryStepper.memory(state) - const committed = SessionEntry.User.fromEvent( - SessionEvent.Prompt.create({ text: "committed", timestamp: time(1) }), - ) - const pending = SessionEntry.User.fromEvent(SessionEvent.Prompt.create({ text: "pending", timestamp: time(2) })) - - adapter.appendEntry(committed) - adapter.appendPending(pending) - - expect(state.entries).toEqual([committed]) - expect(state.pending).toEqual([pending]) - }) - - test("stepWith through memory records reasoning", () => { - const state = active() - - SessionEntryStepper.stepWith( - SessionEntryStepper.memory(state), - SessionEvent.Reasoning.Started.create({ timestamp: time(1) }), - ) - SessionEntryStepper.stepWith( - SessionEntryStepper.memory(state), - SessionEvent.Reasoning.Delta.create({ delta: "draft", timestamp: time(2) }), - ) - SessionEntryStepper.stepWith( - SessionEntryStepper.memory(state), - SessionEvent.Reasoning.Ended.create({ text: "final", timestamp: time(3) }), - ) - - expect(reasons(state)).toEqual([{ type: "reasoning", text: "final" }]) - }) - - test("stepWith through memory records retries", () => { - const state = active() - - SessionEntryStepper.stepWith( - SessionEntryStepper.memory(state), - SessionEvent.Retried.create({ - attempt: 1, - error: retryError("rate limited"), - timestamp: time(1), - }), - ) - - expect(retriesOf(state)).toEqual([retry(1, "rate limited", 1)]) - }) - }) - - describe("step", () => { - describe("seeded pending assistant", () => { - test("stores prompts in entries when no assistant is pending", () => { - FastCheck.assert( - FastCheck.property(word, (body) => { - const next = SessionEntryStepper.step( - memoryState(), - SessionEvent.Prompt.create({ text: body, timestamp: time(1) }), - ) - expect(next.entries).toHaveLength(1) - expect(next.entries[0]?.type).toBe("user") - if (next.entries[0]?.type !== "user") return - expect(next.entries[0].text).toBe(body) - }), - { numRuns: 50 }, - ) - }) - - test("stores prompts in pending when an assistant is pending", () => { - FastCheck.assert( - FastCheck.property(word, (body) => { - const next = SessionEntryStepper.step( - active(), - SessionEvent.Prompt.create({ text: body, timestamp: time(1) }), - ) - expect(next.pending).toHaveLength(1) - expect(next.pending[0]?.type).toBe("user") - if (next.pending[0]?.type !== "user") return - expect(next.pending[0].text).toBe(body) - }), - { numRuns: 50 }, - ) - }) - - test("accumulates text deltas on the latest text part", () => { - FastCheck.assert( - FastCheck.property(texts, (parts) => { - const next = parts.reduce( - (state, part, i) => - SessionEntryStepper.step( - state, - SessionEvent.Text.Delta.create({ delta: part, timestamp: time(i + 2) }), - ), - SessionEntryStepper.step(active(), SessionEvent.Text.Started.create({ timestamp: time(1) })), - ) - - expect(texts_of(next)).toEqual([ - { - type: "text", - text: parts.join(""), - }, - ]) - }), - { numRuns: 100 }, - ) - }) - - test("routes later text deltas to the latest text segment", () => { - FastCheck.assert( - FastCheck.property(texts, texts, (a, b) => { - const next = run( - [ - SessionEvent.Text.Started.create({ timestamp: time(1) }), - ...a.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 2) })), - SessionEvent.Text.Started.create({ timestamp: time(a.length + 2) }), - ...b.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + a.length + 3) })), - ], - active(), - ) - - expect(texts_of(next)).toEqual([ - { type: "text", text: a.join("") }, - { type: "text", text: b.join("") }, - ]) - }), - { numRuns: 50 }, - ) - }) - - test("reasoning.ended replaces buffered reasoning text", () => { - FastCheck.assert( - FastCheck.property(texts, text, (parts, end) => { - const next = run( - [ - SessionEvent.Reasoning.Started.create({ timestamp: time(1) }), - ...parts.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 2) })), - SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(parts.length + 2) }), - ], - active(), - ) - - expect(reasons(next)).toEqual([ - { - type: "reasoning", - text: end, - }, - ]) - }), - { numRuns: 100 }, - ) - }) - - test("tool.success completes the latest running tool", () => { - FastCheck.assert( - FastCheck.property( - word, - word, - dict, - maybe(text), - maybe(dict), - maybe(files), - texts, - (callID, title, input, output, metadata, attachments, parts) => { - const next = run( - [ - SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }), - ...parts.map((x, i) => - SessionEvent.Tool.Input.Delta.create({ callID, delta: x, timestamp: time(i + 2) }), - ), - SessionEvent.Tool.Called.create({ - callID, - tool: "bash", - input, - provider: { executed: true }, - timestamp: time(parts.length + 2), - }), - SessionEvent.Tool.Success.create({ - callID, - title, - output, - metadata, - attachments, - provider: { executed: true }, - timestamp: time(parts.length + 3), - }), - ], - active(), - ) - - const match = tool(next, callID) - expect(match?.state.status).toBe("completed") - if (match?.state.status !== "completed") return - - expect(match.time.ran).toEqual(time(parts.length + 2)) - expect(match.state.input).toEqual(input) - expect(match.state.output).toBe(output ?? "") - expect(match.state.title).toBe(title) - expect(match.state.metadata).toEqual(metadata ?? {}) - expect(match.state.attachments).toEqual(attachments ?? []) - }, - ), - { numRuns: 50 }, - ) - }) - - test("tool.error completes the latest running tool with an error", () => { - FastCheck.assert( - FastCheck.property(word, dict, word, maybe(dict), (callID, input, error, metadata) => { - const next = run( - [ - SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }), - SessionEvent.Tool.Called.create({ - callID, - tool: "bash", - input, - provider: { executed: true }, - timestamp: time(2), - }), - SessionEvent.Tool.Error.create({ - callID, - error, - metadata, - provider: { executed: true }, - timestamp: time(3), - }), - ], - active(), - ) - - const match = tool(next, callID) - expect(match?.state.status).toBe("error") - if (match?.state.status !== "error") return - - expect(match.time.ran).toEqual(time(2)) - expect(match.state.input).toEqual(input) - expect(match.state.error).toBe(error) - expect(match.state.metadata).toEqual(metadata ?? {}) - }), - { numRuns: 50 }, - ) - }) - - test("tool.success is ignored before tool.called promotes the tool to running", () => { - FastCheck.assert( - FastCheck.property(word, word, (callID, title) => { - const next = run( - [ - SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }), - SessionEvent.Tool.Success.create({ - callID, - title, - provider: { executed: true }, - timestamp: time(2), - }), - ], - active(), - ) - const match = tool(next, callID) - expect(match?.state).toEqual({ - status: "pending", - input: "", - }) - }), - { numRuns: 50 }, - ) - }) - - test("step.ended copies completion fields onto the pending assistant", () => { - FastCheck.assert( - FastCheck.property(FastCheck.integer({ min: 1, max: 1000 }), (n) => { - const event = SessionEvent.Step.Ended.create({ - reason: "stop", - cost: 1, - tokens: { - input: 1, - output: 2, - reasoning: 3, - cache: { - read: 4, - write: 5, - }, - }, - timestamp: time(n), - }) - const next = SessionEntryStepper.step(active(), event) - const entry = last(next) - expect(entry).toBeDefined() - if (!entry) return - - expect(entry.time.completed).toEqual(event.timestamp) - expect(entry.cost).toBe(event.cost) - expect(entry.tokens).toEqual(event.tokens) - }), - { numRuns: 50 }, - ) - }) - }) - - describe("known reducer gaps", () => { - test("prompt appends immutably when no assistant is pending", () => { - FastCheck.assert( - FastCheck.property(word, (body) => { - const old = memoryState() - const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) - expect(old).not.toBe(next) - expect(old.entries).toHaveLength(0) - expect(next.entries).toHaveLength(1) - }), - { numRuns: 50 }, - ) - }) - - test("prompt appends immutably when an assistant is pending", () => { - FastCheck.assert( - FastCheck.property(word, (body) => { - const old = active() - const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) - expect(old).not.toBe(next) - expect(old.pending).toHaveLength(0) - expect(next.pending).toHaveLength(1) - }), - { numRuns: 50 }, - ) - }) - - test("step.started creates an assistant consumed by follow-up events", () => { - FastCheck.assert( - FastCheck.property(texts, (parts) => { - const next = run([ - SessionEvent.Step.Started.create({ - model: { - id: "model", - providerID: "provider", - }, - timestamp: time(1), - }), - SessionEvent.Text.Started.create({ timestamp: time(2) }), - ...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })), - SessionEvent.Step.Ended.create({ - reason: "stop", - cost: 1, - tokens: { - input: 1, - output: 2, - reasoning: 3, - cache: { - read: 4, - write: 5, - }, - }, - timestamp: time(parts.length + 3), - }), - ]) - const entry = last(next) - - expect(entry).toBeDefined() - if (!entry) return - - expect(entry.content).toEqual([ - { - type: "text", - text: parts.join(""), - }, - ]) - expect(entry.time.completed).toEqual(time(parts.length + 3)) - }), - { numRuns: 100 }, - ) - }) - - test("replays prompt -> step -> text -> step.ended", () => { - FastCheck.assert( - FastCheck.property(word, texts, (body, parts) => { - const next = run([ - SessionEvent.Prompt.create({ text: body, timestamp: time(0) }), - SessionEvent.Step.Started.create({ - model: { - id: "model", - providerID: "provider", - }, - timestamp: time(1), - }), - SessionEvent.Text.Started.create({ timestamp: time(2) }), - ...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })), - SessionEvent.Step.Ended.create({ - reason: "stop", - cost: 1, - tokens: { - input: 1, - output: 2, - reasoning: 3, - cache: { - read: 4, - write: 5, - }, - }, - timestamp: time(parts.length + 3), - }), - ]) - - expect(next.entries).toHaveLength(2) - expect(next.entries[0]?.type).toBe("user") - expect(next.entries[1]?.type).toBe("assistant") - if (next.entries[1]?.type !== "assistant") return - - expect(next.entries[1].content).toEqual([ - { - type: "text", - text: parts.join(""), - }, - ]) - expect(next.entries[1].time.completed).toEqual(time(parts.length + 3)) - }), - { numRuns: 50 }, - ) - }) - - test("replays prompt -> step -> reasoning -> tool -> success -> step.ended", () => { - FastCheck.assert( - FastCheck.property( - word, - texts, - text, - dict, - word, - maybe(text), - maybe(dict), - maybe(files), - (body, reason, end, input, title, output, metadata, attachments) => { - const callID = "call" - const next = run([ - SessionEvent.Prompt.create({ text: body, timestamp: time(0) }), - SessionEvent.Step.Started.create({ - model: { - id: "model", - providerID: "provider", - }, - timestamp: time(1), - }), - SessionEvent.Reasoning.Started.create({ timestamp: time(2) }), - ...reason.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 3) })), - SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(reason.length + 3) }), - SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(reason.length + 4) }), - SessionEvent.Tool.Called.create({ - callID, - tool: "bash", - input, - provider: { executed: true }, - timestamp: time(reason.length + 5), - }), - SessionEvent.Tool.Success.create({ - callID, - title, - output, - metadata, - attachments, - provider: { executed: true }, - timestamp: time(reason.length + 6), - }), - SessionEvent.Step.Ended.create({ - reason: "stop", - cost: 1, - tokens: { - input: 1, - output: 2, - reasoning: 3, - cache: { - read: 4, - write: 5, - }, - }, - timestamp: time(reason.length + 7), - }), - ]) - - expect(next.entries.at(-1)?.type).toBe("assistant") - const entry = next.entries.at(-1) - if (entry?.type !== "assistant") return - - expect(entry.content).toHaveLength(2) - expect(entry.content[0]).toEqual({ - type: "reasoning", - text: end, - }) - expect(entry.content[1]?.type).toBe("tool") - if (entry.content[1]?.type !== "tool") return - expect(entry.content[1].state.status).toBe("completed") - expect(entry.time.completed).toEqual(time(reason.length + 7)) - }, - ), - { numRuns: 50 }, - ) - }) - - test("starting a new step completes the old assistant and appends a new active assistant", () => { - const next = run( - [ - SessionEvent.Step.Started.create({ - model: { - id: "model", - providerID: "provider", - }, - timestamp: time(1), - }), - ], - active(), - ) - expect(next.entries).toHaveLength(2) - expect(next.entries[0]?.type).toBe("assistant") - expect(next.entries[1]?.type).toBe("assistant") - if (next.entries[0]?.type !== "assistant" || next.entries[1]?.type !== "assistant") return - - expect(next.entries[0].time.completed).toEqual(time(1)) - expect(next.entries[1].time.created).toEqual(time(1)) - expect(next.entries[1].time.completed).toBeUndefined() - }) - - test("handles sequential tools independently", () => { - FastCheck.assert( - FastCheck.property(dict, dict, word, word, (a, b, title, error) => { - const next = run( - [ - SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }), - SessionEvent.Tool.Called.create({ - callID: "a", - tool: "bash", - input: a, - provider: { executed: true }, - timestamp: time(2), - }), - SessionEvent.Tool.Success.create({ - callID: "a", - title, - output: "done", - provider: { executed: true }, - timestamp: time(3), - }), - SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(4) }), - SessionEvent.Tool.Called.create({ - callID: "b", - tool: "bash", - input: b, - provider: { executed: true }, - timestamp: time(5), - }), - SessionEvent.Tool.Error.create({ - callID: "b", - error, - provider: { executed: true }, - timestamp: time(6), - }), - ], - active(), - ) - - const first = tool(next, "a") - const second = tool(next, "b") - - expect(first?.state.status).toBe("completed") - if (first?.state.status !== "completed") return - expect(first.state.input).toEqual(a) - expect(first.state.output).toBe("done") - expect(first.state.title).toBe(title) - - expect(second?.state.status).toBe("error") - if (second?.state.status !== "error") return - expect(second.state.input).toEqual(b) - expect(second.state.error).toBe(error) - }), - { numRuns: 50 }, - ) - }) - - test("routes tool events by callID when tool streams interleave", () => { - FastCheck.assert( - FastCheck.property(dict, dict, word, word, text, text, (a, b, titleA, titleB, deltaA, deltaB) => { - const next = run( - [ - SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }), - SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(2) }), - SessionEvent.Tool.Input.Delta.create({ callID: "a", delta: deltaA, timestamp: time(3) }), - SessionEvent.Tool.Input.Delta.create({ callID: "b", delta: deltaB, timestamp: time(4) }), - SessionEvent.Tool.Called.create({ - callID: "a", - tool: "bash", - input: a, - provider: { executed: true }, - timestamp: time(5), - }), - SessionEvent.Tool.Called.create({ - callID: "b", - tool: "grep", - input: b, - provider: { executed: true }, - timestamp: time(6), - }), - SessionEvent.Tool.Success.create({ - callID: "a", - title: titleA, - output: "done-a", - provider: { executed: true }, - timestamp: time(7), - }), - SessionEvent.Tool.Success.create({ - callID: "b", - title: titleB, - output: "done-b", - provider: { executed: true }, - timestamp: time(8), - }), - ], - active(), - ) - - const first = tool(next, "a") - const second = tool(next, "b") - - expect(first?.state.status).toBe("completed") - expect(second?.state.status).toBe("completed") - if (first?.state.status !== "completed" || second?.state.status !== "completed") return - - expect(first.state.input).toEqual(a) - expect(second.state.input).toEqual(b) - expect(first.state.title).toBe(titleA) - expect(second.state.title).toBe(titleB) - }), - { numRuns: 50 }, - ) - }) - - test("records synthetic events", () => { - FastCheck.assert( - FastCheck.property(word, (body) => { - const next = SessionEntryStepper.step( - memoryState(), - SessionEvent.Synthetic.create({ text: body, timestamp: time(1) }), - ) - expect(next.entries).toHaveLength(1) - expect(next.entries[0]?.type).toBe("synthetic") - if (next.entries[0]?.type !== "synthetic") return - expect(next.entries[0].text).toBe(body) - }), - { numRuns: 50 }, - ) - }) - - test("records compaction events", () => { - FastCheck.assert( - FastCheck.property(FastCheck.boolean(), maybe(FastCheck.boolean()), (auto, overflow) => { - const next = SessionEntryStepper.step( - memoryState(), - SessionEvent.Compacted.create({ auto, overflow, timestamp: time(1) }), - ) - expect(next.entries).toHaveLength(1) - expect(next.entries[0]?.type).toBe("compaction") - if (next.entries[0]?.type !== "compaction") return - expect(next.entries[0].auto).toBe(auto) - expect(next.entries[0].overflow).toBe(overflow) - }), - { numRuns: 50 }, - ) - }) - }) - }) -}) diff --git a/packages/opencode/test/v2/session-message-updater.test.ts b/packages/opencode/test/v2/session-message-updater.test.ts new file mode 100644 index 000000000000..e42fc78eede8 --- /dev/null +++ b/packages/opencode/test/v2/session-message-updater.test.ts @@ -0,0 +1,193 @@ +import { expect, test } from "bun:test" +import * as DateTime from "effect/DateTime" +import { SessionID } from "../../src/session/schema" +import { SessionEvent } from "../../src/v2/session-event" +import { SessionMessageUpdater } from "../../src/v2/session-message-updater" + +test("step snapshots carry over to assistant messages", () => { + const state: SessionMessageUpdater.MemoryState = { messages: [] } + const sessionID = SessionID.make("session") + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.step.started", + data: { + id: SessionEvent.ID.create(), + sessionID, + timestamp: DateTime.makeUnsafe(1), + agent: "build", + model: { id: "model", providerID: "provider" }, + snapshot: "before", + }, + } satisfies SessionEvent.Event) + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.step.ended", + data: { + sessionID, + timestamp: DateTime.makeUnsafe(2), + finish: "stop", + cost: 0, + tokens: { + input: 1, + output: 2, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + snapshot: "after", + }, + } satisfies SessionEvent.Event) + + expect(state.messages[0]?.type).toBe("assistant") + if (state.messages[0]?.type !== "assistant") return + expect(state.messages[0].snapshot).toEqual({ start: "before", end: "after" }) + expect(state.messages[0].finish).toBe("stop") +}) + +test("text ended populates assistant text content", () => { + const state: SessionMessageUpdater.MemoryState = { messages: [] } + const sessionID = SessionID.make("session") + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.step.started", + data: { + id: SessionEvent.ID.create(), + sessionID, + timestamp: DateTime.makeUnsafe(1), + agent: "build", + model: { id: "model", providerID: "provider" }, + }, + } satisfies SessionEvent.Event) + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.text.started", + data: { + sessionID, + timestamp: DateTime.makeUnsafe(2), + }, + } satisfies SessionEvent.Event) + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.text.ended", + data: { + sessionID, + timestamp: DateTime.makeUnsafe(3), + text: "hello assistant", + }, + } satisfies SessionEvent.Event) + + expect(state.messages[0]?.type).toBe("assistant") + if (state.messages[0]?.type !== "assistant") return + expect(state.messages[0].content).toEqual([{ type: "text", text: "hello assistant" }]) +}) + +test("tool completion stores completed timestamp", () => { + const state: SessionMessageUpdater.MemoryState = { messages: [] } + const sessionID = SessionID.make("session") + const callID = "call" + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.step.started", + data: { + id: SessionEvent.ID.create(), + sessionID, + timestamp: DateTime.makeUnsafe(1), + agent: "build", + model: { id: "model", providerID: "provider" }, + }, + } satisfies SessionEvent.Event) + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.tool.input.started", + data: { + sessionID, + timestamp: DateTime.makeUnsafe(2), + callID, + name: "bash", + }, + } satisfies SessionEvent.Event) + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.tool.called", + data: { + sessionID, + timestamp: DateTime.makeUnsafe(3), + callID, + tool: "bash", + input: { command: "pwd" }, + provider: { executed: true, metadata: { source: "provider" } }, + }, + } satisfies SessionEvent.Event) + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.tool.success", + data: { + sessionID, + timestamp: DateTime.makeUnsafe(4), + callID, + structured: {}, + content: [{ type: "text", text: "/tmp" }], + provider: { executed: true, metadata: { status: "done" } }, + }, + } satisfies SessionEvent.Event) + + expect(state.messages[0]?.type).toBe("assistant") + if (state.messages[0]?.type !== "assistant") return + expect(state.messages[0].content[0]?.type).toBe("tool") + if (state.messages[0].content[0]?.type !== "tool") return + expect(state.messages[0].content[0].time.completed).toEqual(DateTime.makeUnsafe(4)) + expect(state.messages[0].content[0].provider).toEqual({ executed: true, metadata: { status: "done" } }) +}) + +test("compaction events reduce to compaction message", () => { + const state: SessionMessageUpdater.MemoryState = { messages: [] } + const sessionID = SessionID.make("session") + const id = SessionEvent.ID.create() + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.compaction.started", + data: { + id, + sessionID, + timestamp: DateTime.makeUnsafe(1), + reason: "auto", + }, + } satisfies SessionEvent.Event) + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.compaction.delta", + data: { + sessionID, + timestamp: DateTime.makeUnsafe(2), + text: "hello ", + }, + } satisfies SessionEvent.Event) + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.compaction.delta", + data: { + sessionID, + timestamp: DateTime.makeUnsafe(3), + text: "summary", + }, + } satisfies SessionEvent.Event) + + SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + type: "session.next.compaction.ended", + data: { + sessionID, + timestamp: DateTime.makeUnsafe(4), + text: "final summary", + include: "recent context", + }, + } satisfies SessionEvent.Event) + + expect(state.messages).toHaveLength(1) + expect(state.messages[0]).toMatchObject({ + id, + type: "compaction", + reason: "auto", + summary: "final summary", + include: "recent context", + time: { created: DateTime.makeUnsafe(1) }, + }) +}) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 67261d7499a8..dd6783e21ddf 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -187,6 +187,8 @@ import type { TuiSelectSessionResponses, TuiShowToastResponses, TuiSubmitPromptResponses, + V2SessionMessagesErrors, + V2SessionMessagesResponses, VcsDiffResponses, VcsGetResponses, WorktreeCreateErrors, @@ -3179,6 +3181,47 @@ export class Sync extends HeyApiClient { } } +export class Session3 extends HeyApiClient { + /** + * Get v2 session messages + * + * Retrieve projected v2 messages for a session directly from the message database. + */ + public messages( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/session/{sessionID}/message", + ...options, + ...params, + }) + } +} + +export class V2 extends HeyApiClient { + private _session?: Session3 + get session(): Session3 { + return (this._session ??= new Session3({ client: this.client })) + } +} + export class Find extends HeyApiClient { /** * Find text @@ -4440,6 +4483,11 @@ export class OpencodeClient extends HeyApiClient { return (this._sync ??= new Sync({ client: this.client })) } + private _v2?: V2 + get v2(): V2 { + return (this._v2 ??= new V2({ client: this.client })) + } + private _find?: Find get find(): Find { return (this._find ??= new Find({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 31bd40ab4ffc..898cc7fc0ac2 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -974,6 +974,7 @@ export type EventSessionDeleted = { } } +<<<<<<< HEAD export type EventServerConnected = { type: "server.connected" properties: { @@ -985,124 +986,757 @@ export type EventGlobalDisposed = { type: "global.disposed" properties: { [key: string]: unknown +======= +export type PromptSource = { + start: number + end: number + text: string +} + +export type PromptFileAttachment = { + uri: string + mime: string + name?: string + description?: string + source?: PromptSource +} + +export type PromptAgentAttachment = { + name: string + source?: PromptSource +} + +export type Prompt = { + text: string + files?: Array + agents?: Array +} + +export type EventSessionNextPrompted = { + type: "session.next.prompted" + properties: { + timestamp: number + sessionID: string + id: string + prompt: Prompt + } +} + +export type EventSessionNextSynthetic = { + type: "session.next.synthetic" + properties: { + timestamp: number + sessionID: string + id: string + text: string + } +} + +export type EventSessionNextStepStarted = { + type: "session.next.step.started" + properties: { + timestamp: number + sessionID: string + id: string + agent: string + model: { + id: string + providerID: string + variant?: string + } + snapshot?: string + } +} + +export type EventSessionNextStepEnded = { + type: "session.next.step.ended" + properties: { + timestamp: number + sessionID: string + finish: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + snapshot?: string + } +} + +export type EventSessionNextTextStarted = { + type: "session.next.text.started" + properties: { + timestamp: number + sessionID: string + } +} + +export type EventSessionNextTextDelta = { + type: "session.next.text.delta" + properties: { + timestamp: number + sessionID: string + delta: string + } +} + +export type EventSessionNextTextEnded = { + type: "session.next.text.ended" + properties: { + timestamp: number + sessionID: string + text: string + } +} + +export type EventSessionNextReasoningStarted = { + type: "session.next.reasoning.started" + properties: { + timestamp: number + sessionID: string + reasoningID: string + } +} + +export type EventSessionNextReasoningDelta = { + type: "session.next.reasoning.delta" + properties: { + timestamp: number + sessionID: string + reasoningID: string + delta: string + } +} + +export type EventSessionNextReasoningEnded = { + type: "session.next.reasoning.ended" + properties: { + timestamp: number + sessionID: string + reasoningID: string + text: string + } +} + +export type EventSessionNextToolInputStarted = { + type: "session.next.tool.input.started" + properties: { + timestamp: number + sessionID: string + callID: string + name: string + } +} + +export type EventSessionNextToolInputDelta = { + type: "session.next.tool.input.delta" + properties: { + timestamp: number + sessionID: string + callID: string + delta: string + } +} + +export type EventSessionNextToolInputEnded = { + type: "session.next.tool.input.ended" + properties: { + timestamp: number + sessionID: string + callID: string + text: string + } +} + +export type EventSessionNextToolCalled = { + type: "session.next.tool.called" + properties: { + timestamp: number + sessionID: string + callID: string + tool: string + input: { + [key: string]: unknown + } + provider: { + executed: boolean + metadata?: { + [key: string]: unknown + } + } + } +} + +export type ToolTextContent = { + type: "text" + text: string +} + +export type ToolFileContent = { + type: "file" + uri: string + mime: string + name?: string +} + +export type EventSessionNextToolProgress = { + type: "session.next.tool.progress" + properties: { + timestamp: number + sessionID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + } +} + +export type EventSessionNextToolSuccess = { + type: "session.next.tool.success" + properties: { + timestamp: number + sessionID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + provider: { + executed: boolean + metadata?: { + [key: string]: unknown + } + } + } +} + +export type EventSessionNextToolError = { + type: "session.next.tool.error" + properties: { + timestamp: number + sessionID: string + callID: string + error: { + type: string + message: string + } + provider: { + executed: boolean + metadata?: { + [key: string]: unknown + } + } + } +} + +export type SessionNextRetryError = { + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: { + [key: string]: string + } + responseBody?: string + metadata?: { + [key: string]: string + } +} + +export type EventSessionNextRetried = { + type: "session.next.retried" + properties: { + timestamp: number + sessionID: string + attempt: number + error: SessionNextRetryError + } +} + +export type EventSessionNextCompactionStarted = { + type: "session.next.compaction.started" + properties: { + timestamp: number + sessionID: string + id: string + reason: "auto" | "manual" + } +} + +export type EventSessionNextCompactionDelta = { + type: "session.next.compaction.delta" + properties: { + timestamp: number + sessionID: string + text: string + } +} + +export type EventSessionNextCompactionEnded = { + type: "session.next.compaction.ended" + properties: { + timestamp: number + sessionID: string + text: string + include?: string +>>>>>>> 9d5f5e1ff (refactor(session): define v2 session event schemas) + } +} + +export type SyncEventMessageUpdated = { + type: "sync" + name: "message.updated.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + sessionID: string + info: Message + } +} + +export type SyncEventMessageRemoved = { + type: "sync" + name: "message.removed.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + sessionID: string + messageID: string + } +} + +export type SyncEventMessagePartUpdated = { + type: "sync" + name: "message.part.updated.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + sessionID: string + part: Part + time: number + } +} + +export type SyncEventMessagePartRemoved = { + type: "sync" + name: "message.part.removed.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + sessionID: string + messageID: string + partID: string + } +} + +export type SyncEventSessionCreated = { + type: "sync" + name: "session.created.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + sessionID: string + info: Session + } +} + +export type SyncEventSessionUpdated = { + type: "sync" + name: "session.updated.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + sessionID: string + info: { + id?: string | null + slug?: string | null + projectID?: string | null + workspaceID?: string | null + directory?: string | null + path?: string | null + parentID?: string | null + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } | null + share?: { + url?: string | null + } + title?: string | null + version?: string | null + time?: { + created?: number | null + updated?: number | null + compacting?: number | null + archived?: number | null + } + permission?: PermissionRuleset | null + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } | null + } + } +} + +export type SyncEventSessionDeleted = { + type: "sync" + name: "session.deleted.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + sessionID: string + info: Session + } +} + +export type SyncEventSessionNextPrompted = { + type: "sync" + name: "session.next.prompted.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + id: string + prompt: Prompt + } +} + +export type SyncEventSessionNextSynthetic = { + type: "sync" + name: "session.next.synthetic.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + id: string + text: string + } +} + +export type SyncEventSessionNextStepStarted = { + type: "sync" + name: "session.next.step.started.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + id: string + agent: string + model: { + id: string + providerID: string + variant?: string + } + snapshot?: string + } +} + +export type SyncEventSessionNextStepEnded = { + type: "sync" + name: "session.next.step.ended.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + finish: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + snapshot?: string + } +} + +export type SyncEventSessionNextTextStarted = { + type: "sync" + name: "session.next.text.started.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + } +} + +export type SyncEventSessionNextTextDelta = { + type: "sync" + name: "session.next.text.delta.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + delta: string + } +} + +export type SyncEventSessionNextTextEnded = { + type: "sync" + name: "session.next.text.ended.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + text: string + } +} + +export type SyncEventSessionNextReasoningStarted = { + type: "sync" + name: "session.next.reasoning.started.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + reasoningID: string + } +} + +export type SyncEventSessionNextReasoningDelta = { + type: "sync" + name: "session.next.reasoning.delta.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + reasoningID: string + delta: string + } +} + +export type SyncEventSessionNextReasoningEnded = { + type: "sync" + name: "session.next.reasoning.ended.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + reasoningID: string + text: string } } -export type SyncEventMessageUpdated = { +export type SyncEventSessionNextToolInputStarted = { type: "sync" - name: "message.updated.1" + name: "session.next.tool.input.started.1" id: string seq: number aggregateID: "sessionID" data: { + timestamp: number sessionID: string - info: Message + callID: string + name: string } } -export type SyncEventMessageRemoved = { +export type SyncEventSessionNextToolInputDelta = { type: "sync" - name: "message.removed.1" + name: "session.next.tool.input.delta.1" id: string seq: number aggregateID: "sessionID" data: { + timestamp: number sessionID: string - messageID: string + callID: string + delta: string } } -export type SyncEventMessagePartUpdated = { +export type SyncEventSessionNextToolInputEnded = { type: "sync" - name: "message.part.updated.1" + name: "session.next.tool.input.ended.1" id: string seq: number aggregateID: "sessionID" data: { + timestamp: number sessionID: string - part: Part - time: number + callID: string + text: string } } -export type SyncEventMessagePartRemoved = { +export type SyncEventSessionNextToolCalled = { type: "sync" - name: "message.part.removed.1" + name: "session.next.tool.called.1" id: string seq: number aggregateID: "sessionID" data: { + timestamp: number sessionID: string - messageID: string - partID: string + callID: string + tool: string + input: { + [key: string]: unknown + } + provider: { + executed: boolean + metadata?: { + [key: string]: unknown + } + } } } -export type SyncEventSessionCreated = { +export type SyncEventSessionNextToolProgress = { type: "sync" - name: "session.created.1" + name: "session.next.tool.progress.1" id: string seq: number aggregateID: "sessionID" data: { + timestamp: number sessionID: string - info: Session + callID: string + structured: { + [key: string]: unknown + } + content: Array } } -export type SyncEventSessionUpdated = { +export type SyncEventSessionNextToolSuccess = { type: "sync" - name: "session.updated.1" + name: "session.next.tool.success.1" id: string seq: number aggregateID: "sessionID" data: { + timestamp: number sessionID: string - info: { - id?: string | null - slug?: string | null - projectID?: string | null - workspaceID?: string | null - directory?: string | null - path?: string | null - parentID?: string | null - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } | null - share?: { - url?: string | null + callID: string + structured: { + [key: string]: unknown + } + content: Array + provider: { + executed: boolean + metadata?: { + [key: string]: unknown } - title?: string | null - version?: string | null - time?: { - created?: number | null - updated?: number | null - compacting?: number | null - archived?: number | null + } + } +} + +export type SyncEventSessionNextToolError = { + type: "sync" + name: "session.next.tool.error.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + callID: string + error: { + type: string + message: string + } + provider: { + executed: boolean + metadata?: { + [key: string]: unknown } - permission?: PermissionRuleset | null - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } | null } } } -export type SyncEventSessionDeleted = { +export type SyncEventSessionNextRetried = { type: "sync" - name: "session.deleted.1" + name: "session.next.retried.1" id: string seq: number aggregateID: "sessionID" data: { + timestamp: number sessionID: string - info: Session + attempt: number + error: SessionNextRetryError + } +} + +export type SyncEventSessionNextCompactionStarted = { + type: "sync" + name: "session.next.compaction.started.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + id: string + reason: "auto" | "manual" + } +} + +export type SyncEventSessionNextCompactionDelta = { + type: "sync" + name: "session.next.compaction.delta.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + text: string + } +} + +export type SyncEventSessionNextCompactionEnded = { + type: "sync" + name: "session.next.compaction.ended.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + text: string + include?: string } } @@ -1156,8 +1790,32 @@ export type GlobalEvent = { | EventSessionCreated | EventSessionUpdated | EventSessionDeleted +<<<<<<< HEAD | EventServerConnected | EventGlobalDisposed +======= + | EventSessionNextPrompted + | EventSessionNextSynthetic + | EventSessionNextStepStarted + | EventSessionNextStepEnded + | EventSessionNextTextStarted + | EventSessionNextTextDelta + | EventSessionNextTextEnded + | EventSessionNextReasoningStarted + | EventSessionNextReasoningDelta + | EventSessionNextReasoningEnded + | EventSessionNextToolInputStarted + | EventSessionNextToolInputDelta + | EventSessionNextToolInputEnded + | EventSessionNextToolCalled + | EventSessionNextToolProgress + | EventSessionNextToolSuccess + | EventSessionNextToolError + | EventSessionNextRetried + | EventSessionNextCompactionStarted + | EventSessionNextCompactionDelta + | EventSessionNextCompactionEnded +>>>>>>> 9d5f5e1ff (refactor(session): define v2 session event schemas) | SyncEventMessageUpdated | SyncEventMessageRemoved | SyncEventMessagePartUpdated @@ -1165,6 +1823,27 @@ export type GlobalEvent = { | SyncEventSessionCreated | SyncEventSessionUpdated | SyncEventSessionDeleted + | SyncEventSessionNextPrompted + | SyncEventSessionNextSynthetic + | SyncEventSessionNextStepStarted + | SyncEventSessionNextStepEnded + | SyncEventSessionNextTextStarted + | SyncEventSessionNextTextDelta + | SyncEventSessionNextTextEnded + | SyncEventSessionNextReasoningStarted + | SyncEventSessionNextReasoningDelta + | SyncEventSessionNextReasoningEnded + | SyncEventSessionNextToolInputStarted + | SyncEventSessionNextToolInputDelta + | SyncEventSessionNextToolInputEnded + | SyncEventSessionNextToolCalled + | SyncEventSessionNextToolProgress + | SyncEventSessionNextToolSuccess + | SyncEventSessionNextToolError + | SyncEventSessionNextRetried + | SyncEventSessionNextCompactionStarted + | SyncEventSessionNextCompactionDelta + | SyncEventSessionNextCompactionEnded } /** @@ -2013,6 +2692,165 @@ export type ProviderAuthAuthorization = { instructions: string } +export type SessionMessageUser = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + text: string + files?: Array + agents?: Array + type: "user" +} + +export type SessionMessageSynthetic = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + sessionID: string + text: string + type: "synthetic" +} + +export type SessionMessageAssistantText = { + type: "text" + text: string +} + +export type SessionMessageAssistantReasoning = { + type: "reasoning" + id: string + text: string +} + +export type SessionMessageToolStatePending = { + status: "pending" + input: string +} + +export type SessionMessageToolStateRunning = { + status: "running" + input: { + [key: string]: unknown + } + structured: { + [key: string]: unknown + } + content: Array +} + +export type SessionMessageToolStateCompleted = { + status: "completed" + input: { + [key: string]: unknown + } + attachments?: Array + content: Array + structured: { + [key: string]: unknown + } +} + +export type SessionMessageToolStateError = { + status: "error" + input: { + [key: string]: unknown + } + content: Array + structured: { + [key: string]: unknown + } + error: { + type: string + message: string + } +} + +export type SessionMessageAssistantTool = { + type: "tool" + id: string + name: string + provider?: { + executed: boolean + metadata?: { + [key: string]: unknown + } + } + state: + | SessionMessageToolStatePending + | SessionMessageToolStateRunning + | SessionMessageToolStateCompleted + | SessionMessageToolStateError + time: { + created: number + ran?: number + completed?: number + pruned?: number + } +} + +export type SessionMessageAssistant = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + completed?: number + } + type: "assistant" + agent: string + model: { + id: string + providerID: string + variant?: string + } + content: Array + snapshot?: { + start?: string + end?: string + } + finish?: string + cost?: number + tokens?: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + error?: string +} + +export type SessionMessageCompaction = { + type: "compaction" + reason: "auto" | "manual" + summary: string + include?: string + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } +} + +export type SessionMessage = + | SessionMessageUser + | SessionMessageSynthetic + | SessionMessageAssistant + | SessionMessageCompaction + export type Symbol = { name: string kind: number @@ -2105,8 +2943,32 @@ export type Event = | EventSessionCreated | EventSessionUpdated | EventSessionDeleted +<<<<<<< HEAD | EventServerConnected | EventGlobalDisposed +======= + | EventSessionNextPrompted + | EventSessionNextSynthetic + | EventSessionNextStepStarted + | EventSessionNextStepEnded + | EventSessionNextTextStarted + | EventSessionNextTextDelta + | EventSessionNextTextEnded + | EventSessionNextReasoningStarted + | EventSessionNextReasoningDelta + | EventSessionNextReasoningEnded + | EventSessionNextToolInputStarted + | EventSessionNextToolInputDelta + | EventSessionNextToolInputEnded + | EventSessionNextToolCalled + | EventSessionNextToolProgress + | EventSessionNextToolSuccess + | EventSessionNextToolError + | EventSessionNextRetried + | EventSessionNextCompactionStarted + | EventSessionNextCompactionDelta + | EventSessionNextCompactionEnded +>>>>>>> 9d5f5e1ff (refactor(session): define v2 session event schemas) export type McpStatusConnected = { status: "connected" @@ -4670,6 +5532,40 @@ export type SyncHistoryListResponses = { export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses] +export type V2SessionMessagesData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/api/session/{sessionID}/message" +} + +export type V2SessionMessagesErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type V2SessionMessagesError = V2SessionMessagesErrors[keyof V2SessionMessagesErrors] + +export type V2SessionMessagesResponses = { + /** + * List of v2 session messages + */ + 200: Array +} + +export type V2SessionMessagesResponse = V2SessionMessagesResponses[keyof V2SessionMessagesResponses] + export type FindTextData = { body?: never path?: never diff --git a/specs/v2/session-concepts-gap.md b/specs/v2/session-concepts-gap.md new file mode 100644 index 000000000000..20d84c8f4748 --- /dev/null +++ b/specs/v2/session-concepts-gap.md @@ -0,0 +1,131 @@ +# Session V2 Concept Gaps + +Compared with `packages/opencode/src/session/message-v2.ts` and `packages/opencode/src/session/processor.ts`, `packages/opencode/src/v2` currently captures the rough event stream for prompts, assistant steps, text, reasoning, tools, retries, and compaction, but it does not yet capture several persisted-message and processor concepts. + +## Message Metadata + +- User messages are missing selected `agent`, `model`, `system`, enabled `tools`, output `format`, and summary metadata. +- Assistant messages are missing `parentID`, `agent`, `providerID`, `modelID`, `variant`, `path.cwd`, `path.root`, deprecated `mode`, `summary`, `structured`, `finish`, and typed `error`. + +## Output Format + +- Text output format. +- JSON-schema output format. +- Structured-output retry count. +- Structured assistant result payload. +- Structured-output error classification. + +## Errors + +- Aborted error. +- Provider auth error. +- API error with status, retryability, headers, body, and metadata. +- Context-overflow error. +- Output-length error. +- Unknown error. +- V2 mostly reduces assistant errors to strings, except retry errors. + +## Part Identity + +- V1 has stable `MessageID`, `PartID`, `sessionID`, and `messageID` on every part. +- V2 assistant content does not preserve stable per-content IDs. +- Stable content IDs matter for deltas, updates, removals, sync events, and UI reconciliation. + +## Part Timing And Metadata + +- V1 text, reasoning, and tool states carry timing and provider metadata. +- V2 assistant text and reasoning content only store text. +- V2 events include metadata, but `SessionEntry` currently drops most provider metadata. + +## Snapshots And Patches + +- Snapshot parts. +- Patch parts. +- Step-start snapshot references. +- Step-finish snapshot references. +- Processor behavior that tracks a snapshot before the stream and emits patches after step finish or cleanup. + +## Step Boundaries + +- V1 stores `step-start` and `step-finish` as first-class parts. +- V2 has `step.started` and `step.ended` events, but the assistant entry only stores aggregate cost and tokens. +- V2 does not preserve step boundary parts, finish reason, or snapshot details in the entry model. + +## Compaction + +- V1 compaction parts have `auto`, `overflow`, and `tail_start_id`. +- V2 compacted events have `auto` and optional `overflow`, but no retained-tail marker. +- V1 also has history filtering semantics around completed summary messages and retained tails. + +## Files And Sources + +- V1 file parts have `mime`, `filename`, `url`, and typed source information. +- V1 source variants include file, symbol, and resource sources. +- Symbol sources include LSP range, name, and kind. +- Resource sources include client name and URI. +- V2 file attachments have `uri`, `mime`, `name`, `description`, and a generic text source, but lose source type, LSP metadata, and resource metadata. + +## Agents And Subtasks + +- Agent parts. +- Subtask parts. +- Subtask prompt, description, agent, model, and command. +- V2 has agent attachments on prompts, but no assistant/session content equivalent for subtask execution. + +## Text Flags + +- Synthetic text flag. +- Ignored text flag. +- V2 has a separate synthetic entry, but no ignored text concept. + +## Tool Calls + +- V1 pending tool state stores parsed input and raw input text separately. +- V2 pending tool state stores a string input but does not preserve a separate raw field. +- V1 completed tool state has `time.start`, `time.end`, and optional `time.compacted`. +- V2 tool time has `created`, `ran`, `completed`, and `pruned`, but the stepper currently does not set `completed` or `pruned`. +- V1 error tool state has `time.start` and `time.end`. +- V1 supports interrupted tool errors with `metadata.interrupted` and preserved partial output. +- V1 tracks provider execution and provider call metadata. +- V2 events include provider info, but `SessionEntryStepper` drops it from entries. +- V1 has tool-output compaction and truncation behavior via `time.compacted`. + +## Media Handling + +- V1 models tool attachments as file parts and has provider-specific handling for media in tool results. +- V1 can strip media, inject synthetic user messages for unsupported providers, and uses a synthetic attachment prompt. +- V2 has attachments but not these model-message conversion semantics. + +## Retries + +- V1 stores retries as independently addressable retry parts. +- V2 stores retries as an assistant aggregate. +- V2 captures some retry information, but not the independent part identity/update model. + +## Processor Control Flow + +- Session status transitions: busy, retry, and idle. +- Retry policy integration. +- Context-overflow-driven compaction. +- Abort and interrupt handling. +- Permission-denied blocking. +- Doom-loop detection. +- Plugin hook for `experimental.text.complete`. +- Background summary generation after steps. +- Cleanup semantics for open text, reasoning, and tool calls. + +## Sync And Bus Events + +- Message updated. +- Message removed. +- Message part updated. +- Message part delta. +- Message part removed. +- V2 has domain events, but not the sync/bus event model for persisted message and part updates/removals. + +## History Retrieval + +- Cursor encoding and decoding. +- Paged message retrieval. +- Reverse streaming through history. +- Compaction-aware history filtering. From 199b5cf3aa03a770e23b205371c6cbb62758ba0f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 30 Apr 2026 19:17:38 -0400 Subject: [PATCH 02/24] core: simplify message history pagination with unified cursor API Replace separate before/after query parameters with a single cursor that carries direction info. Chat clients can now use 'start' or 'end' keywords to jump to the beginning or newest messages, and navigate history with a single cursor parameter instead of managing multiple pagination states. --- .../src/server/routes/instance/httpapi/api.ts | 2 +- .../instance/httpapi/{ => groups}/v2.ts | 22 ++--- .../routes/instance/httpapi/handlers/v2.ts | 27 +++--- packages/opencode/src/v2/session.ts | 89 ++++++++----------- 4 files changed, 59 insertions(+), 81 deletions(-) rename packages/opencode/src/server/routes/instance/httpapi/{ => groups}/v2.ts (78%) diff --git a/packages/opencode/src/server/routes/instance/httpapi/api.ts b/packages/opencode/src/server/routes/instance/httpapi/api.ts index 722be3e66748..1cf1584e3eea 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/api.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/api.ts @@ -19,7 +19,7 @@ import { SessionApi } from "./groups/session" import { SyncApi } from "./groups/sync" import { TuiApi } from "./groups/tui" import { WorkspaceApi } from "./groups/workspace" -import { V2Api } from "./v2" +import { V2Api } from "./groups/v2" // SSE event schemas built from the same BusEvent/SyncEvent registries that // the Hono spec uses, so both specs emit identical Event/SyncEvent components. diff --git a/packages/opencode/src/server/routes/instance/httpapi/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts similarity index 78% rename from packages/opencode/src/server/routes/instance/httpapi/v2.ts rename to packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts index 4d54cad32fe3..909b942c2051 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/v2.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts @@ -4,7 +4,7 @@ import { Prompt } from "@/v2/session-prompt" import { SessionV2 } from "@/v2/session" import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" -import { Authorization } from "./middleware/authorization" +import { Authorization } from "../middleware/authorization" export const V2Api = HttpApi.make("v2") .add( @@ -23,23 +23,17 @@ export const V2Api = HttpApi.make("v2") description: "Maximum number of messages to return. When omitted, the endpoint returns its default page size. Use limit without a cursor to fetch the newest page for chat history.", }), - before: Schema.optional(Schema.String).annotate({ + cursor: Schema.optional(Schema.String).annotate({ description: - "Opaque pagination cursor for the item at the start of the current window. Returns messages older than this cursor. Mutually exclusive with after.", - }), - after: Schema.optional(Schema.String).annotate({ - description: - "Opaque pagination cursor for the item at the end of the current window. Returns messages newer than this cursor. Mutually exclusive with before.", - }), - from: Schema.optional(Schema.Literal("start")).annotate({ - description: - "Start from the beginning of session history instead of the newest messages. Mutually exclusive with before and after.", + "Opaque pagination cursor returned as before or after in the previous response. The cursor encodes whether to fetch older or newer messages. Use start to read from the beginning or end to read from the latest messages; end is the default.", }), }).annotate({ identifier: "V2SessionMessagesQuery" }), success: Schema.Struct({ items: Schema.Array(SessionMessage.Message), - before: Schema.String.pipe(Schema.optional), - after: Schema.String.pipe(Schema.optional), + cursor: Schema.Struct({ + before: Schema.String.pipe(Schema.optional), + after: Schema.String.pipe(Schema.optional), + }), }).annotate({ identifier: "V2SessionMessagesResponse" }), error: HttpApiError.BadRequest, }).annotateMerge( @@ -47,7 +41,7 @@ export const V2Api = HttpApi.make("v2") identifier: "v2.session.messages", summary: "Get v2 session messages", description: - "Retrieve projected v2 messages for a session. For chat clients, request the latest page with limit, page backward through older history with before, and catch up with newer messages using after.", + "Retrieve projected v2 messages for a session. For chat clients, request the latest page with limit, page backward through older history with the before cursor, and catch up with newer messages using the after cursor.", }), ), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts index dac1b0336966..b337b08aad98 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts @@ -10,14 +10,15 @@ const DefaultMessagesLimit = 50 const Cursor = Schema.Struct({ id: SessionMessage.ID, time: Schema.Number, + from: Schema.Union([Schema.Literal("start"), Schema.Literal("end")]), }) const decodeCursor = Schema.decodeUnknownSync(Cursor) const cursor = { - encode(message: SessionMessage.Message) { + encode(message: SessionMessage.Message, from: "start" | "end") { return Buffer.from( - JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created) }), + JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), from }), ).toString("base64url") }, decode(input: string) { @@ -33,29 +34,27 @@ export const v2Handlers = HttpApiBuilder.group(InstanceHttpApi, "v2", (handlers) .handle( "messages", Effect.fn(function* (ctx) { - if (ctx.query.before && ctx.query.after) return yield* new HttpApiError.BadRequest({}) - if (ctx.query.from && (ctx.query.before || ctx.query.after)) return yield* new HttpApiError.BadRequest({}) const decoded = yield* Effect.try({ - try: () => { - return { - before: ctx.query.before ? cursor.decode(ctx.query.before) : undefined, - after: ctx.query.after ? cursor.decode(ctx.query.after) : undefined, - } - }, + try: () => + ctx.query.cursor && ctx.query.cursor !== "start" && ctx.query.cursor !== "end" + ? cursor.decode(ctx.query.cursor) + : undefined, catch: () => new HttpApiError.BadRequest({}), }) const messages = yield* session.messages({ sessionID: ctx.params.sessionID, limit: ctx.query.limit ?? DefaultMessagesLimit, - cursor: decoded.before ?? decoded.after, - direction: decoded.after ? "after" : "before", + from: decoded?.from ?? (ctx.query.cursor === "start" ? "start" : "end"), + cursor: decoded ? { id: decoded.id, time: decoded.time } : undefined, }) const oldest = messages[0] const newest = messages.at(-1) return { items: messages, - before: oldest ? cursor.encode(oldest) : undefined, - after: newest ? cursor.encode(newest) : undefined, + cursor: { + before: oldest ? cursor.encode(oldest, "end") : undefined, + after: newest ? cursor.encode(newest, "start") : undefined, + }, } }), ) diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index 7d134cc29feb..5268f4587df0 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -16,32 +16,16 @@ export type Delivery = Schema.Schema.Type export const DefaultDelivery = "immediate" satisfies Delivery -export type MessagesCursor = { - id: SessionMessage.ID - time: number -} - -export type MessagesInput = { - sessionID: SessionID - limit?: number - cursor?: MessagesCursor - direction?: "before" | "after" -} - -const older = (item: MessagesCursor) => - or( - lt(SessionMessageTable.time_created, item.time), - and(eq(SessionMessageTable.time_created, item.time), lt(SessionMessageTable.id, item.id)), - ) - -const newer = (item: MessagesCursor) => - or( - gt(SessionMessageTable.time_created, item.time), - and(eq(SessionMessageTable.time_created, item.time), gt(SessionMessageTable.id, item.id)), - ) - export interface Interface { - readonly messages: (input: MessagesInput) => Effect.Effect + readonly messages: (input: { + sessionID: SessionID + limit?: number + from?: "start" | "end" + cursor?: { + id: SessionMessage.ID + time: number + } + }) => Effect.Effect readonly prompt: (input: { id?: Event.ID sessionID: SessionID @@ -64,44 +48,45 @@ export const layer = Layer.effect( const result: Interface = { messages: Effect.fn("V2Session.messages")(function* (input) { - if (input.limit === undefined) { - const rows = Database.use((db) => - db - .select() - .from(SessionMessageTable) - .where(eq(SessionMessageTable.session_id, input.sessionID)) - .orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id)) - .all(), - ) - return rows.map((row) => decode(row)) - } - - const limit = input.limit - const direction = input.direction ?? "before" - const where = input.cursor - ? and( - eq(SessionMessageTable.session_id, input.sessionID), - direction === "after" ? newer(input.cursor) : older(input.cursor), - ) + const from = input.from ?? (input.limit === undefined && input.cursor === undefined ? "start" : "end") + const boundary = input.cursor + ? from === "start" + ? or( + gt(SessionMessageTable.time_created, input.cursor.time), + and( + eq(SessionMessageTable.time_created, input.cursor.time), + gt(SessionMessageTable.id, input.cursor.id), + ), + ) + : or( + lt(SessionMessageTable.time_created, input.cursor.time), + and( + eq(SessionMessageTable.time_created, input.cursor.time), + lt(SessionMessageTable.id, input.cursor.id), + ), + ) + : undefined + const where = boundary + ? and(eq(SessionMessageTable.session_id, input.sessionID), boundary) : eq(SessionMessageTable.session_id, input.sessionID) + const rows = Database.use((db) => { - if (direction === "after") { - return db + if (from === "start") { + const query = db .select() .from(SessionMessageTable) .where(where) .orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id)) - .limit(limit) - .all() + return input.limit === undefined ? query.all() : query.limit(input.limit).all() } - const ids = db + const idsQuery = db .select({ id: SessionMessageTable.id }) .from(SessionMessageTable) .where(where) .orderBy(desc(SessionMessageTable.time_created), desc(SessionMessageTable.id)) - .limit(limit) - .all() - .map((row) => row.id) + const ids = (input.limit === undefined ? idsQuery.all() : idsQuery.limit(input.limit).all()).map( + (row) => row.id, + ) if (ids.length === 0) return [] return db .select() From 90544a536d1658522d100ee9c2b935a591179cfd Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 30 Apr 2026 19:30:34 -0400 Subject: [PATCH 03/24] core: add pagination support for session messages with cursor-based navigation Enables loading messages in chunks for better performance with long conversations. Users can now navigate through large session histories without loading all messages at once. Includes before/after cursors for bi-directional pagination. --- .../src/cli/cmd/tui/context/sync-v2.tsx | 6 +- .../opencode/src/server/routes/instance/v2.ts | 66 ++++++++++++++++++- packages/sdk/js/src/v2/gen/sdk.gen.ts | 4 ++ packages/sdk/js/src/v2/gen/types.gen.ts | 14 +++- 4 files changed, 82 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx index d9df4a1ab2ce..7667b56f0dac 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx @@ -254,10 +254,8 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( session: { message: { async sync(sessionID: string) { - const response = await sdk.client.v2.session.messages({ - sessionID, - }) - setStore("messages", sessionID, reconcile(response.data ?? [])) + const response = await sdk.client.v2.session.messages({ sessionID }) + setStore("messages", sessionID, reconcile(response.data?.items ?? [])) }, fromSession(sessionID: string) { const messages = store.messages[sessionID] diff --git a/packages/opencode/src/server/routes/instance/v2.ts b/packages/opencode/src/server/routes/instance/v2.ts index 6338eceb409e..1b76216e19b6 100644 --- a/packages/opencode/src/server/routes/instance/v2.ts +++ b/packages/opencode/src/server/routes/instance/v2.ts @@ -4,12 +4,43 @@ import { SessionV2 } from "@/v2/session" import { zod } from "@/util/effect-zod" import { lazy } from "@/util/lazy" import { Effect, Schema } from "effect" +import * as DateTime from "effect/DateTime" import { Hono } from "hono" import { describeRoute, resolver, validator } from "hono-openapi" +import { HTTPException } from "hono/http-exception" import z from "zod" import { errors } from "../../error" import { jsonRequest } from "./trace" +const DefaultMessagesLimit = 50 + +const Cursor = Schema.Struct({ + id: SessionMessage.ID, + time: Schema.Number, + from: Schema.Union([Schema.Literal("start"), Schema.Literal("end")]), +}) + +const MessagesResponse = Schema.Struct({ + items: Schema.Array(SessionMessage.Message), + cursor: Schema.Struct({ + before: Schema.String.pipe(Schema.optional), + after: Schema.String.pipe(Schema.optional), + }), +}).annotate({ identifier: "V2SessionMessagesResponse" }) + +const decodeCursor = Schema.decodeUnknownSync(Cursor) + +const cursor = { + encode(message: SessionMessage.Message, from: "start" | "end") { + return Buffer.from( + JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), from }), + ).toString("base64url") + }, + decode(input: string) { + return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) + }, +} + export const V2Routes = lazy(() => new Hono().get( "/session/:sessionID/message", @@ -22,7 +53,7 @@ export const V2Routes = lazy(() => description: "List of v2 session messages", content: { "application/json": { - schema: resolver(zod(Schema.Array(SessionMessage.Message))), + schema: resolver(zod(MessagesResponse)), }, }, }, @@ -30,12 +61,43 @@ export const V2Routes = lazy(() => }, }), validator("param", z.object({ sessionID: SessionID.zod })), + validator( + "query", + z.object({ + limit: z.coerce.number().int().min(1).max(200).optional(), + cursor: z.string().optional(), + }), + ), async (c) => { const sessionID = c.req.valid("param").sessionID + const query = c.req.valid("query") + const decoded = (() => { + try { + return query.cursor && query.cursor !== "start" && query.cursor !== "end" + ? cursor.decode(query.cursor) + : undefined + } catch { + throw new HTTPException(400) + } + })() return jsonRequest("V2Routes.messages", c, function* () { return yield* Effect.gen(function* () { const session = yield* SessionV2.Service - return yield* session.messages({ sessionID }) + const messages = yield* session.messages({ + sessionID, + limit: query.limit ?? DefaultMessagesLimit, + from: decoded?.from ?? (query.cursor === "start" ? "start" : "end"), + cursor: decoded ? { id: decoded.id, time: decoded.time } : undefined, + }) + const oldest = messages[0] + const newest = messages.at(-1) + return { + items: messages, + cursor: { + before: oldest ? cursor.encode(oldest, "end") : undefined, + after: newest ? cursor.encode(newest, "start") : undefined, + }, + } }).pipe(Effect.provide(SessionV2.defaultLayer)) }) }, diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index dd6783e21ddf..dfafb5e1edae 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -3192,6 +3192,8 @@ export class Session3 extends HeyApiClient { sessionID: string directory?: string workspace?: string + limit?: number + cursor?: string }, options?: Options, ) { @@ -3203,6 +3205,8 @@ export class Session3 extends HeyApiClient { { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "limit" }, + { in: "query", key: "cursor" }, ], }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 898cc7fc0ac2..f0b0678f843e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2851,6 +2851,14 @@ export type SessionMessage = | SessionMessageAssistant | SessionMessageCompaction +export type V2SessionMessagesResponse = { + items: Array + cursor: { + before?: string + after?: string + } +} + export type Symbol = { name: string kind: number @@ -5540,6 +5548,8 @@ export type V2SessionMessagesData = { query?: { directory?: string workspace?: string + limit?: number + cursor?: string } url: "/api/session/{sessionID}/message" } @@ -5561,10 +5571,10 @@ export type V2SessionMessagesResponses = { /** * List of v2 session messages */ - 200: Array + 200: V2SessionMessagesResponse } -export type V2SessionMessagesResponse = V2SessionMessagesResponses[keyof V2SessionMessagesResponses] +export type V2SessionMessagesResponse2 = V2SessionMessagesResponses[keyof V2SessionMessagesResponses] export type FindTextData = { body?: never From 76aebc15c9c2db8d6e4f862a5c7035704de89175 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 30 Apr 2026 20:53:33 -0400 Subject: [PATCH 04/24] core: add session listing API with filtering and improved pagination Users can now browse sessions through the new /api/session endpoint with filters for directory, workspace, date range, and title search. Pagination cursors are now labeled 'previous' and 'next' instead of 'before' and 'after' to make navigation direction clearer. Both session lists and message history now support explicit 'asc' or 'desc' ordering so users can choose between newest-first or oldest-first views. The TUI session view now displays messages with the newest at the bottom, matching standard chat interfaces. --- .../tui/feature-plugins/system/session-v2.tsx | 5 +- .../routes/instance/httpapi/groups/v2.ts | 100 +------- .../instance/httpapi/groups/v2/message.ts | 69 +++++ .../instance/httpapi/groups/v2/session.ts | 129 ++++++++++ .../routes/instance/httpapi/handlers/v2.ts | 88 +------ .../instance/httpapi/handlers/v2/message.ts | 60 +++++ .../instance/httpapi/handlers/v2/session.ts | 110 ++++++++ .../opencode/src/server/routes/instance/v2.ts | 241 +++++++++++++----- packages/opencode/src/v2/session.ts | 99 +++++-- packages/sdk/js/src/v2/gen/sdk.gen.ts | 48 ++++ packages/sdk/js/src/v2/gen/types.gen.ts | 48 +++- 11 files changed, 730 insertions(+), 267 deletions(-) create mode 100644 packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts create mode 100644 packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts create mode 100644 packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts create mode 100644 packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx index 7d2814b1c624..eba670e7430a 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx @@ -39,7 +39,8 @@ function View(props: { api: TuiPluginApi; sessionID: string }) { const dimensions = useTerminalDimensions() const { theme, syntax, subtleSyntax } = useTheme() const messages = createMemo(() => sync.data.messages[props.sessionID] ?? []) - const lastAssistant = createMemo(() => messages().findLast((message) => message.type === "assistant")) + const renderedMessages = createMemo(() => messages().toReversed()) + const lastAssistant = createMemo(() => renderedMessages().findLast((message) => message.type === "assistant")) createEffect(() => { void sync.session.message.sync(props.sessionID) @@ -67,7 +68,7 @@ function View(props: { api: TuiPluginApi; sessionID: string }) { - + {(message, index) => ( diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts index 909b942c2051..05da5b720de2 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts @@ -1,98 +1,10 @@ -import { SessionID } from "@/session/schema" -import { SessionMessage } from "@/v2/session-message" -import { Prompt } from "@/v2/session-prompt" -import { SessionV2 } from "@/v2/session" -import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" -import { Authorization } from "../middleware/authorization" +import { HttpApi, OpenApi } from "effect/unstable/httpapi" +import { MessageGroup } from "./v2/message" +import { SessionGroup } from "./v2/session" export const V2Api = HttpApi.make("v2") - .add( - HttpApiGroup.make("v2") - .add( - HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", { - params: { sessionID: SessionID }, - query: Schema.Struct({ - limit: Schema.optional( - Schema.NumberFromString.check( - Schema.isInt(), - Schema.isGreaterThanOrEqualTo(1), - Schema.isLessThanOrEqualTo(200), - ), - ).annotate({ - description: - "Maximum number of messages to return. When omitted, the endpoint returns its default page size. Use limit without a cursor to fetch the newest page for chat history.", - }), - cursor: Schema.optional(Schema.String).annotate({ - description: - "Opaque pagination cursor returned as before or after in the previous response. The cursor encodes whether to fetch older or newer messages. Use start to read from the beginning or end to read from the latest messages; end is the default.", - }), - }).annotate({ identifier: "V2SessionMessagesQuery" }), - success: Schema.Struct({ - items: Schema.Array(SessionMessage.Message), - cursor: Schema.Struct({ - before: Schema.String.pipe(Schema.optional), - after: Schema.String.pipe(Schema.optional), - }), - }).annotate({ identifier: "V2SessionMessagesResponse" }), - error: HttpApiError.BadRequest, - }).annotateMerge( - OpenApi.annotations({ - identifier: "v2.session.messages", - summary: "Get v2 session messages", - description: - "Retrieve projected v2 messages for a session. For chat clients, request the latest page with limit, page backward through older history with the before cursor, and catch up with newer messages using the after cursor.", - }), - ), - ) - .add( - HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", { - params: { sessionID: SessionID }, - payload: Schema.Struct({ - prompt: Prompt, - delivery: SessionV2.Delivery.pipe(Schema.optional), - }), - success: SessionMessage.Message, - }).annotateMerge( - OpenApi.annotations({ - identifier: "v2.session.prompt", - summary: "Send v2 message", - description: "Create a v2 session message and queue it for the agent loop.", - }), - ), - ) - .add( - HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", { - params: { sessionID: SessionID }, - success: HttpApiSchema.NoContent, - }).annotateMerge( - OpenApi.annotations({ - identifier: "v2.session.compact", - summary: "Compact v2 session", - description: "Compact a v2 session conversation.", - }), - ), - ) - .add( - HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", { - params: { sessionID: SessionID }, - success: HttpApiSchema.NoContent, - }).annotateMerge( - OpenApi.annotations({ - identifier: "v2.session.wait", - summary: "Wait for v2 session", - description: "Wait for a v2 session agent loop to become idle.", - }), - ), - ) - .annotateMerge( - OpenApi.annotations({ - title: "v2", - description: "Experimental v2 routes.", - }), - ) - .middleware(Authorization), - ) + .add(SessionGroup) + .add(MessageGroup) .annotateMerge( OpenApi.annotations({ title: "opencode experimental HttpApi", @@ -100,5 +12,3 @@ export const V2Api = HttpApi.make("v2") description: "Experimental HttpApi surface for selected instance routes.", }), ) - -export * as V2HttpApi from "./v2" diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts new file mode 100644 index 000000000000..3b0b2fa5b10e --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts @@ -0,0 +1,69 @@ +import { SessionID } from "@/session/schema" +import { SessionMessage } from "@/v2/session-message" +import { Schema } from "effect" +import { HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "../../middleware/authorization" + +export const MessageGroup = HttpApiGroup.make("v2.message") + .add( + HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", { + params: { sessionID: SessionID }, + query: Schema.Union([ + Schema.Struct({ + limit: Schema.optional( + Schema.NumberFromString.check( + Schema.isInt(), + Schema.isGreaterThanOrEqualTo(1), + Schema.isLessThanOrEqualTo(200), + ), + ).annotate({ + description: + "Maximum number of messages to return. When omitted, the endpoint returns its default page size.", + }), + order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({ + description: "Message order for the first page. Use desc for newest first or asc for oldest first.", + }), + cursor: Schema.optional(Schema.Never), + }), + Schema.Struct({ + limit: Schema.optional( + Schema.NumberFromString.check( + Schema.isInt(), + Schema.isGreaterThanOrEqualTo(1), + Schema.isLessThanOrEqualTo(200), + ), + ).annotate({ + description: + "Maximum number of messages to return. When omitted, the endpoint returns its default page size.", + }), + cursor: Schema.String.annotate({ + description: + "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.", + }), + order: Schema.optional(Schema.Never), + }), + ]).annotate({ identifier: "V2SessionMessagesQuery" }), + success: Schema.Struct({ + items: Schema.Array(SessionMessage.Message), + cursor: Schema.Struct({ + previous: Schema.String.pipe(Schema.optional), + next: Schema.String.pipe(Schema.optional), + }), + }).annotate({ identifier: "V2SessionMessagesResponse" }), + error: HttpApiError.BadRequest, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.messages", + summary: "Get v2 session messages", + description: + "Retrieve projected v2 messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "v2 messages", + description: "Experimental v2 message routes.", + }), + ) + .middleware(Authorization) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts new file mode 100644 index 000000000000..086b35e6019a --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts @@ -0,0 +1,129 @@ +import { WorkspaceID } from "@/control-plane/schema" +import { SessionID } from "@/session/schema" +import { Session } from "@/session/session" +import { SessionMessage } from "@/v2/session-message" +import { Prompt } from "@/v2/session-prompt" +import { SessionV2 } from "@/v2/session" +import { Schema, SchemaGetter } from "effect" +import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "../../middleware/authorization" + +export const SessionGroup = HttpApiGroup.make("v2.session") + .add( + HttpApiEndpoint.get("sessions", "/api/session", { + query: Schema.Union([ + Schema.Struct({ + limit: Schema.optional( + Schema.NumberFromString.check( + Schema.isInt(), + Schema.isGreaterThanOrEqualTo(1), + Schema.isLessThanOrEqualTo(200), + ), + ).annotate({ + description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.", + }), + order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({ + description: "Session order for the first page. Use desc for newest first or asc for oldest first.", + }), + directory: Schema.String.pipe(Schema.optional), + path: Schema.String.pipe(Schema.optional), + workspace: WorkspaceID.pipe(Schema.optional), + roots: Schema.Literals(["true", "false"]) + .pipe( + Schema.decodeTo(Schema.Boolean, { + decode: SchemaGetter.transform((value) => value === "true"), + encode: SchemaGetter.transform((value) => (value ? "true" : "false")), + }), + ) + .pipe(Schema.optional), + start: Schema.NumberFromString.pipe(Schema.optional), + search: Schema.String.pipe(Schema.optional), + cursor: Schema.optional(Schema.Never), + }), + Schema.Struct({ + limit: Schema.optional( + Schema.NumberFromString.check( + Schema.isInt(), + Schema.isGreaterThanOrEqualTo(1), + Schema.isLessThanOrEqualTo(200), + ), + ).annotate({ + description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.", + }), + cursor: Schema.String.annotate({ + description: + "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.", + }), + order: Schema.optional(Schema.Never), + directory: Schema.optional(Schema.Never), + path: Schema.optional(Schema.Never), + workspace: Schema.optional(Schema.Never), + roots: Schema.optional(Schema.Never), + start: Schema.optional(Schema.Never), + search: Schema.optional(Schema.Never), + }), + ]).annotate({ identifier: "V2SessionsQuery" }), + success: Schema.Struct({ + items: Schema.Array(Session.Info), + cursor: Schema.Struct({ + previous: Schema.String.pipe(Schema.optional), + next: Schema.String.pipe(Schema.optional), + }), + }).annotate({ identifier: "V2SessionsResponse" }), + error: HttpApiError.BadRequest, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.list", + summary: "List v2 sessions", + description: + "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.", + }), + ), + ) + .add( + HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", { + params: { sessionID: SessionID }, + payload: Schema.Struct({ + prompt: Prompt, + delivery: SessionV2.Delivery.pipe(Schema.optional), + }), + success: SessionMessage.Message, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.prompt", + summary: "Send v2 message", + description: "Create a v2 session message and queue it for the agent loop.", + }), + ), + ) + .add( + HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", { + params: { sessionID: SessionID }, + success: HttpApiSchema.NoContent, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.compact", + summary: "Compact v2 session", + description: "Compact a v2 session conversation.", + }), + ), + ) + .add( + HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", { + params: { sessionID: SessionID }, + success: HttpApiSchema.NoContent, + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.wait", + summary: "Wait for v2 session", + description: "Wait for a v2 session agent loop to become idle.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "v2", + description: "Experimental v2 routes.", + }), + ) + .middleware(Authorization) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts index b337b08aad98..55cb53458172 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts @@ -1,86 +1,6 @@ -import { SessionMessage } from "@/v2/session-message" import { SessionV2 } from "@/v2/session" -import { Effect, Layer, Schema } from "effect" -import * as DateTime from "effect/DateTime" -import { HttpApiBuilder, HttpApiError, HttpApiSchema } from "effect/unstable/httpapi" -import { InstanceHttpApi } from "../api" +import { Layer } from "effect" +import { messageHandlers } from "./v2/message" +import { sessionHandlers } from "./v2/session" -const DefaultMessagesLimit = 50 - -const Cursor = Schema.Struct({ - id: SessionMessage.ID, - time: Schema.Number, - from: Schema.Union([Schema.Literal("start"), Schema.Literal("end")]), -}) - -const decodeCursor = Schema.decodeUnknownSync(Cursor) - -const cursor = { - encode(message: SessionMessage.Message, from: "start" | "end") { - return Buffer.from( - JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), from }), - ).toString("base64url") - }, - decode(input: string) { - return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) - }, -} - -export const v2Handlers = HttpApiBuilder.group(InstanceHttpApi, "v2", (handlers) => - Effect.gen(function* () { - const session = yield* SessionV2.Service - - return handlers - .handle( - "messages", - Effect.fn(function* (ctx) { - const decoded = yield* Effect.try({ - try: () => - ctx.query.cursor && ctx.query.cursor !== "start" && ctx.query.cursor !== "end" - ? cursor.decode(ctx.query.cursor) - : undefined, - catch: () => new HttpApiError.BadRequest({}), - }) - const messages = yield* session.messages({ - sessionID: ctx.params.sessionID, - limit: ctx.query.limit ?? DefaultMessagesLimit, - from: decoded?.from ?? (ctx.query.cursor === "start" ? "start" : "end"), - cursor: decoded ? { id: decoded.id, time: decoded.time } : undefined, - }) - const oldest = messages[0] - const newest = messages.at(-1) - return { - items: messages, - cursor: { - before: oldest ? cursor.encode(oldest, "end") : undefined, - after: newest ? cursor.encode(newest, "start") : undefined, - }, - } - }), - ) - .handle( - "prompt", - Effect.fn(function* (ctx) { - return yield* session.prompt({ - sessionID: ctx.params.sessionID, - prompt: ctx.payload.prompt, - delivery: ctx.payload.delivery ?? SessionV2.DefaultDelivery, - }) - }), - ) - .handle( - "compact", - Effect.fn(function* (ctx) { - yield* session.compact(ctx.params.sessionID) - return HttpApiSchema.NoContent.make() - }), - ) - .handle( - "wait", - Effect.fn(function* (ctx) { - yield* session.wait(ctx.params.sessionID) - return HttpApiSchema.NoContent.make() - }), - ) - }), -).pipe(Layer.provide(SessionV2.defaultLayer)) +export const v2Handlers = Layer.mergeAll(sessionHandlers, messageHandlers).pipe(Layer.provide(SessionV2.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts new file mode 100644 index 000000000000..a353027b51a2 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts @@ -0,0 +1,60 @@ +import { SessionMessage } from "@/v2/session-message" +import { SessionV2 } from "@/v2/session" +import { Effect, Schema } from "effect" +import * as DateTime from "effect/DateTime" +import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi" +import { InstanceHttpApi } from "../../api" + +const DefaultMessagesLimit = 50 + +const Cursor = Schema.Struct({ + id: SessionMessage.ID, + time: Schema.Number, + order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]), + direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]), +}) + +const decodeCursor = Schema.decodeUnknownSync(Cursor) + +const cursor = { + encode(message: SessionMessage.Message, order: "asc" | "desc", direction: "previous" | "next") { + return Buffer.from( + JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), order, direction }), + ).toString("base64url") + }, + decode(input: string) { + return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) + }, +} + +export const messageHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.message", (handlers) => + Effect.gen(function* () { + const session = yield* SessionV2.Service + + return handlers.handle( + "messages", + Effect.fn(function* (ctx) { + const decoded = yield* Effect.try({ + try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined), + catch: () => new HttpApiError.BadRequest({}), + }) + const order = decoded?.order ?? ctx.query.order ?? "desc" + const messages = yield* session.messages({ + sessionID: ctx.params.sessionID, + limit: ctx.query.limit ?? DefaultMessagesLimit, + order, + cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined, + }) + const first = messages[0] + const last = messages.at(-1) + return { + items: messages, + cursor: { + previous: first ? cursor.encode(first, order, "previous") : undefined, + next: last ? cursor.encode(last, order, "next") : undefined, + }, + } + }), + ) + }), +) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts new file mode 100644 index 000000000000..e03d2fcf7e52 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts @@ -0,0 +1,110 @@ +import { WorkspaceID } from "@/control-plane/schema" +import { Session } from "@/session/session" +import { SessionV2 } from "@/v2/session" +import { Effect, Schema } from "effect" +import { HttpApiBuilder, HttpApiError, HttpApiSchema } from "effect/unstable/httpapi" +import { InstanceHttpApi } from "../../api" + +const DefaultSessionsLimit = 50 + +const SessionCursor = Schema.Struct({ + id: Session.Info.fields.id, + time: Schema.Number, + order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]), + direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]), + directory: Schema.String.pipe(Schema.optional), + path: Schema.String.pipe(Schema.optional), + workspaceID: WorkspaceID.pipe(Schema.optional), + roots: Schema.Boolean.pipe(Schema.optional), + start: Schema.Number.pipe(Schema.optional), + search: Schema.String.pipe(Schema.optional), +}) +type SessionCursor = typeof SessionCursor.Type + +const decodeCursor = Schema.decodeUnknownSync(SessionCursor) + +const sessionCursor = { + encode( + session: Session.Info, + order: "asc" | "desc", + direction: "previous" | "next", + filters: Pick, + ) { + return Buffer.from( + JSON.stringify({ id: session.id, time: session.time.created, order, direction, ...filters }), + ).toString("base64url") + }, + decode(input: string) { + return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) + }, +} + +export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session", (handlers) => + Effect.gen(function* () { + const session = yield* SessionV2.Service + + return handlers + .handle( + "sessions", + Effect.fn(function* (ctx) { + const decoded = yield* Effect.try({ + try: () => (ctx.query.cursor ? sessionCursor.decode(ctx.query.cursor) : undefined), + catch: () => new HttpApiError.BadRequest({}), + }) + const order = decoded?.order ?? ctx.query.order ?? "desc" + const filters = decoded ?? { + directory: ctx.query.directory, + path: ctx.query.path, + workspaceID: ctx.query.workspace ? WorkspaceID.make(ctx.query.workspace) : undefined, + roots: ctx.query.roots, + start: ctx.query.start, + search: ctx.query.search, + } + const sessions = yield* session.list({ + limit: ctx.query.limit ?? DefaultSessionsLimit, + order, + directory: filters.directory, + path: filters.path, + workspaceID: filters.workspaceID, + roots: filters.roots, + start: filters.start, + search: filters.search, + cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined, + }) + const first = sessions[0] + const last = sessions.at(-1) + return { + items: sessions, + cursor: { + previous: first ? sessionCursor.encode(first, order, "previous", filters) : undefined, + next: last ? sessionCursor.encode(last, order, "next", filters) : undefined, + }, + } + }), + ) + .handle( + "prompt", + Effect.fn(function* (ctx) { + return yield* session.prompt({ + sessionID: ctx.params.sessionID, + prompt: ctx.payload.prompt, + delivery: ctx.payload.delivery ?? SessionV2.DefaultDelivery, + }) + }), + ) + .handle( + "compact", + Effect.fn(function* (ctx) { + yield* session.compact(ctx.params.sessionID) + return HttpApiSchema.NoContent.make() + }), + ) + .handle( + "wait", + Effect.fn(function* (ctx) { + yield* session.wait(ctx.params.sessionID) + return HttpApiSchema.NoContent.make() + }), + ) + }), +) diff --git a/packages/opencode/src/server/routes/instance/v2.ts b/packages/opencode/src/server/routes/instance/v2.ts index 1b76216e19b6..b819aaa1e8c3 100644 --- a/packages/opencode/src/server/routes/instance/v2.ts +++ b/packages/opencode/src/server/routes/instance/v2.ts @@ -1,4 +1,6 @@ +import { WorkspaceID } from "@/control-plane/schema" import { SessionID } from "@/session/schema" +import { Session } from "@/session/session" import { SessionMessage } from "@/v2/session-message" import { SessionV2 } from "@/v2/session" import { zod } from "@/util/effect-zod" @@ -13,27 +15,68 @@ import { errors } from "../../error" import { jsonRequest } from "./trace" const DefaultMessagesLimit = 50 +const DefaultSessionsLimit = 50 + +const SessionCursor = Schema.Struct({ + id: SessionID, + time: Schema.Number, + order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]), + direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]), + directory: Schema.String.pipe(Schema.optional), + path: Schema.String.pipe(Schema.optional), + workspaceID: WorkspaceID.pipe(Schema.optional), + roots: Schema.Boolean.pipe(Schema.optional), + start: Schema.Number.pipe(Schema.optional), + search: Schema.String.pipe(Schema.optional), +}) +type SessionCursor = typeof SessionCursor.Type + +const SessionsResponse = Schema.Struct({ + items: Schema.Array(Session.Info), + cursor: Schema.Struct({ + previous: Schema.String.pipe(Schema.optional), + next: Schema.String.pipe(Schema.optional), + }), +}).annotate({ identifier: "V2SessionsResponse" }) const Cursor = Schema.Struct({ id: SessionMessage.ID, time: Schema.Number, - from: Schema.Union([Schema.Literal("start"), Schema.Literal("end")]), + order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]), + direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]), }) const MessagesResponse = Schema.Struct({ items: Schema.Array(SessionMessage.Message), cursor: Schema.Struct({ - before: Schema.String.pipe(Schema.optional), - after: Schema.String.pipe(Schema.optional), + previous: Schema.String.pipe(Schema.optional), + next: Schema.String.pipe(Schema.optional), }), }).annotate({ identifier: "V2SessionMessagesResponse" }) const decodeCursor = Schema.decodeUnknownSync(Cursor) +const decodeSessionCursor = Schema.decodeUnknownSync(SessionCursor) + +const sessionCursor = { + encode( + session: Session.Info, + order: "asc" | "desc", + direction: "previous" | "next", + filters: Pick, + ) { + return Buffer.from( + JSON.stringify({ id: session.id, time: session.time.created, order, direction, ...filters }), + ).toString("base64url") + }, + decode(input: string) { + return decodeSessionCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) + }, +} const cursor = { - encode(message: SessionMessage.Message, from: "start" | "end") { + encode(message: SessionMessage.Message, order: "asc" | "desc", direction: "previous" | "next") { return Buffer.from( - JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), from }), + JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), order, direction }), ).toString("base64url") }, decode(input: string) { @@ -42,64 +85,146 @@ const cursor = { } export const V2Routes = lazy(() => - new Hono().get( - "/session/:sessionID/message", - describeRoute({ - summary: "Get v2 session messages", - description: "Retrieve projected v2 messages for a session directly from the message database.", - operationId: "v2.session.messages", - responses: { - 200: { - description: "List of v2 session messages", - content: { - "application/json": { - schema: resolver(zod(MessagesResponse)), + new Hono() + .get( + "/session", + describeRoute({ + summary: "List v2 sessions", + description: + "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.", + operationId: "v2.session.list", + responses: { + 200: { + description: "List of v2 sessions", + content: { + "application/json": { + schema: resolver(zod(SessionsResponse)), + }, }, }, + ...errors(400), }, - ...errors(400, 404), - }, - }), - validator("param", z.object({ sessionID: SessionID.zod })), - validator( - "query", - z.object({ - limit: z.coerce.number().int().min(1).max(200).optional(), - cursor: z.string().optional(), }), - ), - async (c) => { - const sessionID = c.req.valid("param").sessionID - const query = c.req.valid("query") - const decoded = (() => { - try { - return query.cursor && query.cursor !== "start" && query.cursor !== "end" - ? cursor.decode(query.cursor) - : undefined - } catch { - throw new HTTPException(400) + validator( + "query", + z.object({ + limit: z.coerce.number().int().min(1).max(200).optional(), + cursor: z.string().optional(), + order: z.enum(["asc", "desc"]).optional(), + directory: z.string().optional(), + path: z.string().optional(), + workspace: WorkspaceID.zod.optional(), + roots: z + .enum(["true", "false"]) + .transform((value) => value === "true") + .optional(), + start: z.coerce.number().optional(), + search: z.string().optional(), + }), + ), + async (c) => { + const query = c.req.valid("query") + const decoded = (() => { + try { + return query.cursor ? sessionCursor.decode(query.cursor) : undefined + } catch { + throw new HTTPException(400) + } + })() + const order = decoded?.order ?? query.order ?? "desc" + const filters = decoded ?? { + directory: query.directory, + path: query.path, + workspaceID: query.workspace, + roots: query.roots, + start: query.start, + search: query.search, } - })() - return jsonRequest("V2Routes.messages", c, function* () { - return yield* Effect.gen(function* () { - const session = yield* SessionV2.Service - const messages = yield* session.messages({ - sessionID, - limit: query.limit ?? DefaultMessagesLimit, - from: decoded?.from ?? (query.cursor === "start" ? "start" : "end"), - cursor: decoded ? { id: decoded.id, time: decoded.time } : undefined, - }) - const oldest = messages[0] - const newest = messages.at(-1) - return { - items: messages, - cursor: { - before: oldest ? cursor.encode(oldest, "end") : undefined, - after: newest ? cursor.encode(newest, "start") : undefined, + return jsonRequest("V2Routes.sessions", c, function* () { + return yield* Effect.gen(function* () { + const session = yield* SessionV2.Service + const sessions = yield* session.list({ + limit: query.limit ?? DefaultSessionsLimit, + order, + directory: filters.directory, + path: filters.path, + workspaceID: filters.workspaceID, + roots: filters.roots, + start: filters.start, + search: filters.search, + cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined, + }) + const first = sessions[0] + const last = sessions.at(-1) + return { + items: sessions, + cursor: { + previous: first ? sessionCursor.encode(first, order, "previous", filters) : undefined, + next: last ? sessionCursor.encode(last, order, "next", filters) : undefined, + }, + } + }).pipe(Effect.provide(SessionV2.defaultLayer)) + }) + }, + ) + .get( + "/session/:sessionID/message", + describeRoute({ + summary: "Get v2 session messages", + description: "Retrieve projected v2 messages for a session directly from the message database.", + operationId: "v2.session.messages", + responses: { + 200: { + description: "List of v2 session messages", + content: { + "application/json": { + schema: resolver(zod(MessagesResponse)), + }, }, + }, + ...errors(400, 404), + }, + }), + validator("param", z.object({ sessionID: SessionID.zod })), + validator( + "query", + z.object({ + limit: z.coerce.number().int().min(1).max(200).optional(), + cursor: z.string().optional(), + order: z.enum(["asc", "desc"]).optional(), + }), + ), + async (c) => { + const sessionID = c.req.valid("param").sessionID + const query = c.req.valid("query") + const decoded = (() => { + try { + return query.cursor ? cursor.decode(query.cursor) : undefined + } catch { + throw new HTTPException(400) } - }).pipe(Effect.provide(SessionV2.defaultLayer)) - }) - }, - ), + })() + const order = decoded?.order ?? query.order ?? "desc" + return jsonRequest("V2Routes.messages", c, function* () { + return yield* Effect.gen(function* () { + const session = yield* SessionV2.Service + const messages = yield* session.messages({ + sessionID, + limit: query.limit ?? DefaultMessagesLimit, + order, + cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined, + }) + const first = messages[0] + const last = messages.at(-1) + return { + items: messages, + cursor: { + previous: first ? cursor.encode(first, order, "previous") : undefined, + next: last ? cursor.encode(last, order, "next") : undefined, + }, + } + }).pipe(Effect.provide(SessionV2.defaultLayer)) + }) + }, + ), ) diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index 5268f4587df0..b42fb39e5faa 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -1,6 +1,7 @@ -import { SessionMessageTable } from "@/session/session.sql" +import { SessionMessageTable, SessionTable } from "@/session/session.sql" import type { SessionID } from "@/session/schema" -import { and, asc, desc, eq, gt, inArray, lt, or } from "@/storage/db" +import type { WorkspaceID } from "@/control-plane/schema" +import { and, asc, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "@/storage/db" import * as Database from "@/storage/db" import { Context, Effect, Layer, Schema } from "effect" import { SessionMessage } from "./session-message" @@ -17,13 +18,29 @@ export type Delivery = Schema.Schema.Type export const DefaultDelivery = "immediate" satisfies Delivery export interface Interface { + readonly list: (input: { + limit?: number + order?: "asc" | "desc" + directory?: string + path?: string + workspaceID?: WorkspaceID + roots?: boolean + start?: number + search?: string + cursor?: { + id: SessionID + time: number + direction: "previous" | "next" + } + }) => Effect.Effect readonly messages: (input: { sessionID: SessionID limit?: number - from?: "start" | "end" + order?: "asc" | "desc" cursor?: { id: SessionMessage.ID time: number + direction: "previous" | "next" } }) => Effect.Effect readonly prompt: (input: { @@ -47,10 +64,53 @@ export const layer = Layer.effect( decodeMessage({ ...row.data, id: row.id, type: row.type }) const result: Interface = { + list: Effect.fn("V2Session.list")(function* (input) { + const direction = input.cursor?.direction ?? "next" + let order = input.order ?? "desc" + // Query the adjacent rows in reverse, then flip them back into the requested order below. + if (direction === "previous" && order === "asc") order = "desc" + if (direction === "previous" && order === "desc") order = "asc" + const conditions: SQL[] = [] + if (input.directory) conditions.push(eq(SessionTable.directory, input.directory)) + if (input.path) + conditions.push(or(eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`))!) + if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID)) + if (input.roots) conditions.push(isNull(SessionTable.parent_id)) + if (input.start) conditions.push(gte(SessionTable.time_created, input.start)) + if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`)) + if (input.cursor) { + conditions.push( + order === "asc" + ? or( + gt(SessionTable.time_created, input.cursor.time), + and(eq(SessionTable.time_created, input.cursor.time), gt(SessionTable.id, input.cursor.id)), + )! + : or( + lt(SessionTable.time_created, input.cursor.time), + and(eq(SessionTable.time_created, input.cursor.time), lt(SessionTable.id, input.cursor.id)), + )!, + ) + } + const query = Database.Client() + .select() + .from(SessionTable) + .where(conditions.length > 0 ? and(...conditions) : undefined) + .orderBy( + order === "asc" ? asc(SessionTable.time_created) : desc(SessionTable.time_created), + order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id), + ) + + const rows = input.limit === undefined ? query.all() : query.limit(input.limit).all() + return (direction === "previous" ? rows.toReversed() : rows).map((row) => Session.fromRow(row)) + }), messages: Effect.fn("V2Session.messages")(function* (input) { - const from = input.from ?? (input.limit === undefined && input.cursor === undefined ? "start" : "end") + const direction = input.cursor?.direction ?? "next" + let order = input.order ?? "desc" + // Query the adjacent rows in reverse, then flip them back into the requested order below. + if (direction === "previous" && order === "asc") order = "desc" + if (direction === "previous" && order === "desc") order = "asc" const boundary = input.cursor - ? from === "start" + ? order === "asc" ? or( gt(SessionMessageTable.time_created, input.cursor.time), and( @@ -71,29 +131,16 @@ export const layer = Layer.effect( : eq(SessionMessageTable.session_id, input.sessionID) const rows = Database.use((db) => { - if (from === "start") { - const query = db - .select() - .from(SessionMessageTable) - .where(where) - .orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id)) - return input.limit === undefined ? query.all() : query.limit(input.limit).all() - } - const idsQuery = db - .select({ id: SessionMessageTable.id }) - .from(SessionMessageTable) - .where(where) - .orderBy(desc(SessionMessageTable.time_created), desc(SessionMessageTable.id)) - const ids = (input.limit === undefined ? idsQuery.all() : idsQuery.limit(input.limit).all()).map( - (row) => row.id, - ) - if (ids.length === 0) return [] - return db + const query = db .select() .from(SessionMessageTable) - .where(inArray(SessionMessageTable.id, ids)) - .orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id)) - .all() + .where(where) + .orderBy( + order === "asc" ? asc(SessionMessageTable.time_created) : desc(SessionMessageTable.time_created), + order === "asc" ? asc(SessionMessageTable.id) : desc(SessionMessageTable.id), + ) + const rows = input.limit === undefined ? query.all() : query.limit(input.limit).all() + return direction === "previous" ? rows.toReversed() : rows }) return rows.map((row) => decode(row)) }), diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index dfafb5e1edae..3e076b004535 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -187,6 +187,8 @@ import type { TuiSelectSessionResponses, TuiShowToastResponses, TuiSubmitPromptResponses, + V2SessionListErrors, + V2SessionListResponses, V2SessionMessagesErrors, V2SessionMessagesResponses, VcsDiffResponses, @@ -3182,6 +3184,50 @@ export class Sync extends HeyApiClient { } export class Session3 extends HeyApiClient { + /** + * List v2 sessions + * + * Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list. + */ + public list( + parameters?: { + directory?: string + workspace?: string + limit?: number + cursor?: string + order?: "asc" | "desc" + path?: string + roots?: "true" | "false" + start?: number + search?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "limit" }, + { in: "query", key: "cursor" }, + { in: "query", key: "order" }, + { in: "query", key: "path" }, + { in: "query", key: "roots" }, + { in: "query", key: "start" }, + { in: "query", key: "search" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/session", + ...options, + ...params, + }) + } + /** * Get v2 session messages * @@ -3194,6 +3240,7 @@ export class Session3 extends HeyApiClient { workspace?: string limit?: number cursor?: string + order?: "asc" | "desc" }, options?: Options, ) { @@ -3207,6 +3254,7 @@ export class Session3 extends HeyApiClient { { in: "query", key: "workspace" }, { in: "query", key: "limit" }, { in: "query", key: "cursor" }, + { in: "query", key: "order" }, ], }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index f0b0678f843e..5d16dede85e1 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2692,6 +2692,14 @@ export type ProviderAuthAuthorization = { instructions: string } +export type V2SessionsResponse = { + items: Array + cursor: { + previous?: string + next?: string + } +} + export type SessionMessageUser = { id: string metadata?: { @@ -2854,8 +2862,8 @@ export type SessionMessage = export type V2SessionMessagesResponse = { items: Array cursor: { - before?: string - after?: string + previous?: string + next?: string } } @@ -5540,6 +5548,41 @@ export type SyncHistoryListResponses = { export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses] +export type V2SessionListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + limit?: number + cursor?: string + order?: "asc" | "desc" + path?: string + roots?: "true" | "false" + start?: number + search?: string + } + url: "/api/session" +} + +export type V2SessionListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type V2SessionListError = V2SessionListErrors[keyof V2SessionListErrors] + +export type V2SessionListResponses = { + /** + * List of v2 sessions + */ + 200: V2SessionsResponse +} + +export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses] + export type V2SessionMessagesData = { body?: never path: { @@ -5550,6 +5593,7 @@ export type V2SessionMessagesData = { workspace?: string limit?: number cursor?: string + order?: "asc" | "desc" } url: "/api/session/{sessionID}/message" } From b94817158602bdebd47153d4128f5dc97132943e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 30 Apr 2026 22:55:30 -0400 Subject: [PATCH 05/24] core: expose complete session metadata schema for agent session introspection --- packages/opencode/src/v2/session.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index b42fb39e5faa..7fcd899e5a44 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -17,6 +17,23 @@ export type Delivery = Schema.Schema.Type export const DefaultDelivery = "immediate" satisfies Delivery +export const Info = Schema.Struct({ + id: SessionID, + slug: Schema.String, + projectID: ProjectID, + workspaceID: optionalOmitUndefined(WorkspaceID), + directory: Schema.String, + path: optionalOmitUndefined(Schema.String), + parentID: optionalOmitUndefined(SessionID), + summary: optionalOmitUndefined(Summary), + share: optionalOmitUndefined(Share), + title: Schema.String, + version: Schema.String, + time: Time, + permission: optionalOmitUndefined(Permission.Ruleset), + revert: optionalOmitUndefined(Revert), +}).annotate({ identifier: "Session" }) + export interface Interface { readonly list: (input: { limit?: number From 7dfd3466bde65dc25244cbcf68f553d7fcb29935 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 30 Apr 2026 23:29:19 -0400 Subject: [PATCH 06/24] core: simplify Session.Info schema to empty struct for flexible event handling This change removes the predefined fields from Session.Info to allow more dynamic event-driven session data. Instead of fixed schema fields, session information will be populated through the event system, enabling better support for evolving session states without schema migrations. The empty struct serves as a base that can be extended through the event model, making it easier to add new session attributes without modifying core schema definitions. --- packages/opencode/src/v2/session.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index 7fcd899e5a44..7874de7edd69 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -17,22 +17,7 @@ export type Delivery = Schema.Schema.Type export const DefaultDelivery = "immediate" satisfies Delivery -export const Info = Schema.Struct({ - id: SessionID, - slug: Schema.String, - projectID: ProjectID, - workspaceID: optionalOmitUndefined(WorkspaceID), - directory: Schema.String, - path: optionalOmitUndefined(Schema.String), - parentID: optionalOmitUndefined(SessionID), - summary: optionalOmitUndefined(Summary), - share: optionalOmitUndefined(Share), - title: Schema.String, - version: Schema.String, - time: Time, - permission: optionalOmitUndefined(Permission.Ruleset), - revert: optionalOmitUndefined(Revert), -}).annotate({ identifier: "Session" }) +export const Info = Schema.Struct({}).annotate({ identifier: "Session" }) export interface Interface { readonly list: (input: { From b61f237cd584802efcc3100646aa90cbad981fb8 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 1 May 2026 15:26:29 -0400 Subject: [PATCH 07/24] core: track agent and model used in each session Store the active agent and model in the session table so users can see which configuration was used when browsing session history. This helps identify sessions that used specific agents or models for easier filtering and organization. --- .../snapshot.json | 176 +- .../20260501142318_next_venus/migration.sql | 2 + .../20260501142318_next_venus/snapshot.json | 1511 +++++++++++++++++ .../instance/httpapi/groups/v2/session.ts | 3 +- .../instance/httpapi/handlers/v2/session.ts | 5 +- .../opencode/src/server/routes/instance/v2.ts | 5 +- .../opencode/src/session/projectors-next.ts | 24 +- packages/opencode/src/session/prompt.ts | 31 + packages/opencode/src/session/session.sql.ts | 6 + packages/opencode/src/v2/session-event.ts | 25 + .../src/v2/session-message-updater.ts | 2 + packages/opencode/src/v2/session.ts | 107 +- 12 files changed, 1822 insertions(+), 75 deletions(-) create mode 100644 packages/opencode/migration/20260501142318_next_venus/migration.sql create mode 100644 packages/opencode/migration/20260501142318_next_venus/snapshot.json diff --git a/packages/opencode/migration/20260428004200_add_session_path/snapshot.json b/packages/opencode/migration/20260428004200_add_session_path/snapshot.json index d79324fedf86..1f3bc493c132 100644 --- a/packages/opencode/migration/20260428004200_add_session_path/snapshot.json +++ b/packages/opencode/migration/20260428004200_add_session_path/snapshot.json @@ -2,7 +2,9 @@ "version": "7", "dialect": "sqlite", "id": "aaa2ebeb-caa4-478d-8365-4fc595d16856", - "prevIds": ["66cbe0d7-def0-451b-b88a-7608513a9b44"], + "prevIds": [ + "61f807f9-6398-4067-be05-804acc2561bc" + ], "ddl": [ { "name": "account_state", @@ -37,7 +39,7 @@ "entityType": "tables" }, { - "name": "session_entry", + "name": "session_message", "entityType": "tables" }, { @@ -598,7 +600,7 @@ "generated": null, "name": "id", "entityType": "columns", - "table": "session_entry" + "table": "session_message" }, { "type": "text", @@ -608,7 +610,7 @@ "generated": null, "name": "session_id", "entityType": "columns", - "table": "session_entry" + "table": "session_message" }, { "type": "text", @@ -618,7 +620,7 @@ "generated": null, "name": "type", "entityType": "columns", - "table": "session_entry" + "table": "session_message" }, { "type": "integer", @@ -628,7 +630,7 @@ "generated": null, "name": "time_created", "entityType": "columns", - "table": "session_entry" + "table": "session_message" }, { "type": "integer", @@ -638,7 +640,7 @@ "generated": null, "name": "time_updated", "entityType": "columns", - "table": "session_entry" + "table": "session_message" }, { "type": "text", @@ -648,7 +650,7 @@ "generated": null, "name": "data", "entityType": "columns", - "table": "session_entry" + "table": "session_message" }, { "type": "text", @@ -1051,9 +1053,13 @@ "table": "event" }, { - "columns": ["active_account_id"], + "columns": [ + "active_account_id" + ], "tableTo": "account", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "SET NULL", "nameExplicit": false, @@ -1062,9 +1068,13 @@ "table": "account_state" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1073,9 +1083,13 @@ "table": "workspace" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1084,9 +1098,13 @@ "table": "message" }, { - "columns": ["message_id"], + "columns": [ + "message_id" + ], "tableTo": "message", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1095,9 +1113,13 @@ "table": "part" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1106,20 +1128,28 @@ "table": "permission" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, - "name": "fk_session_entry_session_id_session_id_fk", + "name": "fk_session_message_session_id_session_id_fk", "entityType": "fks", - "table": "session_entry" + "table": "session_message" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1128,9 +1158,13 @@ "table": "session" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1139,9 +1173,13 @@ "table": "todo" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1150,9 +1188,13 @@ "table": "session_share" }, { - "columns": ["aggregate_id"], + "columns": [ + "aggregate_id" + ], "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], + "columnsTo": [ + "aggregate_id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1161,98 +1203,128 @@ "table": "event" }, { - "columns": ["email", "url"], + "columns": [ + "email", + "url" + ], "nameExplicit": false, "name": "control_account_pk", "entityType": "pks", "table": "control_account" }, { - "columns": ["session_id", "position"], + "columns": [ + "session_id", + "position" + ], "nameExplicit": false, "name": "todo_pk", "entityType": "pks", "table": "todo" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "account_state_pk", "table": "account_state", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "account_pk", "table": "account", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "workspace_pk", "table": "workspace", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "project_pk", "table": "project", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "message_pk", "table": "message", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "part_pk", "table": "part", "entityType": "pks" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "nameExplicit": false, "name": "permission_pk", "table": "permission", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, - "name": "session_entry_pk", - "table": "session_entry", + "name": "session_message_pk", + "table": "session_message", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "session_pk", "table": "session", "entityType": "pks" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "nameExplicit": false, "name": "session_share_pk", "table": "session_share", "entityType": "pks" }, { - "columns": ["aggregate_id"], + "columns": [ + "aggregate_id" + ], "nameExplicit": false, "name": "event_sequence_pk", "table": "event_sequence", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "event_pk", "table": "event", @@ -1322,9 +1394,9 @@ "isUnique": false, "where": null, "origin": "manual", - "name": "session_entry_session_idx", + "name": "session_message_session_idx", "entityType": "indexes", - "table": "session_entry" + "table": "session_message" }, { "columns": [ @@ -1340,9 +1412,9 @@ "isUnique": false, "where": null, "origin": "manual", - "name": "session_entry_session_type_idx", + "name": "session_message_session_type_idx", "entityType": "indexes", - "table": "session_entry" + "table": "session_message" }, { "columns": [ @@ -1354,9 +1426,9 @@ "isUnique": false, "where": null, "origin": "manual", - "name": "session_entry_time_created_idx", + "name": "session_message_time_created_idx", "entityType": "indexes", - "table": "session_entry" + "table": "session_message" }, { "columns": [ diff --git a/packages/opencode/migration/20260501142318_next_venus/migration.sql b/packages/opencode/migration/20260501142318_next_venus/migration.sql new file mode 100644 index 000000000000..e0ffe7823c46 --- /dev/null +++ b/packages/opencode/migration/20260501142318_next_venus/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE `session` ADD `agent` text;--> statement-breakpoint +ALTER TABLE `session` ADD `model` text; \ No newline at end of file diff --git a/packages/opencode/migration/20260501142318_next_venus/snapshot.json b/packages/opencode/migration/20260501142318_next_venus/snapshot.json new file mode 100644 index 000000000000..e594de2f0488 --- /dev/null +++ b/packages/opencode/migration/20260501142318_next_venus/snapshot.json @@ -0,0 +1,1511 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "2ec89846-dcf1-4977-ab5e-244ddc9e3d67", + "prevIds": [ + "aaa2ebeb-caa4-478d-8365-4fc595d16856" + ], + "ddl": [ + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "columns": [ + "active_account_id" + ], + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": [ + "email", + "url" + ], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "project_id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts index 086b35e6019a..e1439c8f0522 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts @@ -1,6 +1,5 @@ import { WorkspaceID } from "@/control-plane/schema" import { SessionID } from "@/session/schema" -import { Session } from "@/session/session" import { SessionMessage } from "@/v2/session-message" import { Prompt } from "@/v2/session-prompt" import { SessionV2 } from "@/v2/session" @@ -64,7 +63,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session") }), ]).annotate({ identifier: "V2SessionsQuery" }), success: Schema.Struct({ - items: Schema.Array(Session.Info), + items: Schema.Array(SessionV2.Info), cursor: Schema.Struct({ previous: Schema.String.pipe(Schema.optional), next: Schema.String.pipe(Schema.optional), diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts index e03d2fcf7e52..dcd6e03f1336 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts @@ -1,5 +1,4 @@ import { WorkspaceID } from "@/control-plane/schema" -import { Session } from "@/session/session" import { SessionV2 } from "@/v2/session" import { Effect, Schema } from "effect" import { HttpApiBuilder, HttpApiError, HttpApiSchema } from "effect/unstable/httpapi" @@ -8,7 +7,7 @@ import { InstanceHttpApi } from "../../api" const DefaultSessionsLimit = 50 const SessionCursor = Schema.Struct({ - id: Session.Info.fields.id, + id: SessionV2.Info.fields.id, time: Schema.Number, order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]), direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]), @@ -25,7 +24,7 @@ const decodeCursor = Schema.decodeUnknownSync(SessionCursor) const sessionCursor = { encode( - session: Session.Info, + session: SessionV2.Info, order: "asc" | "desc", direction: "previous" | "next", filters: Pick, diff --git a/packages/opencode/src/server/routes/instance/v2.ts b/packages/opencode/src/server/routes/instance/v2.ts index b819aaa1e8c3..d1efef0d6000 100644 --- a/packages/opencode/src/server/routes/instance/v2.ts +++ b/packages/opencode/src/server/routes/instance/v2.ts @@ -1,6 +1,5 @@ import { WorkspaceID } from "@/control-plane/schema" import { SessionID } from "@/session/schema" -import { Session } from "@/session/session" import { SessionMessage } from "@/v2/session-message" import { SessionV2 } from "@/v2/session" import { zod } from "@/util/effect-zod" @@ -32,7 +31,7 @@ const SessionCursor = Schema.Struct({ type SessionCursor = typeof SessionCursor.Type const SessionsResponse = Schema.Struct({ - items: Schema.Array(Session.Info), + items: Schema.Array(SessionV2.Info), cursor: Schema.Struct({ previous: Schema.String.pipe(Schema.optional), next: Schema.String.pipe(Schema.optional), @@ -59,7 +58,7 @@ const decodeSessionCursor = Schema.decodeUnknownSync(SessionCursor) const sessionCursor = { encode( - session: Session.Info, + session: SessionV2.Info, order: "asc" | "desc", direction: "previous" | "next", filters: Pick, diff --git a/packages/opencode/src/session/projectors-next.ts b/packages/opencode/src/session/projectors-next.ts index 32f5ae83ea54..be33d31e342a 100644 --- a/packages/opencode/src/session/projectors-next.ts +++ b/packages/opencode/src/session/projectors-next.ts @@ -5,7 +5,7 @@ import { SessionMessageUpdater } from "@/v2/session-message-updater" import { SessionEvent } from "@/v2/session-event" import * as DateTime from "effect/DateTime" import { SyncEvent } from "@/sync" -import { SessionMessageTable } from "./session.sql" +import { SessionMessageTable, SessionTable } from "./session.sql" import type { SessionID } from "./schema" import { Schema } from "effect" @@ -96,6 +96,28 @@ function update(db: Database.TxOrDb, event: SessionEvent.Event) { } export default [ + SyncEvent.project(SessionEvent.AgentSwitched.Sync, (db, data) => { + db.update(SessionTable) + .set({ + agent: data.agent, + time_updated: DateTime.toEpochMillis(data.timestamp), + }) + .where(eq(SessionTable.id, data.sessionID)) + .run() + }), + SyncEvent.project(SessionEvent.ModelSwitched.Sync, (db, data) => { + db.update(SessionTable) + .set({ + model: { + id: data.id, + providerID: data.providerID, + variant: data.variant, + }, + time_updated: DateTime.toEpochMillis(data.timestamp), + }) + .where(eq(SessionTable.id, data.sessionID)) + .run() + }), SyncEvent.project(SessionEvent.Prompted.Sync, (db, data) => { update(db, { type: "session.next.prompted", data }) }), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 1bdcc8066e5f..520eb59704a6 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -58,6 +58,9 @@ import { SessionEvent } from "@/v2/session-event" import { AgentAttachment, FileAttachment, Source } from "@/v2/session-prompt" import { SyncEvent } from "@/sync" import * as DateTime from "effect/DateTime" +import { eq } from "@/storage/db" +import * as Database from "@/storage/db" +import { SessionTable } from "./session.sql" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -938,6 +941,34 @@ NOTE: At any point in time through this workflow you should feel free to ask the format: input.format, } + const current = Database.use((db) => + db + .select({ agent: SessionTable.agent, model: SessionTable.model }) + .from(SessionTable) + .where(eq(SessionTable.id, input.sessionID)) + .get(), + ) + if (current?.agent !== info.agent) { + SyncEvent.run(SessionEvent.AgentSwitched.Sync, { + sessionID: input.sessionID, + timestamp: DateTime.makeUnsafe(info.time.created), + agent: info.agent, + }) + } + if ( + current?.model?.providerID !== info.model.providerID || + current.model.id !== info.model.modelID || + current.model.variant !== info.model.variant + ) { + SyncEvent.run(SessionEvent.ModelSwitched.Sync, { + sessionID: input.sessionID, + timestamp: DateTime.makeUnsafe(info.time.created), + id: info.model.modelID, + providerID: info.model.providerID, + variant: info.model.variant, + }) + } + yield* Effect.addFinalizer(() => instruction.clear(info.id)) type Draft = T extends MessageV2.Part ? Omit & { id?: string } : never diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index ae6351d58843..421fa68694d2 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -35,6 +35,12 @@ export const SessionTable = sqliteTable( summary_diffs: text({ mode: "json" }).$type(), revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: PartID; snapshot?: string; diff?: string }>(), permission: text({ mode: "json" }).$type(), + agent: text(), + model: text({ mode: "json" }).$type<{ + id: string + providerID: string + variant?: string + }>(), ...Timestamps, time_compacting: integer(), time_archived: integer(), diff --git a/packages/opencode/src/v2/session-event.ts b/packages/opencode/src/v2/session-event.ts index 99de3cff2510..279c84239a63 100644 --- a/packages/opencode/src/v2/session-event.ts +++ b/packages/opencode/src/v2/session-event.ts @@ -5,6 +5,7 @@ import { FileAttachment, Prompt } from "./session-prompt" import { Schema } from "effect" export { FileAttachment } import { ToolOutput } from "./tool-output" +import { ModelID, ProviderID } from "@/provider/schema" export const ID = Event.ID export type ID = Schema.Schema.Type @@ -23,6 +24,28 @@ const Base = { sessionID: SessionID, } +export const AgentSwitched = Event.define({ + type: "session.next.agent.switched", + aggregate: "sessionID", + version: 1, + schema: { + ...Base, + agent: Schema.String, + }, +}) + +export const ModelSwitched = Event.define({ + type: "session.next.model.switched", + aggregate: "sessionID", + version: 1, + schema: { + ...Base, + id: ModelID, + providerID: ProviderID, + variant: Schema.String.pipe(Schema.optional), + }, +}) + export const Prompted = Event.define({ type: "session.next.prompted", aggregate: "sessionID", @@ -308,6 +331,8 @@ export namespace Compaction { export const All = Schema.Union( [ + AgentSwitched, + ModelSwitched, Prompted, Synthetic, Step.Started, diff --git a/packages/opencode/src/v2/session-message-updater.ts b/packages/opencode/src/v2/session-message-updater.ts index bba605910f24..93b7b8ce0b35 100644 --- a/packages/opencode/src/v2/session-message-updater.ts +++ b/packages/opencode/src/v2/session-message-updater.ts @@ -77,6 +77,8 @@ export function update(adapter: Adapter, event: SessionEvent.Eve ) SessionEvent.All.match(event, { + "session.next.agent.switched": () => {}, + "session.next.model.switched": () => {}, "session.next.prompted": (event) => { adapter.appendMessage(SessionMessage.User.fromEvent(event)) }, diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index 7874de7edd69..aa81be90ba46 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -1,14 +1,16 @@ import { SessionMessageTable, SessionTable } from "@/session/session.sql" -import type { SessionID } from "@/session/schema" -import type { WorkspaceID } from "@/control-plane/schema" +import { SessionID } from "@/session/schema" +import { WorkspaceID } from "@/control-plane/schema" import { and, asc, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "@/storage/db" import * as Database from "@/storage/db" -import { Context, Effect, Layer, Schema } from "effect" +import { Context, DateTime, Effect, Layer, Schema } from "effect" import { SessionMessage } from "./session-message" import type { Prompt } from "./session-prompt" -import { Session } from "@/session/session" -import { SessionPrompt } from "@/session/prompt" import type { Event } from "./event" +import { ProjectID } from "@/project/schema" +import { ModelID, ProviderID } from "@/provider/schema" +import { SessionEvent } from "./session-event" +import { SyncEvent } from "@/sync" export const Delivery = Schema.Union([Schema.Literal("immediate"), Schema.Literal("deferred")]).annotate({ identifier: "Session.Delivery", @@ -17,7 +19,38 @@ export type Delivery = Schema.Schema.Type export const DefaultDelivery = "immediate" satisfies Delivery -export const Info = Schema.Struct({}).annotate({ identifier: "Session" }) +export class Info extends Schema.Class("Session.Info")({ + id: SessionID, + parentID: SessionID.pipe(Schema.optional), + projectID: ProjectID, + workspaceID: WorkspaceID.pipe(Schema.optional), + path: Schema.String.pipe(Schema.optional), + agent: Schema.String.pipe(Schema.optional), + model: Schema.Struct({ + id: ModelID, + providerID: ProviderID, + variant: Schema.String.pipe(Schema.optional), + }).pipe(Schema.optional), + time: Schema.Struct({ + created: Schema.DateTimeUtcFromMillis, + updated: Schema.DateTimeUtcFromMillis, + archived: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + }), + title: Schema.String, + /* + slug: Schema.String, + directory: Schema.String, + path: optionalOmitUndefined(Schema.String), + parentID: optionalOmitUndefined(SessionID), + summary: optionalOmitUndefined(Summary), + share: optionalOmitUndefined(Share), + title: Schema.String, + version: Schema.String, + time: Time, + permission: optionalOmitUndefined(Permission.Ruleset), + revert: optionalOmitUndefined(Revert), + */ +}) {} export interface Interface { readonly list: (input: { @@ -34,7 +67,7 @@ export interface Interface { time: number direction: "previous" | "next" } - }) => Effect.Effect + }) => Effect.Effect readonly messages: (input: { sessionID: SessionID limit?: number @@ -51,6 +84,13 @@ export interface Interface { prompt: Prompt delivery?: Delivery }) => Effect.Effect + readonly switchAgent: (input: { sessionID: SessionID; agent: string }) => Effect.Effect + readonly switchModel: (input: { + sessionID: SessionID + id: ModelID + providerID: ProviderID + variant?: string + }) => Effect.Effect readonly compact: (sessionID: SessionID) => Effect.Effect readonly wait: (sessionID: SessionID) => Effect.Effect } @@ -60,11 +100,35 @@ export class Service extends Context.Service()("@opencode/v2 export const layer = Layer.effect( Service, Effect.gen(function* () { - const prompt = yield* SessionPrompt.Service const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message) + const decode = (row: typeof SessionMessageTable.$inferSelect) => decodeMessage({ ...row.data, id: row.id, type: row.type }) + function fromRow(row: typeof SessionTable.$inferSelect): Info { + return { + id: SessionID.make(row.id), + projectID: ProjectID.make(row.project_id), + workspaceID: row.workspace_id ? WorkspaceID.make(row.workspace_id) : undefined, + title: row.title, + parentID: row.parent_id ? SessionID.make(row.parent_id) : undefined, + path: row.path ?? "", + agent: row.agent ?? undefined, + model: row.model + ? { + id: ModelID.make(row.model.id), + providerID: ProviderID.make(row.model.providerID), + variant: row.model.variant, + } + : undefined, + time: { + created: DateTime.makeUnsafe(row.time_created), + updated: DateTime.makeUnsafe(row.time_updated), + archived: row.time_archived ? DateTime.makeUnsafe(row.time_archived) : undefined, + }, + } + } + const result: Interface = { list: Effect.fn("V2Session.list")(function* (input) { const direction = input.cursor?.direction ?? "next" @@ -103,7 +167,7 @@ export const layer = Layer.effect( ) const rows = input.limit === undefined ? query.all() : query.limit(input.limit).all() - return (direction === "previous" ? rows.toReversed() : rows).map((row) => Session.fromRow(row)) + return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row)) }), messages: Effect.fn("V2Session.messages")(function* (input) { const direction = input.cursor?.direction ?? "next" @@ -146,18 +210,33 @@ export const layer = Layer.effect( }) return rows.map((row) => decode(row)) }), - prompt: Effect.fn("V2Session.prompt")(function* (input) { - const delivery = input.delivery ?? DefaultDelivery + prompt: Effect.fn("V2Session.prompt")(function* (_input) { return {} as any }), - compact: Effect.fn("V2Session.compact")(function* (sessionID) {}), - wait: Effect.fn("V2Session.wait")(function* (sessionID) {}), + switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) { + SyncEvent.run(SessionEvent.AgentSwitched.Sync, { + sessionID: input.sessionID, + timestamp: DateTime.makeUnsafe(Date.now()), + agent: input.agent, + }) + }), + switchModel: Effect.fn("V2Session.switchModel")(function* (input) { + SyncEvent.run(SessionEvent.ModelSwitched.Sync, { + sessionID: input.sessionID, + timestamp: DateTime.makeUnsafe(Date.now()), + id: input.id, + providerID: input.providerID, + variant: input.variant, + }) + }), + compact: Effect.fn("V2Session.compact")(function* (_sessionID) {}), + wait: Effect.fn("V2Session.wait")(function* (_sessionID) {}), } return Service.of(result) }), ) -export const defaultLayer = layer.pipe(Layer.provide(SessionPrompt.defaultLayer)) +export const defaultLayer = layer export * as SessionV2 from "./session" From 60806eeeeeb78de7eb9b642f86b26bad45500d5e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 1 May 2026 16:55:30 -0400 Subject: [PATCH 08/24] core: add unique IDs to all events for reliable tracking and debugging Events now include unique identifiers at the payload level, making it easier to trace event flow through the system and debug issues. Session events have been restructured so IDs are consistent across the event bus, database projections, and API responses. Sessions now persist which agent and model were used, preserving this context in session history. Agent and model switches are now tracked as dedicated message types in sessions, providing a clearer timeline of how the conversation evolved. --- packages/opencode/src/bus/bus-event.ts | 2 + packages/opencode/src/bus/global.ts | 14 +++- packages/opencode/src/bus/index.ts | 25 ++++-- .../cli/cmd/tui/component/prompt/index.tsx | 12 ++- .../src/cli/cmd/tui/context/sync-v2.tsx | 8 +- .../tui/feature-plugins/system/session-v2.tsx | 39 ++++++++++ packages/opencode/src/server/routes/global.ts | 3 + .../src/server/routes/instance/event.ts | 8 +- .../server/routes/instance/httpapi/event.ts | 4 +- .../instance/httpapi/handlers/global.ts | 5 +- .../src/server/routes/instance/index.ts | 2 +- packages/opencode/src/session/compaction.ts | 1 - packages/opencode/src/session/processor.ts | 1 - .../opencode/src/session/projectors-next.ts | 76 ++++++++++--------- packages/opencode/src/session/prompt.ts | 2 - packages/opencode/src/session/session.ts | 29 +++++++ packages/opencode/src/sync/index.ts | 8 +- packages/opencode/src/v2/event.ts | 1 + packages/opencode/src/v2/session-event.ts | 9 +-- .../src/v2/session-message-updater.ts | 8 +- packages/opencode/src/v2/session-message.ts | 50 ++++++++++-- .../test/acp/event-subscription.test.ts | 1 + .../opencode/test/cli/tui/use-event.test.tsx | 2 + .../test/v2/session-message-updater.test.ts | 20 +++-- packages/sdk/js/src/v2/gen/sdk.gen.ts | 8 ++ packages/sdk/js/src/v2/gen/types.gen.ts | 36 +++++++++ 26 files changed, 289 insertions(+), 85 deletions(-) diff --git a/packages/opencode/src/bus/bus-event.ts b/packages/opencode/src/bus/bus-event.ts index cf9fcfbeec8c..3250c166ab4e 100644 --- a/packages/opencode/src/bus/bus-event.ts +++ b/packages/opencode/src/bus/bus-event.ts @@ -24,6 +24,7 @@ export function payloads() { .map(([type, def]) => { return z .object({ + id: z.string(), type: z.literal(type), properties: zodObject(def.properties), }) @@ -39,6 +40,7 @@ export function effectPayloads() { .entries() .map(([type, def]) => Schema.Struct({ + id: Schema.String, type: Schema.Literal(type), properties: def.properties, }).annotate({ identifier: `Event.${type}` }), diff --git a/packages/opencode/src/bus/global.ts b/packages/opencode/src/bus/global.ts index b5392a81b9b7..3cfd453624c1 100644 --- a/packages/opencode/src/bus/global.ts +++ b/packages/opencode/src/bus/global.ts @@ -1,4 +1,5 @@ import { EventEmitter } from "events" +import { Identifier } from "@/id/id" export type GlobalEvent = { directory?: string @@ -7,6 +8,15 @@ export type GlobalEvent = { payload: any } -export const GlobalBus = new EventEmitter<{ +class GlobalBusEmitter extends EventEmitter<{ event: [GlobalEvent] -}>() +}> { + override emit(eventName: "event", event: GlobalEvent): boolean { + if (event.payload && typeof event.payload === "object" && !("id" in event.payload)) { + event.payload.id = event.payload.syncEvent?.id ?? Identifier.create("evt", "ascending") + } + return super.emit(eventName, event) + } +} + +export const GlobalBus = new GlobalBusEmitter() diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index 9ee8e6fb039b..449694a53a3a 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -5,6 +5,7 @@ import { BusEvent } from "./bus-event" import { GlobalBus } from "./global" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" +import { Identifier } from "@/id/id" const log = Log.create({ service: "bus" }) @@ -18,6 +19,7 @@ export const InstanceDisposed = BusEvent.define( ) type Payload = { + id: string type: D["type"] properties: BusProperties } @@ -28,7 +30,11 @@ type State = { } export interface Interface { - readonly publish: (def: D, properties: BusProperties) => Effect.Effect + readonly publish: ( + def: D, + properties: BusProperties, + options?: { id?: string }, + ) => Effect.Effect readonly subscribe: (def: D) => Stream.Stream> readonly subscribeAll: () => Stream.Stream readonly subscribeCallback: ( @@ -53,6 +59,7 @@ export const layer = Layer.effect( // Publish InstanceDisposed before shutting down so subscribers see it yield* PubSub.publish(wildcard, { type: InstanceDisposed.type, + id: createID(), properties: { directory: ctx.directory }, }) yield* PubSub.shutdown(wildcard) @@ -77,10 +84,10 @@ export const layer = Layer.effect( }) } - function publish(def: D, properties: BusProperties) { + function publish(def: D, properties: BusProperties, options?: { id?: string }) { return Effect.gen(function* () { const s = yield* InstanceState.get(state) - const payload: Payload = { type: def.type, properties } + const payload: Payload = { id: options?.id ?? createID(), type: def.type, properties } log.info("publishing", { type: def.type }) const ps = s.typed.get(def.type) @@ -173,8 +180,16 @@ const { runPromise, runSync } = makeRuntime(Service, layer) // runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe, // Scope.make, Effect.forkScoped) is entirely synchronous. If any step becomes async, this will throw. -export async function publish(def: D, properties: BusProperties) { - return runPromise((svc) => svc.publish(def, properties)) +export function createID() { + return Identifier.create("evt", "ascending") +} + +export async function publish( + def: D, + properties: BusProperties, + options?: { id?: string }, +) { + return runPromise((svc) => svc.publish(def, properties, options)) } export function subscribe(def: D, callback: (event: Payload) => unknown) { diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 79034a01bb3c..a6ba797f33dd 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -750,9 +750,18 @@ export function Prompt(props: PromptProps) { return false } + const variant = local.model.variant.current() let sessionID = props.sessionID if (sessionID == null) { - const res = await sdk.client.session.create({ workspace: props.workspaceID }) + const res = await sdk.client.session.create({ + workspace: props.workspaceID, + agent: agent.name, + model: { + providerID: selectedModel.providerID, + id: selectedModel.modelID, + variant, + }, + }) if (res.error) { console.log("Creating a session failed:", res.error) @@ -792,7 +801,6 @@ export function Prompt(props: PromptProps) { // Capture mode before it gets reset const currentMode = store.mode - const variant = local.model.variant.current() const editorSelection = editorContext() const currentEditorSelectionKey = editorSelectionKey(editorSelection) const editorParts = diff --git a/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx index 7667b56f0dac..b179492d2280 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx @@ -68,7 +68,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( case "session.next.prompted": { update(event.properties.sessionID, (draft) => { draft.push({ - id: event.properties.id, + id: event.id, type: "user", text: event.properties.prompt.text, files: event.properties.prompt.files, @@ -81,7 +81,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( case "session.next.synthetic": update(event.properties.sessionID, (draft) => { draft.push({ - id: event.properties.id, + id: event.id, type: "synthetic", sessionID: event.properties.sessionID, text: event.properties.text, @@ -94,7 +94,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( const currentAssistant = activeAssistant(draft) if (currentAssistant) currentAssistant.time.completed = event.properties.timestamp draft.push({ - id: event.properties.id, + id: event.id, type: "assistant", agent: event.properties.agent, model: event.properties.model, @@ -224,7 +224,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( case "session.next.compaction.started": update(event.properties.sessionID, (draft) => { draft.push({ - id: event.properties.id, + id: event.id, type: "compaction", reason: event.properties.reason, summary: "", diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx index eba670e7430a..8e6479401264 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx @@ -12,11 +12,13 @@ import path from "path" import stripAnsi from "strip-ansi" import type { SessionMessage, + SessionMessageAgentSwitched, SessionMessageAssistant, SessionMessageAssistantReasoning, SessionMessageAssistantText, SessionMessageAssistantTool, SessionMessageCompaction, + SessionMessageModelSwitched, SessionMessageSynthetic, SessionMessageUser, ToolFileContent, @@ -88,6 +90,12 @@ function View(props: { api: TuiPluginApi; sessionID: string }) { + + + + + + @@ -213,6 +221,37 @@ function CompactionMessage(props: { message: SessionMessageCompaction }) { ) } +function AgentSwitchedMessage(props: { message: SessionMessageAgentSwitched }) { + const { theme } = useTheme() + const local = useLocal() + return ( + + + + Switched agent to + {Locale.titlecase(props.message.agent)} + + + ) +} + +function ModelSwitchedMessage(props: { message: SessionMessageModelSwitched }) { + const { theme } = useTheme() + const model = createMemo(() => { + const variant = props.message.model.variant ? `/${props.message.model.variant}` : "" + return `${props.message.model.providerID}/${props.message.model.id}${variant}` + }) + return ( + + + + Switched model to + {model()} + + + ) +} + function UnknownMessage(props: { message: SessionMessage }) { return } diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts index 4a491d95b6ae..da3614d2283e 100644 --- a/packages/opencode/src/server/routes/global.ts +++ b/packages/opencode/src/server/routes/global.ts @@ -6,6 +6,7 @@ import z from "zod" import { BusEvent } from "@/bus/bus-event" import { SyncEvent } from "@/sync" import { GlobalBus } from "@/bus/global" +import { Bus } from "@/bus" import { AppRuntime } from "@/effect/app-runtime" import { AsyncQueue } from "@/util/queue" import { Installation } from "@/installation" @@ -26,6 +27,7 @@ async function streamEvents(c: Context, subscribe: (q: AsyncQueue q.push( JSON.stringify({ payload: { + id: Bus.createID(), type: "server.connected", properties: {}, }, @@ -37,6 +39,7 @@ async function streamEvents(c: Context, subscribe: (q: AsyncQueue q.push( JSON.stringify({ payload: { + id: Bus.createID(), type: "server.heartbeat", properties: {}, }, diff --git a/packages/opencode/src/server/routes/instance/event.ts b/packages/opencode/src/server/routes/instance/event.ts index 474d92b31b33..52e9bc196447 100644 --- a/packages/opencode/src/server/routes/instance/event.ts +++ b/packages/opencode/src/server/routes/instance/event.ts @@ -42,6 +42,7 @@ export const EventRoutes = () => q.push( JSON.stringify({ + id: Bus.createID(), type: "server.connected", properties: {}, }), @@ -50,9 +51,10 @@ export const EventRoutes = () => // Send heartbeat every 10s to prevent stalled proxy streams. const heartbeat = setInterval(() => { q.push( - JSON.stringify({ - type: "server.heartbeat", - properties: {}, + JSON.stringify({ + id: Bus.createID(), + type: "server.heartbeat", + properties: {}, }), ) }, 10_000) diff --git a/packages/opencode/src/server/routes/instance/httpapi/event.ts b/packages/opencode/src/server/routes/instance/httpapi/event.ts index 25e810753e21..a5c328ac0e34 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/event.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/event.ts @@ -41,12 +41,12 @@ function eventResponse(bus: Bus.Interface) { const events = bus.subscribeAll().pipe(Stream.takeUntil((event) => event.type === Bus.InstanceDisposed.type)) const heartbeat = Stream.tick("10 seconds").pipe( Stream.drop(1), - Stream.map(() => ({ type: "server.heartbeat", properties: {} })), + Stream.map(() => ({ id: Bus.createID(), type: "server.heartbeat", properties: {} })), ) log.info("event connected") return HttpServerResponse.stream( - Stream.make({ type: "server.connected", properties: {} }).pipe( + Stream.make({ id: Bus.createID(), type: "server.connected", properties: {} }).pipe( Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))), Stream.map(eventData), Stream.pipeThroughChannel(Sse.encode()), diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts index f9be57f4fd89..f80869b64d3f 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts @@ -1,6 +1,7 @@ import { Config } from "@/config/config" import { GlobalBus, type GlobalEvent as GlobalBusEvent } from "@/bus/global" import { EffectBridge } from "@/effect/bridge" +import { Bus } from "@/bus" import { Installation } from "@/installation" import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle" import { InstallationVersion } from "@opencode-ai/core/installation/version" @@ -43,11 +44,11 @@ function eventResponse() { }) const heartbeat = Stream.tick("10 seconds").pipe( Stream.drop(1), - Stream.map(() => ({ payload: { type: "server.heartbeat", properties: {} } })), + Stream.map(() => ({ payload: { id: Bus.createID(), type: "server.heartbeat", properties: {} } })), ) return HttpServerResponse.stream( - Stream.make({ payload: { type: "server.connected", properties: {} } }).pipe( + Stream.make({ payload: { id: Bus.createID(), type: "server.connected", properties: {} } }).pipe( Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))), Stream.map(eventData), Stream.pipeThroughChannel(Sse.encode()), diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 95fc0d7347c7..e037e11407f1 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -147,7 +147,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.post(TuiPaths.selectSession, (c) => handler(c.req.raw, context)) app.get(TuiPaths.controlNext, (c) => handler(c.req.raw, context)) app.post(TuiPaths.controlResponse, (c) => handler(c.req.raw, context)) - app.get(WorkspacePaths.adaptors, (c) => handler(c.req.raw, context)) + app.get(WorkspacePaths.adapters, (c) => handler(c.req.raw, context)) app.post(WorkspacePaths.list, (c) => handler(c.req.raw, context)) app.get(WorkspacePaths.list, (c) => handler(c.req.raw, context)) app.get(WorkspacePaths.status, (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index e194e783da1d..0f08e65ff791 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -601,7 +601,6 @@ export const layer: Layer.Layer< overflow: input.overflow, }) SyncEvent.run(SessionEvent.Compaction.Started.Sync, { - id: SessionEvent.ID.create(), sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(Date.now()), reason: input.auto ? "auto" : "manual", diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 6da1013ebc42..ec7c4243e84c 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -436,7 +436,6 @@ export const layer: Layer.Layer< if (!ctx.assistantMessage.summary) { // TODO(v2): Temporary dual-write while migrating session messages to v2 events. SyncEvent.run(SessionEvent.Step.Started.Sync, { - id: SessionEvent.ID.create(), sessionID: ctx.sessionID, agent: input.assistantMessage.agent, model: { diff --git a/packages/opencode/src/session/projectors-next.ts b/packages/opencode/src/session/projectors-next.ts index be33d31e342a..cc25374b4fda 100644 --- a/packages/opencode/src/session/projectors-next.ts +++ b/packages/opencode/src/session/projectors-next.ts @@ -10,7 +10,7 @@ import type { SessionID } from "./schema" import { Schema } from "effect" const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message) -type SessionMessageData = NonNullable +type SessionMessageData = NonNullable<(typeof SessionMessageTable.$inferInsert)["data"]> function encodeDateTimes(value: unknown): unknown { if (DateTime.isDateTime(value)) return DateTime.toEpochMillis(value) @@ -96,7 +96,7 @@ function update(db: Database.TxOrDb, event: SessionEvent.Event) { } export default [ - SyncEvent.project(SessionEvent.AgentSwitched.Sync, (db, data) => { + SyncEvent.project(SessionEvent.AgentSwitched.Sync, (db, data, event) => { db.update(SessionTable) .set({ agent: data.agent, @@ -104,8 +104,9 @@ export default [ }) .where(eq(SessionTable.id, data.sessionID)) .run() + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.agent.switched", data }) }), - SyncEvent.project(SessionEvent.ModelSwitched.Sync, (db, data) => { + SyncEvent.project(SessionEvent.ModelSwitched.Sync, (db, data, event) => { db.update(SessionTable) .set({ model: { @@ -117,59 +118,60 @@ export default [ }) .where(eq(SessionTable.id, data.sessionID)) .run() + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.model.switched", data }) }), - SyncEvent.project(SessionEvent.Prompted.Sync, (db, data) => { - update(db, { type: "session.next.prompted", data }) + SyncEvent.project(SessionEvent.Prompted.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.prompted", data }) }), - SyncEvent.project(SessionEvent.Synthetic.Sync, (db, data) => { - update(db, { type: "session.next.synthetic", data }) + SyncEvent.project(SessionEvent.Synthetic.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.synthetic", data }) }), - SyncEvent.project(SessionEvent.Step.Started.Sync, (db, data) => { - update(db, { type: "session.next.step.started", data }) + SyncEvent.project(SessionEvent.Step.Started.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.step.started", data }) }), - SyncEvent.project(SessionEvent.Step.Ended.Sync, (db, data) => { - update(db, { type: "session.next.step.ended", data }) + SyncEvent.project(SessionEvent.Step.Ended.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.step.ended", data }) }), - SyncEvent.project(SessionEvent.Text.Started.Sync, (db, data) => { - update(db, { type: "session.next.text.started", data }) + SyncEvent.project(SessionEvent.Text.Started.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.text.started", data }) }), SyncEvent.project(SessionEvent.Text.Delta.Sync, () => {}), - SyncEvent.project(SessionEvent.Text.Ended.Sync, (db, data) => { - update(db, { type: "session.next.text.ended", data }) + SyncEvent.project(SessionEvent.Text.Ended.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.text.ended", data }) }), - SyncEvent.project(SessionEvent.Tool.Input.Started.Sync, (db, data) => { - update(db, { type: "session.next.tool.input.started", data }) + SyncEvent.project(SessionEvent.Tool.Input.Started.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.input.started", data }) }), SyncEvent.project(SessionEvent.Tool.Input.Delta.Sync, () => {}), - SyncEvent.project(SessionEvent.Tool.Input.Ended.Sync, (db, data) => { - update(db, { type: "session.next.tool.input.ended", data }) + SyncEvent.project(SessionEvent.Tool.Input.Ended.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.input.ended", data }) }), - SyncEvent.project(SessionEvent.Tool.Called.Sync, (db, data) => { - update(db, { type: "session.next.tool.called", data }) + SyncEvent.project(SessionEvent.Tool.Called.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.called", data }) }), - SyncEvent.project(SessionEvent.Tool.Success.Sync, (db, data) => { - update(db, { type: "session.next.tool.success", data }) + SyncEvent.project(SessionEvent.Tool.Success.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.success", data }) }), - SyncEvent.project(SessionEvent.Tool.Error.Sync, (db, data) => { - update(db, { type: "session.next.tool.error", data }) + SyncEvent.project(SessionEvent.Tool.Error.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.error", data }) }), - SyncEvent.project(SessionEvent.Reasoning.Started.Sync, (db, data) => { - update(db, { type: "session.next.reasoning.started", data }) + SyncEvent.project(SessionEvent.Reasoning.Started.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.reasoning.started", data }) }), SyncEvent.project(SessionEvent.Reasoning.Delta.Sync, () => {}), - SyncEvent.project(SessionEvent.Reasoning.Ended.Sync, (db, data) => { - update(db, { type: "session.next.reasoning.ended", data }) + SyncEvent.project(SessionEvent.Reasoning.Ended.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.reasoning.ended", data }) }), - SyncEvent.project(SessionEvent.Retried.Sync, (db, data) => { - update(db, { type: "session.next.retried", data }) + SyncEvent.project(SessionEvent.Retried.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.retried", data }) }), - SyncEvent.project(SessionEvent.Compaction.Started.Sync, (db, data) => { - update(db, { type: "session.next.compaction.started", data }) + SyncEvent.project(SessionEvent.Compaction.Started.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.started", data }) }), - SyncEvent.project(SessionEvent.Compaction.Delta.Sync, (db, data) => { - update(db, { type: "session.next.compaction.delta", data }) + SyncEvent.project(SessionEvent.Compaction.Delta.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.delta", data }) }), - SyncEvent.project(SessionEvent.Compaction.Ended.Sync, (db, data) => { - update(db, { type: "session.next.compaction.ended", data }) + SyncEvent.project(SessionEvent.Compaction.Ended.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.ended", data }) }), ] diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 520eb59704a6..2d7e48c0f3f8 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1332,7 +1332,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the ) // TODO(v2): Temporary dual-write while migrating session messages to v2 events. SyncEvent.run(SessionEvent.Prompted.Sync, { - id: SessionEvent.ID.create(), sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(info.time.created), prompt: { @@ -1344,7 +1343,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the for (const text of nextPrompt.synthetic) { // TODO(v2): Temporary dual-write while migrating session messages to v2 events. SyncEvent.run(SessionEvent.Synthetic.Sync, { - id: SessionEvent.ID.create(), sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(info.time.created), text, diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index e1d0c527aa86..fedfa8996e9f 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -32,6 +32,7 @@ import { Snapshot } from "@/snapshot" import { ProjectID } from "../project/schema" import { WorkspaceID } from "../control-plane/schema" import { SessionID, MessageID, PartID } from "./schema" +import { ModelID, ProviderID } from "@/provider/schema" import type { Provider } from "@/provider/provider" import { Permission } from "@/permission" @@ -78,6 +79,10 @@ export function fromRow(row: SessionRow): Info { path: row.path ?? undefined, parentID: row.parent_id ?? undefined, title: row.title, + agent: row.agent ?? undefined, + model: row.model + ? { id: ModelID.make(row.model.id), providerID: ProviderID.make(row.model.providerID), variant: row.model.variant } + : undefined, version: row.version, summary, share, @@ -102,6 +107,8 @@ export function toRow(info: Info) { directory: info.directory, path: info.path, title: info.title, + agent: info.agent, + model: info.model, version: info.version, share_url: info.share?.url, summary_additions: info.summary?.additions, @@ -160,6 +167,12 @@ const Revert = Schema.Struct({ diff: optionalOmitUndefined(Schema.String), }) +const Model = Schema.Struct({ + id: ModelID, + providerID: ProviderID, + variant: optionalOmitUndefined(Schema.String), +}) + export const Info = Schema.Struct({ id: SessionID, slug: Schema.String, @@ -171,6 +184,8 @@ export const Info = Schema.Struct({ summary: optionalOmitUndefined(Summary), share: optionalOmitUndefined(Share), title: Schema.String, + agent: optionalOmitUndefined(Schema.String), + model: optionalOmitUndefined(Model), version: Schema.String, time: Time, permission: optionalOmitUndefined(Permission.Ruleset), @@ -201,6 +216,8 @@ export const CreateInput = Schema.optional( Schema.Struct({ parentID: Schema.optional(SessionID), title: Schema.optional(Schema.String), + agent: Schema.optional(Schema.String), + model: Schema.optional(Model), permission: Schema.optional(Permission.Ruleset), workspaceID: Schema.optional(WorkspaceID), }), @@ -272,6 +289,8 @@ const UpdatedInfo = Schema.Struct({ summary: Schema.optional(Schema.NullOr(Summary)), share: Schema.optional(UpdatedShare), title: Schema.optional(Schema.NullOr(Schema.String)), + agent: Schema.optional(Schema.NullOr(Schema.String)), + model: Schema.optional(Schema.NullOr(Model)), version: Schema.optional(Schema.NullOr(Schema.String)), time: Schema.optional(UpdatedTime), permission: Schema.optional(Schema.NullOr(Permission.Ruleset)), @@ -404,6 +423,8 @@ export interface Interface { readonly create: (input?: { parentID?: SessionID title?: string + agent?: string + model?: Schema.Schema.Type permission?: Permission.Ruleset workspaceID?: WorkspaceID }) => Effect.Effect @@ -464,6 +485,8 @@ export const layer: Layer.Layer parentID?: SessionID workspaceID?: WorkspaceID directory: string @@ -481,6 +504,8 @@ export const layer: Layer.Layer permission?: Permission.Ruleset workspaceID?: WorkspaceID }) { @@ -601,6 +628,8 @@ export const layer: Layer.Layer = EffectSchema.Schem export type SerializedEvent = Event & { type: string } -type ProjectorFunc = (db: Database.TxOrDb, data: unknown) => void +type ProjectorFunc = (db: Database.TxOrDb, data: unknown, event: Event) => void type ConvertEvent = (type: string, data: Event["data"]) => unknown | Promise type PublishContext = { instance?: InstanceContext @@ -255,7 +255,7 @@ export function define< export function project( def: Def, - func: (db: Database.TxOrDb, data: Event["data"]) => void, + func: (db: Database.TxOrDb, data: Event["data"], event: Event) => void, ): [Definition, ProjectorFunc] { return [def, func as ProjectorFunc] } @@ -277,7 +277,7 @@ function process( // idempotent: need to ignore any events already logged Database.transaction((tx) => { - projector(tx, event.data) + projector(tx, event.data, event) if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { tx.insert(EventSequenceTable) @@ -308,7 +308,7 @@ function process( } const result = convertEvent(def.type, event.data) - const publish = (data: unknown) => ProjectBus.publish(def, data as Properties) + const publish = (data: unknown) => ProjectBus.publish(def, data as Properties, { id: event.id }) if (result instanceof Promise) { void result.then(publish) } else { diff --git a/packages/opencode/src/v2/event.ts b/packages/opencode/src/v2/event.ts index a5c1c4182439..af0c87d34479 100644 --- a/packages/opencode/src/v2/event.ts +++ b/packages/opencode/src/v2/event.ts @@ -18,6 +18,7 @@ export function define - export const Source = Schema.Struct({ start: NonNegativeInt, end: NonNegativeInt, @@ -33,6 +30,7 @@ export const AgentSwitched = Event.define({ agent: Schema.String, }, }) +export type AgentSwitched = Schema.Schema.Type export const ModelSwitched = Event.define({ type: "session.next.model.switched", @@ -45,6 +43,7 @@ export const ModelSwitched = Event.define({ variant: Schema.String.pipe(Schema.optional), }, }) +export type ModelSwitched = Schema.Schema.Type export const Prompted = Event.define({ type: "session.next.prompted", @@ -52,7 +51,6 @@ export const Prompted = Event.define({ version: 1, schema: { ...Base, - id: ID, prompt: Prompt, }, }) @@ -63,7 +61,6 @@ export const Synthetic = Event.define({ aggregate: "sessionID", schema: { ...Base, - id: ID, text: Schema.String, }, }) @@ -75,7 +72,6 @@ export namespace Step { aggregate: "sessionID", schema: { ...Base, - id: ID, agent: Schema.String, model: Schema.Struct({ id: Schema.String, @@ -302,7 +298,6 @@ export namespace Compaction { aggregate: "sessionID", schema: { ...Base, - id: ID, reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]), }, }) diff --git a/packages/opencode/src/v2/session-message-updater.ts b/packages/opencode/src/v2/session-message-updater.ts index 93b7b8ce0b35..e97dd03e7b2a 100644 --- a/packages/opencode/src/v2/session-message-updater.ts +++ b/packages/opencode/src/v2/session-message-updater.ts @@ -77,8 +77,12 @@ export function update(adapter: Adapter, event: SessionEvent.Eve ) SessionEvent.All.match(event, { - "session.next.agent.switched": () => {}, - "session.next.model.switched": () => {}, + "session.next.agent.switched": (event) => { + adapter.appendMessage(SessionMessage.AgentSwitched.fromEvent(event)) + }, + "session.next.model.switched": (event) => { + adapter.appendMessage(SessionMessage.ModelSwitched.fromEvent(event)) + }, "session.next.prompted": (event) => { adapter.appendMessage(SessionMessage.User.fromEvent(event)) }, diff --git a/packages/opencode/src/v2/session-message.ts b/packages/opencode/src/v2/session-message.ts index a805bd62ce24..98ef9c585c42 100644 --- a/packages/opencode/src/v2/session-message.ts +++ b/packages/opencode/src/v2/session-message.ts @@ -15,6 +15,46 @@ const Base = { }), } +export class AgentSwitched extends Schema.Class("Session.Message.AgentSwitched")({ + ...Base, + type: Schema.Literal("agent-switched"), + agent: SessionEvent.AgentSwitched.fields.data.fields.agent, +}) { + static fromEvent(event: SessionEvent.AgentSwitched) { + return new AgentSwitched({ + id: event.id, + type: "agent-switched", + metadata: event.metadata, + agent: event.data.agent, + time: { created: event.data.timestamp }, + }) + } +} + +export class ModelSwitched extends Schema.Class("Session.Message.ModelSwitched")({ + ...Base, + type: Schema.Literal("model-switched"), + model: Schema.Struct({ + id: SessionEvent.ModelSwitched.fields.data.fields.id, + providerID: SessionEvent.ModelSwitched.fields.data.fields.providerID, + variant: SessionEvent.ModelSwitched.fields.data.fields.variant, + }), +}) { + static fromEvent(event: SessionEvent.ModelSwitched) { + return new ModelSwitched({ + id: event.id, + type: "model-switched", + metadata: event.metadata, + model: { + id: event.data.id, + providerID: event.data.providerID, + variant: event.data.variant, + }, + time: { created: event.data.timestamp }, + }) + } +} + export class User extends Schema.Class("Session.Message.User")({ ...Base, text: Prompt.fields.text, @@ -27,7 +67,7 @@ export class User extends Schema.Class("Session.Message.User")({ }) { static fromEvent(event: SessionEvent.Prompted) { return new User({ - id: event.data.id, + id: event.id, type: "user", metadata: event.metadata, text: event.data.prompt.text, @@ -48,7 +88,7 @@ export class Synthetic extends Schema.Class("Session.Message.Syntheti return new Synthetic({ sessionID: event.data.sessionID, text: event.data.text, - id: event.data.id, + id: event.id, type: "synthetic", time: { created: event.data.timestamp }, }) @@ -153,7 +193,7 @@ export class Assistant extends Schema.Class("Session.Message.Assistan }) { static fromEvent(event: SessionEvent.Step.Started) { return new Assistant({ - id: event.data.id, + id: event.id, type: "assistant", agent: event.data.agent, model: event.data.model, @@ -175,7 +215,7 @@ export class Compaction extends Schema.Class("Session.Message.Compac }) { static fromEvent(event: SessionEvent.Compaction.Started) { return new Compaction({ - id: event.data.id, + id: event.id, type: "compaction", metadata: event.metadata, reason: event.data.reason, @@ -185,7 +225,7 @@ export class Compaction extends Schema.Class("Session.Message.Compac } } -export const Message = Schema.Union([User, Synthetic, Assistant, Compaction]) +export const Message = Schema.Union([AgentSwitched, ModelSwitched, User, Synthetic, Assistant, Compaction]) .pipe(Schema.toTaggedUnion("type")) .annotate({ identifier: "Session.Message" }) diff --git a/packages/opencode/test/acp/event-subscription.test.ts b/packages/opencode/test/acp/event-subscription.test.ts index 9a92fc507212..2722757ab9e3 100644 --- a/packages/opencode/test/acp/event-subscription.test.ts +++ b/packages/opencode/test/acp/event-subscription.test.ts @@ -59,6 +59,7 @@ function toolEvent( raw: opts.raw, } const payload: EventMessagePartUpdated = { + id: `evt_${opts.callID}`, type: "message.part.updated", properties: { sessionID: sessionId, diff --git a/packages/opencode/test/cli/tui/use-event.test.tsx b/packages/opencode/test/cli/tui/use-event.test.tsx index 5b0fcad3c928..78253361b76c 100644 --- a/packages/opencode/test/cli/tui/use-event.test.tsx +++ b/packages/opencode/test/cli/tui/use-event.test.tsx @@ -25,6 +25,7 @@ function event(payload: Event, input: { directory: string; workspace?: string }) function vcs(branch: string): Event { return { + id: `evt_vcs_${branch}`, type: "vcs.branch.updated", properties: { branch, @@ -34,6 +35,7 @@ function vcs(branch: string): Event { function update(version: string): Event { return { + id: `evt_update_${version}`, type: "installation.update-available", properties: { version, diff --git a/packages/opencode/test/v2/session-message-updater.test.ts b/packages/opencode/test/v2/session-message-updater.test.ts index e42fc78eede8..abdb3469a61d 100644 --- a/packages/opencode/test/v2/session-message-updater.test.ts +++ b/packages/opencode/test/v2/session-message-updater.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test" import * as DateTime from "effect/DateTime" import { SessionID } from "../../src/session/schema" +import { Event } from "../../src/v2/event" import { SessionEvent } from "../../src/v2/session-event" import { SessionMessageUpdater } from "../../src/v2/session-message-updater" @@ -9,9 +10,9 @@ test("step snapshots carry over to assistant messages", () => { const sessionID = SessionID.make("session") SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.step.started", data: { - id: SessionEvent.ID.create(), sessionID, timestamp: DateTime.makeUnsafe(1), agent: "build", @@ -21,6 +22,7 @@ test("step snapshots carry over to assistant messages", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.step.ended", data: { sessionID, @@ -48,9 +50,9 @@ test("text ended populates assistant text content", () => { const sessionID = SessionID.make("session") SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.step.started", data: { - id: SessionEvent.ID.create(), sessionID, timestamp: DateTime.makeUnsafe(1), agent: "build", @@ -59,6 +61,7 @@ test("text ended populates assistant text content", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.text.started", data: { sessionID, @@ -67,6 +70,7 @@ test("text ended populates assistant text content", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.text.ended", data: { sessionID, @@ -86,9 +90,9 @@ test("tool completion stores completed timestamp", () => { const callID = "call" SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.step.started", data: { - id: SessionEvent.ID.create(), sessionID, timestamp: DateTime.makeUnsafe(1), agent: "build", @@ -97,6 +101,7 @@ test("tool completion stores completed timestamp", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.tool.input.started", data: { sessionID, @@ -107,6 +112,7 @@ test("tool completion stores completed timestamp", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.tool.called", data: { sessionID, @@ -119,6 +125,7 @@ test("tool completion stores completed timestamp", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.tool.success", data: { sessionID, @@ -141,12 +148,12 @@ test("tool completion stores completed timestamp", () => { test("compaction events reduce to compaction message", () => { const state: SessionMessageUpdater.MemoryState = { messages: [] } const sessionID = SessionID.make("session") - const id = SessionEvent.ID.create() + const id = Event.ID.create() SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id, type: "session.next.compaction.started", data: { - id, sessionID, timestamp: DateTime.makeUnsafe(1), reason: "auto", @@ -154,6 +161,7 @@ test("compaction events reduce to compaction message", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.compaction.delta", data: { sessionID, @@ -163,6 +171,7 @@ test("compaction events reduce to compaction message", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.compaction.delta", data: { sessionID, @@ -172,6 +181,7 @@ test("compaction events reduce to compaction message", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { + id: Event.ID.create(), type: "session.next.compaction.ended", data: { sessionID, diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 3e076b004535..54d15aad1d2e 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -1695,6 +1695,12 @@ export class Session2 extends HeyApiClient { workspace?: string parentID?: string title?: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } permission?: PermissionRuleset workspaceID?: string }, @@ -1709,6 +1715,8 @@ export class Session2 extends HeyApiClient { { in: "query", key: "workspace" }, { in: "body", key: "parentID" }, { in: "body", key: "title" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, { in: "body", key: "permission" }, { in: "body", key: "workspaceID" }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 5d16dede85e1..eb66ee507000 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4,6 +4,40 @@ export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}) } +<<<<<<< HEAD +======= +export type Project = { + id: string + worktree: string + vcs?: "git" + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + time: { + created: number + updated: number + initialized?: number + } + sandboxes: Array +} + +export type EventProjectUpdated = { + id: string + type: "project.updated" + properties: Project +} +<<<<<<< HEAD + +>>>>>>> cbb60d3a5 (core: add unique IDs to all events for reliable tracking and debugging) export type EventServerInstanceDisposed = { type: "server.instance.disposed" properties: { @@ -6500,3 +6534,5 @@ export type FormatterStatusResponses = { } export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] +======= +>>>>>>> 13afcef87 (core: add unique IDs to all events for reliable tracking and debugging) From f49370a353afc1cb7e80c336c36254d2b7a5cb33 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 1 May 2026 19:56:39 -0400 Subject: [PATCH 09/24] tui: close open dialogs when navigating to a new session to prevent UI state from lingering --- .../src/cli/cmd/tui/feature-plugins/system/session-v2.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx index 8e6479401264..ac3ca878de6f 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx @@ -997,6 +997,7 @@ const tui: TuiPlugin = async (api) => { const sessionID = currentSessionID(api) if (!sessionID) return api.route.navigate(route, { sessionID }) + api.ui.dialog.clear() }, }, ]) From 57e45583c397df32650b7449670d54dbea571ef1 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 1 May 2026 23:16:01 -0400 Subject: [PATCH 10/24] tui: show shell command execution and collapsible tool outputs Users can now see when shell commands are running in the session view with real-time output display. Long tool outputs are now collapsible with click-to-expand/collapse interaction to keep the interface clean when dealing with verbose command results. --- .../src/cli/cmd/tui/context/sync-v2.tsx | 27 +++++ .../tui/feature-plugins/system/session-v2.tsx | 96 +++++++++++++-- .../opencode/src/session/projectors-next.ts | 29 +++++ packages/opencode/src/session/prompt.ts | 21 +++- packages/opencode/src/v2/session-event.ts | 26 +++++ .../src/v2/session-message-updater.ts | 110 +++++++++++++++++- packages/opencode/src/v2/session-message.ts | 97 +++------------ 7 files changed, 309 insertions(+), 97 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx index b179492d2280..f82bb4d96227 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx @@ -24,6 +24,13 @@ function activeCompaction(messages: SessionMessage[]) { return compaction?.type === "compaction" ? compaction : undefined } +function activeShell(messages: SessionMessage[], callID: string) { + const index = messages.findLastIndex((message) => message.type === "shell" && message.callID === callID) + if (index < 0) return + const shell = messages[index] + return shell?.type === "shell" ? shell : undefined +} + function latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) { return assistant?.content.findLast( (item): item is SessionMessageAssistantTool => item.type === "tool" && (callID === undefined || item.id === callID), @@ -89,6 +96,26 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext( }) }) break + case "session.next.shell.started": + update(event.properties.sessionID, (draft) => { + draft.push({ + id: event.id, + type: "shell", + callID: event.properties.callID, + command: event.properties.command, + output: "", + time: { created: event.properties.timestamp }, + }) + }) + break + case "session.next.shell.ended": + update(event.properties.sessionID, (draft) => { + const match = activeShell(draft, event.properties.callID) + if (!match) return + match.output = event.properties.output + match.time.completed = event.properties.timestamp + }) + break case "session.next.step.started": update(event.properties.sessionID, (draft) => { const currentAssistant = activeAssistant(draft) diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx index ac3ca878de6f..7270a9c3b7f7 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx @@ -4,7 +4,7 @@ import { SplitBorder } from "@tui/component/border" import { Spinner } from "@tui/component/spinner" import { useTheme } from "@tui/context/theme" import { useLocal } from "@tui/context/local" -import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid" +import { useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" import type { SyntaxStyle } from "@opentui/core" import { Locale } from "@/util/locale" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" @@ -19,12 +19,13 @@ import type { SessionMessageAssistantTool, SessionMessageCompaction, SessionMessageModelSwitched, + SessionMessageShell, SessionMessageSynthetic, SessionMessageUser, ToolFileContent, ToolTextContent, } from "@opencode-ai/sdk/v2" -import { createEffect, createMemo, For, Match, Show, Switch } from "solid-js" +import { createEffect, createMemo, createSignal, For, Match, Show, Switch } from "solid-js" const id = "internal:session-v2-debug" const route = "session.v2.messages" @@ -87,6 +88,9 @@ function View(props: { api: TuiPluginApi; sessionID: string }) { + + + @@ -203,6 +207,35 @@ function SyntheticMessage(props: { message: SessionMessageSynthetic; index: numb ) } +function ShellMessage(props: { message: SessionMessageShell }) { + const { theme } = useTheme() + const output = createMemo(() => stripAnsi(props.message.output.trim())) + const [expanded, setExpanded] = createSignal(false) + const lines = createMemo(() => output().split("\n")) + const overflow = createMemo(() => lines().length > 10) + const limited = createMemo(() => { + if (expanded() || !overflow()) return output() + return [...lines().slice(0, 10), "…"].join("\n") + }) + return ( + setExpanded((prev) => !prev) : undefined} + > + + $ {props.message.command} + + {limited()} + + + {expanded() ? "Click to collapse" : "Click to expand"} + + + + ) +} + function CompactionMessage(props: { message: SessionMessageCompaction }) { const { theme } = useTheme() return ( @@ -444,17 +477,36 @@ type ToolProps = { } function GenericTool(props: ToolProps) { + const { theme } = useTheme() + const output = createMemo(() => props.output?.trim() ?? "") + const [expanded, setExpanded] = createSignal(false) + const lines = createMemo(() => output().split("\n")) + const maxLines = 3 + const overflow = createMemo(() => lines().length > maxLines) + const limited = createMemo(() => { + if (expanded() || !overflow()) return output() + return [...lines().slice(0, maxLines), "…"].join("\n") + }) return ( {props.part.name} {input(props.input)} } > - - {props.output} + setExpanded((prev) => !prev) : undefined} + > + + {limited()} + + {expanded() ? "Click to collapse" : "Click to expand"} + + ) @@ -503,11 +555,14 @@ function InlineTool(props: { function BlockTool(props: { title: string children: JSX.Element - part: SessionMessageAssistantTool + part?: SessionMessageAssistantTool + onClick?: () => void spinner?: boolean }) { const { theme } = useTheme() - const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error.message : undefined)) + const renderer = useRenderer() + const [hover, setHover] = createSignal(false) + const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error.message : undefined)) return ( props.onClick && setHover(true)} + onMouseOut={() => setHover(false)} + onMouseUp={() => { + if (renderer.getSelection()?.getSelectedText()) return + props.onClick?.() + }} flexShrink={0} > stripAnsi((stringValue(props.metadata.output) ?? props.output ?? "").trim())) const command = createMemo(() => stringValue(props.input.command) ?? pendingInput(props.part)) const title = createMemo(() => `# ${stringValue(props.input.description) ?? "Shell"}`) + const [expanded, setExpanded] = createSignal(false) + const lines = createMemo(() => output().split("\n")) + const overflow = createMemo(() => lines().length > 10) + const limited = createMemo(() => { + if (expanded() || !overflow()) return output() + return [...lines().slice(0, 10), "…"].join("\n") + }) return ( - + setExpanded((prev) => !prev) : undefined} + > $ {command()} - {output()} + {limited()} + + {expanded() ? "Click to collapse" : "Click to expand"} + diff --git a/packages/opencode/src/session/projectors-next.ts b/packages/opencode/src/session/projectors-next.ts index cc25374b4fda..dbab58a74c8d 100644 --- a/packages/opencode/src/session/projectors-next.ts +++ b/packages/opencode/src/session/projectors-next.ts @@ -47,6 +47,16 @@ function sqlite(db: Database.TxOrDb, sessionID: SessionID): SessionMessageUpdate .map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type })) .find((message): message is SessionMessage.Compaction => message.type === "compaction") }, + getCurrentShell(callID) { + return db + .select() + .from(SessionMessageTable) + .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "shell"))) + .orderBy(desc(SessionMessageTable.id)) + .all() + .map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type })) + .find((message): message is SessionMessage.Shell => message.type === "shell" && message.callID === callID) + }, updateAssistant(assistant) { const { id, type, ...data } = assistant db.update(SessionMessageTable) @@ -73,6 +83,19 @@ function sqlite(db: Database.TxOrDb, sessionID: SessionID): SessionMessageUpdate ) .run() }, + updateShell(shell) { + const { id, type, ...data } = shell + db.update(SessionMessageTable) + .set({ data: encodeMessageData(data) }) + .where( + and( + eq(SessionMessageTable.id, id), + eq(SessionMessageTable.session_id, sessionID), + eq(SessionMessageTable.type, type), + ), + ) + .run() + }, appendMessage(message) { const { id, type, ...data } = message db.insert(SessionMessageTable) @@ -126,6 +149,12 @@ export default [ SyncEvent.project(SessionEvent.Synthetic.Sync, (db, data, event) => { update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.synthetic", data }) }), + SyncEvent.project(SessionEvent.Shell.Started.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.shell.started", data }) + }), + SyncEvent.project(SessionEvent.Shell.Ended.Sync, (db, data, event) => { + update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.shell.ended", data }) + }), SyncEvent.project(SessionEvent.Step.Started.Sync, (db, data, event) => { update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.step.started", data }) }), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 2d7e48c0f3f8..9a4db5436bc2 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -792,6 +792,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the providerID: model.providerID, } yield* sessions.updateMessage(msg) + const callID = ulid() + const started = Date.now() const part: MessageV2.ToolPart = { type: "tool", id: PartID.ascending(), @@ -801,11 +803,17 @@ NOTE: At any point in time through this workflow you should feel free to ask the callID: ulid(), state: { status: "running", - time: { start: Date.now() }, + time: { start: started }, input: { command: input.command }, }, } yield* sessions.updatePart(part) + SyncEvent.run(SessionEvent.Shell.Started.Sync, { + sessionID: input.sessionID, + timestamp: DateTime.makeUnsafe(started), + callID, + command: input.command, + }) return { msg, part, cwd: ctx.directory } }).pipe(Effect.ensuring(markReady)) @@ -820,14 +828,21 @@ NOTE: At any point in time through this workflow you should feel free to ask the if (aborted) { output += "\n\n" + ["", "User aborted the command", ""].join("\n") } + const completed = Date.now() + SyncEvent.run(SessionEvent.Shell.Ended.Sync, { + sessionID: input.sessionID, + timestamp: DateTime.makeUnsafe(completed), + callID: part.callID, + output, + }) if (!msg.time.completed) { - msg.time.completed = Date.now() + msg.time.completed = completed yield* sessions.updateMessage(msg) } if (part.state.status === "running") { part.state = { status: "completed", - time: { ...part.state.time, end: Date.now() }, + time: { ...part.state.time, end: completed }, input: part.state.input, title: "", metadata: { output, description: "" }, diff --git a/packages/opencode/src/v2/session-event.ts b/packages/opencode/src/v2/session-event.ts index 2daec35c6344..f21de780731a 100644 --- a/packages/opencode/src/v2/session-event.ts +++ b/packages/opencode/src/v2/session-event.ts @@ -66,6 +66,30 @@ export const Synthetic = Event.define({ }) export type Synthetic = Schema.Schema.Type +export namespace Shell { + export const Started = Event.define({ + type: "session.next.shell.started", + aggregate: "sessionID", + schema: { + ...Base, + callID: Schema.String, + command: Schema.String, + }, + }) + export type Started = Schema.Schema.Type + + export const Ended = Event.define({ + type: "session.next.shell.ended", + aggregate: "sessionID", + schema: { + ...Base, + callID: Schema.String, + output: Schema.String, + }, + }) + export type Ended = Schema.Schema.Type +} + export namespace Step { export const Started = Event.define({ type: "session.next.step.started", @@ -330,6 +354,8 @@ export const All = Schema.Union( ModelSwitched, Prompted, Synthetic, + Shell.Started, + Shell.Ended, Step.Started, Step.Ended, Text.Started, diff --git a/packages/opencode/src/v2/session-message-updater.ts b/packages/opencode/src/v2/session-message-updater.ts index e97dd03e7b2a..844f6fe2d17e 100644 --- a/packages/opencode/src/v2/session-message-updater.ts +++ b/packages/opencode/src/v2/session-message-updater.ts @@ -9,8 +9,10 @@ export type MemoryState = { export interface Adapter { readonly getCurrentAssistant: () => SessionMessage.Assistant | undefined readonly getCurrentCompaction: () => SessionMessage.Compaction | undefined + readonly getCurrentShell: (callID: string) => SessionMessage.Shell | undefined readonly updateAssistant: (assistant: SessionMessage.Assistant) => void readonly updateCompaction: (compaction: SessionMessage.Compaction) => void + readonly updateShell: (shell: SessionMessage.Shell) => void readonly appendMessage: (message: SessionMessage.Message) => void readonly finish: () => Result } @@ -19,6 +21,8 @@ export function memory(state: MemoryState): Adapter { const activeAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant" && !message.time.completed) const activeCompactionIndex = () => state.messages.findLastIndex((message) => message.type === "compaction") + const activeShellIndex = (callID: string) => + state.messages.findLastIndex((message) => message.type === "shell" && message.callID === callID) return { getCurrentAssistant() { @@ -33,6 +37,12 @@ export function memory(state: MemoryState): Adapter { const compaction = state.messages[index] return compaction?.type === "compaction" ? compaction : undefined }, + getCurrentShell(callID) { + const index = activeShellIndex(callID) + if (index < 0) return + const shell = state.messages[index] + return shell?.type === "shell" ? shell : undefined + }, updateAssistant(assistant) { const index = activeAssistantIndex() if (index < 0) return @@ -47,6 +57,13 @@ export function memory(state: MemoryState): Adapter { if (current?.type !== "compaction") return state.messages[index] = compaction }, + updateShell(shell) { + const index = activeShellIndex(shell.callID) + if (index < 0) return + const current = state.messages[index] + if (current?.type !== "shell") return + state.messages[index] = shell + }, appendMessage(message) { state.messages.push(message) }, @@ -78,16 +95,78 @@ export function update(adapter: Adapter, event: SessionEvent.Eve SessionEvent.All.match(event, { "session.next.agent.switched": (event) => { - adapter.appendMessage(SessionMessage.AgentSwitched.fromEvent(event)) + adapter.appendMessage( + new SessionMessage.AgentSwitched({ + id: event.id, + type: "agent-switched", + metadata: event.metadata, + agent: event.data.agent, + time: { created: event.data.timestamp }, + }), + ) }, "session.next.model.switched": (event) => { - adapter.appendMessage(SessionMessage.ModelSwitched.fromEvent(event)) + adapter.appendMessage( + new SessionMessage.ModelSwitched({ + id: event.id, + type: "model-switched", + metadata: event.metadata, + model: { + id: event.data.id, + providerID: event.data.providerID, + variant: event.data.variant, + }, + time: { created: event.data.timestamp }, + }), + ) }, "session.next.prompted": (event) => { - adapter.appendMessage(SessionMessage.User.fromEvent(event)) + adapter.appendMessage( + new SessionMessage.User({ + id: event.id, + type: "user", + metadata: event.metadata, + text: event.data.prompt.text, + files: event.data.prompt.files, + agents: event.data.prompt.agents, + time: { created: event.data.timestamp }, + }), + ) }, "session.next.synthetic": (event) => { - adapter.appendMessage(SessionMessage.Synthetic.fromEvent(event)) + adapter.appendMessage( + new SessionMessage.Synthetic({ + sessionID: event.data.sessionID, + text: event.data.text, + id: event.id, + type: "synthetic", + time: { created: event.data.timestamp }, + }), + ) + }, + "session.next.shell.started": (event) => { + adapter.appendMessage( + new SessionMessage.Shell({ + id: event.id, + type: "shell", + metadata: event.metadata, + callID: event.data.callID, + command: event.data.command, + output: "", + time: { created: event.data.timestamp }, + }), + ) + }, + "session.next.shell.ended": (event) => { + const currentShell = adapter.getCurrentShell(event.data.callID) + if (currentShell) { + adapter.updateShell( + produce(currentShell, (draft) => { + draft.output = event.data.output + draft.time.completed = event.data.timestamp + }), + ) + } }, "session.next.step.started": (event) => { if (currentAssistant) { @@ -97,7 +176,17 @@ export function update(adapter: Adapter, event: SessionEvent.Eve }), ) } - adapter.appendMessage(SessionMessage.Assistant.fromEvent(event)) + adapter.appendMessage( + new SessionMessage.Assistant({ + id: event.id, + type: "assistant", + agent: event.data.agent, + model: event.data.model, + time: { created: event.data.timestamp }, + content: [], + snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined, + }), + ) }, "session.next.step.ended": (event) => { if (currentAssistant) { @@ -282,7 +371,16 @@ export function update(adapter: Adapter, event: SessionEvent.Eve }, "session.next.retried": () => {}, "session.next.compaction.started": (event) => { - adapter.appendMessage(SessionMessage.Compaction.fromEvent(event)) + adapter.appendMessage( + new SessionMessage.Compaction({ + id: event.id, + type: "compaction", + metadata: event.metadata, + reason: event.data.reason, + summary: "", + time: { created: event.data.timestamp }, + }), + ) }, "session.next.compaction.delta": (event) => { const currentCompaction = adapter.getCurrentCompaction() diff --git a/packages/opencode/src/v2/session-message.ts b/packages/opencode/src/v2/session-message.ts index 98ef9c585c42..e4766d8ef707 100644 --- a/packages/opencode/src/v2/session-message.ts +++ b/packages/opencode/src/v2/session-message.ts @@ -19,17 +19,7 @@ export class AgentSwitched extends Schema.Class("Session.Message. ...Base, type: Schema.Literal("agent-switched"), agent: SessionEvent.AgentSwitched.fields.data.fields.agent, -}) { - static fromEvent(event: SessionEvent.AgentSwitched) { - return new AgentSwitched({ - id: event.id, - type: "agent-switched", - metadata: event.metadata, - agent: event.data.agent, - time: { created: event.data.timestamp }, - }) - } -} +}) {} export class ModelSwitched extends Schema.Class("Session.Message.ModelSwitched")({ ...Base, @@ -39,21 +29,7 @@ export class ModelSwitched extends Schema.Class("Session.Message. providerID: SessionEvent.ModelSwitched.fields.data.fields.providerID, variant: SessionEvent.ModelSwitched.fields.data.fields.variant, }), -}) { - static fromEvent(event: SessionEvent.ModelSwitched) { - return new ModelSwitched({ - id: event.id, - type: "model-switched", - metadata: event.metadata, - model: { - id: event.data.id, - providerID: event.data.providerID, - variant: event.data.variant, - }, - time: { created: event.data.timestamp }, - }) - } -} +}) {} export class User extends Schema.Class("Session.Message.User")({ ...Base, @@ -64,36 +40,26 @@ export class User extends Schema.Class("Session.Message.User")({ time: Schema.Struct({ created: Schema.DateTimeUtcFromMillis, }), -}) { - static fromEvent(event: SessionEvent.Prompted) { - return new User({ - id: event.id, - type: "user", - metadata: event.metadata, - text: event.data.prompt.text, - files: event.data.prompt.files, - agents: event.data.prompt.agents, - time: { created: event.data.timestamp }, - }) - } -} +}) {} export class Synthetic extends Schema.Class("Session.Message.Synthetic")({ ...Base, sessionID: SessionEvent.Synthetic.fields.data.fields.sessionID, text: SessionEvent.Synthetic.fields.data.fields.text, type: Schema.Literal("synthetic"), -}) { - static fromEvent(event: SessionEvent.Synthetic) { - return new Synthetic({ - sessionID: event.data.sessionID, - text: event.data.text, - id: event.id, - type: "synthetic", - time: { created: event.data.timestamp }, - }) - } -} +}) {} + +export class Shell extends Schema.Class("Session.Message.Shell")({ + ...Base, + type: Schema.Literal("shell"), + callID: SessionEvent.Shell.Started.fields.data.fields.callID, + command: SessionEvent.Shell.Started.fields.data.fields.command, + output: Schema.String, + time: Schema.Struct({ + created: Schema.DateTimeUtcFromMillis, + completed: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + }), +}) {} export class ToolStatePending extends Schema.Class("Session.Message.ToolState.Pending")({ status: Schema.Literal("pending"), @@ -190,21 +156,7 @@ export class Assistant extends Schema.Class("Session.Message.Assistan created: Schema.DateTimeUtcFromMillis, completed: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), }), -}) { - static fromEvent(event: SessionEvent.Step.Started) { - return new Assistant({ - id: event.id, - type: "assistant", - agent: event.data.agent, - model: event.data.model, - time: { - created: event.data.timestamp, - }, - content: [], - snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined, - }) - } -} +}) {} export class Compaction extends Schema.Class("Session.Message.Compaction")({ type: Schema.Literal("compaction"), @@ -212,20 +164,9 @@ export class Compaction extends Schema.Class("Session.Message.Compac summary: Schema.String, include: Schema.String.pipe(Schema.optional), ...Base, -}) { - static fromEvent(event: SessionEvent.Compaction.Started) { - return new Compaction({ - id: event.id, - type: "compaction", - metadata: event.metadata, - reason: event.data.reason, - summary: "", - time: { created: event.data.timestamp }, - }) - } -} +}) {} -export const Message = Schema.Union([AgentSwitched, ModelSwitched, User, Synthetic, Assistant, Compaction]) +export const Message = Schema.Union([AgentSwitched, ModelSwitched, User, Synthetic, Shell, Assistant, Compaction]) .pipe(Schema.toTaggedUnion("type")) .annotate({ identifier: "Session.Message" }) From b6152d66566d05b67dba801a8d511d854ce4338f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 13:58:58 -0400 Subject: [PATCH 11/24] sync --- packages/core/src/flag/flag.ts | 1 + packages/opencode/src/session/compaction.ts | 6 +-- packages/opencode/src/session/processor.ts | 30 +++++------ packages/opencode/src/session/prompt.ts | 14 ++--- packages/opencode/src/v2/event.ts | 12 ++++- packages/opencode/src/v2/session-event.ts | 52 +++++++++---------- packages/opencode/src/v2/session-message.ts | 4 +- packages/opencode/src/v2/session.ts | 9 ++-- .../test/v2/session-message-updater.test.ts | 28 +++++----- 9 files changed, 83 insertions(+), 73 deletions(-) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index ed52f90e6095..0daae55800c1 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -95,6 +95,7 @@ export const Flag = { truthy("OPENCODE_EXPERIMENTAL_HTTPAPI") || (!falsy("OPENCODE_EXPERIMENTAL_HTTPAPI") && HTTPAPI_DEFAULT_ON_CHANNELS.has(InstallationChannel)), OPENCODE_EXPERIMENTAL_WORKSPACES: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES"), + OPENCODE_EXPERIMENTAL_EVENT_SYSTEM: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"), // Evaluated at access time (not module load) because tests, the CLI, and // external tooling set these env vars at runtime. diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 0f08e65ff791..067d43da2e25 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -19,7 +19,7 @@ import { InstanceState } from "@/effect/instance-state" import { isOverflow as overflow, usable } from "./overflow" import { makeRuntime } from "@/effect/run-service" import { fn } from "@/util/fn" -import { SyncEvent } from "@/sync" +import { EventV2 } from "@/v2/event" import { SessionEvent } from "@/v2/session-event" const log = Log.create({ service: "session.compaction" }) @@ -566,7 +566,7 @@ export const layer: Layer.Layer< parts: [], }, ) - SyncEvent.run(SessionEvent.Compaction.Ended.Sync, { + EventV2.run(SessionEvent.Compaction.Ended.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(Date.now()), text: summary ?? "", @@ -600,7 +600,7 @@ export const layer: Layer.Layer< auto: input.auto, overflow: input.overflow, }) - SyncEvent.run(SessionEvent.Compaction.Started.Sync, { + EventV2.run(SessionEvent.Compaction.Started.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(Date.now()), reason: input.auto ? "auto" : "manual", diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index ec7c4243e84c..ee9a8af1a0ca 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -20,7 +20,7 @@ import { Question } from "@/question" import { errorMessage } from "@/util/error" import * as Log from "@opencode-ai/core/util/log" import { isRecord } from "@/util/record" -import { SyncEvent } from "@/sync" +import { EventV2 } from "@/v2/event" import { SessionEvent } from "@/v2/session-event" import * as DateTime from "effect/DateTime" @@ -225,7 +225,7 @@ export const layer: Layer.Layer< case "reasoning-start": if (value.id in ctx.reasoningMap) return // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Reasoning.Started.Sync, { + EventV2.run(SessionEvent.Reasoning.Started.Sync, { sessionID: ctx.sessionID, reasoningID: value.id, timestamp: DateTime.makeUnsafe(Date.now()), @@ -245,7 +245,7 @@ export const layer: Layer.Layer< case "reasoning-delta": if (!(value.id in ctx.reasoningMap)) return // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Reasoning.Delta.Sync, { + EventV2.run(SessionEvent.Reasoning.Delta.Sync, { sessionID: ctx.sessionID, reasoningID: value.id, delta: value.text, @@ -265,7 +265,7 @@ export const layer: Layer.Layer< case "reasoning-end": if (!(value.id in ctx.reasoningMap)) return // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Reasoning.Ended.Sync, { + EventV2.run(SessionEvent.Reasoning.Ended.Sync, { sessionID: ctx.sessionID, reasoningID: value.id, text: ctx.reasoningMap[value.id].text, @@ -284,7 +284,7 @@ export const layer: Layer.Layer< throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`) } // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Tool.Input.Started.Sync, { + EventV2.run(SessionEvent.Tool.Input.Started.Sync, { sessionID: ctx.sessionID, callID: value.id, name: value.toolName, @@ -313,7 +313,7 @@ export const layer: Layer.Layer< case "tool-input-end": { // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Tool.Input.Ended.Sync, { + EventV2.run(SessionEvent.Tool.Input.Ended.Sync, { sessionID: ctx.sessionID, callID: value.id, text: "", @@ -328,7 +328,7 @@ export const layer: Layer.Layer< } const toolCall = yield* readToolCall(value.toolCallId) // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Tool.Called.Sync, { + EventV2.run(SessionEvent.Tool.Called.Sync, { sessionID: ctx.sessionID, callID: value.toolCallId, tool: value.toolName, @@ -384,7 +384,7 @@ export const layer: Layer.Layer< case "tool-result": { const toolCall = yield* readToolCall(value.toolCallId) // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Tool.Success.Sync, { + EventV2.run(SessionEvent.Tool.Success.Sync, { sessionID: ctx.sessionID, callID: value.toolCallId, structured: value.output.metadata, @@ -412,7 +412,7 @@ export const layer: Layer.Layer< case "tool-error": { const toolCall = yield* readToolCall(value.toolCallId) // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Tool.Error.Sync, { + EventV2.run(SessionEvent.Tool.Error.Sync, { sessionID: ctx.sessionID, callID: value.toolCallId, error: { @@ -435,7 +435,7 @@ export const layer: Layer.Layer< if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track() if (!ctx.assistantMessage.summary) { // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Step.Started.Sync, { + EventV2.run(SessionEvent.Step.Started.Sync, { sessionID: ctx.sessionID, agent: input.assistantMessage.agent, model: { @@ -465,7 +465,7 @@ export const layer: Layer.Layer< }) if (!ctx.assistantMessage.summary) { // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Step.Ended.Sync, { + EventV2.run(SessionEvent.Step.Ended.Sync, { sessionID: ctx.sessionID, finish: value.finishReason, cost: usage.cost, @@ -520,7 +520,7 @@ export const layer: Layer.Layer< case "text-start": if (!ctx.assistantMessage.summary) { // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Text.Started.Sync, { + EventV2.run(SessionEvent.Text.Started.Sync, { sessionID: ctx.sessionID, timestamp: DateTime.makeUnsafe(Date.now()), }) @@ -540,7 +540,7 @@ export const layer: Layer.Layer< case "text-delta": if (!ctx.currentText) return if (ctx.assistantMessage.summary) { - SyncEvent.run(SessionEvent.Compaction.Delta.Sync, { + EventV2.run(SessionEvent.Compaction.Delta.Sync, { sessionID: ctx.sessionID, text: value.text, timestamp: DateTime.makeUnsafe(Date.now()), @@ -572,7 +572,7 @@ export const layer: Layer.Layer< )).text if (!ctx.assistantMessage.summary) { // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Text.Ended.Sync, { + EventV2.run(SessionEvent.Text.Ended.Sync, { sessionID: ctx.sessionID, text: ctx.currentText.text, timestamp: DateTime.makeUnsafe(Date.now()), @@ -706,7 +706,7 @@ export const layer: Layer.Layer< parse, set: (info) => { // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Retried.Sync, { + EventV2.run(SessionEvent.Retried.Sync, { sessionID: ctx.sessionID, attempt: info.attempt, error: { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 9a4db5436bc2..0590fc38274c 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -54,9 +54,9 @@ import { InstanceState } from "@/effect/instance-state" import { TaskTool, type TaskPromptOps } from "@/tool/task" import { SessionRunState } from "./run-state" import { EffectBridge } from "@/effect/bridge" +import { EventV2 } from "@/v2/event" import { SessionEvent } from "@/v2/session-event" import { AgentAttachment, FileAttachment, Source } from "@/v2/session-prompt" -import { SyncEvent } from "@/sync" import * as DateTime from "effect/DateTime" import { eq } from "@/storage/db" import * as Database from "@/storage/db" @@ -808,7 +808,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, } yield* sessions.updatePart(part) - SyncEvent.run(SessionEvent.Shell.Started.Sync, { + EventV2.run(SessionEvent.Shell.Started.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(started), callID, @@ -829,7 +829,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the output += "\n\n" + ["", "User aborted the command", ""].join("\n") } const completed = Date.now() - SyncEvent.run(SessionEvent.Shell.Ended.Sync, { + EventV2.run(SessionEvent.Shell.Ended.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(completed), callID: part.callID, @@ -964,7 +964,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the .get(), ) if (current?.agent !== info.agent) { - SyncEvent.run(SessionEvent.AgentSwitched.Sync, { + EventV2.run(SessionEvent.AgentSwitched.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(info.time.created), agent: info.agent, @@ -975,7 +975,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the current.model.id !== info.model.modelID || current.model.variant !== info.model.variant ) { - SyncEvent.run(SessionEvent.ModelSwitched.Sync, { + EventV2.run(SessionEvent.ModelSwitched.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(info.time.created), id: info.model.modelID, @@ -1346,7 +1346,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, ) // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Prompted.Sync, { + EventV2.run(SessionEvent.Prompted.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(info.time.created), prompt: { @@ -1357,7 +1357,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the }) for (const text of nextPrompt.synthetic) { // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - SyncEvent.run(SessionEvent.Synthetic.Sync, { + EventV2.run(SessionEvent.Synthetic.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(info.time.created), text, diff --git a/packages/opencode/src/v2/event.ts b/packages/opencode/src/v2/event.ts index af0c87d34479..fde8d4326f4f 100644 --- a/packages/opencode/src/v2/event.ts +++ b/packages/opencode/src/v2/event.ts @@ -1,6 +1,7 @@ import { Identifier } from "@/id/id" import { SyncEvent } from "@/sync" import { withStatics } from "@/util/schema" +import { Flag } from "@opencode-ai/core/flag/flag" import * as Schema from "effect/Schema" export const ID = Schema.String.pipe( @@ -40,4 +41,13 @@ export function define( + def: Def, + data: SyncEvent.Event["data"], + options?: { publish?: boolean }, +) { + if (!Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) return + SyncEvent.run(def, data, options) +} + +export * as EventV2 from "./event" diff --git a/packages/opencode/src/v2/session-event.ts b/packages/opencode/src/v2/session-event.ts index f21de780731a..62814686e61d 100644 --- a/packages/opencode/src/v2/session-event.ts +++ b/packages/opencode/src/v2/session-event.ts @@ -1,6 +1,6 @@ import { SessionID } from "@/session/schema" import { NonNegativeInt } from "@/util/schema" -import { Event } from "./event" +import { EventV2 } from "./event" import { FileAttachment, Prompt } from "./session-prompt" import { Schema } from "effect" export { FileAttachment } @@ -21,7 +21,7 @@ const Base = { sessionID: SessionID, } -export const AgentSwitched = Event.define({ +export const AgentSwitched = EventV2.define({ type: "session.next.agent.switched", aggregate: "sessionID", version: 1, @@ -32,7 +32,7 @@ export const AgentSwitched = Event.define({ }) export type AgentSwitched = Schema.Schema.Type -export const ModelSwitched = Event.define({ +export const ModelSwitched = EventV2.define({ type: "session.next.model.switched", aggregate: "sessionID", version: 1, @@ -45,7 +45,7 @@ export const ModelSwitched = Event.define({ }) export type ModelSwitched = Schema.Schema.Type -export const Prompted = Event.define({ +export const Prompted = EventV2.define({ type: "session.next.prompted", aggregate: "sessionID", version: 1, @@ -56,7 +56,7 @@ export const Prompted = Event.define({ }) export type Prompted = Schema.Schema.Type -export const Synthetic = Event.define({ +export const Synthetic = EventV2.define({ type: "session.next.synthetic", aggregate: "sessionID", schema: { @@ -67,7 +67,7 @@ export const Synthetic = Event.define({ export type Synthetic = Schema.Schema.Type export namespace Shell { - export const Started = Event.define({ + export const Started = EventV2.define({ type: "session.next.shell.started", aggregate: "sessionID", schema: { @@ -78,7 +78,7 @@ export namespace Shell { }) export type Started = Schema.Schema.Type - export const Ended = Event.define({ + export const Ended = EventV2.define({ type: "session.next.shell.ended", aggregate: "sessionID", schema: { @@ -91,7 +91,7 @@ export namespace Shell { } export namespace Step { - export const Started = Event.define({ + export const Started = EventV2.define({ type: "session.next.step.started", aggregate: "sessionID", schema: { @@ -107,7 +107,7 @@ export namespace Step { }) export type Started = Schema.Schema.Type - export const Ended = Event.define({ + export const Ended = EventV2.define({ type: "session.next.step.ended", aggregate: "sessionID", schema: { @@ -130,7 +130,7 @@ export namespace Step { } export namespace Text { - export const Started = Event.define({ + export const Started = EventV2.define({ type: "session.next.text.started", aggregate: "sessionID", schema: { @@ -139,7 +139,7 @@ export namespace Text { }) export type Started = Schema.Schema.Type - export const Delta = Event.define({ + export const Delta = EventV2.define({ type: "session.next.text.delta", aggregate: "sessionID", schema: { @@ -149,7 +149,7 @@ export namespace Text { }) export type Delta = Schema.Schema.Type - export const Ended = Event.define({ + export const Ended = EventV2.define({ type: "session.next.text.ended", aggregate: "sessionID", schema: { @@ -161,7 +161,7 @@ export namespace Text { } export namespace Reasoning { - export const Started = Event.define({ + export const Started = EventV2.define({ type: "session.next.reasoning.started", aggregate: "sessionID", schema: { @@ -171,7 +171,7 @@ export namespace Reasoning { }) export type Started = Schema.Schema.Type - export const Delta = Event.define({ + export const Delta = EventV2.define({ type: "session.next.reasoning.delta", aggregate: "sessionID", schema: { @@ -182,7 +182,7 @@ export namespace Reasoning { }) export type Delta = Schema.Schema.Type - export const Ended = Event.define({ + export const Ended = EventV2.define({ type: "session.next.reasoning.ended", aggregate: "sessionID", schema: { @@ -196,7 +196,7 @@ export namespace Reasoning { export namespace Tool { export namespace Input { - export const Started = Event.define({ + export const Started = EventV2.define({ type: "session.next.tool.input.started", aggregate: "sessionID", schema: { @@ -207,7 +207,7 @@ export namespace Tool { }) export type Started = Schema.Schema.Type - export const Delta = Event.define({ + export const Delta = EventV2.define({ type: "session.next.tool.input.delta", aggregate: "sessionID", schema: { @@ -218,7 +218,7 @@ export namespace Tool { }) export type Delta = Schema.Schema.Type - export const Ended = Event.define({ + export const Ended = EventV2.define({ type: "session.next.tool.input.ended", aggregate: "sessionID", schema: { @@ -230,7 +230,7 @@ export namespace Tool { export type Ended = Schema.Schema.Type } - export const Called = Event.define({ + export const Called = EventV2.define({ type: "session.next.tool.called", aggregate: "sessionID", schema: { @@ -246,7 +246,7 @@ export namespace Tool { }) export type Called = Schema.Schema.Type - export const Progress = Event.define({ + export const Progress = EventV2.define({ type: "session.next.tool.progress", aggregate: "sessionID", schema: { @@ -258,7 +258,7 @@ export namespace Tool { }) export type Progress = Schema.Schema.Type - export const Success = Event.define({ + export const Success = EventV2.define({ type: "session.next.tool.success", aggregate: "sessionID", schema: { @@ -274,7 +274,7 @@ export namespace Tool { }) export type Success = Schema.Schema.Type - export const Error = Event.define({ + export const Error = EventV2.define({ type: "session.next.tool.error", aggregate: "sessionID", schema: { @@ -305,7 +305,7 @@ export const RetryError = Schema.Struct({ }) export type RetryError = Schema.Schema.Type -export const Retried = Event.define({ +export const Retried = EventV2.define({ type: "session.next.retried", aggregate: "sessionID", schema: { @@ -317,7 +317,7 @@ export const Retried = Event.define({ export type Retried = Schema.Schema.Type export namespace Compaction { - export const Started = Event.define({ + export const Started = EventV2.define({ type: "session.next.compaction.started", aggregate: "sessionID", schema: { @@ -327,7 +327,7 @@ export namespace Compaction { }) export type Started = Schema.Schema.Type - export const Delta = Event.define({ + export const Delta = EventV2.define({ type: "session.next.compaction.delta", aggregate: "sessionID", schema: { @@ -336,7 +336,7 @@ export namespace Compaction { }, }) - export const Ended = Event.define({ + export const Ended = EventV2.define({ type: "session.next.compaction.ended", aggregate: "sessionID", schema: { diff --git a/packages/opencode/src/v2/session-message.ts b/packages/opencode/src/v2/session-message.ts index e4766d8ef707..d18edfce6150 100644 --- a/packages/opencode/src/v2/session-message.ts +++ b/packages/opencode/src/v2/session-message.ts @@ -1,10 +1,10 @@ import { Schema } from "effect" import { Prompt } from "./session-prompt" import { SessionEvent } from "./session-event" -import { Event } from "./event" +import { EventV2 } from "./event" import { ToolOutput } from "./tool-output" -export const ID = Event.ID +export const ID = EventV2.ID export type ID = Schema.Schema.Type const Base = { diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index aa81be90ba46..a8f93bee554d 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -6,11 +6,10 @@ import * as Database from "@/storage/db" import { Context, DateTime, Effect, Layer, Schema } from "effect" import { SessionMessage } from "./session-message" import type { Prompt } from "./session-prompt" -import type { Event } from "./event" +import { EventV2 } from "./event" import { ProjectID } from "@/project/schema" import { ModelID, ProviderID } from "@/provider/schema" import { SessionEvent } from "./session-event" -import { SyncEvent } from "@/sync" export const Delivery = Schema.Union([Schema.Literal("immediate"), Schema.Literal("deferred")]).annotate({ identifier: "Session.Delivery", @@ -79,7 +78,7 @@ export interface Interface { } }) => Effect.Effect readonly prompt: (input: { - id?: Event.ID + id?: EventV2.ID sessionID: SessionID prompt: Prompt delivery?: Delivery @@ -214,14 +213,14 @@ export const layer = Layer.effect( return {} as any }), switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) { - SyncEvent.run(SessionEvent.AgentSwitched.Sync, { + EventV2.run(SessionEvent.AgentSwitched.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(Date.now()), agent: input.agent, }) }), switchModel: Effect.fn("V2Session.switchModel")(function* (input) { - SyncEvent.run(SessionEvent.ModelSwitched.Sync, { + EventV2.run(SessionEvent.ModelSwitched.Sync, { sessionID: input.sessionID, timestamp: DateTime.makeUnsafe(Date.now()), id: input.id, diff --git a/packages/opencode/test/v2/session-message-updater.test.ts b/packages/opencode/test/v2/session-message-updater.test.ts index abdb3469a61d..128177167cbb 100644 --- a/packages/opencode/test/v2/session-message-updater.test.ts +++ b/packages/opencode/test/v2/session-message-updater.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "bun:test" import * as DateTime from "effect/DateTime" import { SessionID } from "../../src/session/schema" -import { Event } from "../../src/v2/event" +import { EventV2 } from "../../src/v2/event" import { SessionEvent } from "../../src/v2/session-event" import { SessionMessageUpdater } from "../../src/v2/session-message-updater" @@ -10,7 +10,7 @@ test("step snapshots carry over to assistant messages", () => { const sessionID = SessionID.make("session") SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.step.started", data: { sessionID, @@ -22,7 +22,7 @@ test("step snapshots carry over to assistant messages", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.step.ended", data: { sessionID, @@ -50,7 +50,7 @@ test("text ended populates assistant text content", () => { const sessionID = SessionID.make("session") SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.step.started", data: { sessionID, @@ -61,7 +61,7 @@ test("text ended populates assistant text content", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.text.started", data: { sessionID, @@ -70,7 +70,7 @@ test("text ended populates assistant text content", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.text.ended", data: { sessionID, @@ -90,7 +90,7 @@ test("tool completion stores completed timestamp", () => { const callID = "call" SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.step.started", data: { sessionID, @@ -101,7 +101,7 @@ test("tool completion stores completed timestamp", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.tool.input.started", data: { sessionID, @@ -112,7 +112,7 @@ test("tool completion stores completed timestamp", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.tool.called", data: { sessionID, @@ -125,7 +125,7 @@ test("tool completion stores completed timestamp", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.tool.success", data: { sessionID, @@ -148,7 +148,7 @@ test("tool completion stores completed timestamp", () => { test("compaction events reduce to compaction message", () => { const state: SessionMessageUpdater.MemoryState = { messages: [] } const sessionID = SessionID.make("session") - const id = Event.ID.create() + const id = EventV2.ID.create() SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id, @@ -161,7 +161,7 @@ test("compaction events reduce to compaction message", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.compaction.delta", data: { sessionID, @@ -171,7 +171,7 @@ test("compaction events reduce to compaction message", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.compaction.delta", data: { sessionID, @@ -181,7 +181,7 @@ test("compaction events reduce to compaction message", () => { } satisfies SessionEvent.Event) SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { - id: Event.ID.create(), + id: EventV2.ID.create(), type: "session.next.compaction.ended", data: { sessionID, From 34fbaa4666532f555845ecf82c30d0fc61afe975 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 14:00:24 -0400 Subject: [PATCH 12/24] sync --- packages/sdk/js/src/v2/gen/types.gen.ts | 735 ++++++++++++++++++++++++ 1 file changed, 735 insertions(+) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index eb66ee507000..b1f5c7b8e139 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -36,6 +36,7 @@ export type EventProjectUpdated = { properties: Project } <<<<<<< HEAD +<<<<<<< HEAD >>>>>>> cbb60d3a5 (core: add unique IDs to all events for reliable tracking and debugging) export type EventServerInstanceDisposed = { @@ -45,6 +46,35 @@ export type EventServerInstanceDisposed = { } } +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +export type EventServerConnected = { +======= + +export type EventServerConnected = { + id: string +>>>>>>> 1a2dc3e57 (sync) + type: "server.connected" + properties: { + [key: string]: unknown + } +} + +export type EventGlobalDisposed = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) + type: "global.disposed" + properties: { + [key: string]: unknown + } +} + +<<<<<<< HEAD +>>>>>>> 9a1d0085d (sync) export type EventFileEdited = { type: "file.edited" properties: { @@ -57,10 +87,28 @@ export type EventFileWatcherUpdated = { properties: { file: string event: "add" | "change" | "unlink" +<<<<<<< HEAD + } +} + +======= +======= +export type EventServerInstanceDisposed = { + id: string + type: "server.instance.disposed" + properties: { + directory: string +>>>>>>> 1a2dc3e57 (sync) } } +>>>>>>> d0ca805a7 (sync) +>>>>>>> 9a1d0085d (sync) export type EventLspClientDiagnostics = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "lsp.client.diagnostics" properties: { serverID: string @@ -69,6 +117,10 @@ export type EventLspClientDiagnostics = { } export type EventLspUpdated = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "lsp.updated" properties: { [key: string]: unknown @@ -76,6 +128,10 @@ export type EventLspUpdated = { } export type EventMessagePartDelta = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "message.part.delta" properties: { sessionID: string @@ -102,11 +158,19 @@ export type PermissionRequest = { } export type EventPermissionAsked = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "permission.asked" properties: PermissionRequest } export type EventPermissionReplied = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "permission.replied" properties: { sessionID: string @@ -124,6 +188,10 @@ export type SnapshotFileDiff = { } export type EventSessionDiff = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.diff" properties: { sessionID: string @@ -193,6 +261,10 @@ export type ApiError = { } export type EventSessionError = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.error" properties: { sessionID?: string @@ -208,6 +280,10 @@ export type EventSessionError = { } export type EventInstallationUpdated = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "installation.updated" properties: { version: string @@ -215,12 +291,97 @@ export type EventInstallationUpdated = { } export type EventInstallationUpdateAvailable = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "installation.update-available" properties: { version: string } } +<<<<<<< HEAD +======= +<<<<<<< HEAD +export type EventWorkspaceReady = { +======= +<<<<<<< HEAD +======= +export type EventWorkspaceReady = { + id: string +>>>>>>> d0ca805a7 (sync) + type: "workspace.ready" + properties: { + name: string + } +} + +export type EventWorkspaceFailed = { +<<<<<<< HEAD +======= + id: string +>>>>>>> d0ca805a7 (sync) + type: "workspace.failed" + properties: { + message: string + } +} + +export type EventWorkspaceRestore = { +<<<<<<< HEAD +======= + id: string +>>>>>>> d0ca805a7 (sync) + type: "workspace.restore" + properties: { + workspaceID: string + sessionID: string + total: number + step: number + } +} + +export type EventWorkspaceStatus = { +<<<<<<< HEAD +======= + id: string +>>>>>>> d0ca805a7 (sync) + type: "workspace.status" + properties: { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + } +} + +export type EventFileEdited = { +<<<<<<< HEAD +======= + id: string +>>>>>>> d0ca805a7 (sync) + type: "file.edited" + properties: { + file: string + } +} + +export type EventFileWatcherUpdated = { +<<<<<<< HEAD +======= + id: string +>>>>>>> d0ca805a7 (sync) + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + +<<<<<<< HEAD +======= +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) +>>>>>>> 9a1d0085d (sync) export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -271,6 +432,10 @@ export type QuestionRequest = { } export type EventQuestionAsked = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "question.asked" properties: QuestionRequest } @@ -284,6 +449,10 @@ export type QuestionReplied = { } export type EventQuestionReplied = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "question.replied" properties: QuestionReplied } @@ -294,6 +463,10 @@ export type QuestionRejected = { } export type EventQuestionRejected = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "question.rejected" properties: QuestionRejected } @@ -314,6 +487,10 @@ export type Todo = { } export type EventTodoUpdated = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "todo.updated" properties: { sessionID: string @@ -336,6 +513,10 @@ export type SessionStatus = } export type EventSessionStatus = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.status" properties: { sessionID: string @@ -344,6 +525,10 @@ export type EventSessionStatus = { } export type EventSessionIdle = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.idle" properties: { sessionID: string @@ -351,6 +536,10 @@ export type EventSessionIdle = { } export type EventSessionCompacted = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.compacted" properties: { sessionID: string @@ -412,6 +601,10 @@ export type EventTuiSessionSelect = { } export type EventMcpToolsChanged = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "mcp.tools.changed" properties: { server: string @@ -419,6 +612,10 @@ export type EventMcpToolsChanged = { } export type EventMcpBrowserOpenFailed = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "mcp.browser.open.failed" properties: { mcpName: string @@ -427,6 +624,10 @@ export type EventMcpBrowserOpenFailed = { } export type EventCommandExecuted = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "command.executed" properties: { name: string @@ -466,12 +667,22 @@ export type EventProjectUpdated = { } export type EventVcsBranchUpdated = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "vcs.branch.updated" properties: { branch?: string } } +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +<<<<<<< HEAD +>>>>>>> 9a1d0085d (sync) export type EventWorkspaceReady = { type: "workspace.ready" properties: { @@ -504,7 +715,15 @@ export type EventWorkspaceStatus = { } } +<<<<<<< HEAD +======= +>>>>>>> d0ca805a7 (sync) +>>>>>>> 9a1d0085d (sync) export type EventWorktreeReady = { +======= +export type EventWorktreeReady = { + id: string +>>>>>>> 1a2dc3e57 (sync) type: "worktree.ready" properties: { name: string @@ -513,6 +732,10 @@ export type EventWorktreeReady = { } export type EventWorktreeFailed = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "worktree.failed" properties: { message: string @@ -530,6 +753,10 @@ export type Pty = { } export type EventPtyCreated = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "pty.created" properties: { info: Pty @@ -537,6 +764,10 @@ export type EventPtyCreated = { } export type EventPtyUpdated = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "pty.updated" properties: { info: Pty @@ -544,6 +775,10 @@ export type EventPtyUpdated = { } export type EventPtyExited = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "pty.exited" properties: { id: string @@ -552,6 +787,10 @@ export type EventPtyExited = { } export type EventPtyDeleted = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "pty.deleted" properties: { id: string @@ -644,6 +883,10 @@ export type AssistantMessage = { export type Message = UserMessage | AssistantMessage export type EventMessageUpdated = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "message.updated" properties: { sessionID: string @@ -652,6 +895,10 @@ export type EventMessageUpdated = { } export type EventMessageRemoved = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "message.removed" properties: { sessionID: string @@ -923,6 +1170,10 @@ export type Part = | CompactionPart export type EventMessagePartUpdated = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "message.part.updated" properties: { sessionID: string @@ -932,6 +1183,10 @@ export type EventMessagePartUpdated = { } export type EventMessagePartRemoved = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "message.part.removed" properties: { sessionID: string @@ -968,6 +1223,15 @@ export type Session = { url: string } title: string +<<<<<<< HEAD +======= + agent?: string + model?: { + id: string + providerID: string + variant?: string + } +>>>>>>> 1a2dc3e57 (sync) version: string time: { created: number @@ -985,6 +1249,10 @@ export type Session = { } export type EventSessionCreated = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.created" properties: { sessionID: string @@ -993,6 +1261,10 @@ export type EventSessionCreated = { } export type EventSessionUpdated = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.updated" properties: { sessionID: string @@ -1001,6 +1273,10 @@ export type EventSessionUpdated = { } export type EventSessionDeleted = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.deleted" properties: { sessionID: string @@ -1008,6 +1284,7 @@ export type EventSessionDeleted = { } } +<<<<<<< HEAD <<<<<<< HEAD export type EventServerConnected = { type: "server.connected" @@ -1021,6 +1298,32 @@ export type EventGlobalDisposed = { properties: { [key: string]: unknown ======= +======= +======= +export type EventSessionNextAgentSwitched = { + id: string + type: "session.next.agent.switched" + properties: { + timestamp: number + sessionID: string + agent: string + } +} + +export type EventSessionNextModelSwitched = { + id: string + type: "session.next.model.switched" + properties: { + timestamp: number + sessionID: string + id: string + providerID: string + variant?: string + } +} + +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) export type PromptSource = { start: number end: number @@ -1047,31 +1350,75 @@ export type Prompt = { } export type EventSessionNextPrompted = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.prompted" properties: { timestamp: number sessionID: string +<<<<<<< HEAD id: string +======= +>>>>>>> 1a2dc3e57 (sync) prompt: Prompt } } export type EventSessionNextSynthetic = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.synthetic" properties: { timestamp: number sessionID: string +<<<<<<< HEAD id: string +======= +>>>>>>> 1a2dc3e57 (sync) text: string } } +<<<<<<< HEAD +export type EventSessionNextStepStarted = { +======= +export type EventSessionNextShellStarted = { + id: string + type: "session.next.shell.started" + properties: { + timestamp: number + sessionID: string + callID: string + command: string + } +} + +export type EventSessionNextShellEnded = { + id: string + type: "session.next.shell.ended" + properties: { + timestamp: number + sessionID: string + callID: string + output: string + } +} + export type EventSessionNextStepStarted = { + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.step.started" properties: { timestamp: number sessionID: string +<<<<<<< HEAD id: string +======= +>>>>>>> 1a2dc3e57 (sync) agent: string model: { id: string @@ -1083,6 +1430,10 @@ export type EventSessionNextStepStarted = { } export type EventSessionNextStepEnded = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.step.ended" properties: { timestamp: number @@ -1103,6 +1454,10 @@ export type EventSessionNextStepEnded = { } export type EventSessionNextTextStarted = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.text.started" properties: { timestamp: number @@ -1111,6 +1466,10 @@ export type EventSessionNextTextStarted = { } export type EventSessionNextTextDelta = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.text.delta" properties: { timestamp: number @@ -1120,6 +1479,10 @@ export type EventSessionNextTextDelta = { } export type EventSessionNextTextEnded = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.text.ended" properties: { timestamp: number @@ -1129,6 +1492,10 @@ export type EventSessionNextTextEnded = { } export type EventSessionNextReasoningStarted = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.reasoning.started" properties: { timestamp: number @@ -1138,6 +1505,10 @@ export type EventSessionNextReasoningStarted = { } export type EventSessionNextReasoningDelta = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.reasoning.delta" properties: { timestamp: number @@ -1148,6 +1519,10 @@ export type EventSessionNextReasoningDelta = { } export type EventSessionNextReasoningEnded = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.reasoning.ended" properties: { timestamp: number @@ -1158,6 +1533,10 @@ export type EventSessionNextReasoningEnded = { } export type EventSessionNextToolInputStarted = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.input.started" properties: { timestamp: number @@ -1168,6 +1547,10 @@ export type EventSessionNextToolInputStarted = { } export type EventSessionNextToolInputDelta = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.input.delta" properties: { timestamp: number @@ -1178,6 +1561,10 @@ export type EventSessionNextToolInputDelta = { } export type EventSessionNextToolInputEnded = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.input.ended" properties: { timestamp: number @@ -1188,6 +1575,10 @@ export type EventSessionNextToolInputEnded = { } export type EventSessionNextToolCalled = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.called" properties: { timestamp: number @@ -1219,6 +1610,10 @@ export type ToolFileContent = { } export type EventSessionNextToolProgress = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.progress" properties: { timestamp: number @@ -1232,6 +1627,10 @@ export type EventSessionNextToolProgress = { } export type EventSessionNextToolSuccess = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.success" properties: { timestamp: number @@ -1251,6 +1650,10 @@ export type EventSessionNextToolSuccess = { } export type EventSessionNextToolError = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.error" properties: { timestamp: number @@ -1283,6 +1686,10 @@ export type SessionNextRetryError = { } export type EventSessionNextRetried = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.retried" properties: { timestamp: number @@ -1293,16 +1700,27 @@ export type EventSessionNextRetried = { } export type EventSessionNextCompactionStarted = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.compaction.started" properties: { timestamp: number sessionID: string +<<<<<<< HEAD id: string +======= +>>>>>>> 1a2dc3e57 (sync) reason: "auto" | "manual" } } export type EventSessionNextCompactionDelta = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.compaction.delta" properties: { timestamp: number @@ -1312,6 +1730,10 @@ export type EventSessionNextCompactionDelta = { } export type EventSessionNextCompactionEnded = { +<<<<<<< HEAD +======= + id: string +>>>>>>> 1a2dc3e57 (sync) type: "session.next.compaction.ended" properties: { timestamp: number @@ -1410,6 +1832,15 @@ export type SyncEventSessionUpdated = { url?: string | null } title?: string | null +<<<<<<< HEAD +======= + agent?: string | null + model?: { + id: string + providerID: string + variant?: string + } | null +>>>>>>> 1a2dc3e57 (sync) version?: string | null time?: { created?: number | null @@ -1440,6 +1871,37 @@ export type SyncEventSessionDeleted = { } } +<<<<<<< HEAD +======= +export type SyncEventSessionNextAgentSwitched = { + type: "sync" + name: "session.next.agent.switched.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + agent: string + } +} + +export type SyncEventSessionNextModelSwitched = { + type: "sync" + name: "session.next.model.switched.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + id: string + providerID: string + variant?: string + } +} + +>>>>>>> 1a2dc3e57 (sync) export type SyncEventSessionNextPrompted = { type: "sync" name: "session.next.prompted.1" @@ -1449,7 +1911,10 @@ export type SyncEventSessionNextPrompted = { data: { timestamp: number sessionID: string +<<<<<<< HEAD id: string +======= +>>>>>>> 1a2dc3e57 (sync) prompt: Prompt } } @@ -1463,11 +1928,45 @@ export type SyncEventSessionNextSynthetic = { data: { timestamp: number sessionID: string +<<<<<<< HEAD id: string +======= +>>>>>>> 1a2dc3e57 (sync) text: string } } +<<<<<<< HEAD +======= +export type SyncEventSessionNextShellStarted = { + type: "sync" + name: "session.next.shell.started.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + callID: string + command: string + } +} + +export type SyncEventSessionNextShellEnded = { + type: "sync" + name: "session.next.shell.ended.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + callID: string + output: string + } +} + +>>>>>>> 1a2dc3e57 (sync) export type SyncEventSessionNextStepStarted = { type: "sync" name: "session.next.step.started.1" @@ -1477,7 +1976,10 @@ export type SyncEventSessionNextStepStarted = { data: { timestamp: number sessionID: string +<<<<<<< HEAD id: string +======= +>>>>>>> 1a2dc3e57 (sync) agent: string model: { id: string @@ -1742,7 +2244,10 @@ export type SyncEventSessionNextCompactionStarted = { data: { timestamp: number sessionID: string +<<<<<<< HEAD id: string +======= +>>>>>>> 1a2dc3e57 (sync) reason: "auto" | "manual" } } @@ -1779,9 +2284,27 @@ export type GlobalEvent = { project?: string workspace?: string payload: +<<<<<<< HEAD + | EventServerInstanceDisposed + | EventFileEdited + | EventFileWatcherUpdated +======= + | EventProjectUpdated +<<<<<<< HEAD | EventServerInstanceDisposed +<<<<<<< HEAD +======= + | EventServerConnected + | EventGlobalDisposed | EventFileEdited | EventFileWatcherUpdated +======= + | EventServerConnected + | EventGlobalDisposed + | EventServerInstanceDisposed +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) +>>>>>>> 9a1d0085d (sync) | EventLspClientDiagnostics | EventLspUpdated | EventMessagePartDelta @@ -1791,6 +2314,24 @@ export type GlobalEvent = { | EventSessionError | EventInstallationUpdated | EventInstallationUpdateAvailable +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +>>>>>>> d0ca805a7 (sync) + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceRestore + | EventWorkspaceStatus + | EventFileEdited + | EventFileWatcherUpdated +<<<<<<< HEAD +======= +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) +>>>>>>> 9a1d0085d (sync) | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected @@ -1807,10 +2348,22 @@ export type GlobalEvent = { | EventCommandExecuted | EventProjectUpdated | EventVcsBranchUpdated +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +<<<<<<< HEAD +>>>>>>> 9a1d0085d (sync) | EventWorkspaceReady | EventWorkspaceFailed | EventWorkspaceRestore | EventWorkspaceStatus +<<<<<<< HEAD +======= +======= +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) +>>>>>>> 9a1d0085d (sync) | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated @@ -1824,12 +2377,23 @@ export type GlobalEvent = { | EventSessionCreated | EventSessionUpdated | EventSessionDeleted +<<<<<<< HEAD <<<<<<< HEAD | EventServerConnected | EventGlobalDisposed ======= +======= +>>>>>>> d0ca805a7 (sync) + | EventSessionNextPrompted + | EventSessionNextSynthetic +======= + | EventSessionNextAgentSwitched + | EventSessionNextModelSwitched | EventSessionNextPrompted | EventSessionNextSynthetic + | EventSessionNextShellStarted + | EventSessionNextShellEnded +>>>>>>> 1a2dc3e57 (sync) | EventSessionNextStepStarted | EventSessionNextStepEnded | EventSessionNextTextStarted @@ -1857,8 +2421,17 @@ export type GlobalEvent = { | SyncEventSessionCreated | SyncEventSessionUpdated | SyncEventSessionDeleted +<<<<<<< HEAD + | SyncEventSessionNextPrompted + | SyncEventSessionNextSynthetic +======= + | SyncEventSessionNextAgentSwitched + | SyncEventSessionNextModelSwitched | SyncEventSessionNextPrompted | SyncEventSessionNextSynthetic + | SyncEventSessionNextShellStarted + | SyncEventSessionNextShellEnded +>>>>>>> 1a2dc3e57 (sync) | SyncEventSessionNextStepStarted | SyncEventSessionNextStepEnded | SyncEventSessionNextTextStarted @@ -2614,6 +3187,15 @@ export type GlobalSession = { url: string } title: string +<<<<<<< HEAD +======= + agent?: string + model?: { + id: string + providerID: string + variant?: string + } +>>>>>>> 1a2dc3e57 (sync) version: string time: { created: number @@ -2726,14 +3308,70 @@ export type ProviderAuthAuthorization = { instructions: string } +<<<<<<< HEAD export type V2SessionsResponse = { items: Array +======= +export type SessionInfo = { + id: string + parentID?: string + projectID: string + workspaceID?: string + path?: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + time: { + created: number + updated: number + archived?: number + } + title: string +} + +export type V2SessionsResponse = { + items: Array +>>>>>>> 1a2dc3e57 (sync) cursor: { previous?: string next?: string } } +<<<<<<< HEAD +======= +export type SessionMessageAgentSwitched = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + type: "agent-switched" + agent: string +} + +export type SessionMessageModelSwitched = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + type: "model-switched" + model: { + id: string + providerID: string + variant?: string + } +} + +>>>>>>> 1a2dc3e57 (sync) export type SessionMessageUser = { id: string metadata?: { @@ -2761,6 +3399,24 @@ export type SessionMessageSynthetic = { type: "synthetic" } +<<<<<<< HEAD +======= +export type SessionMessageShell = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + completed?: number + } + type: "shell" + callID: string + command: string + output: string +} + +>>>>>>> 1a2dc3e57 (sync) export type SessionMessageAssistantText = { type: "text" text: string @@ -2888,8 +3544,16 @@ export type SessionMessageCompaction = { } export type SessionMessage = +<<<<<<< HEAD + | SessionMessageUser + | SessionMessageSynthetic +======= + | SessionMessageAgentSwitched + | SessionMessageModelSwitched | SessionMessageUser | SessionMessageSynthetic + | SessionMessageShell +>>>>>>> 1a2dc3e57 (sync) | SessionMessageAssistant | SessionMessageCompaction @@ -2948,9 +3612,27 @@ export type File = { } export type Event = +<<<<<<< HEAD + | EventServerInstanceDisposed + | EventFileEdited + | EventFileWatcherUpdated +======= + | EventProjectUpdated +<<<<<<< HEAD | EventServerInstanceDisposed +<<<<<<< HEAD +======= + | EventServerConnected + | EventGlobalDisposed | EventFileEdited | EventFileWatcherUpdated +======= + | EventServerConnected + | EventGlobalDisposed + | EventServerInstanceDisposed +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) +>>>>>>> 9a1d0085d (sync) | EventLspClientDiagnostics | EventLspUpdated | EventMessagePartDelta @@ -2960,6 +3642,24 @@ export type Event = | EventSessionError | EventInstallationUpdated | EventInstallationUpdateAvailable +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +>>>>>>> d0ca805a7 (sync) + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceRestore + | EventWorkspaceStatus + | EventFileEdited + | EventFileWatcherUpdated +<<<<<<< HEAD +======= +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) +>>>>>>> 9a1d0085d (sync) | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected @@ -2976,10 +3676,22 @@ export type Event = | EventCommandExecuted | EventProjectUpdated | EventVcsBranchUpdated +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +<<<<<<< HEAD +>>>>>>> 9a1d0085d (sync) | EventWorkspaceReady | EventWorkspaceFailed | EventWorkspaceRestore | EventWorkspaceStatus +<<<<<<< HEAD +======= +======= +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) +>>>>>>> 9a1d0085d (sync) | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated @@ -2993,12 +3705,23 @@ export type Event = | EventSessionCreated | EventSessionUpdated | EventSessionDeleted +<<<<<<< HEAD <<<<<<< HEAD | EventServerConnected | EventGlobalDisposed ======= +======= +>>>>>>> d0ca805a7 (sync) + | EventSessionNextPrompted + | EventSessionNextSynthetic +======= + | EventSessionNextAgentSwitched + | EventSessionNextModelSwitched | EventSessionNextPrompted | EventSessionNextSynthetic + | EventSessionNextShellStarted + | EventSessionNextShellEnded +>>>>>>> 1a2dc3e57 (sync) | EventSessionNextStepStarted | EventSessionNextStepEnded | EventSessionNextTextStarted @@ -4247,6 +4970,15 @@ export type SessionCreateData = { body?: { parentID?: string title?: string +<<<<<<< HEAD +======= + agent?: string + model?: { + id: string + providerID: string + variant?: string + } +>>>>>>> 1a2dc3e57 (sync) permission?: PermissionRuleset workspaceID?: string } @@ -6534,5 +7266,8 @@ export type FormatterStatusResponses = { } export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] +<<<<<<< HEAD ======= >>>>>>> 13afcef87 (core: add unique IDs to all events for reliable tracking and debugging) +======= +>>>>>>> 1a2dc3e57 (sync) From abf11a9bdf3ad9905ff6796e6bf5d6e926b0511b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 16:14:06 -0400 Subject: [PATCH 13/24] fix --- packages/sdk/js/src/v2/gen/types.gen.ts | 340 ++++++------------------ 1 file changed, 88 insertions(+), 252 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index b1f5c7b8e139..b5b14c7d14bf 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -35,11 +35,10 @@ export type EventProjectUpdated = { type: "project.updated" properties: Project } -<<<<<<< HEAD -<<<<<<< HEAD >>>>>>> cbb60d3a5 (core: add unique IDs to all events for reliable tracking and debugging) export type EventServerInstanceDisposed = { + id: string type: "server.instance.disposed" properties: { directory: string @@ -50,12 +49,8 @@ export type EventServerInstanceDisposed = { ======= <<<<<<< HEAD ======= -export type EventServerConnected = { -======= - export type EventServerConnected = { id: string ->>>>>>> 1a2dc3e57 (sync) type: "server.connected" properties: { [key: string]: unknown @@ -63,19 +58,20 @@ export type EventServerConnected = { } export type EventGlobalDisposed = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "global.disposed" properties: { [key: string]: unknown } } +<<<<<<< HEAD <<<<<<< HEAD >>>>>>> 9a1d0085d (sync) +======= +>>>>>>> 7b56f08df (fix) export type EventFileEdited = { + id: string type: "file.edited" properties: { file: string @@ -83,10 +79,12 @@ export type EventFileEdited = { } export type EventFileWatcherUpdated = { + id: string type: "file.watcher.updated" properties: { file: string event: "add" | "change" | "unlink" +<<<<<<< HEAD <<<<<<< HEAD } } @@ -99,16 +97,15 @@ export type EventServerInstanceDisposed = { properties: { directory: string >>>>>>> 1a2dc3e57 (sync) +======= +>>>>>>> 7b56f08df (fix) } } >>>>>>> d0ca805a7 (sync) >>>>>>> 9a1d0085d (sync) export type EventLspClientDiagnostics = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "lsp.client.diagnostics" properties: { serverID: string @@ -117,10 +114,7 @@ export type EventLspClientDiagnostics = { } export type EventLspUpdated = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "lsp.updated" properties: { [key: string]: unknown @@ -128,10 +122,7 @@ export type EventLspUpdated = { } export type EventMessagePartDelta = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "message.part.delta" properties: { sessionID: string @@ -158,19 +149,13 @@ export type PermissionRequest = { } export type EventPermissionAsked = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "permission.asked" properties: PermissionRequest } export type EventPermissionReplied = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "permission.replied" properties: { sessionID: string @@ -188,10 +173,7 @@ export type SnapshotFileDiff = { } export type EventSessionDiff = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.diff" properties: { sessionID: string @@ -261,10 +243,7 @@ export type ApiError = { } export type EventSessionError = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.error" properties: { sessionID?: string @@ -280,10 +259,7 @@ export type EventSessionError = { } export type EventInstallationUpdated = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "installation.updated" properties: { version: string @@ -291,18 +267,18 @@ export type EventInstallationUpdated = { } export type EventInstallationUpdateAvailable = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "installation.update-available" properties: { version: string } } +<<<<<<< HEAD <<<<<<< HEAD ======= +======= +>>>>>>> 7b56f08df (fix) <<<<<<< HEAD export type EventWorkspaceReady = { ======= @@ -381,7 +357,12 @@ export type EventFileWatcherUpdated = { ======= >>>>>>> 1a2dc3e57 (sync) >>>>>>> d0ca805a7 (sync) +<<<<<<< HEAD >>>>>>> 9a1d0085d (sync) +======= +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -432,10 +413,7 @@ export type QuestionRequest = { } export type EventQuestionAsked = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "question.asked" properties: QuestionRequest } @@ -449,10 +427,7 @@ export type QuestionReplied = { } export type EventQuestionReplied = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "question.replied" properties: QuestionReplied } @@ -463,10 +438,7 @@ export type QuestionRejected = { } export type EventQuestionRejected = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "question.rejected" properties: QuestionRejected } @@ -487,10 +459,7 @@ export type Todo = { } export type EventTodoUpdated = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "todo.updated" properties: { sessionID: string @@ -513,10 +482,7 @@ export type SessionStatus = } export type EventSessionStatus = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.status" properties: { sessionID: string @@ -525,10 +491,7 @@ export type EventSessionStatus = { } export type EventSessionIdle = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.idle" properties: { sessionID: string @@ -536,10 +499,7 @@ export type EventSessionIdle = { } export type EventSessionCompacted = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.compacted" properties: { sessionID: string @@ -601,10 +561,7 @@ export type EventTuiSessionSelect = { } export type EventMcpToolsChanged = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "mcp.tools.changed" properties: { server: string @@ -612,10 +569,7 @@ export type EventMcpToolsChanged = { } export type EventMcpBrowserOpenFailed = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "mcp.browser.open.failed" properties: { mcpName: string @@ -624,10 +578,7 @@ export type EventMcpBrowserOpenFailed = { } export type EventCommandExecuted = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "command.executed" properties: { name: string @@ -667,23 +618,26 @@ export type EventProjectUpdated = { } export type EventVcsBranchUpdated = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "vcs.branch.updated" properties: { branch?: string } } +<<<<<<< HEAD <<<<<<< HEAD ======= <<<<<<< HEAD ======= <<<<<<< HEAD +<<<<<<< HEAD >>>>>>> 9a1d0085d (sync) +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) export type EventWorkspaceReady = { + id: string type: "workspace.ready" properties: { name: string @@ -691,6 +645,7 @@ export type EventWorkspaceReady = { } export type EventWorkspaceFailed = { + id: string type: "workspace.failed" properties: { message: string @@ -698,6 +653,7 @@ export type EventWorkspaceFailed = { } export type EventWorkspaceRestore = { + id: string type: "workspace.restore" properties: { workspaceID: string @@ -708,6 +664,7 @@ export type EventWorkspaceRestore = { } export type EventWorkspaceStatus = { + id: string type: "workspace.status" properties: { workspaceID: string @@ -719,11 +676,8 @@ export type EventWorkspaceStatus = { ======= >>>>>>> d0ca805a7 (sync) >>>>>>> 9a1d0085d (sync) -export type EventWorktreeReady = { -======= export type EventWorktreeReady = { id: string ->>>>>>> 1a2dc3e57 (sync) type: "worktree.ready" properties: { name: string @@ -732,10 +686,7 @@ export type EventWorktreeReady = { } export type EventWorktreeFailed = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "worktree.failed" properties: { message: string @@ -753,10 +704,7 @@ export type Pty = { } export type EventPtyCreated = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "pty.created" properties: { info: Pty @@ -764,10 +712,7 @@ export type EventPtyCreated = { } export type EventPtyUpdated = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "pty.updated" properties: { info: Pty @@ -775,10 +720,7 @@ export type EventPtyUpdated = { } export type EventPtyExited = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "pty.exited" properties: { id: string @@ -787,10 +729,7 @@ export type EventPtyExited = { } export type EventPtyDeleted = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "pty.deleted" properties: { id: string @@ -883,10 +822,7 @@ export type AssistantMessage = { export type Message = UserMessage | AssistantMessage export type EventMessageUpdated = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "message.updated" properties: { sessionID: string @@ -895,10 +831,7 @@ export type EventMessageUpdated = { } export type EventMessageRemoved = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "message.removed" properties: { sessionID: string @@ -1170,10 +1103,7 @@ export type Part = | CompactionPart export type EventMessagePartUpdated = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "message.part.updated" properties: { sessionID: string @@ -1183,10 +1113,7 @@ export type EventMessagePartUpdated = { } export type EventMessagePartRemoved = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "message.part.removed" properties: { sessionID: string @@ -1223,15 +1150,12 @@ export type Session = { url: string } title: string -<<<<<<< HEAD -======= agent?: string model?: { id: string providerID: string variant?: string } ->>>>>>> 1a2dc3e57 (sync) version: string time: { created: number @@ -1249,10 +1173,7 @@ export type Session = { } export type EventSessionCreated = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.created" properties: { sessionID: string @@ -1261,10 +1182,7 @@ export type EventSessionCreated = { } export type EventSessionUpdated = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.updated" properties: { sessionID: string @@ -1273,10 +1191,7 @@ export type EventSessionUpdated = { } export type EventSessionDeleted = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.deleted" properties: { sessionID: string @@ -1284,6 +1199,7 @@ export type EventSessionDeleted = { } } +<<<<<<< HEAD <<<<<<< HEAD <<<<<<< HEAD export type EventServerConnected = { @@ -1300,6 +1216,8 @@ export type EventGlobalDisposed = { ======= ======= ======= +======= +>>>>>>> 3e6a37422 (fix) export type EventSessionNextAgentSwitched = { id: string type: "session.next.agent.switched" @@ -1322,8 +1240,11 @@ export type EventSessionNextModelSwitched = { } } +<<<<<<< HEAD >>>>>>> 1a2dc3e57 (sync) >>>>>>> d0ca805a7 (sync) +======= +>>>>>>> 3e6a37422 (fix) export type PromptSource = { start: number end: number @@ -1350,42 +1271,25 @@ export type Prompt = { } export type EventSessionNextPrompted = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.prompted" properties: { timestamp: number sessionID: string -<<<<<<< HEAD - id: string -======= ->>>>>>> 1a2dc3e57 (sync) prompt: Prompt } } export type EventSessionNextSynthetic = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.synthetic" properties: { timestamp: number sessionID: string -<<<<<<< HEAD - id: string -======= ->>>>>>> 1a2dc3e57 (sync) text: string } } -<<<<<<< HEAD -export type EventSessionNextStepStarted = { -======= export type EventSessionNextShellStarted = { id: string type: "session.next.shell.started" @@ -1410,15 +1314,10 @@ export type EventSessionNextShellEnded = { export type EventSessionNextStepStarted = { id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.step.started" properties: { timestamp: number sessionID: string -<<<<<<< HEAD - id: string -======= ->>>>>>> 1a2dc3e57 (sync) agent: string model: { id: string @@ -1430,10 +1329,7 @@ export type EventSessionNextStepStarted = { } export type EventSessionNextStepEnded = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.step.ended" properties: { timestamp: number @@ -1454,10 +1350,7 @@ export type EventSessionNextStepEnded = { } export type EventSessionNextTextStarted = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.text.started" properties: { timestamp: number @@ -1466,10 +1359,7 @@ export type EventSessionNextTextStarted = { } export type EventSessionNextTextDelta = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.text.delta" properties: { timestamp: number @@ -1479,10 +1369,7 @@ export type EventSessionNextTextDelta = { } export type EventSessionNextTextEnded = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.text.ended" properties: { timestamp: number @@ -1492,10 +1379,7 @@ export type EventSessionNextTextEnded = { } export type EventSessionNextReasoningStarted = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.reasoning.started" properties: { timestamp: number @@ -1505,10 +1389,7 @@ export type EventSessionNextReasoningStarted = { } export type EventSessionNextReasoningDelta = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.reasoning.delta" properties: { timestamp: number @@ -1519,10 +1400,7 @@ export type EventSessionNextReasoningDelta = { } export type EventSessionNextReasoningEnded = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.reasoning.ended" properties: { timestamp: number @@ -1533,10 +1411,7 @@ export type EventSessionNextReasoningEnded = { } export type EventSessionNextToolInputStarted = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.input.started" properties: { timestamp: number @@ -1547,10 +1422,7 @@ export type EventSessionNextToolInputStarted = { } export type EventSessionNextToolInputDelta = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.input.delta" properties: { timestamp: number @@ -1561,10 +1433,7 @@ export type EventSessionNextToolInputDelta = { } export type EventSessionNextToolInputEnded = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.input.ended" properties: { timestamp: number @@ -1575,10 +1444,7 @@ export type EventSessionNextToolInputEnded = { } export type EventSessionNextToolCalled = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.called" properties: { timestamp: number @@ -1610,10 +1476,7 @@ export type ToolFileContent = { } export type EventSessionNextToolProgress = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.progress" properties: { timestamp: number @@ -1627,10 +1490,7 @@ export type EventSessionNextToolProgress = { } export type EventSessionNextToolSuccess = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.success" properties: { timestamp: number @@ -1650,10 +1510,7 @@ export type EventSessionNextToolSuccess = { } export type EventSessionNextToolError = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.tool.error" properties: { timestamp: number @@ -1686,10 +1543,7 @@ export type SessionNextRetryError = { } export type EventSessionNextRetried = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.retried" properties: { timestamp: number @@ -1700,27 +1554,17 @@ export type EventSessionNextRetried = { } export type EventSessionNextCompactionStarted = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.compaction.started" properties: { timestamp: number sessionID: string -<<<<<<< HEAD - id: string -======= ->>>>>>> 1a2dc3e57 (sync) reason: "auto" | "manual" } } export type EventSessionNextCompactionDelta = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.compaction.delta" properties: { timestamp: number @@ -1730,10 +1574,7 @@ export type EventSessionNextCompactionDelta = { } export type EventSessionNextCompactionEnded = { -<<<<<<< HEAD -======= id: string ->>>>>>> 1a2dc3e57 (sync) type: "session.next.compaction.ended" properties: { timestamp: number @@ -1832,15 +1673,12 @@ export type SyncEventSessionUpdated = { url?: string | null } title?: string | null -<<<<<<< HEAD -======= agent?: string | null model?: { id: string providerID: string variant?: string } | null ->>>>>>> 1a2dc3e57 (sync) version?: string | null time?: { created?: number | null @@ -1871,8 +1709,6 @@ export type SyncEventSessionDeleted = { } } -<<<<<<< HEAD -======= export type SyncEventSessionNextAgentSwitched = { type: "sync" name: "session.next.agent.switched.1" @@ -1901,7 +1737,6 @@ export type SyncEventSessionNextModelSwitched = { } } ->>>>>>> 1a2dc3e57 (sync) export type SyncEventSessionNextPrompted = { type: "sync" name: "session.next.prompted.1" @@ -1911,10 +1746,6 @@ export type SyncEventSessionNextPrompted = { data: { timestamp: number sessionID: string -<<<<<<< HEAD - id: string -======= ->>>>>>> 1a2dc3e57 (sync) prompt: Prompt } } @@ -1928,16 +1759,10 @@ export type SyncEventSessionNextSynthetic = { data: { timestamp: number sessionID: string -<<<<<<< HEAD - id: string -======= ->>>>>>> 1a2dc3e57 (sync) text: string } } -<<<<<<< HEAD -======= export type SyncEventSessionNextShellStarted = { type: "sync" name: "session.next.shell.started.1" @@ -1966,7 +1791,6 @@ export type SyncEventSessionNextShellEnded = { } } ->>>>>>> 1a2dc3e57 (sync) export type SyncEventSessionNextStepStarted = { type: "sync" name: "session.next.step.started.1" @@ -1976,10 +1800,6 @@ export type SyncEventSessionNextStepStarted = { data: { timestamp: number sessionID: string -<<<<<<< HEAD - id: string -======= ->>>>>>> 1a2dc3e57 (sync) agent: string model: { id: string @@ -2244,10 +2064,6 @@ export type SyncEventSessionNextCompactionStarted = { data: { timestamp: number sessionID: string -<<<<<<< HEAD - id: string -======= ->>>>>>> 1a2dc3e57 (sync) reason: "auto" | "manual" } } @@ -2290,7 +2106,6 @@ export type GlobalEvent = { | EventFileWatcherUpdated ======= | EventProjectUpdated -<<<<<<< HEAD | EventServerInstanceDisposed <<<<<<< HEAD ======= @@ -2298,13 +2113,19 @@ export type GlobalEvent = { | EventGlobalDisposed | EventFileEdited | EventFileWatcherUpdated +<<<<<<< HEAD ======= | EventServerConnected | EventGlobalDisposed | EventServerInstanceDisposed >>>>>>> 1a2dc3e57 (sync) >>>>>>> d0ca805a7 (sync) +<<<<<<< HEAD >>>>>>> 9a1d0085d (sync) +======= +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) | EventLspClientDiagnostics | EventLspUpdated | EventMessagePartDelta @@ -2315,6 +2136,7 @@ export type GlobalEvent = { | EventInstallationUpdated | EventInstallationUpdateAvailable <<<<<<< HEAD +<<<<<<< HEAD ======= <<<<<<< HEAD ======= @@ -2331,7 +2153,12 @@ export type GlobalEvent = { ======= >>>>>>> 1a2dc3e57 (sync) >>>>>>> d0ca805a7 (sync) +<<<<<<< HEAD >>>>>>> 9a1d0085d (sync) +======= +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected @@ -2349,21 +2176,34 @@ export type GlobalEvent = { | EventProjectUpdated | EventVcsBranchUpdated <<<<<<< HEAD +<<<<<<< HEAD ======= <<<<<<< HEAD ======= <<<<<<< HEAD +<<<<<<< HEAD >>>>>>> 9a1d0085d (sync) +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) | EventWorkspaceReady | EventWorkspaceFailed | EventWorkspaceRestore | EventWorkspaceStatus <<<<<<< HEAD +<<<<<<< HEAD ======= ======= >>>>>>> 1a2dc3e57 (sync) >>>>>>> d0ca805a7 (sync) >>>>>>> 9a1d0085d (sync) +======= +======= +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated @@ -2378,6 +2218,7 @@ export type GlobalEvent = { | EventSessionUpdated | EventSessionDeleted <<<<<<< HEAD +<<<<<<< HEAD <<<<<<< HEAD | EventServerConnected | EventGlobalDisposed @@ -2387,13 +2228,14 @@ export type GlobalEvent = { | EventSessionNextPrompted | EventSessionNextSynthetic ======= +======= +>>>>>>> 3e6a37422 (fix) | EventSessionNextAgentSwitched | EventSessionNextModelSwitched | EventSessionNextPrompted | EventSessionNextSynthetic | EventSessionNextShellStarted | EventSessionNextShellEnded ->>>>>>> 1a2dc3e57 (sync) | EventSessionNextStepStarted | EventSessionNextStepEnded | EventSessionNextTextStarted @@ -2421,17 +2263,12 @@ export type GlobalEvent = { | SyncEventSessionCreated | SyncEventSessionUpdated | SyncEventSessionDeleted -<<<<<<< HEAD - | SyncEventSessionNextPrompted - | SyncEventSessionNextSynthetic -======= | SyncEventSessionNextAgentSwitched | SyncEventSessionNextModelSwitched | SyncEventSessionNextPrompted | SyncEventSessionNextSynthetic | SyncEventSessionNextShellStarted | SyncEventSessionNextShellEnded ->>>>>>> 1a2dc3e57 (sync) | SyncEventSessionNextStepStarted | SyncEventSessionNextStepEnded | SyncEventSessionNextTextStarted @@ -3187,15 +3024,12 @@ export type GlobalSession = { url: string } title: string -<<<<<<< HEAD -======= agent?: string model?: { id: string providerID: string variant?: string } ->>>>>>> 1a2dc3e57 (sync) version: string time: { created: number @@ -3308,10 +3142,6 @@ export type ProviderAuthAuthorization = { instructions: string } -<<<<<<< HEAD -export type V2SessionsResponse = { - items: Array -======= export type SessionInfo = { id: string parentID?: string @@ -3334,15 +3164,12 @@ export type SessionInfo = { export type V2SessionsResponse = { items: Array ->>>>>>> 1a2dc3e57 (sync) cursor: { previous?: string next?: string } } -<<<<<<< HEAD -======= export type SessionMessageAgentSwitched = { id: string metadata?: { @@ -3371,7 +3198,6 @@ export type SessionMessageModelSwitched = { } } ->>>>>>> 1a2dc3e57 (sync) export type SessionMessageUser = { id: string metadata?: { @@ -3399,8 +3225,6 @@ export type SessionMessageSynthetic = { type: "synthetic" } -<<<<<<< HEAD -======= export type SessionMessageShell = { id: string metadata?: { @@ -3416,7 +3240,6 @@ export type SessionMessageShell = { output: string } ->>>>>>> 1a2dc3e57 (sync) export type SessionMessageAssistantText = { type: "text" text: string @@ -3544,16 +3367,11 @@ export type SessionMessageCompaction = { } export type SessionMessage = -<<<<<<< HEAD - | SessionMessageUser - | SessionMessageSynthetic -======= | SessionMessageAgentSwitched | SessionMessageModelSwitched | SessionMessageUser | SessionMessageSynthetic | SessionMessageShell ->>>>>>> 1a2dc3e57 (sync) | SessionMessageAssistant | SessionMessageCompaction @@ -3618,7 +3436,6 @@ export type Event = | EventFileWatcherUpdated ======= | EventProjectUpdated -<<<<<<< HEAD | EventServerInstanceDisposed <<<<<<< HEAD ======= @@ -3626,13 +3443,19 @@ export type Event = | EventGlobalDisposed | EventFileEdited | EventFileWatcherUpdated +<<<<<<< HEAD ======= | EventServerConnected | EventGlobalDisposed | EventServerInstanceDisposed >>>>>>> 1a2dc3e57 (sync) >>>>>>> d0ca805a7 (sync) +<<<<<<< HEAD >>>>>>> 9a1d0085d (sync) +======= +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) | EventLspClientDiagnostics | EventLspUpdated | EventMessagePartDelta @@ -3643,6 +3466,7 @@ export type Event = | EventInstallationUpdated | EventInstallationUpdateAvailable <<<<<<< HEAD +<<<<<<< HEAD ======= <<<<<<< HEAD ======= @@ -3659,7 +3483,12 @@ export type Event = ======= >>>>>>> 1a2dc3e57 (sync) >>>>>>> d0ca805a7 (sync) +<<<<<<< HEAD >>>>>>> 9a1d0085d (sync) +======= +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected @@ -3677,21 +3506,34 @@ export type Event = | EventProjectUpdated | EventVcsBranchUpdated <<<<<<< HEAD +<<<<<<< HEAD ======= <<<<<<< HEAD ======= <<<<<<< HEAD +<<<<<<< HEAD >>>>>>> 9a1d0085d (sync) +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) | EventWorkspaceReady | EventWorkspaceFailed | EventWorkspaceRestore | EventWorkspaceStatus <<<<<<< HEAD +<<<<<<< HEAD ======= ======= >>>>>>> 1a2dc3e57 (sync) >>>>>>> d0ca805a7 (sync) >>>>>>> 9a1d0085d (sync) +======= +======= +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated @@ -3706,6 +3548,7 @@ export type Event = | EventSessionUpdated | EventSessionDeleted <<<<<<< HEAD +<<<<<<< HEAD <<<<<<< HEAD | EventServerConnected | EventGlobalDisposed @@ -3715,13 +3558,14 @@ export type Event = | EventSessionNextPrompted | EventSessionNextSynthetic ======= +======= +>>>>>>> 3e6a37422 (fix) | EventSessionNextAgentSwitched | EventSessionNextModelSwitched | EventSessionNextPrompted | EventSessionNextSynthetic | EventSessionNextShellStarted | EventSessionNextShellEnded ->>>>>>> 1a2dc3e57 (sync) | EventSessionNextStepStarted | EventSessionNextStepEnded | EventSessionNextTextStarted @@ -4970,15 +4814,12 @@ export type SessionCreateData = { body?: { parentID?: string title?: string -<<<<<<< HEAD -======= agent?: string model?: { id: string providerID: string variant?: string } ->>>>>>> 1a2dc3e57 (sync) permission?: PermissionRuleset workspaceID?: string } @@ -7266,8 +7107,3 @@ export type FormatterStatusResponses = { } export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] -<<<<<<< HEAD -======= ->>>>>>> 13afcef87 (core: add unique IDs to all events for reliable tracking and debugging) -======= ->>>>>>> 1a2dc3e57 (sync) From 105b74075f766af016cb3488dba5a21bcb13b2a9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 19:53:06 -0400 Subject: [PATCH 14/24] core: add v2 session context API endpoint Add new GET /api/session/:sessionID/context endpoint that returns all active context messages for a v2 session (messages after the last compaction). This allows users to see exactly what context is currently being used by their session. --- .../instance/httpapi/groups/v2/session.ts | 12 ++++++ .../instance/httpapi/handlers/v2/session.ts | 6 +++ packages/opencode/src/v2/session.ts | 37 +++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts index e1439c8f0522..17ddcaeda3b9 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts @@ -119,6 +119,18 @@ export const SessionGroup = HttpApiGroup.make("v2.session") }), ), ) + .add( + HttpApiEndpoint.get("context", "/api/session/:sessionID/context", { + params: { sessionID: SessionID }, + success: Schema.Array(SessionMessage.Message), + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.context", + summary: "Get v2 session context", + description: "Retrieve the active context messages for a v2 session (all messages after the last compaction).", + }), + ), + ) .annotateMerge( OpenApi.annotations({ title: "v2", diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts index dcd6e03f1336..6ca9b0e71f93 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts @@ -105,5 +105,11 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session return HttpApiSchema.NoContent.make() }), ) + .handle( + "context", + Effect.fn(function* (ctx) { + return yield* session.context(ctx.params.sessionID) + }), + ) }), ) diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index a8f93bee554d..a28c44c0157a 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -77,12 +77,15 @@ export interface Interface { direction: "previous" | "next" } }) => Effect.Effect + readonly context: (sessionID: SessionID) => Effect.Effect readonly prompt: (input: { id?: EventV2.ID sessionID: SessionID prompt: Prompt delivery?: Delivery }) => Effect.Effect + readonly shell: (input: { id?: EventV2.ID; sessionID: SessionID; command: string }) => Effect.Effect + readonly skill: (input: { id?: EventV2.ID; sessionID: SessionID; skill: string }) => Effect.Effect readonly switchAgent: (input: { sessionID: SessionID; agent: string }) => Effect.Effect readonly switchModel: (input: { sessionID: SessionID @@ -209,9 +212,43 @@ export const layer = Layer.effect( }) return rows.map((row) => decode(row)) }), + context: Effect.fn("V2Session.context")(function* (sessionID) { + const rows = Database.use((db) => { + const compaction = db + .select() + .from(SessionMessageTable) + .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction"))) + .orderBy(desc(SessionMessageTable.time_created), desc(SessionMessageTable.id)) + .limit(1) + .get() + + return db + .select() + .from(SessionMessageTable) + .where( + and( + eq(SessionMessageTable.session_id, sessionID), + compaction + ? or( + gt(SessionMessageTable.time_created, compaction.time_created), + and( + eq(SessionMessageTable.time_created, compaction.time_created), + gte(SessionMessageTable.id, compaction.id), + ), + ) + : undefined, + ), + ) + .orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id)) + .all() + }) + return rows.map((row) => decode(row)) + }), prompt: Effect.fn("V2Session.prompt")(function* (_input) { return {} as any }), + shell: Effect.fn("V2Session.shell")(function* (_input) {}), + skill: Effect.fn("V2Session.skill")(function* (_input) {}), switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) { EventV2.run(SessionEvent.AgentSwitched.Sync, { sessionID: input.sessionID, From c92323995c72afd6ad41007af9db8e2038044e55 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 19:59:53 -0400 Subject: [PATCH 15/24] core: remove legacy v2 session API endpoints The v2 session listing and message retrieval endpoints at /api/session have been removed as they were superseded by the standard v1 session API. Users should use the /session and /session/:id/message endpoints instead, which provide the same functionality with better performance and consistency. --- .../src/server/routes/instance/index.ts | 1 - .../opencode/src/server/routes/instance/v2.ts | 229 ------------------ 2 files changed, 230 deletions(-) delete mode 100644 packages/opencode/src/server/routes/instance/v2.ts diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index e037e11407f1..15f685123ff2 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -165,7 +165,6 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { .route("/question", QuestionRoutes()) .route("/provider", ProviderRoutes()) .route("/sync", SyncRoutes()) - .route("/api", V2Routes()) .route("/", FileRoutes()) .route("/", EventRoutes()) .route("/mcp", McpRoutes()) diff --git a/packages/opencode/src/server/routes/instance/v2.ts b/packages/opencode/src/server/routes/instance/v2.ts deleted file mode 100644 index d1efef0d6000..000000000000 --- a/packages/opencode/src/server/routes/instance/v2.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { WorkspaceID } from "@/control-plane/schema" -import { SessionID } from "@/session/schema" -import { SessionMessage } from "@/v2/session-message" -import { SessionV2 } from "@/v2/session" -import { zod } from "@/util/effect-zod" -import { lazy } from "@/util/lazy" -import { Effect, Schema } from "effect" -import * as DateTime from "effect/DateTime" -import { Hono } from "hono" -import { describeRoute, resolver, validator } from "hono-openapi" -import { HTTPException } from "hono/http-exception" -import z from "zod" -import { errors } from "../../error" -import { jsonRequest } from "./trace" - -const DefaultMessagesLimit = 50 -const DefaultSessionsLimit = 50 - -const SessionCursor = Schema.Struct({ - id: SessionID, - time: Schema.Number, - order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]), - direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]), - directory: Schema.String.pipe(Schema.optional), - path: Schema.String.pipe(Schema.optional), - workspaceID: WorkspaceID.pipe(Schema.optional), - roots: Schema.Boolean.pipe(Schema.optional), - start: Schema.Number.pipe(Schema.optional), - search: Schema.String.pipe(Schema.optional), -}) -type SessionCursor = typeof SessionCursor.Type - -const SessionsResponse = Schema.Struct({ - items: Schema.Array(SessionV2.Info), - cursor: Schema.Struct({ - previous: Schema.String.pipe(Schema.optional), - next: Schema.String.pipe(Schema.optional), - }), -}).annotate({ identifier: "V2SessionsResponse" }) - -const Cursor = Schema.Struct({ - id: SessionMessage.ID, - time: Schema.Number, - order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]), - direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]), -}) - -const MessagesResponse = Schema.Struct({ - items: Schema.Array(SessionMessage.Message), - cursor: Schema.Struct({ - previous: Schema.String.pipe(Schema.optional), - next: Schema.String.pipe(Schema.optional), - }), -}).annotate({ identifier: "V2SessionMessagesResponse" }) - -const decodeCursor = Schema.decodeUnknownSync(Cursor) -const decodeSessionCursor = Schema.decodeUnknownSync(SessionCursor) - -const sessionCursor = { - encode( - session: SessionV2.Info, - order: "asc" | "desc", - direction: "previous" | "next", - filters: Pick, - ) { - return Buffer.from( - JSON.stringify({ id: session.id, time: session.time.created, order, direction, ...filters }), - ).toString("base64url") - }, - decode(input: string) { - return decodeSessionCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) - }, -} - -const cursor = { - encode(message: SessionMessage.Message, order: "asc" | "desc", direction: "previous" | "next") { - return Buffer.from( - JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), order, direction }), - ).toString("base64url") - }, - decode(input: string) { - return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) - }, -} - -export const V2Routes = lazy(() => - new Hono() - .get( - "/session", - describeRoute({ - summary: "List v2 sessions", - description: - "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.", - operationId: "v2.session.list", - responses: { - 200: { - description: "List of v2 sessions", - content: { - "application/json": { - schema: resolver(zod(SessionsResponse)), - }, - }, - }, - ...errors(400), - }, - }), - validator( - "query", - z.object({ - limit: z.coerce.number().int().min(1).max(200).optional(), - cursor: z.string().optional(), - order: z.enum(["asc", "desc"]).optional(), - directory: z.string().optional(), - path: z.string().optional(), - workspace: WorkspaceID.zod.optional(), - roots: z - .enum(["true", "false"]) - .transform((value) => value === "true") - .optional(), - start: z.coerce.number().optional(), - search: z.string().optional(), - }), - ), - async (c) => { - const query = c.req.valid("query") - const decoded = (() => { - try { - return query.cursor ? sessionCursor.decode(query.cursor) : undefined - } catch { - throw new HTTPException(400) - } - })() - const order = decoded?.order ?? query.order ?? "desc" - const filters = decoded ?? { - directory: query.directory, - path: query.path, - workspaceID: query.workspace, - roots: query.roots, - start: query.start, - search: query.search, - } - return jsonRequest("V2Routes.sessions", c, function* () { - return yield* Effect.gen(function* () { - const session = yield* SessionV2.Service - const sessions = yield* session.list({ - limit: query.limit ?? DefaultSessionsLimit, - order, - directory: filters.directory, - path: filters.path, - workspaceID: filters.workspaceID, - roots: filters.roots, - start: filters.start, - search: filters.search, - cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined, - }) - const first = sessions[0] - const last = sessions.at(-1) - return { - items: sessions, - cursor: { - previous: first ? sessionCursor.encode(first, order, "previous", filters) : undefined, - next: last ? sessionCursor.encode(last, order, "next", filters) : undefined, - }, - } - }).pipe(Effect.provide(SessionV2.defaultLayer)) - }) - }, - ) - .get( - "/session/:sessionID/message", - describeRoute({ - summary: "Get v2 session messages", - description: "Retrieve projected v2 messages for a session directly from the message database.", - operationId: "v2.session.messages", - responses: { - 200: { - description: "List of v2 session messages", - content: { - "application/json": { - schema: resolver(zod(MessagesResponse)), - }, - }, - }, - ...errors(400, 404), - }, - }), - validator("param", z.object({ sessionID: SessionID.zod })), - validator( - "query", - z.object({ - limit: z.coerce.number().int().min(1).max(200).optional(), - cursor: z.string().optional(), - order: z.enum(["asc", "desc"]).optional(), - }), - ), - async (c) => { - const sessionID = c.req.valid("param").sessionID - const query = c.req.valid("query") - const decoded = (() => { - try { - return query.cursor ? cursor.decode(query.cursor) : undefined - } catch { - throw new HTTPException(400) - } - })() - const order = decoded?.order ?? query.order ?? "desc" - return jsonRequest("V2Routes.messages", c, function* () { - return yield* Effect.gen(function* () { - const session = yield* SessionV2.Service - const messages = yield* session.messages({ - sessionID, - limit: query.limit ?? DefaultMessagesLimit, - order, - cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined, - }) - const first = messages[0] - const last = messages.at(-1) - return { - items: messages, - cursor: { - previous: first ? cursor.encode(first, order, "previous") : undefined, - next: last ? cursor.encode(last, order, "next") : undefined, - }, - } - }).pipe(Effect.provide(SessionV2.defaultLayer)) - }) - }, - ), -) From 474be34f0dab0f13ec8aa732126f01fb768f15f7 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 20:10:42 -0400 Subject: [PATCH 16/24] sync --- .../opencode/src/cli/cmd/tui/plugin/internal.ts | 3 ++- packages/opencode/src/session/processor.ts | 14 -------------- packages/opencode/src/session/projectors-next.ts | 4 +--- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts index 156d66ba9808..2b0d859192d4 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts @@ -9,6 +9,7 @@ import SidebarFooter from "../feature-plugins/sidebar/footer" import PluginManager from "../feature-plugins/system/plugins" import SessionV2Debug from "../feature-plugins/system/session-v2" import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui" +import { Flag } from "@opencode-ai/core/flag/flag" export type InternalTuiPlugin = TuiPluginModule & { id: string @@ -25,5 +26,5 @@ export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [ SidebarFiles, SidebarFooter, PluginManager, - SessionV2Debug, + ...(Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM ? [SessionV2Debug] : []), ] diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index ee9a8af1a0ca..1a32a656d135 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -244,13 +244,6 @@ export const layer: Layer.Layer< case "reasoning-delta": if (!(value.id in ctx.reasoningMap)) return - // TODO(v2): Temporary dual-write while migrating session messages to v2 events. - EventV2.run(SessionEvent.Reasoning.Delta.Sync, { - sessionID: ctx.sessionID, - reasoningID: value.id, - delta: value.text, - timestamp: DateTime.makeUnsafe(Date.now()), - }) ctx.reasoningMap[value.id].text += value.text if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata yield* session.updatePartDelta({ @@ -539,13 +532,6 @@ export const layer: Layer.Layer< case "text-delta": if (!ctx.currentText) return - if (ctx.assistantMessage.summary) { - EventV2.run(SessionEvent.Compaction.Delta.Sync, { - sessionID: ctx.sessionID, - text: value.text, - timestamp: DateTime.makeUnsafe(Date.now()), - }) - } ctx.currentText.text += value.text if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata yield* session.updatePartDelta({ diff --git a/packages/opencode/src/session/projectors-next.ts b/packages/opencode/src/session/projectors-next.ts index dbab58a74c8d..951e3e874f48 100644 --- a/packages/opencode/src/session/projectors-next.ts +++ b/packages/opencode/src/session/projectors-next.ts @@ -197,9 +197,7 @@ export default [ SyncEvent.project(SessionEvent.Compaction.Started.Sync, (db, data, event) => { update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.started", data }) }), - SyncEvent.project(SessionEvent.Compaction.Delta.Sync, (db, data, event) => { - update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.delta", data }) - }), + SyncEvent.project(SessionEvent.Compaction.Delta.Sync, () => {}), SyncEvent.project(SessionEvent.Compaction.Ended.Sync, (db, data, event) => { update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.ended", data }) }), From a62fce143e237ad0af1f9b44e9b18c8c75552e16 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 20:14:02 -0400 Subject: [PATCH 17/24] generate types --- packages/sdk/js/src/v2/gen/sdk.gen.ts | 100 ------ packages/sdk/js/src/v2/gen/types.gen.ts | 438 +++--------------------- 2 files changed, 56 insertions(+), 482 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 54d15aad1d2e..ce1ae6da7ea8 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -187,10 +187,6 @@ import type { TuiSelectSessionResponses, TuiShowToastResponses, TuiSubmitPromptResponses, - V2SessionListErrors, - V2SessionListResponses, - V2SessionMessagesErrors, - V2SessionMessagesResponses, VcsDiffResponses, VcsGetResponses, WorktreeCreateErrors, @@ -3191,97 +3187,6 @@ export class Sync extends HeyApiClient { } } -export class Session3 extends HeyApiClient { - /** - * List v2 sessions - * - * Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list. - */ - public list( - parameters?: { - directory?: string - workspace?: string - limit?: number - cursor?: string - order?: "asc" | "desc" - path?: string - roots?: "true" | "false" - start?: number - search?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "limit" }, - { in: "query", key: "cursor" }, - { in: "query", key: "order" }, - { in: "query", key: "path" }, - { in: "query", key: "roots" }, - { in: "query", key: "start" }, - { in: "query", key: "search" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/session", - ...options, - ...params, - }) - } - - /** - * Get v2 session messages - * - * Retrieve projected v2 messages for a session directly from the message database. - */ - public messages( - parameters: { - sessionID: string - directory?: string - workspace?: string - limit?: number - cursor?: string - order?: "asc" | "desc" - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "limit" }, - { in: "query", key: "cursor" }, - { in: "query", key: "order" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/session/{sessionID}/message", - ...options, - ...params, - }) - } -} - -export class V2 extends HeyApiClient { - private _session?: Session3 - get session(): Session3 { - return (this._session ??= new Session3({ client: this.client })) - } -} - export class Find extends HeyApiClient { /** * Find text @@ -4543,11 +4448,6 @@ export class OpencodeClient extends HeyApiClient { return (this._sync ??= new Sync({ client: this.client })) } - private _v2?: V2 - get v2(): V2 { - return (this._v2 ??= new V2({ client: this.client })) - } - private _find?: Find get find(): Find { return (this._find ??= new Find({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index b5b14c7d14bf..e1d87fcbe689 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -45,6 +45,7 @@ export type EventServerInstanceDisposed = { } } +<<<<<<< HEAD <<<<<<< HEAD ======= <<<<<<< HEAD @@ -104,6 +105,8 @@ export type EventServerInstanceDisposed = { >>>>>>> d0ca805a7 (sync) >>>>>>> 9a1d0085d (sync) +======= +>>>>>>> b967ee6dd (generate types) export type EventLspClientDiagnostics = { id: string type: "lsp.client.diagnostics" @@ -274,6 +277,7 @@ export type EventInstallationUpdateAvailable = { } } +<<<<<<< HEAD <<<<<<< HEAD <<<<<<< HEAD ======= @@ -284,9 +288,10 @@ export type EventWorkspaceReady = { ======= <<<<<<< HEAD ======= +======= +>>>>>>> b967ee6dd (generate types) export type EventWorkspaceReady = { id: string ->>>>>>> d0ca805a7 (sync) type: "workspace.ready" properties: { name: string @@ -294,10 +299,7 @@ export type EventWorkspaceReady = { } export type EventWorkspaceFailed = { -<<<<<<< HEAD -======= id: string ->>>>>>> d0ca805a7 (sync) type: "workspace.failed" properties: { message: string @@ -305,10 +307,7 @@ export type EventWorkspaceFailed = { } export type EventWorkspaceRestore = { -<<<<<<< HEAD -======= id: string ->>>>>>> d0ca805a7 (sync) type: "workspace.restore" properties: { workspaceID: string @@ -319,10 +318,7 @@ export type EventWorkspaceRestore = { } export type EventWorkspaceStatus = { -<<<<<<< HEAD -======= id: string ->>>>>>> d0ca805a7 (sync) type: "workspace.status" properties: { workspaceID: string @@ -331,10 +327,7 @@ export type EventWorkspaceStatus = { } export type EventFileEdited = { -<<<<<<< HEAD -======= id: string ->>>>>>> d0ca805a7 (sync) type: "file.edited" properties: { file: string @@ -342,10 +335,7 @@ export type EventFileEdited = { } export type EventFileWatcherUpdated = { -<<<<<<< HEAD -======= id: string ->>>>>>> d0ca805a7 (sync) type: "file.watcher.updated" properties: { file: string @@ -353,6 +343,7 @@ export type EventFileWatcherUpdated = { } } +<<<<<<< HEAD <<<<<<< HEAD ======= >>>>>>> 1a2dc3e57 (sync) @@ -363,6 +354,8 @@ export type EventFileWatcherUpdated = { ======= >>>>>>> 3e6a37422 (fix) >>>>>>> 7b56f08df (fix) +======= +>>>>>>> b967ee6dd (generate types) export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -625,6 +618,7 @@ export type EventVcsBranchUpdated = { } } +<<<<<<< HEAD <<<<<<< HEAD <<<<<<< HEAD ======= @@ -676,6 +670,8 @@ export type EventWorkspaceStatus = { ======= >>>>>>> d0ca805a7 (sync) >>>>>>> 9a1d0085d (sync) +======= +>>>>>>> b967ee6dd (generate types) export type EventWorktreeReady = { id: string type: "worktree.ready" @@ -1199,25 +1195,6 @@ export type EventSessionDeleted = { } } -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -export type EventServerConnected = { - type: "server.connected" - properties: { - [key: string]: unknown - } -} - -export type EventGlobalDisposed = { - type: "global.disposed" - properties: { - [key: string]: unknown -======= -======= -======= -======= ->>>>>>> 3e6a37422 (fix) export type EventSessionNextAgentSwitched = { id: string type: "session.next.agent.switched" @@ -1240,11 +1217,6 @@ export type EventSessionNextModelSwitched = { } } -<<<<<<< HEAD ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) -======= ->>>>>>> 3e6a37422 (fix) export type PromptSource = { start: number end: number @@ -1581,7 +1553,22 @@ export type EventSessionNextCompactionEnded = { sessionID: string text: string include?: string ->>>>>>> 9d5f5e1ff (refactor(session): define v2 session event schemas) + } +} + +export type EventServerConnected = { + id: string + type: "server.connected" + properties: { + [key: string]: unknown + } +} + +export type EventGlobalDisposed = { + id: string + type: "global.disposed" + properties: { + [key: string]: unknown } } @@ -2108,6 +2095,7 @@ export type GlobalEvent = { | EventProjectUpdated | EventServerInstanceDisposed <<<<<<< HEAD +<<<<<<< HEAD ======= | EventServerConnected | EventGlobalDisposed @@ -2126,6 +2114,8 @@ export type GlobalEvent = { ======= >>>>>>> 3e6a37422 (fix) >>>>>>> 7b56f08df (fix) +======= +>>>>>>> b967ee6dd (generate types) | EventLspClientDiagnostics | EventLspUpdated | EventMessagePartDelta @@ -2137,12 +2127,15 @@ export type GlobalEvent = { | EventInstallationUpdateAvailable <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD ======= <<<<<<< HEAD ======= <<<<<<< HEAD ======= >>>>>>> d0ca805a7 (sync) +======= +>>>>>>> b967ee6dd (generate types) | EventWorkspaceReady | EventWorkspaceFailed | EventWorkspaceRestore @@ -2150,6 +2143,7 @@ export type GlobalEvent = { | EventFileEdited | EventFileWatcherUpdated <<<<<<< HEAD +<<<<<<< HEAD ======= >>>>>>> 1a2dc3e57 (sync) >>>>>>> d0ca805a7 (sync) @@ -2159,6 +2153,8 @@ export type GlobalEvent = { ======= >>>>>>> 3e6a37422 (fix) >>>>>>> 7b56f08df (fix) +======= +>>>>>>> b967ee6dd (generate types) | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected @@ -2177,6 +2173,7 @@ export type GlobalEvent = { | EventVcsBranchUpdated <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD ======= <<<<<<< HEAD ======= @@ -2204,6 +2201,8 @@ export type GlobalEvent = { ======= >>>>>>> 3e6a37422 (fix) >>>>>>> 7b56f08df (fix) +======= +>>>>>>> b967ee6dd (generate types) | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated @@ -2217,19 +2216,6 @@ export type GlobalEvent = { | EventSessionCreated | EventSessionUpdated | EventSessionDeleted -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD - | EventServerConnected - | EventGlobalDisposed -======= -======= ->>>>>>> d0ca805a7 (sync) - | EventSessionNextPrompted - | EventSessionNextSynthetic -======= -======= ->>>>>>> 3e6a37422 (fix) | EventSessionNextAgentSwitched | EventSessionNextModelSwitched | EventSessionNextPrompted @@ -2255,7 +2241,8 @@ export type GlobalEvent = { | EventSessionNextCompactionStarted | EventSessionNextCompactionDelta | EventSessionNextCompactionEnded ->>>>>>> 9d5f5e1ff (refactor(session): define v2 session event schemas) + | EventServerConnected + | EventGlobalDisposed | SyncEventMessageUpdated | SyncEventMessageRemoved | SyncEventMessagePartUpdated @@ -3142,247 +3129,6 @@ export type ProviderAuthAuthorization = { instructions: string } -export type SessionInfo = { - id: string - parentID?: string - projectID: string - workspaceID?: string - path?: string - agent?: string - model?: { - id: string - providerID: string - variant?: string - } - time: { - created: number - updated: number - archived?: number - } - title: string -} - -export type V2SessionsResponse = { - items: Array - cursor: { - previous?: string - next?: string - } -} - -export type SessionMessageAgentSwitched = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - type: "agent-switched" - agent: string -} - -export type SessionMessageModelSwitched = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - type: "model-switched" - model: { - id: string - providerID: string - variant?: string - } -} - -export type SessionMessageUser = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - text: string - files?: Array - agents?: Array - type: "user" -} - -export type SessionMessageSynthetic = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } - sessionID: string - text: string - type: "synthetic" -} - -export type SessionMessageShell = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - completed?: number - } - type: "shell" - callID: string - command: string - output: string -} - -export type SessionMessageAssistantText = { - type: "text" - text: string -} - -export type SessionMessageAssistantReasoning = { - type: "reasoning" - id: string - text: string -} - -export type SessionMessageToolStatePending = { - status: "pending" - input: string -} - -export type SessionMessageToolStateRunning = { - status: "running" - input: { - [key: string]: unknown - } - structured: { - [key: string]: unknown - } - content: Array -} - -export type SessionMessageToolStateCompleted = { - status: "completed" - input: { - [key: string]: unknown - } - attachments?: Array - content: Array - structured: { - [key: string]: unknown - } -} - -export type SessionMessageToolStateError = { - status: "error" - input: { - [key: string]: unknown - } - content: Array - structured: { - [key: string]: unknown - } - error: { - type: string - message: string - } -} - -export type SessionMessageAssistantTool = { - type: "tool" - id: string - name: string - provider?: { - executed: boolean - metadata?: { - [key: string]: unknown - } - } - state: - | SessionMessageToolStatePending - | SessionMessageToolStateRunning - | SessionMessageToolStateCompleted - | SessionMessageToolStateError - time: { - created: number - ran?: number - completed?: number - pruned?: number - } -} - -export type SessionMessageAssistant = { - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - completed?: number - } - type: "assistant" - agent: string - model: { - id: string - providerID: string - variant?: string - } - content: Array - snapshot?: { - start?: string - end?: string - } - finish?: string - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - error?: string -} - -export type SessionMessageCompaction = { - type: "compaction" - reason: "auto" | "manual" - summary: string - include?: string - id: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - } -} - -export type SessionMessage = - | SessionMessageAgentSwitched - | SessionMessageModelSwitched - | SessionMessageUser - | SessionMessageSynthetic - | SessionMessageShell - | SessionMessageAssistant - | SessionMessageCompaction - -export type V2SessionMessagesResponse = { - items: Array - cursor: { - previous?: string - next?: string - } -} - export type Symbol = { name: string kind: number @@ -3438,6 +3184,7 @@ export type Event = | EventProjectUpdated | EventServerInstanceDisposed <<<<<<< HEAD +<<<<<<< HEAD ======= | EventServerConnected | EventGlobalDisposed @@ -3456,6 +3203,8 @@ export type Event = ======= >>>>>>> 3e6a37422 (fix) >>>>>>> 7b56f08df (fix) +======= +>>>>>>> b967ee6dd (generate types) | EventLspClientDiagnostics | EventLspUpdated | EventMessagePartDelta @@ -3467,12 +3216,15 @@ export type Event = | EventInstallationUpdateAvailable <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD ======= <<<<<<< HEAD ======= <<<<<<< HEAD ======= >>>>>>> d0ca805a7 (sync) +======= +>>>>>>> b967ee6dd (generate types) | EventWorkspaceReady | EventWorkspaceFailed | EventWorkspaceRestore @@ -3480,6 +3232,7 @@ export type Event = | EventFileEdited | EventFileWatcherUpdated <<<<<<< HEAD +<<<<<<< HEAD ======= >>>>>>> 1a2dc3e57 (sync) >>>>>>> d0ca805a7 (sync) @@ -3489,6 +3242,8 @@ export type Event = ======= >>>>>>> 3e6a37422 (fix) >>>>>>> 7b56f08df (fix) +======= +>>>>>>> b967ee6dd (generate types) | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected @@ -3507,6 +3262,7 @@ export type Event = | EventVcsBranchUpdated <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD ======= <<<<<<< HEAD ======= @@ -3534,6 +3290,8 @@ export type Event = ======= >>>>>>> 3e6a37422 (fix) >>>>>>> 7b56f08df (fix) +======= +>>>>>>> b967ee6dd (generate types) | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated @@ -3547,19 +3305,6 @@ export type Event = | EventSessionCreated | EventSessionUpdated | EventSessionDeleted -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD - | EventServerConnected - | EventGlobalDisposed -======= -======= ->>>>>>> d0ca805a7 (sync) - | EventSessionNextPrompted - | EventSessionNextSynthetic -======= -======= ->>>>>>> 3e6a37422 (fix) | EventSessionNextAgentSwitched | EventSessionNextModelSwitched | EventSessionNextPrompted @@ -3585,7 +3330,8 @@ export type Event = | EventSessionNextCompactionStarted | EventSessionNextCompactionDelta | EventSessionNextCompactionEnded ->>>>>>> 9d5f5e1ff (refactor(session): define v2 session event schemas) + | EventServerConnected + | EventGlobalDisposed export type McpStatusConnected = { status: "connected" @@ -6155,78 +5901,6 @@ export type SyncHistoryListResponses = { export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses] -export type V2SessionListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - limit?: number - cursor?: string - order?: "asc" | "desc" - path?: string - roots?: "true" | "false" - start?: number - search?: string - } - url: "/api/session" -} - -export type V2SessionListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type V2SessionListError = V2SessionListErrors[keyof V2SessionListErrors] - -export type V2SessionListResponses = { - /** - * List of v2 sessions - */ - 200: V2SessionsResponse -} - -export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses] - -export type V2SessionMessagesData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - limit?: number - cursor?: string - order?: "asc" | "desc" - } - url: "/api/session/{sessionID}/message" -} - -export type V2SessionMessagesErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type V2SessionMessagesError = V2SessionMessagesErrors[keyof V2SessionMessagesErrors] - -export type V2SessionMessagesResponses = { - /** - * List of v2 session messages - */ - 200: V2SessionMessagesResponse -} - -export type V2SessionMessagesResponse2 = V2SessionMessagesResponses[keyof V2SessionMessagesResponses] - export type FindTextData = { body?: never path?: never From 6353fefb97e772f54a1b3377a02f8b43c84468cc Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 20:21:54 -0400 Subject: [PATCH 18/24] sync --- .../server/routes/instance/httpapi/public.ts | 10 +- .../src/server/routes/instance/index.ts | 7 +- packages/sdk/js/script/build.ts | 2 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 3005 ++++---- packages/sdk/js/src/v2/gen/types.gen.ts | 6845 ++++++++++------- 5 files changed, 5484 insertions(+), 4385 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/public.ts b/packages/opencode/src/server/routes/instance/httpapi/public.ts index c9668336ae92..fc930551326a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/public.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/public.ts @@ -446,7 +446,7 @@ function stripOptionalNull(schema: OpenApiSchema): OpenApiSchema { if (isEmptyObjectUnion(schema)) return { type: "object", properties: {} } const options = flattenOptions(schema.anyOf ?? schema.oneOf) if (options) { - const withoutNull = options.filter((item) => item.type !== "null") + const withoutNull = stripSpecialNumberStrings(options).filter((item) => item.type !== "null") if (withoutNull.length === 1) return stripOptionalNull(withoutNull[0]) if (schema.anyOf) schema.anyOf = withoutNull.map(stripOptionalNull) if (schema.oneOf) schema.oneOf = withoutNull.map(stripOptionalNull) @@ -490,6 +490,14 @@ function flattenOptions(options: OpenApiSchema[] | undefined): OpenApiSchema[] | return options?.flatMap((item) => flattenOptions(item.anyOf ?? item.oneOf) ?? [item]) } +function stripSpecialNumberStrings(options: OpenApiSchema[]) { + if (!options.some((item) => item.type === "number" || item.type === "integer")) return options + return options.filter( + (item) => + item.type !== "string" || !item.enum?.every((value) => ["NaN", "Infinity", "-Infinity"].includes(String(value))), + ) +} + function normalizeParameter(param: OpenApiParameter, route: string) { if (!param.schema || typeof param.schema !== "object") return if (param.in === "path") { diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 15f685123ff2..3f9f3f6607c1 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -42,11 +42,12 @@ import { WorkspacePaths } from "./httpapi/groups/workspace" export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { const app = new Hono() + const handler = ExperimentalHttpApiServer.webHandler().handler + const context = Context.empty() as Context.Context + + app.all("/api/*", (c) => handler(c.req.raw, context)) if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { - const handler = ExperimentalHttpApiServer.webHandler().handler - const context = Context.empty() as Context.Context - app.all("/api/*", (c) => handler(c.req.raw, context)) app.get(EventPaths.event, (c) => handler(c.req.raw, context)) app.get("/question", (c) => handler(c.req.raw, context)) app.post("/question/:requestID/reply", (c) => handler(c.req.raw, context)) diff --git a/packages/sdk/js/script/build.ts b/packages/sdk/js/script/build.ts index e920cc0fdb15..c490a0be7079 100755 --- a/packages/sdk/js/script/build.ts +++ b/packages/sdk/js/script/build.ts @@ -9,7 +9,7 @@ import path from "path" import { createClient } from "@hey-api/openapi-ts" -const openapiSource = process.env.OPENCODE_SDK_OPENAPI === "httpapi" ? "httpapi" : "hono" +const openapiSource = process.env.OPENCODE_SDK_OPENAPI === "hono" ? "hono" : "httpapi" const opencode = path.resolve(dir, "../../opencode") if (openapiSource === "httpapi") { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index ce1ae6da7ea8..74c5844626ee 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -20,10 +20,10 @@ import type { ConfigUpdateErrors, ConfigUpdateResponses, EventSubscribeResponses, - EventTuiCommandExecute, - EventTuiPromptAppend, - EventTuiSessionSelect, - EventTuiToastShow, + EventTuiCommandExecute2, + EventTuiPromptAppend2, + EventTuiSessionSelect2, + EventTuiToastShow2, ExperimentalConsoleGetResponses, ExperimentalConsoleListOrgsResponses, ExperimentalConsoleSwitchOrgResponses, @@ -90,6 +90,7 @@ import type { ProjectListResponses, ProjectUpdateErrors, ProjectUpdateResponses, + Prompt, ProviderAuthResponses, ProviderListResponses, ProviderOauthAuthorizeErrors, @@ -126,6 +127,7 @@ import type { SessionDeleteMessageErrors, SessionDeleteMessageResponses, SessionDeleteResponses, + SessionDelivery, SessionDiffResponses, SessionForkResponses, SessionGetErrors, @@ -187,6 +189,14 @@ import type { TuiSelectSessionResponses, TuiShowToastResponses, TuiSubmitPromptResponses, + V2SessionCompactResponses, + V2SessionContextResponses, + V2SessionListErrors, + V2SessionListResponses, + V2SessionMessagesErrors, + V2SessionMessagesResponses, + V2SessionPromptResponses, + V2SessionWaitResponses, VcsDiffResponses, VcsGetResponses, WorktreeCreateErrors, @@ -244,111 +254,6 @@ class HeyApiRegistry { } } -export class Config extends HeyApiClient { - /** - * Get global configuration - * - * Retrieve the current global OpenCode configuration settings and preferences. - */ - public get(options?: Options) { - return (options?.client ?? this.client).get({ - url: "/global/config", - ...options, - }) - } - - /** - * Update global configuration - * - * Update global OpenCode configuration settings and preferences. - */ - public update( - parameters?: { - config?: Config3 - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }]) - return (options?.client ?? this.client).patch({ - url: "/global/config", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Global extends HeyApiClient { - /** - * Get health - * - * Get health information about the OpenCode server. - */ - public health(options?: Options) { - return (options?.client ?? this.client).get({ - url: "/global/health", - ...options, - }) - } - - /** - * Get global events - * - * Subscribe to global events from the OpenCode system using server-sent events. - */ - public event(options?: Options) { - return (options?.client ?? this.client).sse.get({ - url: "/global/event", - ...options, - }) - } - - /** - * Dispose instance - * - * Clean up and dispose all OpenCode instances, releasing all resources. - */ - public dispose(options?: Options) { - return (options?.client ?? this.client).post({ - url: "/global/dispose", - ...options, - }) - } - - /** - * Upgrade opencode - * - * Upgrade opencode to the specified version or latest if not specified. - */ - public upgrade( - parameters?: { - target?: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "body", key: "target" }] }]) - return (options?.client ?? this.client).post({ - url: "/global/upgrade", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - private _config?: Config - get config(): Config { - return (this._config ??= new Config({ client: this.client })) - } -} - export class Auth extends HeyApiClient { /** * Remove auth credentials @@ -512,13 +417,118 @@ export class App extends HeyApiClient { } } -export class Adapter extends HeyApiClient { +export class Config extends HeyApiClient { /** - * List workspace adapters + * Get global configuration * - * List all available workspace adapters for the current project. + * Retrieve the current global OpenCode configuration settings and preferences. */ - public list( + public get(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/global/config", + ...options, + }) + } + + /** + * Update global configuration + * + * Update global OpenCode configuration settings and preferences. + */ + public update( + parameters?: { + config?: Config3 + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }]) + return (options?.client ?? this.client).patch({ + url: "/global/config", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Global extends HeyApiClient { + /** + * Get health + * + * Get health information about the OpenCode server. + */ + public health(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/global/health", + ...options, + }) + } + + /** + * Get global events + * + * Subscribe to global events from the OpenCode system using server-sent events. + */ + public event(options?: Options) { + return (options?.client ?? this.client).sse.get({ + url: "/global/event", + ...options, + }) + } + + /** + * Dispose instance + * + * Clean up and dispose all OpenCode instances, releasing all resources. + */ + public dispose(options?: Options) { + return (options?.client ?? this.client).post({ + url: "/global/dispose", + ...options, + }) + } + + /** + * Upgrade opencode + * + * Upgrade opencode to the specified version or latest if not specified. + */ + public upgrade( + parameters?: { + target?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "body", key: "target" }] }]) + return (options?.client ?? this.client).post({ + url: "/global/upgrade", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _config?: Config + get config(): Config { + return (this._config ??= new Config({ client: this.client })) + } +} + +export class Event extends HeyApiClient { + /** + * Subscribe to events + * + * Get events + */ + public subscribe( parameters?: { directory?: string workspace?: string @@ -536,21 +546,21 @@ export class Adapter extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/experimental/workspace/adapter", + return (options?.client ?? this.client).sse.get({ + url: "/event", ...options, ...params, }) } } -export class Workspace extends HeyApiClient { +export class Config2 extends HeyApiClient { /** - * List workspaces + * Get configuration * - * List all workspaces. + * Retrieve the current OpenCode configuration settings and preferences. */ - public list( + public get( parameters?: { directory?: string workspace?: string @@ -568,26 +578,23 @@ export class Workspace extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/experimental/workspace", + return (options?.client ?? this.client).get({ + url: "/config", ...options, ...params, }) } /** - * Create workspace + * Update configuration * - * Create a workspace for the current project. + * Update OpenCode configuration settings and preferences. */ - public create( + public update( parameters?: { directory?: string workspace?: string - id?: string - type?: string - branch?: string | null - extra?: unknown | null + config?: Config3 }, options?: Options, ) { @@ -598,20 +605,13 @@ export class Workspace extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "id" }, - { in: "body", key: "type" }, - { in: "body", key: "branch" }, - { in: "body", key: "extra" }, + { key: "config", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).post< - ExperimentalWorkspaceCreateResponses, - ExperimentalWorkspaceCreateErrors, - ThrowOnError - >({ - url: "/experimental/workspace", + return (options?.client ?? this.client).patch({ + url: "/config", ...options, ...params, headers: { @@ -623,11 +623,11 @@ export class Workspace extends HeyApiClient { } /** - * Workspace status + * List config providers * - * Get connection status for workspaces in the current project. + * Get a list of all configured AI providers and their default models. */ - public status( + public providers( parameters?: { directory?: string workspace?: string @@ -645,96 +645,12 @@ export class Workspace extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/experimental/workspace/status", - ...options, - ...params, - }) - } - - /** - * Remove workspace - * - * Remove an existing workspace. - */ - public remove( - parameters: { - id: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "id" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete< - ExperimentalWorkspaceRemoveResponses, - ExperimentalWorkspaceRemoveErrors, - ThrowOnError - >({ - url: "/experimental/workspace/{id}", - ...options, - ...params, - }) - } - - /** - * Restore session into workspace - * - * Replay a session's sync events into the target workspace in batches. - */ - public sessionRestore( - parameters: { - id: string - directory?: string - workspace?: string - sessionID?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "id" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "sessionID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - ExperimentalWorkspaceSessionRestoreResponses, - ExperimentalWorkspaceSessionRestoreErrors, - ThrowOnError - >({ - url: "/experimental/workspace/{id}/session-restore", + return (options?.client ?? this.client).get({ + url: "/config/providers", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } - - private _adapter?: Adapter - get adapter(): Adapter { - return (this._adapter ??= new Adapter({ client: this.client })) - } } export class Console extends HeyApiClient { @@ -914,33 +830,11 @@ export class Resource extends HeyApiClient { } } -export class Experimental extends HeyApiClient { - private _workspace?: Workspace - get workspace(): Workspace { - return (this._workspace ??= new Workspace({ client: this.client })) - } - - private _console?: Console - get console(): Console { - return (this._console ??= new Console({ client: this.client })) - } - - private _session?: Session - get session(): Session { - return (this._session ??= new Session({ client: this.client })) - } - - private _resource?: Resource - get resource(): Resource { - return (this._resource ??= new Resource({ client: this.client })) - } -} - -export class Project extends HeyApiClient { +export class Adapter extends HeyApiClient { /** - * List all projects + * List workspace adapters * - * Get a list of projects that have been opened with OpenCode. + * List all available workspace adapters for the current project. */ public list( parameters?: { @@ -960,19 +854,21 @@ export class Project extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/project", + return (options?.client ?? this.client).get({ + url: "/experimental/workspace/adapter", ...options, ...params, }) } +} +export class Workspace extends HeyApiClient { /** - * Get current project + * List workspaces * - * Retrieve the currently active project that OpenCode is working with. + * List all workspaces. */ - public current( + public list( parameters?: { directory?: string workspace?: string @@ -990,22 +886,26 @@ export class Project extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/project/current", + return (options?.client ?? this.client).get({ + url: "/experimental/workspace", ...options, ...params, }) } /** - * Initialize git repository + * Create workspace * - * Create a git repository for the current project and return the refreshed project info. + * Create a workspace for the current project. */ - public initGit( + public create( parameters?: { directory?: string workspace?: string + id?: string + type?: string + branch?: string | null + extra?: unknown | null }, options?: Options, ) { @@ -1016,39 +916,39 @@ export class Project extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "id" }, + { in: "body", key: "type" }, + { in: "body", key: "branch" }, + { in: "body", key: "extra" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/project/git/init", + return (options?.client ?? this.client).post< + ExperimentalWorkspaceCreateResponses, + ExperimentalWorkspaceCreateErrors, + ThrowOnError + >({ + url: "/experimental/workspace", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } /** - * Update project + * Workspace status * - * Update project properties such as name, icon, and commands. + * Get connection status for workspaces in the current project. */ - public update( - parameters: { - projectID: string + public status( + parameters?: { directory?: string workspace?: string - name?: string - icon?: { - url?: string - override?: string - color?: string - } - commands?: { - /** - * Startup script to run when creating a new workspace (worktree) - */ - start?: string - } }, options?: Options, ) { @@ -1057,37 +957,27 @@ export class Project extends HeyApiClient { [ { args: [ - { in: "path", key: "projectID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "name" }, - { in: "body", key: "icon" }, - { in: "body", key: "commands" }, ], }, ], ) - return (options?.client ?? this.client).patch({ - url: "/project/{projectID}", + return (options?.client ?? this.client).get({ + url: "/experimental/workspace/status", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } -} -export class Pty extends HeyApiClient { /** - * List available shells + * Remove workspace * - * Get a list of available shells on the system. + * Remove an existing workspace. */ - public shells( - parameters?: { + public remove( + parameters: { + id: string directory?: string workspace?: string }, @@ -1098,28 +988,35 @@ export class Pty extends HeyApiClient { [ { args: [ + { in: "path", key: "id" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/pty/shells", + return (options?.client ?? this.client).delete< + ExperimentalWorkspaceRemoveResponses, + ExperimentalWorkspaceRemoveErrors, + ThrowOnError + >({ + url: "/experimental/workspace/{id}", ...options, ...params, }) } /** - * List PTY sessions + * Restore session into workspace * - * Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode. + * Replay a session's sync events into the target workspace in batches. */ - public list( - parameters?: { + public sessionRestore( + parameters: { + id: string directory?: string workspace?: string + sessionID?: string }, options?: Options, ) { @@ -1128,76 +1025,70 @@ export class Pty extends HeyApiClient { [ { args: [ + { in: "path", key: "id" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "sessionID" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/pty", + return (options?.client ?? this.client).post< + ExperimentalWorkspaceSessionRestoreResponses, + ExperimentalWorkspaceSessionRestoreErrors, + ThrowOnError + >({ + url: "/experimental/workspace/{id}/session-restore", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } - /** - * Create PTY session - * - * Create a new pseudo-terminal (PTY) session for running shell commands and processes. - */ - public create( - parameters?: { - directory?: string - workspace?: string - command?: string - args?: Array - cwd?: string - title?: string - env?: { - [key: string]: string - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "command" }, - { in: "body", key: "args" }, - { in: "body", key: "cwd" }, - { in: "body", key: "title" }, - { in: "body", key: "env" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/pty", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) + private _adapter?: Adapter + get adapter(): Adapter { + return (this._adapter ??= new Adapter({ client: this.client })) + } +} + +export class Experimental extends HeyApiClient { + private _console?: Console + get console(): Console { + return (this._console ??= new Console({ client: this.client })) + } + + private _session?: Session + get session(): Session { + return (this._session ??= new Session({ client: this.client })) + } + + private _resource?: Resource + get resource(): Resource { + return (this._resource ??= new Resource({ client: this.client })) + } + + private _workspace?: Workspace + get workspace(): Workspace { + return (this._workspace ??= new Workspace({ client: this.client })) } +} +export class Tool extends HeyApiClient { /** - * Remove PTY session + * List tools * - * Remove and terminate a specific pseudo-terminal (PTY) session. + * Get a list of available tools with their JSON schema parameters for a specific provider and model combination. */ - public remove( + public list( parameters: { - ptyID: string directory?: string workspace?: string + provider: string + model: string }, options?: Options, ) { @@ -1206,28 +1097,28 @@ export class Pty extends HeyApiClient { [ { args: [ - { in: "path", key: "ptyID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "provider" }, + { in: "query", key: "model" }, ], }, ], ) - return (options?.client ?? this.client).delete({ - url: "/pty/{ptyID}", + return (options?.client ?? this.client).get({ + url: "/experimental/tool", ...options, ...params, }) } /** - * Get PTY session + * List tool IDs * - * Retrieve detailed information about a specific pseudo-terminal (PTY) session. + * Get a list of all available tool IDs, including both built-in tools and dynamically registered tools. */ - public get( - parameters: { - ptyID: string + public ids( + parameters?: { directory?: string workspace?: string }, @@ -1238,35 +1129,31 @@ export class Pty extends HeyApiClient { [ { args: [ - { in: "path", key: "ptyID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/pty/{ptyID}", + return (options?.client ?? this.client).get({ + url: "/experimental/tool/ids", ...options, ...params, }) } +} +export class Worktree extends HeyApiClient { /** - * Update PTY session + * Remove worktree * - * Update properties of an existing pseudo-terminal (PTY) session. + * Remove a git worktree and delete its branch. */ - public update( - parameters: { - ptyID: string + public remove( + parameters?: { directory?: string workspace?: string - title?: string - size?: { - rows: number - cols: number - } + worktreeRemoveInput?: WorktreeRemoveInput }, options?: Options, ) { @@ -1275,17 +1162,15 @@ export class Pty extends HeyApiClient { [ { args: [ - { in: "path", key: "ptyID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "title" }, - { in: "body", key: "size" }, + { key: "worktreeRemoveInput", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).put({ - url: "/pty/{ptyID}", + return (options?.client ?? this.client).delete({ + url: "/experimental/worktree", ...options, ...params, headers: { @@ -1297,13 +1182,12 @@ export class Pty extends HeyApiClient { } /** - * Connect to PTY session + * List worktrees * - * Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time. + * List all sandbox worktrees for the current project. */ - public connect( - parameters: { - ptyID: string + public list( + parameters?: { directory?: string workspace?: string }, @@ -1314,31 +1198,29 @@ export class Pty extends HeyApiClient { [ { args: [ - { in: "path", key: "ptyID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/pty/{ptyID}/connect", + return (options?.client ?? this.client).get({ + url: "/experimental/worktree", ...options, ...params, }) } -} -export class Config2 extends HeyApiClient { /** - * Get configuration + * Create worktree * - * Retrieve the current OpenCode configuration settings and preferences. + * Create a new git worktree for the current project and run any configured startup scripts. */ - public get( + public create( parameters?: { directory?: string workspace?: string + worktreeCreateInput?: WorktreeCreateInput }, options?: Options, ) { @@ -1349,27 +1231,33 @@ export class Config2 extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { key: "worktreeCreateInput", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/config", + return (options?.client ?? this.client).post({ + url: "/experimental/worktree", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } /** - * Update configuration + * Reset worktree * - * Update OpenCode configuration settings and preferences. + * Reset a worktree branch to the primary default branch. */ - public update( + public reset( parameters?: { directory?: string workspace?: string - config?: Config3 + worktreeResetInput?: WorktreeResetInput }, options?: Options, ) { @@ -1380,13 +1268,13 @@ export class Config2 extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { key: "config", map: "body" }, + { key: "worktreeResetInput", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).patch({ - url: "/config", + return (options?.client ?? this.client).post({ + url: "/experimental/worktree/reset", ...options, ...params, headers: { @@ -1396,16 +1284,19 @@ export class Config2 extends HeyApiClient { }, }) } +} +export class Find extends HeyApiClient { /** - * List config providers + * Find text * - * Get a list of all configured AI providers and their default models. + * Search for text patterns across files in the project using ripgrep. */ - public providers( - parameters?: { + public text( + parameters: { directory?: string workspace?: string + pattern: string }, options?: Options, ) { @@ -1416,28 +1307,31 @@ export class Config2 extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "pattern" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/config/providers", + return (options?.client ?? this.client).get({ + url: "/find", ...options, ...params, }) } -} -export class Tool extends HeyApiClient { /** - * List tool IDs + * Find files * - * Get a list of all available tool IDs, including both built-in tools and dynamically registered tools. + * Search for files or directories by name or pattern in the project directory. */ - public ids( - parameters?: { + public files( + parameters: { directory?: string workspace?: string + query: string + dirs?: "true" | "false" + type?: "file" | "directory" + limit?: number }, options?: Options, ) { @@ -1448,28 +1342,31 @@ export class Tool extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "query" }, + { in: "query", key: "dirs" }, + { in: "query", key: "type" }, + { in: "query", key: "limit" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/experimental/tool/ids", + return (options?.client ?? this.client).get({ + url: "/find/file", ...options, ...params, }) } /** - * List tools + * Find symbols * - * Get a list of available tools with their JSON schema parameters for a specific provider and model combination. + * Search for workspace symbols like functions, classes, and variables using LSP. */ - public list( + public symbols( parameters: { directory?: string workspace?: string - provider: string - model: string + query: string }, options?: Options, ) { @@ -1480,31 +1377,30 @@ export class Tool extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "provider" }, - { in: "query", key: "model" }, + { in: "query", key: "query" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/experimental/tool", + return (options?.client ?? this.client).get({ + url: "/find/symbol", ...options, ...params, }) } } -export class Worktree extends HeyApiClient { +export class File extends HeyApiClient { /** - * Remove worktree + * List files * - * Remove a git worktree and delete its branch. + * List files and directories in a specified path. */ - public remove( - parameters?: { + public list( + parameters: { directory?: string workspace?: string - worktreeRemoveInput?: WorktreeRemoveInput + path: string }, options?: Options, ) { @@ -1515,32 +1411,28 @@ export class Worktree extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { key: "worktreeRemoveInput", map: "body" }, + { in: "query", key: "path" }, ], }, ], ) - return (options?.client ?? this.client).delete({ - url: "/experimental/worktree", + return (options?.client ?? this.client).get({ + url: "/file", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } /** - * List worktrees + * Read file * - * List all sandbox worktrees for the current project. + * Read the content of a specified file. */ - public list( - parameters?: { + public read( + parameters: { directory?: string workspace?: string + path: string }, options?: Options, ) { @@ -1551,27 +1443,27 @@ export class Worktree extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "path" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/experimental/worktree", + return (options?.client ?? this.client).get({ + url: "/file/content", ...options, ...params, }) } /** - * Create worktree + * Get file status * - * Create a new git worktree for the current project and run any configured startup scripts. + * Get the git status of all files in the project. */ - public create( + public status( parameters?: { directory?: string workspace?: string - worktreeCreateInput?: WorktreeCreateInput }, options?: Options, ) { @@ -1582,33 +1474,28 @@ export class Worktree extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { key: "worktreeCreateInput", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/experimental/worktree", + return (options?.client ?? this.client).get({ + url: "/file/status", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } +} +export class Instance extends HeyApiClient { /** - * Reset worktree + * Dispose instance * - * Reset a worktree branch to the primary default branch. + * Clean up and dispose the current OpenCode instance, releasing all resources. */ - public reset( + public dispose( parameters?: { directory?: string workspace?: string - worktreeResetInput?: WorktreeResetInput }, options?: Options, ) { @@ -1619,40 +1506,28 @@ export class Worktree extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { key: "worktreeResetInput", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/experimental/worktree/reset", + return (options?.client ?? this.client).post({ + url: "/instance/dispose", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } } -export class Session2 extends HeyApiClient { +export class Path extends HeyApiClient { /** - * List sessions + * Get paths * - * Get a list of all OpenCode sessions, sorted by most recently updated. + * Retrieve the current working directory and related path information for the OpenCode instance. */ - public list( + public get( parameters?: { directory?: string workspace?: string - scope?: "project" - path?: string - roots?: boolean | "true" | "false" - start?: number - search?: string - limit?: number }, options?: Options, ) { @@ -1663,42 +1538,28 @@ export class Session2 extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "scope" }, - { in: "query", key: "path" }, - { in: "query", key: "roots" }, - { in: "query", key: "start" }, - { in: "query", key: "search" }, - { in: "query", key: "limit" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/session", + return (options?.client ?? this.client).get({ + url: "/path", ...options, ...params, }) } +} +export class Vcs extends HeyApiClient { /** - * Create session + * Get VCS info * - * Create a new OpenCode session for interacting with AI assistants and managing conversations. + * Retrieve version control system (VCS) information for the current project, such as git branch. */ - public create( + public get( parameters?: { directory?: string workspace?: string - parentID?: string - title?: string - agent?: string - model?: { - id: string - providerID: string - variant?: string - } - permission?: PermissionRuleset - workspaceID?: string }, options?: Options, ) { @@ -1709,37 +1570,27 @@ export class Session2 extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "parentID" }, - { in: "body", key: "title" }, - { in: "body", key: "agent" }, - { in: "body", key: "model" }, - { in: "body", key: "permission" }, - { in: "body", key: "workspaceID" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session", + return (options?.client ?? this.client).get({ + url: "/vcs", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } /** - * Get session status + * Get VCS diff * - * Retrieve the current status of all sessions, including active, idle, and completed states. + * Retrieve the current git diff for the working tree or against the default branch. */ - public status( - parameters?: { + public diff( + parameters: { directory?: string workspace?: string + mode: "git" | "branch" }, options?: Options, ) { @@ -1750,25 +1601,27 @@ export class Session2 extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "mode" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/session/status", + return (options?.client ?? this.client).get({ + url: "/vcs/diff", ...options, ...params, }) } +} +export class Command extends HeyApiClient { /** - * Delete session + * List commands * - * Delete a session and permanently remove all associated data, including messages and history. + * Get a list of all available commands in the OpenCode system. */ - public delete( - parameters: { - sessionID: string + public list( + parameters?: { directory?: string workspace?: string }, @@ -1779,28 +1632,28 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).delete({ - url: "/session/{sessionID}", + return (options?.client ?? this.client).get({ + url: "/command", ...options, ...params, }) } +} +export class Lsp extends HeyApiClient { /** - * Get session + * Get LSP status * - * Retrieve detailed information about a specific OpenCode session. + * Get LSP server status */ - public get( - parameters: { - sessionID: string + public status( + parameters?: { directory?: string workspace?: string }, @@ -1811,35 +1664,30 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/session/{sessionID}", + return (options?.client ?? this.client).get({ + url: "/lsp", ...options, ...params, }) } +} +export class Formatter extends HeyApiClient { /** - * Update session + * Get formatter status * - * Update properties of an existing session, such as title or other metadata. + * Get formatter status */ - public update( - parameters: { - sessionID: string + public status( + parameters?: { directory?: string workspace?: string - title?: string - permission?: PermissionRuleset - time?: { - archived?: number - } }, options?: Options, ) { @@ -1848,36 +1696,29 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "title" }, - { in: "body", key: "permission" }, - { in: "body", key: "time" }, ], }, ], ) - return (options?.client ?? this.client).patch({ - url: "/session/{sessionID}", + return (options?.client ?? this.client).get({ + url: "/formatter", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } +} +export class Auth2 extends HeyApiClient { /** - * Get session children + * Remove MCP OAuth * - * Retrieve all child sessions that were forked from the specified parent session. + * Remove OAuth credentials for an MCP server. */ - public children( + public remove( parameters: { - sessionID: string + name: string directory?: string workspace?: string }, @@ -1888,28 +1729,28 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/session/{sessionID}/children", + return (options?.client ?? this.client).delete({ + url: "/mcp/{name}/auth", ...options, ...params, }) } /** - * Get session todos + * Start MCP OAuth * - * Retrieve the todo list associated with a specific session, showing tasks and action items. + * Start OAuth authentication flow for a Model Context Protocol (MCP) server. */ - public todo( + public start( parameters: { - sessionID: string + name: string directory?: string workspace?: string }, @@ -1920,33 +1761,31 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/session/{sessionID}/todo", + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth", ...options, ...params, }) } /** - * Initialize session + * Complete MCP OAuth * - * Analyze the current application and create an AGENTS.md file with project-specific agent configurations. + * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code. */ - public init( + public callback( parameters: { - sessionID: string + name: string directory?: string workspace?: string - modelID?: string - providerID?: string - messageID?: string + code?: string }, options?: Options, ) { @@ -1955,18 +1794,16 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "modelID" }, - { in: "body", key: "providerID" }, - { in: "body", key: "messageID" }, + { in: "body", key: "code" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/init", + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth/callback", ...options, ...params, headers: { @@ -1978,16 +1815,15 @@ export class Session2 extends HeyApiClient { } /** - * Fork session + * Authenticate MCP OAuth * - * Create a new session by forking an existing session at a specific message point. + * Start OAuth flow and wait for callback (opens browser). */ - public fork( + public authenticate( parameters: { - sessionID: string + name: string directory?: string workspace?: string - messageID?: string }, options?: Options, ) { @@ -1996,34 +1832,31 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/fork", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, + return (options?.client ?? this.client).post( + { + url: "/mcp/{name}/auth/authenticate", + ...options, + ...params, }, - }) + ) } +} +export class Mcp extends HeyApiClient { /** - * Abort session + * Get MCP status * - * Abort an active session and stop any ongoing AI processing or command execution. + * Get the status of all Model Context Protocol (MCP) servers. */ - public abort( - parameters: { - sessionID: string + public status( + parameters?: { directory?: string workspace?: string }, @@ -2034,28 +1867,64 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/abort", + return (options?.client ?? this.client).get({ + url: "/mcp", ...options, ...params, }) } /** - * Unshare session + * Add MCP server * - * Remove the shareable link for a session, making it private again. + * Dynamically add a new Model Context Protocol (MCP) server to the system. */ - public unshare( + public add( + parameters?: { + directory?: string + workspace?: string + name?: string + config?: McpLocalConfig | McpRemoteConfig + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "config" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Connect an MCP server. + */ + public connect( parameters: { - sessionID: string + name: string directory?: string workspace?: string }, @@ -2066,28 +1935,316 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).delete({ - url: "/session/{sessionID}/share", + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/connect", ...options, ...params, }) } /** - * Share session + * Disconnect an MCP server. + */ + public disconnect( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/disconnect", + ...options, + ...params, + }) + } + + private _auth?: Auth2 + get auth(): Auth2 { + return (this._auth ??= new Auth2({ client: this.client })) + } +} + +export class Project extends HeyApiClient { + /** + * List all projects * - * Create a shareable link for a session, allowing others to view the conversation. + * Get a list of projects that have been opened with OpenCode. */ - public share( + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project", + ...options, + ...params, + }) + } + + /** + * Get current project + * + * Retrieve the currently active project that OpenCode is working with. + */ + public current( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project/current", + ...options, + ...params, + }) + } + + /** + * Initialize git repository + * + * Create a git repository for the current project and return the refreshed project info. + */ + public initGit( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/project/git/init", + ...options, + ...params, + }) + } + + /** + * Update project + * + * Update project properties such as name, icon, and commands. + */ + public update( + parameters: { + projectID: string + directory?: string + workspace?: string + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "projectID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "icon" }, + { in: "body", key: "commands" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/project/{projectID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Pty extends HeyApiClient { + /** + * List available shells + * + * Get a list of available shells on the system. + */ + public shells( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty/shells", + ...options, + ...params, + }) + } + + /** + * List PTY sessions + * + * Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty", + ...options, + ...params, + }) + } + + /** + * Create PTY session + * + * Create a new pseudo-terminal (PTY) session for running shell commands and processes. + */ + public create( + parameters?: { + directory?: string + workspace?: string + command?: string + args?: Array + cwd?: string + title?: string + env?: { + [key: string]: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "command" }, + { in: "body", key: "args" }, + { in: "body", key: "cwd" }, + { in: "body", key: "title" }, + { in: "body", key: "env" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/pty", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Remove PTY session + * + * Remove and terminate a specific pseudo-terminal (PTY) session. + */ + public remove( parameters: { - sessionID: string + ptyID: string directory?: string workspace?: string }, @@ -2098,31 +2255,30 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "ptyID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/share", + return (options?.client ?? this.client).delete({ + url: "/pty/{ptyID}", ...options, ...params, }) } /** - * Get message diff + * Get PTY session * - * Get the file changes (diff) that resulted from a specific user message in the session. + * Retrieve detailed information about a specific pseudo-terminal (PTY) session. */ - public diff( + public get( parameters: { - sessionID: string + ptyID: string directory?: string workspace?: string - messageID?: string }, options?: Options, ) { @@ -2131,34 +2287,35 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "ptyID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "messageID" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/session/{sessionID}/diff", + return (options?.client ?? this.client).get({ + url: "/pty/{ptyID}", ...options, ...params, }) } /** - * Summarize session + * Update PTY session * - * Generate a concise summary of the session using AI compaction to preserve key information. + * Update properties of an existing pseudo-terminal (PTY) session. */ - public summarize( + public update( parameters: { - sessionID: string + ptyID: string directory?: string workspace?: string - providerID?: string - modelID?: string - auto?: boolean + title?: string + size?: { + rows: number + cols: number + } }, options?: Options, ) { @@ -2167,18 +2324,17 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "ptyID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "providerID" }, - { in: "body", key: "modelID" }, - { in: "body", key: "auto" }, + { in: "body", key: "title" }, + { in: "body", key: "size" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/summarize", + return (options?.client ?? this.client).put({ + url: "/pty/{ptyID}", ...options, ...params, headers: { @@ -2190,17 +2346,15 @@ export class Session2 extends HeyApiClient { } /** - * Get session messages + * Connect to PTY session * - * Retrieve all messages in a session, including user prompts and AI responses. + * Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time. */ - public messages( + public connect( parameters: { - sessionID: string + ptyID: string directory?: string workspace?: string - limit?: number - before?: string }, options?: Options, ) { @@ -2209,46 +2363,31 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "ptyID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "limit" }, - { in: "query", key: "before" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/session/{sessionID}/message", + return (options?.client ?? this.client).get({ + url: "/pty/{ptyID}/connect", ...options, ...params, }) } +} +export class Question extends HeyApiClient { /** - * Send message + * List pending questions * - * Create and send a new message to a session, streaming the AI response. + * Get all pending question requests across all sessions. */ - public prompt( - parameters: { - sessionID: string + public list( + parameters?: { directory?: string workspace?: string - messageID?: string - model?: { - providerID: string - modelID: string - } - agent?: string - noReply?: boolean - tools?: { - [key: string]: boolean - } - format?: OutputFormat - system?: string - variant?: string - parts?: Array }, options?: Options, ) { @@ -2257,45 +2396,30 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, - { in: "body", key: "model" }, - { in: "body", key: "agent" }, - { in: "body", key: "noReply" }, - { in: "body", key: "tools" }, - { in: "body", key: "format" }, - { in: "body", key: "system" }, - { in: "body", key: "variant" }, - { in: "body", key: "parts" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/message", + return (options?.client ?? this.client).get({ + url: "/question", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } /** - * Delete message + * Reply to question request * - * Permanently delete a specific message (and all of its parts) from a session. This does not revert any file changes that may have been made while processing the message. + * Provide answers to a question request from the AI assistant. */ - public deleteMessage( + public reply( parameters: { - sessionID: string - messageID: string + requestID: string directory?: string workspace?: string + answers?: Array }, options?: Options, ) { @@ -2304,34 +2428,34 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "messageID" }, + { in: "path", key: "requestID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "answers" }, ], }, ], ) - return (options?.client ?? this.client).delete< - SessionDeleteMessageResponses, - SessionDeleteMessageErrors, - ThrowOnError - >({ - url: "/session/{sessionID}/message/{messageID}", + return (options?.client ?? this.client).post({ + url: "/question/{requestID}/reply", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } /** - * Get message + * Reject question request * - * Retrieve a specific message from a session by its message ID. + * Reject a question request from the AI assistant. */ - public message( + public reject( parameters: { - sessionID: string - messageID: string + requestID: string directory?: string workspace?: string }, @@ -2342,45 +2466,31 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "messageID" }, + { in: "path", key: "requestID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/session/{sessionID}/message/{messageID}", + return (options?.client ?? this.client).post({ + url: "/question/{requestID}/reject", ...options, ...params, }) } +} +export class Permission extends HeyApiClient { /** - * Send async message + * List pending permissions * - * Create and send a new message to a session asynchronously, starting the session if needed and returning immediately. + * Get all pending permission requests across all sessions. */ - public promptAsync( - parameters: { - sessionID: string + public list( + parameters?: { directory?: string workspace?: string - messageID?: string - model?: { - providerID: string - modelID: string - } - agent?: string - noReply?: boolean - tools?: { - [key: string]: boolean - } - format?: OutputFormat - system?: string - variant?: string - parts?: Array }, options?: Options, ) { @@ -2389,58 +2499,31 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, - { in: "body", key: "model" }, - { in: "body", key: "agent" }, - { in: "body", key: "noReply" }, - { in: "body", key: "tools" }, - { in: "body", key: "format" }, - { in: "body", key: "system" }, - { in: "body", key: "variant" }, - { in: "body", key: "parts" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/prompt_async", + return (options?.client ?? this.client).get({ + url: "/permission", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } /** - * Send command + * Respond to permission request * - * Send a new command to a session for execution by the AI assistant. + * Approve or deny a permission request from the AI assistant. */ - public command( + public reply( parameters: { - sessionID: string + requestID: string directory?: string workspace?: string - messageID?: string - agent?: string - model?: string - arguments?: string - command?: string - variant?: string - parts?: Array<{ - id?: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource - }> + reply?: "once" | "always" | "reject" + message?: string }, options?: Options, ) { @@ -2449,22 +2532,17 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "requestID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, - { in: "body", key: "agent" }, - { in: "body", key: "model" }, - { in: "body", key: "arguments" }, - { in: "body", key: "command" }, - { in: "body", key: "variant" }, - { in: "body", key: "parts" }, + { in: "body", key: "reply" }, + { in: "body", key: "message" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/command", + return (options?.client ?? this.client).post({ + url: "/permission/{requestID}/reply", ...options, ...params, headers: { @@ -2476,22 +2554,19 @@ export class Session2 extends HeyApiClient { } /** - * Run shell command + * Respond to permission * - * Execute a shell command within the session context and return the AI's response. + * Approve or deny a permission request from the AI assistant. + * + * @deprecated */ - public shell( + public respond( parameters: { sessionID: string + permissionID: string directory?: string workspace?: string - messageID?: string - agent?: string - model?: { - providerID: string - modelID: string - } - command?: string + response?: "once" | "always" | "reject" }, options?: Options, ) { @@ -2501,18 +2576,16 @@ export class Session2 extends HeyApiClient { { args: [ { in: "path", key: "sessionID" }, + { in: "path", key: "permissionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, - { in: "body", key: "agent" }, - { in: "body", key: "model" }, - { in: "body", key: "command" }, + { in: "body", key: "response" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/shell", + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/permissions/{permissionID}", ...options, ...params, headers: { @@ -2522,19 +2595,23 @@ export class Session2 extends HeyApiClient { }, }) } +} +export class Oauth extends HeyApiClient { /** - * Revert message + * Start OAuth authorization * - * Revert a specific message in a session, undoing its effects and restoring the previous state. + * Start the OAuth authorization flow for a provider. */ - public revert( + public authorize( parameters: { - sessionID: string + providerID: string directory?: string workspace?: string - messageID?: string - partID?: string + method?: number + inputs?: { + [key: string]: string + } }, options?: Options, ) { @@ -2543,17 +2620,21 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "providerID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "messageID" }, - { in: "body", key: "partID" }, + { in: "body", key: "method" }, + { in: "body", key: "inputs" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/revert", + return (options?.client ?? this.client).post< + ProviderOauthAuthorizeResponses, + ProviderOauthAuthorizeErrors, + ThrowOnError + >({ + url: "/provider/{providerID}/oauth/authorize", ...options, ...params, headers: { @@ -2565,15 +2646,17 @@ export class Session2 extends HeyApiClient { } /** - * Restore reverted messages + * Handle OAuth callback * - * Restore all previously reverted messages in a session. + * Handle the OAuth callback from a provider after user authorization. */ - public unrevert( + public callback( parameters: { - sessionID: string + providerID: string directory?: string workspace?: string + method?: number + code?: string }, options?: Options, ) { @@ -2582,30 +2665,40 @@ export class Session2 extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, + { in: "path", key: "providerID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "method" }, + { in: "body", key: "code" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/unrevert", + return (options?.client ?? this.client).post< + ProviderOauthCallbackResponses, + ProviderOauthCallbackErrors, + ThrowOnError + >({ + url: "/provider/{providerID}/oauth/callback", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } } -export class Part extends HeyApiClient { +export class Provider extends HeyApiClient { /** - * Delete a part from a message + * List providers + * + * Get a list of all available AI providers, including both available and connected ones. */ - public delete( - parameters: { - sessionID: string - messageID: string - partID: string + public list( + parameters?: { directory?: string workspace?: string }, @@ -2616,33 +2709,28 @@ export class Part extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "messageID" }, - { in: "path", key: "partID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).delete({ - url: "/session/{sessionID}/message/{messageID}/part/{partID}", + return (options?.client ?? this.client).get({ + url: "/provider", ...options, ...params, }) } /** - * Update a part in a message + * Get provider auth methods + * + * Retrieve available authentication methods for all AI providers. */ - public update( - parameters: { - sessionID: string - messageID: string - partID: string + public auth( + parameters?: { directory?: string workspace?: string - part?: Part2 }, options?: Options, ) { @@ -2651,44 +2739,41 @@ export class Part extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "messageID" }, - { in: "path", key: "partID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { key: "part", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).patch({ - url: "/session/{sessionID}/message/{messageID}/part/{partID}", + return (options?.client ?? this.client).get({ + url: "/provider/auth", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } + + private _oauth?: Oauth + get oauth(): Oauth { + return (this._oauth ??= new Oauth({ client: this.client })) + } } -export class Permission extends HeyApiClient { +export class Session2 extends HeyApiClient { /** - * Respond to permission - * - * Approve or deny a permission request from the AI assistant. + * List sessions * - * @deprecated + * Get a list of all OpenCode sessions, sorted by most recently updated. */ - public respond( - parameters: { - sessionID: string - permissionID: string + public list( + parameters?: { directory?: string workspace?: string - response?: "once" | "always" | "reject" + scope?: "project" + path?: string + roots?: boolean | "true" | "false" + start?: number + search?: string + limit?: number }, options?: Options, ) { @@ -2697,39 +2782,44 @@ export class Permission extends HeyApiClient { [ { args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "permissionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "response" }, + { in: "query", key: "scope" }, + { in: "query", key: "path" }, + { in: "query", key: "roots" }, + { in: "query", key: "start" }, + { in: "query", key: "search" }, + { in: "query", key: "limit" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/session/{sessionID}/permissions/{permissionID}", + return (options?.client ?? this.client).get({ + url: "/session", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } /** - * Respond to permission request + * Create session * - * Approve or deny a permission request from the AI assistant. + * Create a new OpenCode session for interacting with AI assistants and managing conversations. */ - public reply( - parameters: { - requestID: string + public create( + parameters?: { directory?: string workspace?: string - reply?: "once" | "always" | "reject" - message?: string + parentID?: string + title?: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + permission?: PermissionRuleset + workspaceID?: string }, options?: Options, ) { @@ -2738,17 +2828,20 @@ export class Permission extends HeyApiClient { [ { args: [ - { in: "path", key: "requestID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "reply" }, - { in: "body", key: "message" }, + { in: "body", key: "parentID" }, + { in: "body", key: "title" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "permission" }, + { in: "body", key: "workspaceID" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/permission/{requestID}/reply", + return (options?.client ?? this.client).post({ + url: "/session", ...options, ...params, headers: { @@ -2760,11 +2853,11 @@ export class Permission extends HeyApiClient { } /** - * List pending permissions + * Get session status * - * Get all pending permission requests across all sessions. + * Retrieve the current status of all sessions, including active, idle, and completed states. */ - public list( + public status( parameters?: { directory?: string workspace?: string @@ -2782,22 +2875,21 @@ export class Permission extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/permission", + return (options?.client ?? this.client).get({ + url: "/session/status", ...options, ...params, }) } -} -export class Question extends HeyApiClient { /** - * List pending questions + * Delete session * - * Get all pending question requests across all sessions. + * Delete a session and permanently remove all associated data, including messages and history. */ - public list( - parameters?: { + public delete( + parameters: { + sessionID: string directory?: string workspace?: string }, @@ -2808,30 +2900,30 @@ export class Question extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/question", + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}", ...options, ...params, }) } /** - * Reply to question request + * Get session * - * Provide answers to a question request from the AI assistant. + * Retrieve detailed information about a specific OpenCode session. */ - public reply( + public get( parameters: { - requestID: string + sessionID: string directory?: string workspace?: string - answers?: Array }, options?: Options, ) { @@ -2840,36 +2932,35 @@ export class Question extends HeyApiClient { [ { args: [ - { in: "path", key: "requestID" }, + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "answers" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/question/{requestID}/reply", + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } /** - * Reject question request + * Update session * - * Reject a question request from the AI assistant. + * Update properties of an existing session, such as title or other metadata. */ - public reject( + public update( parameters: { - requestID: string + sessionID: string directory?: string workspace?: string + title?: string + permission?: PermissionRuleset + time?: { + archived?: number + } }, options?: Options, ) { @@ -2878,36 +2969,38 @@ export class Question extends HeyApiClient { [ { args: [ - { in: "path", key: "requestID" }, + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "title" }, + { in: "body", key: "permission" }, + { in: "body", key: "time" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/question/{requestID}/reject", + return (options?.client ?? this.client).patch({ + url: "/session/{sessionID}", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } -} -export class Oauth extends HeyApiClient { /** - * OAuth authorize + * Get session children * - * Initiate OAuth authorization for a specific AI provider to get an authorization URL. + * Retrieve all child sessions that were forked from the specified parent session. */ - public authorize( + public children( parameters: { - providerID: string + sessionID: string directory?: string workspace?: string - method?: number - inputs?: { - [key: string]: string - } }, options?: Options, ) { @@ -2916,43 +3009,30 @@ export class Oauth extends HeyApiClient { [ { args: [ - { in: "path", key: "providerID" }, + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "method" }, - { in: "body", key: "inputs" }, ], }, ], ) - return (options?.client ?? this.client).post< - ProviderOauthAuthorizeResponses, - ProviderOauthAuthorizeErrors, - ThrowOnError - >({ - url: "/provider/{providerID}/oauth/authorize", + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/children", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } /** - * OAuth callback + * Get session todos * - * Handle the OAuth callback from a provider after user authorization. + * Retrieve the todo list associated with a specific session, showing tasks and action items. */ - public callback( + public todo( parameters: { - providerID: string + sessionID: string directory?: string workspace?: string - method?: number - code?: string }, options?: Options, ) { @@ -2961,42 +3041,31 @@ export class Oauth extends HeyApiClient { [ { args: [ - { in: "path", key: "providerID" }, + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "method" }, - { in: "body", key: "code" }, ], }, ], ) - return (options?.client ?? this.client).post< - ProviderOauthCallbackResponses, - ProviderOauthCallbackErrors, - ThrowOnError - >({ - url: "/provider/{providerID}/oauth/callback", + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/todo", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } -} -export class Provider extends HeyApiClient { /** - * List providers + * Get message diff * - * Get a list of all available AI providers, including both available and connected ones. + * Get the file changes (diff) that resulted from a specific user message in the session. */ - public list( - parameters?: { + public diff( + parameters: { + sessionID: string directory?: string workspace?: string + messageID?: string }, options?: Options, ) { @@ -3005,28 +3074,33 @@ export class Provider extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "messageID" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/provider", + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/diff", ...options, ...params, }) } /** - * Get provider auth methods + * Get session messages * - * Retrieve available authentication methods for all AI providers. + * Retrieve all messages in a session, including user prompts and AI responses. */ - public auth( - parameters?: { + public messages( + parameters: { + sessionID: string directory?: string workspace?: string + limit?: number + before?: string }, options?: Options, ) { @@ -3035,38 +3109,46 @@ export class Provider extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "limit" }, + { in: "query", key: "before" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/provider/auth", + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/message", ...options, ...params, }) } - private _oauth?: Oauth - get oauth(): Oauth { - return (this._oauth ??= new Oauth({ client: this.client })) - } -} - -export class History extends HeyApiClient { /** - * List sync events + * Send message * - * List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history. + * Create and send a new message to a session, streaming the AI response. */ - public list( - parameters?: { + public prompt( + parameters: { + sessionID: string directory?: string workspace?: string - body?: { - [key: string]: number + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean } + format?: OutputFormat + system?: string + variant?: string + parts?: Array }, options?: Options, ) { @@ -3075,15 +3157,24 @@ export class History extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { key: "body", map: "body" }, + { in: "body", key: "messageID" }, + { in: "body", key: "model" }, + { in: "body", key: "agent" }, + { in: "body", key: "noReply" }, + { in: "body", key: "tools" }, + { in: "body", key: "format" }, + { in: "body", key: "system" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/sync/history", + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/message", ...options, ...params, headers: { @@ -3093,16 +3184,16 @@ export class History extends HeyApiClient { }, }) } -} -export class Sync extends HeyApiClient { /** - * Start workspace sync + * Delete message * - * Start sync loops for workspaces in the current project that have active sessions. + * Permanently delete a specific message and all of its parts from a session without reverting file changes. */ - public start( - parameters?: { + public deleteMessage( + parameters: { + sessionID: string + messageID: string directory?: string workspace?: string }, @@ -3113,38 +3204,36 @@ export class Sync extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/sync/start", + return (options?.client ?? this.client).delete< + SessionDeleteMessageResponses, + SessionDeleteMessageErrors, + ThrowOnError + >({ + url: "/session/{sessionID}/message/{messageID}", ...options, ...params, }) } /** - * Replay sync events + * Get message * - * Validate and replay a complete sync event history. + * Retrieve a specific message from a session by its message ID. */ - public replay( - parameters?: { - query_directory?: string + public message( + parameters: { + sessionID: string + messageID: string + directory?: string workspace?: string - body_directory?: string - events?: Array<{ - id: string - aggregateID: string - seq: number - type: string - data: { - [key: string]: unknown - } - }> }, options?: Options, ) { @@ -3153,51 +3242,32 @@ export class Sync extends HeyApiClient { [ { args: [ - { - in: "query", - key: "query_directory", - map: "directory", - }, + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { - in: "body", - key: "body_directory", - map: "directory", - }, - { in: "body", key: "events" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/sync/replay", + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/message/{messageID}", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } - private _history?: History - get history(): History { - return (this._history ??= new History({ client: this.client })) - } -} - -export class Find extends HeyApiClient { /** - * Find text + * Fork session * - * Search for text patterns across files in the project using ripgrep. + * Create a new session by forking an existing session at a specific message point. */ - public text( + public fork( parameters: { + sessionID: string directory?: string workspace?: string - pattern: string + messageID?: string }, options?: Options, ) { @@ -3206,33 +3276,36 @@ export class Find extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "pattern" }, + { in: "body", key: "messageID" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/find", + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/fork", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } /** - * Find files + * Abort session * - * Search for files or directories by name or pattern in the project directory. + * Abort an active session and stop any ongoing AI processing or command execution. */ - public files( + public abort( parameters: { + sessionID: string directory?: string workspace?: string - query: string - dirs?: "true" | "false" - type?: "file" | "directory" - limit?: number }, options?: Options, ) { @@ -3241,33 +3314,33 @@ export class Find extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "query" }, - { in: "query", key: "dirs" }, - { in: "query", key: "type" }, - { in: "query", key: "limit" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/find/file", + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/abort", ...options, ...params, }) } /** - * Find symbols + * Initialize session * - * Search for workspace symbols like functions, classes, and variables using LSP. + * Analyze the current application and create an AGENTS.md file with project-specific agent configurations. */ - public symbols( + public init( parameters: { + sessionID: string directory?: string workspace?: string - query: string + modelID?: string + providerID?: string + messageID?: string }, options?: Options, ) { @@ -3276,32 +3349,38 @@ export class Find extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "query" }, + { in: "body", key: "modelID" }, + { in: "body", key: "providerID" }, + { in: "body", key: "messageID" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/find/symbol", + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/init", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } -} -export class File extends HeyApiClient { /** - * List files + * Unshare session * - * List files and directories in a specified path. + * Remove the shareable link for a session, making it private again. */ - public list( + public unshare( parameters: { + sessionID: string directory?: string workspace?: string - path: string }, options?: Options, ) { @@ -3310,30 +3389,30 @@ export class File extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "path" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/file", + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}/share", ...options, ...params, }) } /** - * Read file + * Share session * - * Read the content of a specified file. + * Create a shareable link for a session, allowing others to view the conversation. */ - public read( + public share( parameters: { + sessionID: string directory?: string workspace?: string - path: string }, options?: Options, ) { @@ -3342,29 +3421,33 @@ export class File extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "path" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/file/content", + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/share", ...options, ...params, }) } /** - * Get file status + * Summarize session * - * Get the git status of all files in the project. + * Generate a concise summary of the session using AI compaction to preserve key information. */ - public status( - parameters?: { + public summarize( + parameters: { + sessionID: string directory?: string workspace?: string + providerID?: string + modelID?: string + auto?: boolean }, options?: Options, ) { @@ -3373,30 +3456,52 @@ export class File extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "providerID" }, + { in: "body", key: "modelID" }, + { in: "body", key: "auto" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/file/status", + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/summarize", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } -} -export class Event extends HeyApiClient { /** - * Subscribe to events + * Send async message * - * Get events + * Create and send a new message to a session asynchronously, starting the session if needed and returning immediately. */ - public subscribe( - parameters?: { + public promptAsync( + parameters: { + sessionID: string directory?: string workspace?: string + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts?: Array }, options?: Options, ) { @@ -3405,31 +3510,58 @@ export class Event extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "model" }, + { in: "body", key: "agent" }, + { in: "body", key: "noReply" }, + { in: "body", key: "tools" }, + { in: "body", key: "format" }, + { in: "body", key: "system" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, ], }, ], ) - return (options?.client ?? this.client).sse.get({ - url: "/event", + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/prompt_async", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } -} -export class Auth2 extends HeyApiClient { /** - * Remove MCP OAuth + * Send command * - * Remove OAuth credentials for an MCP server + * Send a new command to a session for execution by the AI assistant. */ - public remove( + public command( parameters: { - name: string + sessionID: string directory?: string workspace?: string + messageID?: string + agent?: string + model?: string + arguments?: string + command?: string + variant?: string + parts?: Array<{ + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource + }> }, options?: Options, ) { @@ -3438,30 +3570,49 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "arguments" }, + { in: "body", key: "command" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, ], }, ], ) - return (options?.client ?? this.client).delete({ - url: "/mcp/{name}/auth", + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/command", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } /** - * Start MCP OAuth + * Run shell command * - * Start OAuth authentication flow for a Model Context Protocol (MCP) server. + * Execute a shell command within the session context and return the AI's response. */ - public start( + public shell( parameters: { - name: string + sessionID: string directory?: string workspace?: string + messageID?: string + agent?: string + model?: { + providerID: string + modelID: string + } + command?: string }, options?: Options, ) { @@ -3470,31 +3621,41 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "command" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/auth", + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/shell", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } /** - * Complete MCP OAuth + * Revert message * - * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code. + * Revert a specific message in a session, undoing its effects and restoring the previous state. */ - public callback( + public revert( parameters: { - name: string + sessionID: string directory?: string workspace?: string - code?: string + messageID?: string + partID?: string }, options?: Options, ) { @@ -3503,16 +3664,17 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "code" }, + { in: "body", key: "messageID" }, + { in: "body", key: "partID" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/auth/callback", + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/revert", ...options, ...params, headers: { @@ -3524,13 +3686,13 @@ export class Auth2 extends HeyApiClient { } /** - * Authenticate MCP OAuth + * Restore reverted messages * - * Start OAuth flow and wait for callback (opens browser) + * Restore all previously reverted messages in a session. */ - public authenticate( + public unrevert( parameters: { - name: string + sessionID: string directory?: string workspace?: string }, @@ -3541,31 +3703,30 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post( - { - url: "/mcp/{name}/auth/authenticate", - ...options, - ...params, - }, - ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/unrevert", + ...options, + ...params, + }) } } -export class Mcp extends HeyApiClient { +export class Part extends HeyApiClient { /** - * Get MCP status - * - * Get the status of all Model Context Protocol (MCP) servers. + * Delete a part from a message. */ - public status( - parameters?: { + public delete( + parameters: { + sessionID: string + messageID: string + partID: string directory?: string workspace?: string }, @@ -3576,30 +3737,33 @@ export class Mcp extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "path", key: "partID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/mcp", + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}/message/{messageID}/part/{partID}", ...options, ...params, }) } /** - * Add MCP server - * - * Dynamically add a new Model Context Protocol (MCP) server to the system. + * Update a part in a message. */ - public add( - parameters?: { + public update( + parameters: { + sessionID: string + messageID: string + partID: string directory?: string workspace?: string - name?: string - config?: McpLocalConfig | McpRemoteConfig + part?: Part2 }, options?: Options, ) { @@ -3608,16 +3772,18 @@ export class Mcp extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "path", key: "partID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "name" }, - { in: "body", key: "config" }, + { key: "part", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp", + return (options?.client ?? this.client).patch({ + url: "/session/{sessionID}/message/{messageID}/part/{partID}", ...options, ...params, headers: { @@ -3627,15 +3793,21 @@ export class Mcp extends HeyApiClient { }, }) } +} +export class History extends HeyApiClient { /** - * Connect an MCP server + * List sync events + * + * List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history. */ - public connect( - parameters: { - name: string + public list( + parameters?: { directory?: string workspace?: string + body?: { + [key: string]: number + } }, options?: Options, ) { @@ -3644,26 +3816,34 @@ export class Mcp extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { key: "body", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/connect", + return (options?.client ?? this.client).post({ + url: "/sync/history", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } +} +export class Sync extends HeyApiClient { /** - * Disconnect an MCP server + * Start workspace sync + * + * Start sync loops for workspaces in the current project that have active sessions. */ - public disconnect( - parameters: { - name: string + public start( + parameters?: { directory?: string workspace?: string }, @@ -3674,36 +3854,38 @@ export class Mcp extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/disconnect", + return (options?.client ?? this.client).post({ + url: "/sync/start", ...options, ...params, }) } - private _auth?: Auth2 - get auth(): Auth2 { - return (this._auth ??= new Auth2({ client: this.client })) - } -} - -export class Control extends HeyApiClient { /** - * Get next TUI request + * Replay sync events * - * Retrieve the next TUI (Terminal User Interface) request from the queue for processing. + * Validate and replay a complete sync event history. */ - public next( + public replay( parameters?: { - directory?: string + query_directory?: string workspace?: string + body_directory?: string + events?: Array<{ + id: string + aggregateID: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> }, options?: Options, ) { @@ -3712,29 +3894,50 @@ export class Control extends HeyApiClient { [ { args: [ - { in: "query", key: "directory" }, + { + in: "query", + key: "query_directory", + map: "directory", + }, { in: "query", key: "workspace" }, + { + in: "body", + key: "body_directory", + map: "directory", + }, + { in: "body", key: "events" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/tui/control/next", + return (options?.client ?? this.client).post({ + url: "/sync/replay", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } + private _history?: History + get history(): History { + return (this._history ??= new History({ client: this.client })) + } +} + +export class Session3 extends HeyApiClient { /** - * Submit TUI response + * List v2 sessions * - * Submit a response to the TUI request queue to complete a pending request. + * Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list. */ - public response( + public list( parameters?: { directory?: string workspace?: string - body?: unknown }, options?: Options, ) { @@ -3745,35 +3948,29 @@ export class Control extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { key: "body", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/control/response", + return (options?.client ?? this.client).get({ + url: "/api/session", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } -} -export class Tui extends HeyApiClient { /** - * Append TUI prompt + * Send v2 message * - * Append prompt to the TUI + * Create a v2 session message and queue it for the agent loop. */ - public appendPrompt( - parameters?: { + public prompt( + parameters: { + sessionID: string directory?: string workspace?: string - text?: string + prompt?: Prompt + delivery?: SessionDelivery }, options?: Options, ) { @@ -3782,15 +3979,17 @@ export class Tui extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "text" }, + { in: "body", key: "prompt" }, + { in: "body", key: "delivery" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/append-prompt", + return (options?.client ?? this.client).post({ + url: "/api/session/{sessionID}/prompt", ...options, ...params, headers: { @@ -3802,12 +4001,13 @@ export class Tui extends HeyApiClient { } /** - * Open help dialog + * Compact v2 session * - * Open the help dialog in the TUI to display user assistance information. + * Compact a v2 session conversation. */ - public openHelp( - parameters?: { + public compact( + parameters: { + sessionID: string directory?: string workspace?: string }, @@ -3818,26 +4018,28 @@ export class Tui extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/open-help", + return (options?.client ?? this.client).post({ + url: "/api/session/{sessionID}/compact", ...options, ...params, }) } /** - * Open sessions dialog + * Wait for v2 session * - * Open the session dialog + * Wait for a v2 session agent loop to become idle. */ - public openSessions( - parameters?: { + public wait( + parameters: { + sessionID: string directory?: string workspace?: string }, @@ -3848,26 +4050,28 @@ export class Tui extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/open-sessions", + return (options?.client ?? this.client).post({ + url: "/api/session/{sessionID}/wait", ...options, ...params, }) } /** - * Open themes dialog + * Get v2 session context * - * Open the theme dialog + * Retrieve the active context messages for a v2 session (all messages after the last compaction). */ - public openThemes( - parameters?: { + public context( + parameters: { + sessionID: string directory?: string workspace?: string }, @@ -3878,26 +4082,28 @@ export class Tui extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/open-themes", + return (options?.client ?? this.client).get({ + url: "/api/session/{sessionID}/context", ...options, ...params, }) } /** - * Open models dialog + * Get v2 session messages * - * Open the model dialog + * Retrieve projected v2 messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline. */ - public openModels( - parameters?: { + public messages( + parameters: { + sessionID: string directory?: string workspace?: string }, @@ -3908,25 +4114,35 @@ export class Tui extends HeyApiClient { [ { args: [ + { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/open-models", + return (options?.client ?? this.client).get({ + url: "/api/session/{sessionID}/message", ...options, ...params, }) } +} + +export class V2 extends HeyApiClient { + private _session?: Session3 + get session(): Session3 { + return (this._session ??= new Session3({ client: this.client })) + } +} +export class Control extends HeyApiClient { /** - * Submit TUI prompt + * Get next TUI request * - * Submit the prompt + * Retrieve the next TUI request from the queue for processing. */ - public submitPrompt( + public next( parameters?: { directory?: string workspace?: string @@ -3944,22 +4160,23 @@ export class Tui extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/submit-prompt", + return (options?.client ?? this.client).get({ + url: "/tui/control/next", ...options, ...params, }) } /** - * Clear TUI prompt + * Submit TUI response * - * Clear the prompt + * Submit a response to the TUI request queue to complete a pending request. */ - public clearPrompt( + public response( parameters?: { directory?: string workspace?: string + body?: unknown }, options?: Options, ) { @@ -3970,27 +4187,35 @@ export class Tui extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { key: "body", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/clear-prompt", + return (options?.client ?? this.client).post({ + url: "/tui/control/response", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } +} +export class Tui extends HeyApiClient { /** - * Execute TUI command + * Append TUI prompt * - * Execute a TUI command (e.g. agent_cycle) + * Append prompt to the TUI. */ - public executeCommand( + public appendPrompt( parameters?: { directory?: string workspace?: string - command?: string + text?: string }, options?: Options, ) { @@ -4001,13 +4226,13 @@ export class Tui extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "command" }, + { in: "body", key: "text" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/execute-command", + return (options?.client ?? this.client).post({ + url: "/tui/append-prompt", ...options, ...params, headers: { @@ -4019,18 +4244,14 @@ export class Tui extends HeyApiClient { } /** - * Show TUI toast + * Open help dialog * - * Show a toast notification in the TUI + * Open the help dialog in the TUI to display user assistance information. */ - public showToast( + public openHelp( parameters?: { directory?: string workspace?: string - title?: string - message?: string - variant?: "info" | "success" | "warning" | "error" - duration?: number }, options?: Options, ) { @@ -4041,36 +4262,26 @@ export class Tui extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "title" }, - { in: "body", key: "message" }, - { in: "body", key: "variant" }, - { in: "body", key: "duration" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/show-toast", + return (options?.client ?? this.client).post({ + url: "/tui/open-help", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } /** - * Publish TUI event + * Open sessions dialog * - * Publish a TUI event + * Open the session dialog. */ - public publish( + public openSessions( parameters?: { directory?: string workspace?: string - body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect }, options?: Options, ) { @@ -4081,33 +4292,26 @@ export class Tui extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { key: "body", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/publish", + return (options?.client ?? this.client).post({ + url: "/tui/open-sessions", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } /** - * Select session + * Open themes dialog * - * Navigate the TUI to display the specified session. + * Open the theme dialog. */ - public selectSession( + public openThemes( parameters?: { directory?: string workspace?: string - sessionID?: string }, options?: Options, ) { @@ -4118,36 +4322,23 @@ export class Tui extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "sessionID" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/tui/select-session", + return (options?.client ?? this.client).post({ + url: "/tui/open-themes", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } - private _control?: Control - get control(): Control { - return (this._control ??= new Control({ client: this.client })) - } -} - -export class Instance extends HeyApiClient { /** - * Dispose instance + * Open models dialog * - * Clean up and dispose the current OpenCode instance, releasing all resources. + * Open the model dialog. */ - public dispose( + public openModels( parameters?: { directory?: string workspace?: string @@ -4165,21 +4356,19 @@ export class Instance extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ - url: "/instance/dispose", + return (options?.client ?? this.client).post({ + url: "/tui/open-models", ...options, ...params, }) } -} -export class Path extends HeyApiClient { /** - * Get paths + * Submit TUI prompt * - * Retrieve the current working directory and related path information for the OpenCode instance. + * Submit the prompt. */ - public get( + public submitPrompt( parameters?: { directory?: string workspace?: string @@ -4197,21 +4386,19 @@ export class Path extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/path", + return (options?.client ?? this.client).post({ + url: "/tui/submit-prompt", ...options, ...params, }) } -} -export class Vcs extends HeyApiClient { /** - * Get VCS info + * Clear TUI prompt * - * Retrieve version control system (VCS) information for the current project, such as git branch. + * Clear the prompt. */ - public get( + public clearPrompt( parameters?: { directory?: string workspace?: string @@ -4229,23 +4416,23 @@ export class Vcs extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/vcs", + return (options?.client ?? this.client).post({ + url: "/tui/clear-prompt", ...options, ...params, }) } /** - * Get VCS diff + * Execute TUI command * - * Retrieve the current git diff for the working tree or against the default branch. + * Execute a TUI command. */ - public diff( - parameters: { + public executeCommand( + parameters?: { directory?: string workspace?: string - mode: "git" | "branch" + command?: string }, options?: Options, ) { @@ -4256,29 +4443,36 @@ export class Vcs extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "mode" }, + { in: "body", key: "command" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/vcs/diff", + return (options?.client ?? this.client).post({ + url: "/tui/execute-command", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } -} -export class Command extends HeyApiClient { /** - * List commands + * Show TUI toast * - * Get a list of all available commands in the OpenCode system. + * Show a toast notification in the TUI. */ - public list( + public showToast( parameters?: { directory?: string workspace?: string + title?: string + message?: string + variant?: "info" | "success" | "warning" | "error" + duration?: number }, options?: Options, ) { @@ -4289,28 +4483,36 @@ export class Command extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "title" }, + { in: "body", key: "message" }, + { in: "body", key: "variant" }, + { in: "body", key: "duration" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/command", + return (options?.client ?? this.client).post({ + url: "/tui/show-toast", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } -} -export class Lsp extends HeyApiClient { /** - * Get LSP status + * Publish TUI event * - * Get LSP server status + * Publish a TUI event. */ - public status( + public publish( parameters?: { directory?: string workspace?: string + body?: EventTuiPromptAppend2 | EventTuiCommandExecute2 | EventTuiToastShow2 | EventTuiSessionSelect2 }, options?: Options, ) { @@ -4321,28 +4523,33 @@ export class Lsp extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { key: "body", map: "body" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/lsp", + return (options?.client ?? this.client).post({ + url: "/tui/publish", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } -} -export class Formatter extends HeyApiClient { /** - * Get formatter status + * Select session * - * Get formatter status + * Navigate the TUI to display the specified session. */ - public status( + public selectSession( parameters?: { directory?: string workspace?: string + sessionID?: string }, options?: Options, ) { @@ -4353,16 +4560,27 @@ export class Formatter extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "sessionID" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/formatter", + return (options?.client ?? this.client).post({ + url: "/tui/select-session", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } + + private _control?: Control + get control(): Control { + return (this._control ??= new Control({ client: this.client })) + } } export class OpencodeClient extends HeyApiClient { @@ -4373,11 +4591,6 @@ export class OpencodeClient extends HeyApiClient { OpencodeClient.__registry.set(this, args?.key) } - private _global?: Global - get global(): Global { - return (this._global ??= new Global({ client: this.client })) - } - private _auth?: Auth get auth(): Auth { return (this._auth ??= new Auth({ client: this.client })) @@ -4388,19 +4601,14 @@ export class OpencodeClient extends HeyApiClient { return (this._app ??= new App({ client: this.client })) } - private _experimental?: Experimental - get experimental(): Experimental { - return (this._experimental ??= new Experimental({ client: this.client })) - } - - private _project?: Project - get project(): Project { - return (this._project ??= new Project({ client: this.client })) + private _global?: Global + get global(): Global { + return (this._global ??= new Global({ client: this.client })) } - private _pty?: Pty - get pty(): Pty { - return (this._pty ??= new Pty({ client: this.client })) + private _event?: Event + get event(): Event { + return (this._event ??= new Event({ client: this.client })) } private _config?: Config2 @@ -4408,6 +4616,11 @@ export class OpencodeClient extends HeyApiClient { return (this._config ??= new Config2({ client: this.client })) } + private _experimental?: Experimental + get experimental(): Experimental { + return (this._experimental ??= new Experimental({ client: this.client })) + } + private _tool?: Tool get tool(): Tool { return (this._tool ??= new Tool({ client: this.client })) @@ -4418,36 +4631,6 @@ export class OpencodeClient extends HeyApiClient { return (this._worktree ??= new Worktree({ client: this.client })) } - private _session?: Session2 - get session(): Session2 { - return (this._session ??= new Session2({ client: this.client })) - } - - private _part?: Part - get part(): Part { - return (this._part ??= new Part({ client: this.client })) - } - - private _permission?: Permission - get permission(): Permission { - return (this._permission ??= new Permission({ client: this.client })) - } - - private _question?: Question - get question(): Question { - return (this._question ??= new Question({ client: this.client })) - } - - private _provider?: Provider - get provider(): Provider { - return (this._provider ??= new Provider({ client: this.client })) - } - - private _sync?: Sync - get sync(): Sync { - return (this._sync ??= new Sync({ client: this.client })) - } - private _find?: Find get find(): Find { return (this._find ??= new Find({ client: this.client })) @@ -4458,21 +4641,6 @@ export class OpencodeClient extends HeyApiClient { return (this._file ??= new File({ client: this.client })) } - private _event?: Event - get event(): Event { - return (this._event ??= new Event({ client: this.client })) - } - - private _mcp?: Mcp - get mcp(): Mcp { - return (this._mcp ??= new Mcp({ client: this.client })) - } - - private _tui?: Tui - get tui(): Tui { - return (this._tui ??= new Tui({ client: this.client })) - } - private _instance?: Instance get instance(): Instance { return (this._instance ??= new Instance({ client: this.client })) @@ -4502,4 +4670,59 @@ export class OpencodeClient extends HeyApiClient { get formatter(): Formatter { return (this._formatter ??= new Formatter({ client: this.client })) } + + private _mcp?: Mcp + get mcp(): Mcp { + return (this._mcp ??= new Mcp({ client: this.client })) + } + + private _project?: Project + get project(): Project { + return (this._project ??= new Project({ client: this.client })) + } + + private _pty?: Pty + get pty(): Pty { + return (this._pty ??= new Pty({ client: this.client })) + } + + private _question?: Question + get question(): Question { + return (this._question ??= new Question({ client: this.client })) + } + + private _permission?: Permission + get permission(): Permission { + return (this._permission ??= new Permission({ client: this.client })) + } + + private _provider?: Provider + get provider(): Provider { + return (this._provider ??= new Provider({ client: this.client })) + } + + private _session?: Session2 + get session(): Session2 { + return (this._session ??= new Session2({ client: this.client })) + } + + private _part?: Part + get part(): Part { + return (this._part ??= new Part({ client: this.client })) + } + + private _sync?: Sync + get sync(): Sync { + return (this._sync ??= new Sync({ client: this.client })) + } + + private _v2?: V2 + get v2(): V2 { + return (this._v2 ??= new V2({ client: this.client })) + } + + private _tui?: Tui + get tui(): Tui { + return (this._tui ??= new Tui({ client: this.client })) + } } diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index e1d87fcbe689..d48c70e14c1e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -5,7 +5,109 @@ export type ClientOptions = { } <<<<<<< HEAD +<<<<<<< HEAD +======= ======= +export type Event = + | EventProjectUpdated + | EventServerInstanceDisposed + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessagePartDelta + | EventPermissionAsked + | EventPermissionReplied + | EventSessionDiff + | EventSessionError + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceRestore + | EventWorkspaceStatus + | EventFileEdited + | EventFileWatcherUpdated + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventTodoUpdated + | EventSessionStatus + | EventSessionIdle + | EventSessionCompacted + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow1 + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed + | EventCommandExecuted + | EventVcsBranchUpdated + | EventWorktreeReady + | EventWorktreeFailed + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | EventSessionNextAgentSwitched + | EventSessionNextModelSwitched + | EventSessionNextPrompted + | EventSessionNextSynthetic + | EventSessionNextShellStarted + | EventSessionNextShellEnded + | EventSessionNextStepStarted + | EventSessionNextStepEnded + | EventSessionNextTextStarted + | EventSessionNextTextDelta + | EventSessionNextTextEnded + | EventSessionNextReasoningStarted + | EventSessionNextReasoningDelta + | EventSessionNextReasoningEnded + | EventSessionNextToolInputStarted + | EventSessionNextToolInputDelta + | EventSessionNextToolInputEnded + | EventSessionNextToolCalled + | EventSessionNextToolProgress + | EventSessionNextToolSuccess + | EventSessionNextToolError + | EventSessionNextRetried + | EventSessionNextCompactionStarted + | EventSessionNextCompactionDelta + | EventSessionNextCompactionEnded + | EventServerConnected + | EventGlobalDisposed + +export type OAuth = { + type: "oauth" + refresh: string + access: string + expires: number + accountId?: string + enterpriseUrl?: string +} + +export type ApiAuth = { + type: "api" + key: string + metadata?: { + [key: string]: string + } +} + +export type WellKnownAuth = { + type: "wellknown" + key: string + token: string +} + +export type Auth = OAuth | ApiAuth | WellKnownAuth + +>>>>>>> eb048016e (sync) export type Project = { id: string worktree: string @@ -30,6 +132,7 @@ export type Project = { sandboxes: Array } +<<<<<<< HEAD export type EventProjectUpdated = { id: string type: "project.updated" @@ -136,6 +239,8 @@ export type EventMessagePartDelta = { } } +======= +>>>>>>> eb048016e (sync) export type PermissionRequest = { id: string sessionID: string @@ -151,22 +256,6 @@ export type PermissionRequest = { } } -export type EventPermissionAsked = { - id: string - type: "permission.asked" - properties: PermissionRequest -} - -export type EventPermissionReplied = { - id: string - type: "permission.replied" - properties: { - sessionID: string - requestID: string - reply: "once" | "always" | "reject" - } -} - export type SnapshotFileDiff = { file: string patch: string @@ -175,15 +264,6 @@ export type SnapshotFileDiff = { status?: "added" | "deleted" | "modified" } -export type EventSessionDiff = { - id: string - type: "session.diff" - properties: { - sessionID: string - diff: Array - } -} - export type ProviderAuthError = { name: "ProviderAuthError" data: { @@ -245,6 +325,7 @@ export type ApiError = { } } +<<<<<<< HEAD export type EventSessionError = { id: string type: "session.error" @@ -356,6 +437,8 @@ export type EventFileWatcherUpdated = { >>>>>>> 7b56f08df (fix) ======= >>>>>>> b967ee6dd (generate types) +======= +>>>>>>> eb048016e (sync) export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -380,13 +463,7 @@ export type QuestionInfo = { * Available choices */ options: Array - /** - * Allow selecting multiple choices - */ multiple?: boolean - /** - * Allow typing a custom answer (default: true) - */ custom?: boolean } @@ -405,12 +482,6 @@ export type QuestionRequest = { tool?: QuestionTool } -export type EventQuestionAsked = { - id: string - type: "question.asked" - properties: QuestionRequest -} - export type QuestionAnswer = Array export type QuestionReplied = { @@ -419,23 +490,11 @@ export type QuestionReplied = { answers: Array } -export type EventQuestionReplied = { - id: string - type: "question.replied" - properties: QuestionReplied -} - export type QuestionRejected = { sessionID: string requestID: string } -export type EventQuestionRejected = { - id: string - type: "question.rejected" - properties: QuestionRejected -} - export type Todo = { /** * Brief description of the task @@ -451,15 +510,6 @@ export type Todo = { priority: string } -export type EventTodoUpdated = { - id: string - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } -} - export type SessionStatus = | { type: "idle" @@ -474,32 +524,8 @@ export type SessionStatus = type: "busy" } -export type EventSessionStatus = { - id: string - type: "session.status" - properties: { - sessionID: string - status: SessionStatus - } -} - -export type EventSessionIdle = { - id: string - type: "session.idle" - properties: { - sessionID: string - } -} - -export type EventSessionCompacted = { - id: string - type: "session.compacted" - properties: { - sessionID: string - } -} - export type EventTuiPromptAppend = { + id: string type: "tui.prompt.append" properties: { text: string @@ -507,6 +533,7 @@ export type EventTuiPromptAppend = { } export type EventTuiCommandExecute = { + id: string type: "tui.command.execute" properties: { command: @@ -531,19 +558,18 @@ export type EventTuiCommandExecute = { } export type EventTuiToastShow = { + id: string type: "tui.toast.show" properties: { title?: string message: string variant: "info" | "success" | "warning" | "error" - /** - * Duration in milliseconds - */ duration?: number } } export type EventTuiSessionSelect = { + id: string type: "tui.session.select" properties: { /** @@ -553,6 +579,7 @@ export type EventTuiSessionSelect = { } } +<<<<<<< HEAD export type EventMcpToolsChanged = { id: string type: "mcp.tools.changed" @@ -689,6 +716,8 @@ export type EventWorktreeFailed = { } } +======= +>>>>>>> eb048016e (sync) export type Pty = { id: string title: string @@ -699,39 +728,6 @@ export type Pty = { pid: number } -export type EventPtyCreated = { - id: string - type: "pty.created" - properties: { - info: Pty - } -} - -export type EventPtyUpdated = { - id: string - type: "pty.updated" - properties: { - info: Pty - } -} - -export type EventPtyExited = { - id: string - type: "pty.exited" - properties: { - id: string - exitCode: number - } -} - -export type EventPtyDeleted = { - id: string - type: "pty.deleted" - properties: { - id: string - } -} - export type OutputFormatText = { type: "text" } @@ -817,24 +813,6 @@ export type AssistantMessage = { export type Message = UserMessage | AssistantMessage -export type EventMessageUpdated = { - id: string - type: "message.updated" - properties: { - sessionID: string - info: Message - } -} - -export type EventMessageRemoved = { - id: string - type: "message.removed" - properties: { - sessionID: string - messageID: string - } -} - export type TextPart = { id: string sessionID: string @@ -1098,27 +1076,7 @@ export type Part = | RetryPart | CompactionPart -export type EventMessagePartUpdated = { - id: string - type: "message.part.updated" - properties: { - sessionID: string - part: Part - time: number - } -} - -export type EventMessagePartRemoved = { - id: string - type: "message.part.removed" - properties: { - sessionID: string - messageID: string - partID: string - } -} - -export type PermissionAction = "allow" | "deny" | "ask" +export type PermissionAction = "allow" | "deny" | "ask" export type PermissionRule = { permission: string @@ -1168,823 +1126,1152 @@ export type Session = { } } -export type EventSessionCreated = { - id: string - type: "session.created" - properties: { - sessionID: string - info: Session - } -} - -export type EventSessionUpdated = { - id: string - type: "session.updated" - properties: { - sessionID: string - info: Session - } -} - -export type EventSessionDeleted = { - id: string - type: "session.deleted" - properties: { - sessionID: string - info: Session - } -} - -export type EventSessionNextAgentSwitched = { - id: string - type: "session.next.agent.switched" - properties: { - timestamp: number - sessionID: string - agent: string - } -} - -export type EventSessionNextModelSwitched = { - id: string - type: "session.next.model.switched" - properties: { - timestamp: number - sessionID: string - id: string - providerID: string - variant?: string - } -} - -export type PromptSource = { - start: number - end: number - text: string -} - -export type PromptFileAttachment = { - uri: string - mime: string - name?: string - description?: string - source?: PromptSource -} - -export type PromptAgentAttachment = { - name: string - source?: PromptSource -} - export type Prompt = { text: string files?: Array agents?: Array } -export type EventSessionNextPrompted = { - id: string - type: "session.next.prompted" - properties: { - timestamp: number - sessionID: string - prompt: Prompt - } +export type GlobalEvent = { + directory: string + project?: string + workspace?: string + payload: + | EventProjectUpdated + | EventServerInstanceDisposed + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessagePartDelta + | EventPermissionAsked + | EventPermissionReplied + | EventSessionDiff + | EventSessionError + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceRestore + | EventWorkspaceStatus + | EventFileEdited + | EventFileWatcherUpdated + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventTodoUpdated + | EventSessionStatus + | EventSessionIdle + | EventSessionCompacted + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed + | EventCommandExecuted + | EventVcsBranchUpdated + | EventWorktreeReady + | EventWorktreeFailed + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | EventSessionNextAgentSwitched + | EventSessionNextModelSwitched + | EventSessionNextPrompted + | EventSessionNextSynthetic + | EventSessionNextShellStarted + | EventSessionNextShellEnded + | EventSessionNextStepStarted + | EventSessionNextStepEnded + | EventSessionNextTextStarted + | EventSessionNextTextDelta + | EventSessionNextTextEnded + | EventSessionNextReasoningStarted + | EventSessionNextReasoningDelta + | EventSessionNextReasoningEnded + | EventSessionNextToolInputStarted + | EventSessionNextToolInputDelta + | EventSessionNextToolInputEnded + | EventSessionNextToolCalled + | EventSessionNextToolProgress + | EventSessionNextToolSuccess + | EventSessionNextToolError + | EventSessionNextRetried + | EventSessionNextCompactionStarted + | EventSessionNextCompactionDelta + | EventSessionNextCompactionEnded + | EventServerConnected + | EventGlobalDisposed + | SyncEventMessageUpdated + | SyncEventMessageRemoved + | SyncEventMessagePartUpdated + | SyncEventMessagePartRemoved + | SyncEventSessionCreated + | SyncEventSessionUpdated + | SyncEventSessionDeleted + | SyncEventSessionNextAgentSwitched + | SyncEventSessionNextModelSwitched + | SyncEventSessionNextPrompted + | SyncEventSessionNextSynthetic + | SyncEventSessionNextShellStarted + | SyncEventSessionNextShellEnded + | SyncEventSessionNextStepStarted + | SyncEventSessionNextStepEnded + | SyncEventSessionNextTextStarted + | SyncEventSessionNextTextDelta + | SyncEventSessionNextTextEnded + | SyncEventSessionNextReasoningStarted + | SyncEventSessionNextReasoningDelta + | SyncEventSessionNextReasoningEnded + | SyncEventSessionNextToolInputStarted + | SyncEventSessionNextToolInputDelta + | SyncEventSessionNextToolInputEnded + | SyncEventSessionNextToolCalled + | SyncEventSessionNextToolProgress + | SyncEventSessionNextToolSuccess + | SyncEventSessionNextToolError + | SyncEventSessionNextRetried + | SyncEventSessionNextCompactionStarted + | SyncEventSessionNextCompactionDelta + | SyncEventSessionNextCompactionEnded } -export type EventSessionNextSynthetic = { - id: string - type: "session.next.synthetic" - properties: { - timestamp: number - sessionID: string - text: string - } -} +/** + * Log level + */ +export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" -export type EventSessionNextShellStarted = { - id: string - type: "session.next.shell.started" - properties: { - timestamp: number - sessionID: string - callID: string - command: string - } +/** + * Server configuration for opencode serve and web commands + */ +export type ServerConfig = { + port?: number + hostname?: string + mdns?: boolean + mdnsDomain?: string + cors?: Array } -export type EventSessionNextShellEnded = { - id: string - type: "session.next.shell.ended" - properties: { - timestamp: number - sessionID: string - callID: string - output: string - } -} +export type PermissionActionConfig = "ask" | "allow" | "deny" -export type EventSessionNextStepStarted = { - id: string - type: "session.next.step.started" - properties: { - timestamp: number - sessionID: string - agent: string - model: { - id: string - providerID: string - variant?: string - } - snapshot?: string - } +export type PermissionObjectConfig = { + [key: string]: PermissionActionConfig } -export type EventSessionNextStepEnded = { - id: string - type: "session.next.step.ended" - properties: { - timestamp: number - sessionID: string - finish: string - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } +export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig + +export type PermissionConfig = + | PermissionActionConfig + | { + read?: PermissionRuleConfig + edit?: PermissionRuleConfig + glob?: PermissionRuleConfig + grep?: PermissionRuleConfig + list?: PermissionRuleConfig + bash?: PermissionRuleConfig + task?: PermissionRuleConfig + external_directory?: PermissionRuleConfig + todowrite?: PermissionActionConfig + question?: PermissionActionConfig + webfetch?: PermissionActionConfig + websearch?: PermissionActionConfig + lsp?: PermissionRuleConfig + doom_loop?: PermissionActionConfig + skill?: PermissionRuleConfig + [key: string]: PermissionRuleConfig | PermissionActionConfig | undefined } - snapshot?: string - } -} -export type EventSessionNextTextStarted = { - id: string - type: "session.next.text.started" - properties: { - timestamp: number - sessionID: string +export type AgentConfig = { + model?: string + variant?: string + temperature?: number + top_p?: number + prompt?: string + tools?: { + [key: string]: boolean } -} - -export type EventSessionNextTextDelta = { - id: string - type: "session.next.text.delta" - properties: { - timestamp: number - sessionID: string - delta: string + disable?: boolean + description?: string + mode?: "subagent" | "primary" | "all" + hidden?: boolean + options?: { + [key: string]: unknown } + /** + * Hex color code (e.g., #FF5733) or theme color (e.g., primary) + */ + color?: string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" + steps?: number + maxSteps?: number + permission?: PermissionConfig + [key: string]: + | unknown + | string + | number + | { + [key: string]: boolean + } + | boolean + | "subagent" + | "primary" + | "all" + | { + [key: string]: unknown + } + | string + | "primary" + | "secondary" + | "accent" + | "success" + | "warning" + | "error" + | "info" + | number + | PermissionConfig + | undefined } -export type EventSessionNextTextEnded = { - id: string - type: "session.next.text.ended" - properties: { - timestamp: number - sessionID: string - text: string +export type ProviderConfig = { + api?: string + name?: string + env?: Array + id?: string + npm?: string + whitelist?: Array + blacklist?: Array + options?: { + apiKey?: string + baseURL?: string + enterpriseUrl?: string + setCacheKey?: boolean + /** + * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. + */ + timeout?: number | false + chunkTimeout?: number + [key: string]: unknown | string | boolean | number | false | number | undefined } -} - -export type EventSessionNextReasoningStarted = { - id: string - type: "session.next.reasoning.started" - properties: { - timestamp: number - sessionID: string - reasoningID: string + models?: { + [key: string]: { + id?: string + name?: string + family?: string + release_date?: string + attachment?: boolean + reasoning?: boolean + temperature?: boolean + tool_call?: boolean + interleaved?: + | true + | { + field: "reasoning_content" | "reasoning_details" + } + cost?: { + input: number + output: number + cache_read?: number + cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } + } + limit?: { + context: number + input?: number + output: number + } + modalities?: { + input: Array<"text" | "audio" | "image" | "video" | "pdf"> + output: Array<"text" | "audio" | "image" | "video" | "pdf"> + } + experimental?: boolean + status?: "alpha" | "beta" | "deprecated" + provider?: { + npm?: string + api?: string + } + options?: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + /** + * Variant-specific configuration + */ + variants?: { + [key: string]: { + disabled?: boolean + [key: string]: unknown | boolean | undefined + } + } + } } } -export type EventSessionNextReasoningDelta = { - id: string - type: "session.next.reasoning.delta" - properties: { - timestamp: number - sessionID: string - reasoningID: string - delta: string +export type McpLocalConfig = { + /** + * Type of MCP server connection + */ + type: "local" + /** + * Command and arguments to run the MCP server + */ + command: Array + environment?: { + [key: string]: string } + enabled?: boolean + timeout?: number } -export type EventSessionNextReasoningEnded = { - id: string - type: "session.next.reasoning.ended" - properties: { - timestamp: number - sessionID: string - reasoningID: string - text: string - } +export type McpOAuthConfig = { + clientId?: string + clientSecret?: string + scope?: string + redirectUri?: string } -export type EventSessionNextToolInputStarted = { - id: string - type: "session.next.tool.input.started" - properties: { - timestamp: number - sessionID: string - callID: string - name: string +export type McpRemoteConfig = { + /** + * Type of MCP server connection + */ + type: "remote" + /** + * URL of the remote MCP server + */ + url: string + enabled?: boolean + headers?: { + [key: string]: string } + /** + * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. + */ + oauth?: McpOAuthConfig | false + timeout?: number } -export type EventSessionNextToolInputDelta = { - id: string - type: "session.next.tool.input.delta" - properties: { - timestamp: number - sessionID: string - callID: string - delta: string - } -} +/** + * @deprecated Always uses stretch layout. + */ +export type LayoutConfig = "auto" | "stretch" -export type EventSessionNextToolInputEnded = { - id: string - type: "session.next.tool.input.ended" - properties: { - timestamp: number - sessionID: string - callID: string - text: string +export type Config = { + $schema?: string + shell?: string + logLevel?: LogLevel + server?: ServerConfig + command?: { + [key: string]: { + template: string + description?: string + agent?: string + model?: string + subtask?: boolean + } + } + skills?: { + paths?: Array + urls?: Array + } + watcher?: { + ignore?: Array + } + snapshot?: boolean + plugin?: Array< + | string + | [ + string, + { + [key: string]: unknown + }, + ] + > + share?: "manual" | "auto" | "disabled" + autoshare?: boolean + /** + * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications + */ + autoupdate?: boolean | "notify" + disabled_providers?: Array + enabled_providers?: Array + model?: string + small_model?: string + default_agent?: string + username?: string + mode?: { + build?: AgentConfig + plan?: AgentConfig + [key: string]: AgentConfig | undefined + } + agent?: { + plan?: AgentConfig + build?: AgentConfig + general?: AgentConfig + explore?: AgentConfig + title?: AgentConfig + summary?: AgentConfig + compaction?: AgentConfig + [key: string]: AgentConfig | undefined + } + provider?: { + [key: string]: ProviderConfig + } + mcp?: { + [key: string]: + | McpLocalConfig + | McpRemoteConfig + | { + enabled: boolean + } + } + /** + * Enable or configure formatters. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides. + */ + formatter?: + | boolean + | { + [key: string]: { + disabled?: boolean + command?: Array + environment?: { + [key: string]: string + } + extensions?: Array + } + } + /** + * Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides. + */ + lsp?: + | boolean + | { + [key: string]: + | { + disabled: true + } + | { + command: Array + extensions?: Array + disabled?: boolean + env?: { + [key: string]: string + } + initialization?: { + [key: string]: unknown + } + } + } + instructions?: Array + layout?: LayoutConfig + permission?: PermissionConfig + tools?: { + [key: string]: boolean + } + enterprise?: { + url?: string + } + tool_output?: { + max_lines?: number + max_bytes?: number + } + compaction?: { + auto?: boolean + prune?: boolean + tail_turns?: number + preserve_recent_tokens?: number + reserved?: number + } + experimental?: { + disable_paste_summary?: boolean + batch_tool?: boolean + openTelemetry?: boolean + primary_tools?: Array + continue_loop_on_deny?: boolean + mcp_timeout?: number } } -export type EventSessionNextToolCalled = { +export type Model = { id: string - type: "session.next.tool.called" - properties: { - timestamp: number - sessionID: string - callID: string - tool: string + providerID: string + api: { + id: string + url: string + npm: string + } + name: string + family?: string + capabilities: { + temperature: boolean + reasoning: boolean + attachment: boolean + toolcall: boolean input: { - [key: string]: unknown + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean } - provider: { - executed: boolean - metadata?: { - [key: string]: unknown + output: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + interleaved: + | boolean + | { + field: "reasoning_content" | "reasoning_details" + } + } + cost: { + input: number + output: number + cache: { + read: number + write: number + } + experimentalOver200K?: { + input: number + output: number + cache: { + read: number + write: number } } } + limit: { + context: number + input?: number + output: number + } + status: "alpha" | "beta" | "deprecated" | "active" + options: { + [key: string]: unknown + } + headers: { + [key: string]: string + } + release_date: string + variants?: { + [key: string]: { + [key: string]: unknown + } + } } -export type ToolTextContent = { - type: "text" - text: string +export type Provider = { + id: string + name: string + source: "env" | "config" | "custom" | "api" + env: Array + key?: string + options: { + [key: string]: unknown + } + models: { + [key: string]: Model + } } -export type ToolFileContent = { - type: "file" - uri: string - mime: string - name?: string +export type ConsoleState = { + consoleManagedProviders: Array + activeOrgName?: string + switchableOrgCount: number } -export type EventSessionNextToolProgress = { +export type ToolListItem = { id: string - type: "session.next.tool.progress" - properties: { - timestamp: number - sessionID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - } + description: string + parameters: unknown } -export type EventSessionNextToolSuccess = { - id: string - type: "session.next.tool.success" - properties: { - timestamp: number - sessionID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array - provider: { - executed: boolean - metadata?: { - [key: string]: unknown - } - } - } -} +export type ToolList = Array -export type EventSessionNextToolError = { - id: string - type: "session.next.tool.error" - properties: { - timestamp: number - sessionID: string - callID: string - error: { - type: string - message: string - } - provider: { - executed: boolean - metadata?: { - [key: string]: unknown - } - } - } +export type ToolIds = Array + +export type WorktreeCreateInput = { + name?: string + /** + * Additional startup script to run after the project's start command + */ + startCommand?: string } -export type SessionNextRetryError = { - message: string - statusCode?: number - isRetryable: boolean - responseHeaders?: { - [key: string]: string - } - responseBody?: string - metadata?: { - [key: string]: string - } +export type Worktree = { + name: string + branch: string + directory: string } -export type EventSessionNextRetried = { - id: string - type: "session.next.retried" - properties: { - timestamp: number - sessionID: string - attempt: number - error: SessionNextRetryError - } +export type WorktreeRemoveInput = { + directory: string } -export type EventSessionNextCompactionStarted = { - id: string - type: "session.next.compaction.started" - properties: { - timestamp: number - sessionID: string - reason: "auto" | "manual" - } +export type WorktreeResetInput = { + directory: string } -export type EventSessionNextCompactionDelta = { +export type ProjectSummary = { id: string - type: "session.next.compaction.delta" - properties: { - timestamp: number - sessionID: string - text: string - } + name?: string + worktree: string } -export type EventSessionNextCompactionEnded = { +export type GlobalSession = { id: string - type: "session.next.compaction.ended" - properties: { - timestamp: number - sessionID: string - text: string - include?: string + slug: string + projectID: string + workspaceID?: string + directory: string + path?: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + share?: { + url: string + } + title: string + agent?: string + model?: { + id: string + providerID: string + variant?: string } + version: string + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } + project: ProjectSummary | null } -export type EventServerConnected = { - id: string - type: "server.connected" - properties: { - [key: string]: unknown - } +export type McpResource = { + name: string + uri: string + description?: string + mimeType?: string + client: string } -export type EventGlobalDisposed = { - id: string - type: "global.disposed" - properties: { - [key: string]: unknown +export type Symbol = { + name: string + kind: number + location: { + uri: string + range: Range } } -export type SyncEventMessageUpdated = { - type: "sync" - name: "message.updated.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - info: Message - } +export type FileNode = { + name: string + path: string + absolute: string + type: "file" | "directory" + ignored: boolean } -export type SyncEventMessageRemoved = { - type: "sync" - name: "message.removed.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - messageID: string +export type FileContent = { + type: "text" | "binary" + content: string + diff?: string + patch?: { + oldFileName: string + newFileName: string + oldHeader?: string + newHeader?: string + hunks: Array<{ + oldStart: number + oldLines: number + newStart: number + newLines: number + lines: Array + }> + index?: string } + encoding?: "base64" + mimeType?: string } -export type SyncEventMessagePartUpdated = { - type: "sync" - name: "message.part.updated.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - part: Part - time: number - } +export type File = { + path: string + added: number + removed: number + status: "added" | "deleted" | "modified" } -export type SyncEventMessagePartRemoved = { - type: "sync" - name: "message.part.removed.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - messageID: string - partID: string - } +export type Path = { + home: string + state: string + config: string + worktree: string + directory: string } -export type SyncEventSessionCreated = { - type: "sync" - name: "session.created.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - info: Session - } +export type VcsInfo = { + branch?: string + default_branch?: string } -export type SyncEventSessionUpdated = { - type: "sync" - name: "session.updated.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - info: { - id?: string | null - slug?: string | null - projectID?: string | null - workspaceID?: string | null - directory?: string | null - path?: string | null - parentID?: string | null - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } | null - share?: { - url?: string | null - } - title?: string | null - agent?: string | null - model?: { - id: string - providerID: string - variant?: string - } | null - version?: string | null - time?: { - created?: number | null - updated?: number | null - compacting?: number | null - archived?: number | null - } - permission?: PermissionRuleset | null - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } | null - } - } -} - -export type SyncEventSessionDeleted = { - type: "sync" - name: "session.deleted.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - info: Session - } +export type VcsFileDiff = { + file: string + patch: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" } -export type SyncEventSessionNextAgentSwitched = { - type: "sync" - name: "session.next.agent.switched.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - agent: string - } +export type Command = { + name: string + description?: string + agent?: string + model?: string + source?: "command" | "mcp" | "skill" + template: string + subtask?: boolean + hints: Array } -export type SyncEventSessionNextModelSwitched = { - type: "sync" - name: "session.next.model.switched.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - id: string +export type Agent = { + name: string + description?: string + mode: "subagent" | "primary" | "all" + native?: boolean + hidden?: boolean + topP?: number + temperature?: number + color?: string + permission: PermissionRuleset + model?: { + modelID: string providerID: string - variant?: string } + variant?: string + prompt?: string + options: { + [key: string]: unknown + } + steps?: number } -export type SyncEventSessionNextPrompted = { - type: "sync" - name: "session.next.prompted.1" +export type LspStatus = { id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - prompt: Prompt - } + name: string + root: string + status: "connected" | "error" } -export type SyncEventSessionNextSynthetic = { - type: "sync" - name: "session.next.synthetic.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - text: string - } +export type FormatterStatus = { + name: string + extensions: Array + enabled: boolean } -export type SyncEventSessionNextShellStarted = { - type: "sync" - name: "session.next.shell.started.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - callID: string - command: string - } +export type McpStatusConnected = { + status: "connected" } -export type SyncEventSessionNextShellEnded = { - type: "sync" - name: "session.next.shell.ended.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - callID: string - output: string - } +export type McpStatusDisabled = { + status: "disabled" } -export type SyncEventSessionNextStepStarted = { - type: "sync" - name: "session.next.step.started.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - agent: string - model: { - id: string - providerID: string - variant?: string - } - snapshot?: string - } +export type McpStatusFailed = { + status: "failed" + error: string } -export type SyncEventSessionNextStepEnded = { - type: "sync" - name: "session.next.step.ended.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - finish: string - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - snapshot?: string - } +export type McpStatusNeedsAuth = { + status: "needs_auth" } -export type SyncEventSessionNextTextStarted = { - type: "sync" - name: "session.next.text.started.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - } +export type McpStatusNeedsClientRegistration = { + status: "needs_client_registration" + error: string } -export type SyncEventSessionNextTextDelta = { - type: "sync" - name: "session.next.text.delta.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string - delta: string - } +export type McpStatus = + | McpStatusConnected + | McpStatusDisabled + | McpStatusFailed + | McpStatusNeedsAuth + | McpStatusNeedsClientRegistration + +export type McpUnsupportedOAuthError = { + error: string } -export type SyncEventSessionNextTextEnded = { - type: "sync" - name: "session.next.text.ended.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - timestamp: number - sessionID: string +export type ProviderAuthMethod = { + type: "oauth" | "api" + label: string + prompts?: Array< + | { + type: "text" + key: string + message: string + placeholder?: string + when?: { + key: string + op: "eq" | "neq" + value: string + } + } + | { + type: "select" + key: string + message: string + options: Array<{ + label: string + value: string + hint?: string + }> + when?: { + key: string + op: "eq" | "neq" + value: string + } + } + > +} + +export type ProviderAuthAuthorization = { + url: string + method: "auto" | "code" + instructions: string +} + +export type TextPartInput = { + id?: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } +} + +export type FilePartInput = { + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type AgentPartInput = { + id?: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type SubtaskPartInput = { + id?: string + type: "subtask" + prompt: string + description: string + agent: string + model?: { + providerID: string + modelID: string + } + command?: string +} + +export type V2SessionsResponse = { + items: Array + cursor: { + previous?: string + next?: string + } +} + +export type V2SessionMessagesResponse = { + items: Array + cursor: { + previous?: string + next?: string + } +} + +export type EventTuiPromptAppend2 = { + type: "tui.prompt.append" + properties: { text: string } } -export type SyncEventSessionNextReasoningStarted = { +export type EventTuiCommandExecute2 = { + 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 EventTuiToastShow2 = { + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + duration?: number + } +} + +export type EventTuiSessionSelect2 = { + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + +export type Workspace = { + id: string + type: string + name: string + branch: string | null + directory: string | null + extra: unknown | null + projectID: string +} + +export type SyncEventMessageUpdated = { type: "sync" - name: "session.next.reasoning.started.1" + name: "message.updated.1" id: string seq: number aggregateID: "sessionID" data: { - timestamp: number sessionID: string - reasoningID: string + info: Message } } -export type SyncEventSessionNextReasoningDelta = { +export type SyncEventMessageRemoved = { type: "sync" - name: "session.next.reasoning.delta.1" + name: "message.removed.1" id: string seq: number aggregateID: "sessionID" data: { - timestamp: number sessionID: string - reasoningID: string - delta: string + messageID: string } } -export type SyncEventSessionNextReasoningEnded = { +export type SyncEventMessagePartUpdated = { type: "sync" - name: "session.next.reasoning.ended.1" + name: "message.part.updated.1" id: string seq: number aggregateID: "sessionID" data: { - timestamp: number sessionID: string - reasoningID: string - text: string + part: Part + time: number } } -export type SyncEventSessionNextToolInputStarted = { +export type SyncEventMessagePartRemoved = { type: "sync" - name: "session.next.tool.input.started.1" + name: "message.part.removed.1" id: string seq: number aggregateID: "sessionID" data: { - timestamp: number sessionID: string - callID: string - name: string + messageID: string + partID: string } } -export type SyncEventSessionNextToolInputDelta = { +export type SyncEventSessionCreated = { type: "sync" - name: "session.next.tool.input.delta.1" + name: "session.created.1" id: string seq: number aggregateID: "sessionID" data: { - timestamp: number sessionID: string - callID: string - delta: string + info: Session } } -export type SyncEventSessionNextToolInputEnded = { +export type SyncEventSessionUpdated = { type: "sync" - name: "session.next.tool.input.ended.1" + name: "session.updated.1" id: string seq: number aggregateID: "sessionID" data: { - timestamp: number sessionID: string - callID: string - text: string + info: { + id?: string | null + slug?: string | null + projectID?: string | null + workspaceID?: string | null + directory?: string | null + path?: string | null + parentID?: string | null + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } | null + share?: { + url?: string | null + } + title?: string | null + agent?: string | null + model?: { + id: string + providerID: string + variant?: string + } | null + version?: string | null + time?: { + created?: number | null + updated?: number | null + compacting?: number | null + archived?: number | null + } + permission?: PermissionRuleset | null + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } | null + } } } -export type SyncEventSessionNextToolCalled = { +export type SyncEventSessionDeleted = { type: "sync" - name: "session.next.tool.called.1" + name: "session.deleted.1" id: string seq: number aggregateID: "sessionID" data: { - timestamp: number sessionID: string - callID: string - tool: string - input: { - [key: string]: unknown - } - provider: { - executed: boolean - metadata?: { - [key: string]: unknown - } - } + info: Session } } -export type SyncEventSessionNextToolProgress = { +export type SyncEventSessionNextAgentSwitched = { type: "sync" - name: "session.next.tool.progress.1" + name: "session.next.agent.switched.1" id: string seq: number aggregateID: "sessionID" data: { timestamp: number sessionID: string - callID: string - structured: { - [key: string]: unknown - } - content: Array + agent: string } } -export type SyncEventSessionNextToolSuccess = { +export type SyncEventSessionNextModelSwitched = { type: "sync" - name: "session.next.tool.success.1" + name: "session.next.model.switched.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + id: string + providerID: string + variant?: string + } +} + +export type SyncEventSessionNextPrompted = { + type: "sync" + name: "session.next.prompted.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + prompt: Prompt + } +} + +export type SyncEventSessionNextSynthetic = { + type: "sync" + name: "session.next.synthetic.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + text: string + } +} + +export type SyncEventSessionNextShellStarted = { + type: "sync" + name: "session.next.shell.started.1" id: string seq: number aggregateID: "sessionID" @@ -1992,22 +2279,13 @@ export type SyncEventSessionNextToolSuccess = { timestamp: number sessionID: string callID: string - structured: { - [key: string]: unknown - } - content: Array - provider: { - executed: boolean - metadata?: { - [key: string]: unknown - } - } + command: string } } -export type SyncEventSessionNextToolError = { +export type SyncEventSessionNextShellEnded = { type: "sync" - name: "session.next.tool.error.1" + name: "session.next.shell.ended.1" id: string seq: number aggregateID: "sessionID" @@ -2015,49 +2293,81 @@ export type SyncEventSessionNextToolError = { timestamp: number sessionID: string callID: string - error: { - type: string - message: string + output: string + } +} + +export type SyncEventSessionNextStepStarted = { + type: "sync" + name: "session.next.step.started.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + agent: string + model: { + id: string + providerID: string + variant?: string } - provider: { - executed: boolean - metadata?: { - [key: string]: unknown + snapshot?: string + } +} + +export type SyncEventSessionNextStepEnded = { + type: "sync" + name: "session.next.step.ended.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + finish: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number } } + snapshot?: string } } -export type SyncEventSessionNextRetried = { +export type SyncEventSessionNextTextStarted = { type: "sync" - name: "session.next.retried.1" + name: "session.next.text.started.1" id: string seq: number aggregateID: "sessionID" data: { timestamp: number sessionID: string - attempt: number - error: SessionNextRetryError } } -export type SyncEventSessionNextCompactionStarted = { +export type SyncEventSessionNextTextDelta = { type: "sync" - name: "session.next.compaction.started.1" + name: "session.next.text.delta.1" id: string seq: number aggregateID: "sessionID" data: { timestamp: number sessionID: string - reason: "auto" | "manual" + delta: string } } -export type SyncEventSessionNextCompactionDelta = { +export type SyncEventSessionNextTextEnded = { type: "sync" - name: "session.next.compaction.delta.1" + name: "session.next.text.ended.1" id: string seq: number aggregateID: "sessionID" @@ -2068,161 +2378,371 @@ export type SyncEventSessionNextCompactionDelta = { } } -export type SyncEventSessionNextCompactionEnded = { +export type SyncEventSessionNextReasoningStarted = { type: "sync" - name: "session.next.compaction.ended.1" + name: "session.next.reasoning.started.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + reasoningID: string + } +} + +export type SyncEventSessionNextReasoningDelta = { + type: "sync" + name: "session.next.reasoning.delta.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + reasoningID: string + delta: string + } +} + +export type SyncEventSessionNextReasoningEnded = { + type: "sync" + name: "session.next.reasoning.ended.1" id: string seq: number aggregateID: "sessionID" data: { timestamp: number sessionID: string + reasoningID: string text: string - include?: string } } -export type GlobalEvent = { - directory: string - project?: string - workspace?: string - payload: -<<<<<<< HEAD - | EventServerInstanceDisposed - | EventFileEdited - | EventFileWatcherUpdated -======= - | EventProjectUpdated - | EventServerInstanceDisposed -<<<<<<< HEAD -<<<<<<< HEAD -======= - | EventServerConnected - | EventGlobalDisposed - | EventFileEdited - | EventFileWatcherUpdated -<<<<<<< HEAD -======= - | EventServerConnected - | EventGlobalDisposed - | EventServerInstanceDisposed ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) -<<<<<<< HEAD ->>>>>>> 9a1d0085d (sync) -======= -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) -======= ->>>>>>> b967ee6dd (generate types) - | EventLspClientDiagnostics - | EventLspUpdated - | EventMessagePartDelta - | EventPermissionAsked - | EventPermissionReplied - | EventSessionDiff - | EventSessionError - | EventInstallationUpdated - | EventInstallationUpdateAvailable -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= ->>>>>>> d0ca805a7 (sync) -======= ->>>>>>> b967ee6dd (generate types) - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceRestore - | EventWorkspaceStatus - | EventFileEdited - | EventFileWatcherUpdated -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) -<<<<<<< HEAD ->>>>>>> 9a1d0085d (sync) -======= -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) -======= ->>>>>>> b967ee6dd (generate types) - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventTodoUpdated - | EventSessionStatus - | EventSessionIdle - | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventMcpBrowserOpenFailed - | EventCommandExecuted - | EventProjectUpdated - | EventVcsBranchUpdated -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -======= +export type SyncEventSessionNextToolInputStarted = { + type: "sync" + name: "session.next.tool.input.started.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + callID: string + name: string + } +} + +export type SyncEventSessionNextToolInputDelta = { + type: "sync" + name: "session.next.tool.input.delta.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + callID: string + delta: string + } +} + +export type SyncEventSessionNextToolInputEnded = { + type: "sync" + name: "session.next.tool.input.ended.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + callID: string + text: string + } +} + +export type SyncEventSessionNextToolCalled = { + type: "sync" + name: "session.next.tool.called.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + callID: string + tool: string + input: { + [key: string]: unknown + } + provider: { + executed: boolean + metadata?: { + [key: string]: unknown + } + } + } +} + +export type SyncEventSessionNextToolProgress = { + type: "sync" + name: "session.next.tool.progress.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + } +} + +export type SyncEventSessionNextToolSuccess = { + type: "sync" + name: "session.next.tool.success.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + provider: { + executed: boolean + metadata?: { + [key: string]: unknown + } + } + } +} + +export type SyncEventSessionNextToolError = { + type: "sync" + name: "session.next.tool.error.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + callID: string + error: { + type: string + message: string + } + provider: { + executed: boolean + metadata?: { + [key: string]: unknown + } + } + } +} + +export type SyncEventSessionNextRetried = { + type: "sync" + name: "session.next.retried.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + attempt: number + error: SessionNextRetryError + } +} + +export type SyncEventSessionNextCompactionStarted = { + type: "sync" + name: "session.next.compaction.started.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + reason: "auto" | "manual" + } +} + +export type SyncEventSessionNextCompactionDelta = { + type: "sync" + name: "session.next.compaction.delta.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + text: string + } +} + +export type SyncEventSessionNextCompactionEnded = { + type: "sync" + name: "session.next.compaction.ended.1" + id: string + seq: number + aggregateID: "sessionID" + data: { + timestamp: number + sessionID: string + text: string + include?: string + } +} + +<<<<<<< HEAD +export type GlobalEvent = { + directory: string + project?: string + workspace?: string + payload: +<<<<<<< HEAD + | EventServerInstanceDisposed + | EventFileEdited + | EventFileWatcherUpdated +======= + | EventProjectUpdated + | EventServerInstanceDisposed +<<<<<<< HEAD <<<<<<< HEAD ======= + | EventServerConnected + | EventGlobalDisposed + | EventFileEdited + | EventFileWatcherUpdated <<<<<<< HEAD +======= + | EventServerConnected + | EventGlobalDisposed + | EventServerInstanceDisposed +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) <<<<<<< HEAD >>>>>>> 9a1d0085d (sync) ======= +======= >>>>>>> 3e6a37422 (fix) >>>>>>> 7b56f08df (fix) +======= +>>>>>>> b967ee6dd (generate types) + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessagePartDelta + | EventPermissionAsked + | EventPermissionReplied + | EventSessionDiff + | EventSessionError + | EventInstallationUpdated + | EventInstallationUpdateAvailable +<<<<<<< HEAD +<<<<<<< HEAD +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +>>>>>>> d0ca805a7 (sync) +======= +>>>>>>> b967ee6dd (generate types) | EventWorkspaceReady | EventWorkspaceFailed | EventWorkspaceRestore | EventWorkspaceStatus + | EventFileEdited + | EventFileWatcherUpdated <<<<<<< HEAD <<<<<<< HEAD ======= -======= >>>>>>> 1a2dc3e57 (sync) >>>>>>> d0ca805a7 (sync) +<<<<<<< HEAD >>>>>>> 9a1d0085d (sync) ======= ======= ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) -======= >>>>>>> 3e6a37422 (fix) >>>>>>> 7b56f08df (fix) ======= >>>>>>> b967ee6dd (generate types) - | EventWorktreeReady - | EventWorktreeFailed - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - | EventSessionNextAgentSwitched - | EventSessionNextModelSwitched - | EventSessionNextPrompted - | EventSessionNextSynthetic - | EventSessionNextShellStarted - | EventSessionNextShellEnded - | EventSessionNextStepStarted + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventTodoUpdated + | EventSessionStatus + | EventSessionIdle + | EventSessionCompacted + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed + | EventCommandExecuted + | EventProjectUpdated + | EventVcsBranchUpdated +<<<<<<< HEAD +<<<<<<< HEAD +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +<<<<<<< HEAD +<<<<<<< HEAD +>>>>>>> 9a1d0085d (sync) +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceRestore + | EventWorkspaceStatus +<<<<<<< HEAD +<<<<<<< HEAD +======= +======= +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) +>>>>>>> 9a1d0085d (sync) +======= +======= +>>>>>>> 1a2dc3e57 (sync) +>>>>>>> d0ca805a7 (sync) +======= +>>>>>>> 3e6a37422 (fix) +>>>>>>> 7b56f08df (fix) +======= +>>>>>>> b967ee6dd (generate types) + | EventWorktreeReady + | EventWorktreeFailed + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | EventSessionNextAgentSwitched + | EventSessionNextModelSwitched + | EventSessionNextPrompted + | EventSessionNextSynthetic + | EventSessionNextShellStarted + | EventSessionNextShellEnded + | EventSessionNextStepStarted | EventSessionNextStepEnded | EventSessionNextTextStarted | EventSessionNextTextDelta @@ -2275,714 +2795,1085 @@ export type GlobalEvent = { | SyncEventSessionNextCompactionStarted | SyncEventSessionNextCompactionDelta | SyncEventSessionNextCompactionEnded +======= +export type EventProjectUpdated = { + id: string + type: "project.updated" + properties: Project +>>>>>>> eb048016e (sync) } -/** - * Log level - */ -export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" +export type EventServerInstanceDisposed = { + id: string + type: "server.instance.disposed" + properties: { + directory: string + } +} -/** - * Server configuration for opencode serve and web commands - */ -export type ServerConfig = { - /** - * Port to listen on - */ - port?: number - /** - * Hostname to listen on - */ - hostname?: string - /** - * Enable mDNS service discovery - */ - mdns?: boolean - /** - * Custom domain name for mDNS service (default: opencode.local) - */ - mdnsDomain?: string - /** - * Additional domains to allow for CORS - */ - cors?: Array +export type EventLspClientDiagnostics = { + id: string + type: "lsp.client.diagnostics" + properties: { + serverID: string + path: string + } } -export type PermissionActionConfig = "ask" | "allow" | "deny" +export type EventLspUpdated = { + id: string + type: "lsp.updated" + properties: { + [key: string]: unknown + } +} -export type PermissionObjectConfig = { - [key: string]: PermissionActionConfig +export type EventMessagePartDelta = { + id: string + type: "message.part.delta" + properties: { + sessionID: string + messageID: string + partID: string + field: string + delta: string + } } -export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig +export type EventPermissionAsked = { + id: string + type: "permission.asked" + properties: PermissionRequest +} -export type PermissionConfig = - | PermissionActionConfig - | { - read?: PermissionRuleConfig - edit?: PermissionRuleConfig - glob?: PermissionRuleConfig - grep?: PermissionRuleConfig - list?: PermissionRuleConfig - bash?: PermissionRuleConfig - task?: PermissionRuleConfig - external_directory?: PermissionRuleConfig - todowrite?: PermissionActionConfig - question?: PermissionActionConfig - webfetch?: PermissionActionConfig - websearch?: PermissionActionConfig - lsp?: PermissionRuleConfig - doom_loop?: PermissionActionConfig - skill?: PermissionRuleConfig - [key: string]: PermissionRuleConfig | PermissionActionConfig | undefined - } +export type EventPermissionReplied = { + id: string + type: "permission.replied" + properties: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" + } +} -export type AgentConfig = { - model?: string - /** - * Default model variant for this agent (applies only when using the agent's configured model). - */ - variant?: string - temperature?: number - top_p?: number - prompt?: string - /** - * @deprecated Use 'permission' field instead - */ - tools?: { - [key: string]: boolean +export type EventSessionDiff = { + id: string + type: "session.diff" + properties: { + sessionID: string + diff: Array } - disable?: boolean - /** - * Description of when to use the agent - */ - description?: string - mode?: "subagent" | "primary" | "all" - /** - * Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent) - */ - hidden?: boolean - options?: { - [key: string]: unknown +} + +export type EventSessionError = { + id: string + type: "session.error" + properties: { + sessionID?: string + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ApiError } - /** - * Hex color code (e.g., #FF5733) or theme color (e.g., primary) - */ - color?: string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" - /** - * Maximum number of agentic iterations before forcing text-only response - */ - steps?: number - /** - * @deprecated Use 'steps' field instead. - */ - maxSteps?: number - permission?: PermissionConfig - [key: string]: - | unknown - | string - | number - | { - [key: string]: boolean - } - | boolean - | "subagent" - | "primary" - | "all" - | { - [key: string]: unknown - } - | string - | "primary" - | "secondary" - | "accent" - | "success" - | "warning" - | "error" - | "info" - | number - | PermissionConfig - | undefined } -export type ProviderConfig = { - api?: string - name?: string - env?: Array - id?: string - npm?: string - whitelist?: Array - blacklist?: Array - options?: { - apiKey?: string - baseURL?: string - /** - * GitHub Enterprise URL for copilot authentication - */ - enterpriseUrl?: string - /** - * Enable promptCacheKey for this provider (default false) - */ - setCacheKey?: boolean - /** - * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. - */ - timeout?: number | false - /** - * Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted. - */ - chunkTimeout?: number - [key: string]: unknown | string | boolean | number | false | number | undefined +export type EventInstallationUpdated = { + id: string + type: "installation.updated" + properties: { + version: string } - models?: { - [key: string]: { - id?: string - name?: string - family?: string - release_date?: string - attachment?: boolean - reasoning?: boolean - temperature?: boolean - tool_call?: boolean - interleaved?: - | true - | { - field: "reasoning_content" | "reasoning_details" - } - cost?: { - input: number - output: number - cache_read?: number - cache_write?: number - context_over_200k?: { - input: number - output: number - cache_read?: number - cache_write?: number - } - } - limit?: { - context: number - input?: number - output: number - } - modalities?: { - input: Array<"text" | "audio" | "image" | "video" | "pdf"> - output: Array<"text" | "audio" | "image" | "video" | "pdf"> - } - experimental?: boolean - status?: "alpha" | "beta" | "deprecated" - provider?: { - npm?: string - api?: string - } - options?: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } - /** - * Variant-specific configuration - */ - variants?: { - [key: string]: { - /** - * Disable this variant for the model - */ - disabled?: boolean - [key: string]: unknown | boolean | undefined - } - } - } +} + +export type EventInstallationUpdateAvailable = { + id: string + type: "installation.update-available" + properties: { + version: string } } -export type McpLocalConfig = { - /** - * Type of MCP server connection - */ - type: "local" - /** - * Command and arguments to run the MCP server - */ - command: Array - /** - * Environment variables to set when running the MCP server - */ - environment?: { - [key: string]: string +export type EventWorkspaceReady = { + id: string + type: "workspace.ready" + properties: { + name: string } - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number } -export type McpOAuthConfig = { - /** - * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. - */ - clientId?: string - /** - * OAuth client secret (if required by the authorization server) - */ - clientSecret?: string - /** - * OAuth scopes to request during authorization - */ - scope?: string - /** - * OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback). - */ - redirectUri?: string +export type EventWorkspaceFailed = { + id: string + type: "workspace.failed" + properties: { + message: string + } } -export type McpRemoteConfig = { - /** - * Type of MCP server connection - */ - type: "remote" - /** - * URL of the remote MCP server - */ - url: string - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Headers to send with the request - */ - headers?: { - [key: string]: string +export type EventWorkspaceRestore = { + id: string + type: "workspace.restore" + properties: { + workspaceID: string + sessionID: string + total: number + step: number } - /** - * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. - */ - oauth?: McpOAuthConfig | false - /** - * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number } -/** - * @deprecated Always uses stretch layout. - */ -export type LayoutConfig = "auto" | "stretch" +export type EventWorkspaceStatus = { + id: string + type: "workspace.status" + properties: { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + } +} -export type Config = { - /** - * JSON schema reference for configuration validation - */ - $schema?: string - /** - * Default shell to use for terminal and bash tool - */ - shell?: string - logLevel?: LogLevel - server?: ServerConfig - /** - * Command configuration, see https://opencode.ai/docs/commands - */ - command?: { - [key: string]: { - template: string - description?: string - agent?: string - model?: string - subtask?: boolean - } +export type EventFileEdited = { + id: string + type: "file.edited" + properties: { + file: string } - /** - * Additional skill folder paths - */ - skills?: { - /** - * Additional paths to skill folders - */ - paths?: Array - /** - * URLs to fetch skills from (e.g., https://example.com/.well-known/skills/) - */ - urls?: Array +} + +export type EventFileWatcherUpdated = { + id: string + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" } - watcher?: { - ignore?: Array +} + +export type EventQuestionAsked = { + id: string + type: "question.asked" + properties: QuestionRequest +} + +export type EventQuestionReplied = { + id: string + type: "question.replied" + properties: QuestionReplied +} + +export type EventQuestionRejected = { + id: string + type: "question.rejected" + properties: QuestionRejected +} + +export type EventTodoUpdated = { + id: string + type: "todo.updated" + properties: { + sessionID: string + todos: Array } - /** - * Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true. - */ - snapshot?: boolean - plugin?: Array< - | string - | [ - string, - { - [key: string]: unknown - }, - ] - > - /** - * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing - */ - share?: "manual" | "auto" | "disabled" - /** - * @deprecated Use 'share' field instead. Share newly created sessions automatically - */ - autoshare?: boolean - /** - * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications - */ - autoupdate?: boolean | "notify" - /** - * Disable providers that are loaded automatically - */ - disabled_providers?: Array - /** - * When set, ONLY these providers will be enabled. All other providers will be ignored - */ - enabled_providers?: Array - /** - * Model to use in the format of provider/model, eg anthropic/claude-2 - */ - model?: string - /** - * Small model to use for tasks like title generation in the format of provider/model - */ - small_model?: string - /** - * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. - */ - default_agent?: string - /** - * Custom username to display in conversations instead of system username - */ - username?: string - /** - * @deprecated Use `agent` field instead. - */ - mode?: { - build?: AgentConfig - plan?: AgentConfig - [key: string]: AgentConfig | undefined +} + +export type EventSessionStatus = { + id: string + type: "session.status" + properties: { + sessionID: string + status: SessionStatus + } +} + +export type EventSessionIdle = { + id: string + type: "session.idle" + properties: { + sessionID: string + } +} + +export type EventSessionCompacted = { + id: string + type: "session.compacted" + properties: { + sessionID: string + } +} + +export type EventMcpToolsChanged = { + id: string + type: "mcp.tools.changed" + properties: { + server: string + } +} + +export type EventMcpBrowserOpenFailed = { + id: string + type: "mcp.browser.open.failed" + properties: { + mcpName: string + url: string + } +} + +export type EventCommandExecuted = { + id: string + type: "command.executed" + properties: { + name: string + sessionID: string + arguments: string + messageID: string + } +} + +export type EventVcsBranchUpdated = { + id: string + type: "vcs.branch.updated" + properties: { + branch?: string + } +} + +export type EventWorktreeReady = { + id: string + type: "worktree.ready" + properties: { + name: string + branch: string + } +} + +export type EventWorktreeFailed = { + id: string + type: "worktree.failed" + properties: { + message: string + } +} + +export type EventPtyCreated = { + id: string + type: "pty.created" + properties: { + info: Pty + } +} + +export type EventPtyUpdated = { + id: string + type: "pty.updated" + properties: { + info: Pty + } +} + +export type EventPtyExited = { + id: string + type: "pty.exited" + properties: { + id: string + exitCode: number + } +} + +export type EventPtyDeleted = { + id: string + type: "pty.deleted" + properties: { + id: string + } +} + +export type EventMessageUpdated = { + id: string + type: "message.updated" + properties: { + sessionID: string + info: Message + } +} + +export type EventMessageRemoved = { + id: string + type: "message.removed" + properties: { + sessionID: string + messageID: string + } +} + +export type EventMessagePartUpdated = { + id: string + type: "message.part.updated" + properties: { + sessionID: string + part: Part + time: number + } +} + +export type EventMessagePartRemoved = { + id: string + type: "message.part.removed" + properties: { + sessionID: string + messageID: string + partID: string + } +} + +export type EventSessionCreated = { + id: string + type: "session.created" + properties: { + sessionID: string + info: Session + } +} + +export type EventSessionUpdated = { + id: string + type: "session.updated" + properties: { + sessionID: string + info: Session + } +} + +export type EventSessionDeleted = { + id: string + type: "session.deleted" + properties: { + sessionID: string + info: Session + } +} + +export type EventSessionNextAgentSwitched = { + id: string + type: "session.next.agent.switched" + properties: { + timestamp: number + sessionID: string + agent: string + } +} + +export type EventSessionNextModelSwitched = { + id: string + type: "session.next.model.switched" + properties: { + timestamp: number + sessionID: string + id: string + providerID: string + variant?: string + } +} + +export type PromptSource = { + start: number + end: number + text: string +} + +export type PromptFileAttachment = { + uri: string + mime: string + name?: string + description?: string + source?: PromptSource +} + +export type PromptAgentAttachment = { + name: string + source?: PromptSource +} + +export type EventSessionNextPrompted = { + id: string + type: "session.next.prompted" + properties: { + timestamp: number + sessionID: string + prompt: Prompt + } +} + +export type EventSessionNextSynthetic = { + id: string + type: "session.next.synthetic" + properties: { + timestamp: number + sessionID: string + text: string + } +} + +export type EventSessionNextShellStarted = { + id: string + type: "session.next.shell.started" + properties: { + timestamp: number + sessionID: string + callID: string + command: string + } +} + +export type EventSessionNextShellEnded = { + id: string + type: "session.next.shell.ended" + properties: { + timestamp: number + sessionID: string + callID: string + output: string + } +} + +export type EventSessionNextStepStarted = { + id: string + type: "session.next.step.started" + properties: { + timestamp: number + sessionID: string + agent: string + model: { + id: string + providerID: string + variant?: string + } + snapshot?: string + } +} + +export type EventSessionNextStepEnded = { + id: string + type: "session.next.step.ended" + properties: { + timestamp: number + sessionID: string + finish: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + snapshot?: string + } +} + +export type EventSessionNextTextStarted = { + id: string + type: "session.next.text.started" + properties: { + timestamp: number + sessionID: string + } +} + +export type EventSessionNextTextDelta = { + id: string + type: "session.next.text.delta" + properties: { + timestamp: number + sessionID: string + delta: string + } +} + +export type EventSessionNextTextEnded = { + id: string + type: "session.next.text.ended" + properties: { + timestamp: number + sessionID: string + text: string + } +} + +export type EventSessionNextReasoningStarted = { + id: string + type: "session.next.reasoning.started" + properties: { + timestamp: number + sessionID: string + reasoningID: string + } +} + +export type EventSessionNextReasoningDelta = { + id: string + type: "session.next.reasoning.delta" + properties: { + timestamp: number + sessionID: string + reasoningID: string + delta: string + } +} + +export type EventSessionNextReasoningEnded = { + id: string + type: "session.next.reasoning.ended" + properties: { + timestamp: number + sessionID: string + reasoningID: string + text: string + } +} + +export type EventSessionNextToolInputStarted = { + id: string + type: "session.next.tool.input.started" + properties: { + timestamp: number + sessionID: string + callID: string + name: string + } +} + +export type EventSessionNextToolInputDelta = { + id: string + type: "session.next.tool.input.delta" + properties: { + timestamp: number + sessionID: string + callID: string + delta: string + } +} + +export type EventSessionNextToolInputEnded = { + id: string + type: "session.next.tool.input.ended" + properties: { + timestamp: number + sessionID: string + callID: string + text: string + } +} + +export type EventSessionNextToolCalled = { + id: string + type: "session.next.tool.called" + properties: { + timestamp: number + sessionID: string + callID: string + tool: string + input: { + [key: string]: unknown + } + provider: { + executed: boolean + metadata?: { + [key: string]: unknown + } + } + } +} + +export type ToolTextContent = { + type: "text" + text: string +} + +export type ToolFileContent = { + type: "file" + uri: string + mime: string + name?: string +} + +export type EventSessionNextToolProgress = { + id: string + type: "session.next.tool.progress" + properties: { + timestamp: number + sessionID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + } +} + +export type EventSessionNextToolSuccess = { + id: string + type: "session.next.tool.success" + properties: { + timestamp: number + sessionID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + provider: { + executed: boolean + metadata?: { + [key: string]: unknown + } + } + } +} + +export type EventSessionNextToolError = { + id: string + type: "session.next.tool.error" + properties: { + timestamp: number + sessionID: string + callID: string + error: { + type: string + message: string + } + provider: { + executed: boolean + metadata?: { + [key: string]: unknown + } + } } - /** - * Agent configuration, see https://opencode.ai/docs/agents - */ - agent?: { - plan?: AgentConfig - build?: AgentConfig - general?: AgentConfig - explore?: AgentConfig - title?: AgentConfig - summary?: AgentConfig - compaction?: AgentConfig - [key: string]: AgentConfig | undefined +} + +export type SessionNextRetryError = { + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: { + [key: string]: string } - /** - * Custom provider configurations and model overrides - */ - provider?: { - [key: string]: ProviderConfig + responseBody?: string + metadata?: { + [key: string]: string } - /** - * MCP (Model Context Protocol) server configurations - */ - mcp?: { - [key: string]: - | McpLocalConfig - | McpRemoteConfig - | { - enabled: boolean - } +} + +export type EventSessionNextRetried = { + id: string + type: "session.next.retried" + properties: { + timestamp: number + sessionID: string + attempt: number + error: SessionNextRetryError } - /** - * Enable or configure formatters. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides. - */ - formatter?: - | boolean - | { - [key: string]: { - disabled?: boolean - command?: Array - environment?: { - [key: string]: string - } - extensions?: Array - } - } - /** - * Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides. - */ - lsp?: - | boolean - | { - [key: string]: - | { - disabled: true - } - | { - command: Array - extensions?: Array - disabled?: boolean - env?: { - [key: string]: string - } - initialization?: { - [key: string]: unknown - } - } - } - /** - * Additional instruction files or patterns to include - */ - instructions?: Array - layout?: LayoutConfig - permission?: PermissionConfig - tools?: { - [key: string]: boolean +} + +export type EventSessionNextCompactionStarted = { + id: string + type: "session.next.compaction.started" + properties: { + timestamp: number + sessionID: string + reason: "auto" | "manual" } - enterprise?: { - /** - * Enterprise URL - */ - url?: string +} + +export type EventSessionNextCompactionDelta = { + id: string + type: "session.next.compaction.delta" + properties: { + timestamp: number + sessionID: string + text: string } - /** - * Thresholds for truncating tool output. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned. - */ - tool_output?: { - /** - * Maximum lines of tool output before it is truncated and saved to disk (default: 2000) - */ - max_lines?: number - /** - * Maximum bytes of tool output before it is truncated and saved to disk (default: 51200) - */ - max_bytes?: number +} + +export type EventSessionNextCompactionEnded = { + id: string + type: "session.next.compaction.ended" + properties: { + timestamp: number + sessionID: string + text: string + include?: string } - compaction?: { - /** - * Enable automatic compaction when context is full (default: true) - */ - auto?: boolean - /** - * Enable pruning of old tool outputs (default: true) - */ - prune?: boolean - /** - * Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2) - */ - tail_turns?: number - /** - * Maximum number of tokens from recent turns to preserve verbatim after compaction - */ - preserve_recent_tokens?: number - /** - * Token buffer for compaction. Leaves enough window to avoid overflow during compaction. - */ - reserved?: number +} + +export type EventServerConnected = { + id: string + type: "server.connected" + properties: { + [key: string]: unknown } - experimental?: { - disable_paste_summary?: boolean - /** - * Enable the batch tool - */ - batch_tool?: boolean - /** - * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) - */ - openTelemetry?: boolean - /** - * Tools that should only be available to primary agents. - */ - primary_tools?: Array - /** - * Continue the agent loop when a tool call is denied - */ - continue_loop_on_deny?: boolean - /** - * Timeout in milliseconds for model context protocol (MCP) requests - */ - mcp_timeout?: number +} + +export type EventGlobalDisposed = { + id: string + type: "global.disposed" + properties: { + [key: string]: unknown + } +} + +export type SessionInfo = { + id: string + parentID?: string + projectID: string + workspaceID?: string + path?: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + time: { + created: number + updated: number + archived?: number + } + title: string +} + +export type SessionDelivery = "immediate" | "deferred" + +export type SessionMessageAgentSwitched = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + type: "agent-switched" + agent: string +} + +export type SessionMessageModelSwitched = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + type: "model-switched" + model: { + id: string + providerID: string + variant?: string + } +} + +export type SessionMessageUser = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + text: string + files?: Array + agents?: Array + type: "user" +} + +export type SessionMessageSynthetic = { + id: string + metadata?: { + [key: string]: unknown } + time: { + created: number + } + sessionID: string + text: string + type: "synthetic" } -export type BadRequestError = { - data: unknown - errors: Array<{ +export type SessionMessageShell = { + id: string + metadata?: { [key: string]: unknown - }> - success: false + } + time: { + created: number + completed?: number + } + type: "shell" + callID: string + command: string + output: string } -export type OAuth = { - type: "oauth" - refresh: string - access: string - expires: number - accountId?: string - enterpriseUrl?: string +export type SessionMessageAssistantText = { + type: "text" + text: string } -export type ApiAuth = { - type: "api" - key: string - metadata?: { - [key: string]: string - } +export type SessionMessageAssistantReasoning = { + type: "reasoning" + id: string + text: string } -export type WellKnownAuth = { - type: "wellknown" - key: string - token: string +export type SessionMessageToolStatePending = { + status: "pending" + input: string } -export type Auth = OAuth | ApiAuth | WellKnownAuth +export type SessionMessageToolStateRunning = { + status: "running" + input: { + [key: string]: unknown + } + structured: { + [key: string]: unknown + } + content: Array +} -export type Workspace = { - id: string - type: string - name: string - branch: string | null - directory: string | null - extra: unknown | null - projectID: string +export type SessionMessageToolStateCompleted = { + status: "completed" + input: { + [key: string]: unknown + } + attachments?: Array + content: Array + structured: { + [key: string]: unknown + } } -export type NotFoundError = { - name: "NotFoundError" - data: { +export type SessionMessageToolStateError = { + status: "error" + input: { + [key: string]: unknown + } + content: Array + structured: { + [key: string]: unknown + } + error: { + type: string message: string } } -export type Model = { +export type SessionMessageAssistantTool = { + type: "tool" id: string - providerID: string - api: { - id: string - url: string - npm: string - } name: string - family?: string - capabilities: { - temperature: boolean - reasoning: boolean - attachment: boolean - toolcall: boolean - input: { - text: boolean - audio: boolean - image: boolean - video: boolean - pdf: boolean - } - output: { - text: boolean - audio: boolean - image: boolean - video: boolean - pdf: boolean + provider?: { + executed: boolean + metadata?: { + [key: string]: unknown } - interleaved: - | boolean - | { - field: "reasoning_content" | "reasoning_details" - } } - cost: { + state: + | SessionMessageToolStatePending + | SessionMessageToolStateRunning + | SessionMessageToolStateCompleted + | SessionMessageToolStateError + time: { + created: number + ran?: number + completed?: number + pruned?: number + } +} + +export type SessionMessageAssistant = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + completed?: number + } + type: "assistant" + agent: string + model: { + id: string + providerID: string + variant?: string + } + content: Array + snapshot?: { + start?: string + end?: string + } + finish?: string + cost?: number + tokens?: { input: number output: number + reasoning: number cache: { read: number write: number } - experimentalOver200K?: { - input: number - output: number - cache: { - read: number - write: number - } - } } - limit: { - context: number - input?: number - output: number + error?: string +} + +export type SessionMessageCompaction = { + type: "compaction" + reason: "auto" | "manual" + summary: string + include?: string + id: string + metadata?: { + [key: string]: unknown } - status: "alpha" | "beta" | "deprecated" | "active" - options: { + time: { + created: number + } +} + +export type SessionMessage = + | SessionMessageAgentSwitched + | SessionMessageModelSwitched + | SessionMessageUser + | SessionMessageSynthetic + | SessionMessageShell + | SessionMessageAssistant + | SessionMessageCompaction + +export type EventTuiToastShow1 = { + id: string + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + duration?: number + } +} + +export type BadRequestError = { + data: unknown + errors: Array<{ [key: string]: unknown + }> + success: false +} + +export type NotFoundError = { + name: "NotFoundError" + data: { + message: string } - headers: { - [key: string]: string +} + +export type AuthRemoveData = { + body?: never + path: { + providerID: string } - release_date: string - variants?: { - [key: string]: { + query?: never + url: "/auth/{providerID}" +} + +export type AuthRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] + +export type AuthRemoveResponses = { + /** + * Successfully removed authentication credentials + */ + 200: boolean +} + +export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] + +export type AuthSetData = { + body?: Auth + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthSetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] + +export type AuthSetResponses = { + /** + * Successfully set authentication credentials + */ + 200: boolean +} + +export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] + +export type AppLogData = { + body?: { + /** + * Service name for the log entry + */ + service: string + /** + * Log level + */ + level: "debug" | "info" | "error" | "warn" + /** + * Log message + */ + message: string + extra?: { [key: string]: unknown } } -} - -export type Provider = { - id: string - name: string - source: "env" | "config" | "custom" | "api" - env: Array - key?: string - options: { - [key: string]: unknown - } - models: { - [key: string]: Model + path?: never + query?: { + directory?: string + workspace?: string } + url: "/log" } -export type ConsoleState = { - consoleManagedProviders: Array - activeOrgName?: string - switchableOrgCount: number -} - -export type ToolIds = Array - -export type ToolListItem = { - id: string - description: string - parameters: unknown +export type AppLogErrors = { + /** + * Bad request + */ + 400: BadRequestError } -export type ToolList = Array - -export type Worktree = { - name: string - branch: string - directory: string -} +export type AppLogError = AppLogErrors[keyof AppLogErrors] -export type WorktreeCreateInput = { - name?: string +export type AppLogResponses = { /** - * Additional startup script to run after the project's start command + * Log entry written successfully */ - startCommand?: string -} - -export type WorktreeRemoveInput = { - directory: string + 200: boolean } +<<<<<<< HEAD export type WorktreeResetInput = { directory: string } @@ -3432,6 +4323,9 @@ export type FormatterStatus = { extensions: Array enabled: boolean } +======= +export type AppLogResponse = AppLogResponses[keyof AppLogResponses] +>>>>>>> eb048016e (sync) export type GlobalHealthData = { body?: never @@ -3560,1158 +4454,1240 @@ export type GlobalUpgradeResponses = { export type GlobalUpgradeResponse = GlobalUpgradeResponses[keyof GlobalUpgradeResponses] -export type AuthRemoveData = { +export type EventSubscribeData = { body?: never - path: { - providerID: string + path?: never + query?: { + directory?: string + workspace?: string } - query?: never - url: "/auth/{providerID}" + url: "/event" } -export type AuthRemoveErrors = { +export type EventSubscribeResponses = { + /** + * Event stream + */ + 200: Event +} + +export type EventSubscribeResponse = EventSubscribeResponses[keyof EventSubscribeResponses] + +export type ConfigGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigGetResponses = { + /** + * Get config info + */ + 200: Config +} + +export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] + +export type ConfigUpdateData = { + body?: Config + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigUpdateErrors = { /** * Bad request */ 400: BadRequestError } -export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] +export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors] -export type AuthRemoveResponses = { +export type ConfigUpdateResponses = { /** - * Successfully removed authentication credentials + * Successfully updated config + */ + 200: Config +} + +export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] + +export type ConfigProvidersData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config/providers" +} + +export type ConfigProvidersResponses = { + /** + * List of providers + */ + 200: { + providers: Array + default: { + [key: string]: string + } + } +} + +export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] + +export type ExperimentalConsoleGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/console" +} + +export type ExperimentalConsoleGetResponses = { + /** + * Active Console provider metadata + */ + 200: ConsoleState +} + +export type ExperimentalConsoleGetResponse = ExperimentalConsoleGetResponses[keyof ExperimentalConsoleGetResponses] + +export type ExperimentalConsoleListOrgsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/console/orgs" +} + +export type ExperimentalConsoleListOrgsResponses = { + /** + * Switchable Console orgs + */ + 200: { + orgs: Array<{ + accountID: string + accountEmail: string + accountUrl: string + orgID: string + orgName: string + active: boolean + }> + } +} + +export type ExperimentalConsoleListOrgsResponse = + ExperimentalConsoleListOrgsResponses[keyof ExperimentalConsoleListOrgsResponses] + +export type ExperimentalConsoleSwitchOrgData = { + body?: { + accountID: string + orgID: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/console/switch" +} + +export type ExperimentalConsoleSwitchOrgResponses = { + /** + * Switch success */ 200: boolean } -export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] +export type ExperimentalConsoleSwitchOrgResponse = + ExperimentalConsoleSwitchOrgResponses[keyof ExperimentalConsoleSwitchOrgResponses] -export type AuthSetData = { - body?: Auth - path: { - providerID: string +export type ToolListData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + provider: string + model: string } - query?: never - url: "/auth/{providerID}" + url: "/experimental/tool" } -export type AuthSetErrors = { +export type ToolListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ToolListError = ToolListErrors[keyof ToolListErrors] + +export type ToolListResponses = { + /** + * Tools + */ + 200: ToolList +} + +export type ToolListResponse = ToolListResponses[keyof ToolListResponses] + +export type ToolIdsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/tool/ids" +} + +export type ToolIdsErrors = { /** * Bad request */ 400: BadRequestError } -export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] +export type ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors] -export type AuthSetResponses = { +export type ToolIdsResponses = { /** - * Successfully set authentication credentials + * Tool IDs */ - 200: boolean + 200: ToolIds } -export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] +export type ToolIdsResponse = ToolIdsResponses[keyof ToolIdsResponses] -export type AppLogData = { - body?: { - /** - * Service name for the log entry - */ - service: string - /** - * Log level - */ - level: "debug" | "info" | "error" | "warn" - /** - * Log message - */ - message: string - /** - * Additional metadata for the log entry - */ - extra?: { - [key: string]: unknown - } - } +export type WorktreeRemoveData = { + body?: WorktreeRemoveInput path?: never query?: { directory?: string workspace?: string } - url: "/log" + url: "/experimental/worktree" } -export type AppLogErrors = { +export type WorktreeRemoveErrors = { /** * Bad request */ 400: BadRequestError } -export type AppLogError = AppLogErrors[keyof AppLogErrors] +export type WorktreeRemoveError = WorktreeRemoveErrors[keyof WorktreeRemoveErrors] -export type AppLogResponses = { +export type WorktreeRemoveResponses = { /** - * Log entry written successfully + * Worktree removed */ 200: boolean } -export type AppLogResponse = AppLogResponses[keyof AppLogResponses] +export type WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses] -export type ExperimentalWorkspaceAdapterListData = { +export type WorktreeListData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/experimental/workspace/adapter" + url: "/experimental/worktree" } -export type ExperimentalWorkspaceAdapterListResponses = { +export type WorktreeListResponses = { /** - * Workspace adapters + * List of worktree directories */ - 200: Array<{ - type: string - name: string - description: string - }> + 200: Array } -export type ExperimentalWorkspaceAdapterListResponse = - ExperimentalWorkspaceAdapterListResponses[keyof ExperimentalWorkspaceAdapterListResponses] +export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses] -export type ExperimentalWorkspaceListData = { - body?: never +export type WorktreeCreateData = { + body?: WorktreeCreateInput path?: never query?: { directory?: string workspace?: string } - url: "/experimental/workspace" + url: "/experimental/worktree" } -export type ExperimentalWorkspaceListResponses = { +export type WorktreeCreateErrors = { /** - * Workspaces + * Bad request */ - 200: Array + 400: BadRequestError } -export type ExperimentalWorkspaceListResponse = - ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses] +export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors] -export type ExperimentalWorkspaceCreateData = { - body?: { - id?: string - type: string - branch: string | null - extra: unknown | null - } +export type WorktreeCreateResponses = { + /** + * Worktree created + */ + 200: Worktree +} + +export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses] + +export type WorktreeResetData = { + body?: WorktreeResetInput path?: never query?: { directory?: string workspace?: string } - url: "/experimental/workspace" + url: "/experimental/worktree/reset" } -export type ExperimentalWorkspaceCreateErrors = { +export type WorktreeResetErrors = { /** * Bad request */ 400: BadRequestError } -export type ExperimentalWorkspaceCreateError = - ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors] +export type WorktreeResetError = WorktreeResetErrors[keyof WorktreeResetErrors] -export type ExperimentalWorkspaceCreateResponses = { +export type WorktreeResetResponses = { /** - * Workspace created + * Worktree reset */ - 200: Workspace + 200: boolean } -export type ExperimentalWorkspaceCreateResponse = - ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses] +export type WorktreeResetResponse = WorktreeResetResponses[keyof WorktreeResetResponses] -export type ExperimentalWorkspaceStatusData = { +export type ExperimentalSessionListData = { body?: never path?: never query?: { directory?: string workspace?: string + roots?: boolean | "true" | "false" + start?: number + cursor?: number + search?: string + limit?: number + archived?: boolean | "true" | "false" } - url: "/experimental/workspace/status" + url: "/experimental/session" } -export type ExperimentalWorkspaceStatusResponses = { +export type ExperimentalSessionListResponses = { /** - * Workspace status + * List of sessions */ - 200: Array<{ - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - }> + 200: Array } -export type ExperimentalWorkspaceStatusResponse = - ExperimentalWorkspaceStatusResponses[keyof ExperimentalWorkspaceStatusResponses] +export type ExperimentalSessionListResponse = ExperimentalSessionListResponses[keyof ExperimentalSessionListResponses] -export type ExperimentalWorkspaceRemoveData = { +export type ExperimentalResourceListData = { body?: never - path: { - id: string - } + path?: never query?: { directory?: string workspace?: string } - url: "/experimental/workspace/{id}" + url: "/experimental/resource" } -export type ExperimentalWorkspaceRemoveErrors = { +export type ExperimentalResourceListResponses = { /** - * Bad request + * MCP resources */ - 400: BadRequestError + 200: { + [key: string]: McpResource + } } -export type ExperimentalWorkspaceRemoveError = - ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors] +export type ExperimentalResourceListResponse = + ExperimentalResourceListResponses[keyof ExperimentalResourceListResponses] -export type ExperimentalWorkspaceRemoveResponses = { +export type FindTextData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + pattern: string + } + url: "/find" +} + +export type FindTextResponses = { /** - * Workspace removed + * Matches */ - 200: Workspace + 200: Array<{ + path: { + text: string + } + lines: { + text: string + } + line_number: number + absolute_offset: number + submatches: Array<{ + match: { + text: string + } + start: number + end: number + }> + }> } -export type ExperimentalWorkspaceRemoveResponse = - ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] +export type FindTextResponse = FindTextResponses[keyof FindTextResponses] -export type ExperimentalWorkspaceSessionRestoreData = { - body?: { - sessionID: string - } - path: { - id: string - } - query?: { +export type FindFilesData = { + body?: never + path?: never + query: { directory?: string workspace?: string + query: string + dirs?: "true" | "false" + type?: "file" | "directory" + limit?: number } - url: "/experimental/workspace/{id}/session-restore" + url: "/find/file" } -export type ExperimentalWorkspaceSessionRestoreErrors = { +export type FindFilesResponses = { /** - * Bad request + * File paths */ - 400: BadRequestError + 200: Array } -export type ExperimentalWorkspaceSessionRestoreError = - ExperimentalWorkspaceSessionRestoreErrors[keyof ExperimentalWorkspaceSessionRestoreErrors] +export type FindFilesResponse = FindFilesResponses[keyof FindFilesResponses] -export type ExperimentalWorkspaceSessionRestoreResponses = { +export type FindSymbolsData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + query: string + } + url: "/find/symbol" +} + +export type FindSymbolsResponses = { /** - * Session replay started + * Symbols */ - 200: { - total: number + 200: Array +} + +export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses] + +export type FileListData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + path: string } + url: "/file" } -export type ExperimentalWorkspaceSessionRestoreResponse = - ExperimentalWorkspaceSessionRestoreResponses[keyof ExperimentalWorkspaceSessionRestoreResponses] +export type FileListResponses = { + /** + * Files and directories + */ + 200: Array +} -export type ProjectListData = { +export type FileListResponse = FileListResponses[keyof FileListResponses] + +export type FileReadData = { body?: never path?: never - query?: { + query: { directory?: string workspace?: string + path: string } - url: "/project" + url: "/file/content" } -export type ProjectListResponses = { +export type FileReadResponses = { /** - * List of projects + * File content */ - 200: Array + 200: FileContent } -export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] +export type FileReadResponse = FileReadResponses[keyof FileReadResponses] -export type ProjectCurrentData = { +export type FileStatusData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/project/current" + url: "/file/status" } -export type ProjectCurrentResponses = { +export type FileStatusResponses = { /** - * Current project information + * File status */ - 200: Project + 200: Array } -export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] +export type FileStatusResponse = FileStatusResponses[keyof FileStatusResponses] -export type ProjectInitGitData = { +export type InstanceDisposeData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/project/git/init" + url: "/instance/dispose" } -export type ProjectInitGitResponses = { +export type InstanceDisposeResponses = { /** - * Project information after git initialization + * Instance disposed */ - 200: Project + 200: boolean } -export type ProjectInitGitResponse = ProjectInitGitResponses[keyof ProjectInitGitResponses] +export type InstanceDisposeResponse = InstanceDisposeResponses[keyof InstanceDisposeResponses] -export type ProjectUpdateData = { - body?: { - name?: string - icon?: { - url?: string - override?: string - color?: string - } - commands?: { - /** - * Startup script to run when creating a new workspace (worktree) - */ - start?: string - } - } - path: { - projectID: string - } +export type PathGetData = { + body?: never + path?: never query?: { directory?: string workspace?: string } - url: "/project/{projectID}" -} - -export type ProjectUpdateErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError + url: "/path" } -export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors] - -export type ProjectUpdateResponses = { +export type PathGetResponses = { /** - * Updated project information + * Path */ - 200: Project + 200: Path } -export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] +export type PathGetResponse = PathGetResponses[keyof PathGetResponses] -export type PtyShellsData = { +export type VcsGetData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/pty/shells" + url: "/vcs" } -export type PtyShellsResponses = { +export type VcsGetResponses = { /** - * List of shells + * VCS info */ - 200: Array<{ - path: string - name: string - acceptable: boolean - }> + 200: VcsInfo } -export type PtyShellsResponse = PtyShellsResponses[keyof PtyShellsResponses] +export type VcsGetResponse = VcsGetResponses[keyof VcsGetResponses] -export type PtyListData = { +export type VcsDiffData = { body?: never path?: never - query?: { + query: { directory?: string workspace?: string + mode: "git" | "branch" } - url: "/pty" + url: "/vcs/diff" } -export type PtyListResponses = { +export type VcsDiffResponses = { /** - * List of sessions + * VCS diff */ - 200: Array + 200: Array } -export type PtyListResponse = PtyListResponses[keyof PtyListResponses] +export type VcsDiffResponse = VcsDiffResponses[keyof VcsDiffResponses] -export type PtyCreateData = { - body?: { - command?: string - args?: Array - cwd?: string - title?: string - env?: { - [key: string]: string - } - } +export type CommandListData = { + body?: never path?: never query?: { directory?: string workspace?: string } - url: "/pty" -} - -export type PtyCreateErrors = { - /** - * Bad request - */ - 400: BadRequestError + url: "/command" } -export type PtyCreateError = PtyCreateErrors[keyof PtyCreateErrors] - -export type PtyCreateResponses = { +export type CommandListResponses = { /** - * Created session + * List of commands */ - 200: Pty + 200: Array } -export type PtyCreateResponse = PtyCreateResponses[keyof PtyCreateResponses] +export type CommandListResponse = CommandListResponses[keyof CommandListResponses] -export type PtyRemoveData = { +export type AppAgentsData = { body?: never - path: { - ptyID: string - } + path?: never query?: { directory?: string workspace?: string } - url: "/pty/{ptyID}" -} - -export type PtyRemoveErrors = { - /** - * Not found - */ - 404: NotFoundError + url: "/agent" } -export type PtyRemoveError = PtyRemoveErrors[keyof PtyRemoveErrors] - -export type PtyRemoveResponses = { +export type AppAgentsResponses = { /** - * Session removed + * List of agents */ - 200: boolean + 200: Array } -export type PtyRemoveResponse = PtyRemoveResponses[keyof PtyRemoveResponses] +export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses] -export type PtyGetData = { +export type AppSkillsData = { body?: never - path: { - ptyID: string - } + path?: never query?: { directory?: string workspace?: string } - url: "/pty/{ptyID}" -} - -export type PtyGetErrors = { - /** - * Not found - */ - 404: NotFoundError + url: "/skill" } -export type PtyGetError = PtyGetErrors[keyof PtyGetErrors] - -export type PtyGetResponses = { +export type AppSkillsResponses = { /** - * Session info + * List of skills */ - 200: Pty + 200: Array<{ + name: string + description: string + location: string + content: string + }> } -export type PtyGetResponse = PtyGetResponses[keyof PtyGetResponses] +export type AppSkillsResponse = AppSkillsResponses[keyof AppSkillsResponses] -export type PtyUpdateData = { - body?: { - title?: string - size?: { - rows: number - cols: number - } - } - path: { - ptyID: string - } +export type LspStatusData = { + body?: never + path?: never query?: { directory?: string workspace?: string } - url: "/pty/{ptyID}" -} - -export type PtyUpdateErrors = { - /** - * Bad request - */ - 400: BadRequestError + url: "/lsp" } -export type PtyUpdateError = PtyUpdateErrors[keyof PtyUpdateErrors] - -export type PtyUpdateResponses = { +export type LspStatusResponses = { /** - * Updated session + * LSP server status */ - 200: Pty + 200: Array } -export type PtyUpdateResponse = PtyUpdateResponses[keyof PtyUpdateResponses] +export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses] -export type PtyConnectData = { +export type FormatterStatusData = { body?: never - path: { - ptyID: string - } + path?: never query?: { directory?: string workspace?: string } - url: "/pty/{ptyID}/connect" -} - -export type PtyConnectErrors = { - /** - * Not found - */ - 404: NotFoundError + url: "/formatter" } -export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors] - -export type PtyConnectResponses = { +export type FormatterStatusResponses = { /** - * Connected session + * Formatter status */ - 200: boolean + 200: Array } -export type PtyConnectResponse = PtyConnectResponses[keyof PtyConnectResponses] +export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] -export type ConfigGetData = { +export type McpStatusData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/config" + url: "/mcp" } -export type ConfigGetResponses = { +export type McpStatusResponses = { /** - * Get config info + * MCP server status */ - 200: Config + 200: { + [key: string]: McpStatus + } } -export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] +export type McpStatusResponse = McpStatusResponses[keyof McpStatusResponses] -export type ConfigUpdateData = { - body?: Config +export type McpAddData = { + body?: { + name: string + config: McpLocalConfig | McpRemoteConfig + } path?: never query?: { directory?: string workspace?: string } - url: "/config" + url: "/mcp" } -export type ConfigUpdateErrors = { +export type McpAddErrors = { /** * Bad request */ 400: BadRequestError } -export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors] +export type McpAddError = McpAddErrors[keyof McpAddErrors] -export type ConfigUpdateResponses = { +export type McpAddResponses = { /** - * Successfully updated config + * MCP server added successfully */ - 200: Config + 200: { + [key: string]: McpStatus + } } -export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] +export type McpAddResponse = McpAddResponses[keyof McpAddResponses] -export type ConfigProvidersData = { +export type McpAuthRemoveData = { body?: never - path?: never + path: { + name: string + } query?: { directory?: string workspace?: string } - url: "/config/providers" + url: "/mcp/{name}/auth" } -export type ConfigProvidersResponses = { +export type McpAuthRemoveErrors = { /** - * List of providers + * Not found + */ + 404: NotFoundError +} + +export type McpAuthRemoveError = McpAuthRemoveErrors[keyof McpAuthRemoveErrors] + +export type McpAuthRemoveResponses = { + /** + * OAuth credentials removed */ 200: { - providers: Array - default: { - [key: string]: string - } + success: true } } -export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] +export type McpAuthRemoveResponse = McpAuthRemoveResponses[keyof McpAuthRemoveResponses] -export type ExperimentalConsoleGetData = { +export type McpAuthStartData = { body?: never - path?: never + path: { + name: string + } query?: { directory?: string workspace?: string } - url: "/experimental/console" + url: "/mcp/{name}/auth" } -export type ExperimentalConsoleGetResponses = { +export type McpAuthStartErrors = { /** - * Active Console provider metadata + * McpUnsupportedOAuthError */ - 200: ConsoleState + 400: McpUnsupportedOAuthError + /** + * Not found + */ + 404: NotFoundError } -export type ExperimentalConsoleGetResponse = ExperimentalConsoleGetResponses[keyof ExperimentalConsoleGetResponses] - -export type ExperimentalConsoleListOrgsData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/console/orgs" -} +export type McpAuthStartError = McpAuthStartErrors[keyof McpAuthStartErrors] -export type ExperimentalConsoleListOrgsResponses = { +export type McpAuthStartResponses = { /** - * Switchable Console orgs + * OAuth flow started */ 200: { - orgs: Array<{ - accountID: string - accountEmail: string - accountUrl: string - orgID: string - orgName: string - active: boolean - }> + authorizationUrl: string + oauthState: string } } -export type ExperimentalConsoleListOrgsResponse = - ExperimentalConsoleListOrgsResponses[keyof ExperimentalConsoleListOrgsResponses] +export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartResponses] -export type ExperimentalConsoleSwitchOrgData = { +export type McpAuthCallbackData = { body?: { - accountID: string - orgID: string + code: string } - path?: never - query?: { - directory?: string - workspace?: string + path: { + name: string } - url: "/experimental/console/switch" -} - -export type ExperimentalConsoleSwitchOrgResponses = { - /** - * Switch success - */ - 200: boolean -} - -export type ExperimentalConsoleSwitchOrgResponse = - ExperimentalConsoleSwitchOrgResponses[keyof ExperimentalConsoleSwitchOrgResponses] - -export type ToolIdsData = { - body?: never - path?: never query?: { directory?: string workspace?: string } - url: "/experimental/tool/ids" + url: "/mcp/{name}/auth/callback" } -export type ToolIdsErrors = { +export type McpAuthCallbackErrors = { /** * Bad request */ 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError } -export type ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors] +export type McpAuthCallbackError = McpAuthCallbackErrors[keyof McpAuthCallbackErrors] -export type ToolIdsResponses = { +export type McpAuthCallbackResponses = { /** - * Tool IDs + * OAuth authentication completed */ - 200: ToolIds + 200: McpStatus } -export type ToolIdsResponse = ToolIdsResponses[keyof ToolIdsResponses] +export type McpAuthCallbackResponse = McpAuthCallbackResponses[keyof McpAuthCallbackResponses] -export type ToolListData = { +export type McpAuthAuthenticateData = { body?: never - path?: never - query: { + path: { + name: string + } + query?: { directory?: string workspace?: string - provider: string - model: string } - url: "/experimental/tool" + url: "/mcp/{name}/auth/authenticate" } -export type ToolListErrors = { +export type McpAuthAuthenticateErrors = { /** - * Bad request + * McpUnsupportedOAuthError */ - 400: BadRequestError + 400: McpUnsupportedOAuthError + /** + * Not found + */ + 404: NotFoundError } -export type ToolListError = ToolListErrors[keyof ToolListErrors] +export type McpAuthAuthenticateError = McpAuthAuthenticateErrors[keyof McpAuthAuthenticateErrors] -export type ToolListResponses = { +export type McpAuthAuthenticateResponses = { /** - * Tools + * OAuth authentication completed */ - 200: ToolList + 200: McpStatus } -export type ToolListResponse = ToolListResponses[keyof ToolListResponses] +export type McpAuthAuthenticateResponse = McpAuthAuthenticateResponses[keyof McpAuthAuthenticateResponses] -export type WorktreeRemoveData = { - body?: WorktreeRemoveInput - path?: never +export type McpConnectData = { + body?: never + path: { + name: string + } query?: { directory?: string workspace?: string } - url: "/experimental/worktree" + url: "/mcp/{name}/connect" } -export type WorktreeRemoveErrors = { +export type McpConnectResponses = { /** - * Bad request + * MCP server connected successfully */ - 400: BadRequestError + 200: boolean } -export type WorktreeRemoveError = WorktreeRemoveErrors[keyof WorktreeRemoveErrors] +export type McpConnectResponse = McpConnectResponses[keyof McpConnectResponses] -export type WorktreeRemoveResponses = { +export type McpDisconnectData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/disconnect" +} + +export type McpDisconnectResponses = { /** - * Worktree removed + * MCP server disconnected successfully */ 200: boolean } -export type WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses] +export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses] -export type WorktreeListData = { +export type ProjectListData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/experimental/worktree" + url: "/project" } -export type WorktreeListResponses = { +export type ProjectListResponses = { /** - * List of worktree directories + * List of projects */ - 200: Array + 200: Array } -export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses] +export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] -export type WorktreeCreateData = { - body?: WorktreeCreateInput +export type ProjectCurrentData = { + body?: never path?: never query?: { directory?: string workspace?: string } - url: "/experimental/worktree" + url: "/project/current" } -export type WorktreeCreateErrors = { +export type ProjectCurrentResponses = { /** - * Bad request + * Current project information */ - 400: BadRequestError + 200: Project +} + +export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] + +export type ProjectInitGitData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project/git/init" } -export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors] - -export type WorktreeCreateResponses = { +export type ProjectInitGitResponses = { /** - * Worktree created + * Project information after git initialization */ - 200: Worktree + 200: Project } -export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses] +export type ProjectInitGitResponse = ProjectInitGitResponses[keyof ProjectInitGitResponses] -export type WorktreeResetData = { - body?: WorktreeResetInput - path?: never +export type ProjectUpdateData = { + body?: { + name?: string + icon?: { + url?: string + override?: string + color?: string + } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } + } + path: { + projectID: string + } query?: { directory?: string workspace?: string } - url: "/experimental/worktree/reset" + url: "/project/{projectID}" } -export type WorktreeResetErrors = { +export type ProjectUpdateErrors = { /** * Bad request */ 400: BadRequestError -} - -export type WorktreeResetError = WorktreeResetErrors[keyof WorktreeResetErrors] - -export type WorktreeResetResponses = { /** - * Worktree reset + * Not found */ - 200: boolean + 404: NotFoundError } -export type WorktreeResetResponse = WorktreeResetResponses[keyof WorktreeResetResponses] - -export type ExperimentalSessionListData = { - body?: never - path?: never - query?: { - /** - * Filter sessions by project directory - */ - directory?: string - workspace?: string - /** - * Only return root sessions (no parentID) - */ - roots?: boolean | "true" | "false" - /** - * Filter sessions updated on or after this timestamp (milliseconds since epoch) - */ - start?: number - /** - * Return sessions updated before this timestamp (milliseconds since epoch) - */ - cursor?: number - /** - * Filter sessions by title (case-insensitive) - */ - search?: string - /** - * Maximum number of sessions to return - */ - limit?: number - /** - * Include archived sessions (default false) - */ - archived?: boolean | "true" | "false" - } - url: "/experimental/session" -} +export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors] -export type ExperimentalSessionListResponses = { +export type ProjectUpdateResponses = { /** - * List of sessions + * Updated project information */ - 200: Array + 200: Project } -export type ExperimentalSessionListResponse = ExperimentalSessionListResponses[keyof ExperimentalSessionListResponses] +export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] -export type ExperimentalResourceListData = { +export type PtyShellsData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/experimental/resource" + url: "/pty/shells" } -export type ExperimentalResourceListResponses = { +export type PtyShellsResponses = { /** - * MCP resources + * List of shells */ - 200: { - [key: string]: McpResource - } + 200: Array<{ + path: string + name: string + acceptable: boolean + }> } -export type ExperimentalResourceListResponse = - ExperimentalResourceListResponses[keyof ExperimentalResourceListResponses] +export type PtyShellsResponse = PtyShellsResponses[keyof PtyShellsResponses] -export type SessionListData = { +export type PtyListData = { body?: never path?: never query?: { - /** - * Filter sessions by directory - */ directory?: string workspace?: string - /** - * List all sessions for the current project - */ - scope?: "project" - /** - * Filter sessions by project-relative path - */ - path?: string - /** - * Only return root sessions (no parentID) - */ - roots?: boolean | "true" | "false" - /** - * Filter sessions updated on or after this timestamp (milliseconds since epoch) - */ - start?: number - /** - * Filter sessions by title (case-insensitive) - */ - search?: string - /** - * Maximum number of sessions to return - */ - limit?: number } - url: "/session" + url: "/pty" } -export type SessionListResponses = { +export type PtyListResponses = { /** * List of sessions */ - 200: Array + 200: Array } -export type SessionListResponse = SessionListResponses[keyof SessionListResponses] +export type PtyListResponse = PtyListResponses[keyof PtyListResponses] -export type SessionCreateData = { +export type PtyCreateData = { body?: { - parentID?: string + command?: string + args?: Array + cwd?: string title?: string - agent?: string - model?: { - id: string - providerID: string - variant?: string + env?: { + [key: string]: string } - permission?: PermissionRuleset - workspaceID?: string } path?: never query?: { directory?: string workspace?: string } - url: "/session" + url: "/pty" } -export type SessionCreateErrors = { +export type PtyCreateErrors = { /** * Bad request */ 400: BadRequestError } -export type SessionCreateError = SessionCreateErrors[keyof SessionCreateErrors] +export type PtyCreateError = PtyCreateErrors[keyof PtyCreateErrors] -export type SessionCreateResponses = { +export type PtyCreateResponses = { /** - * Successfully created session + * Created session */ - 200: Session + 200: Pty } -export type SessionCreateResponse = SessionCreateResponses[keyof SessionCreateResponses] +export type PtyCreateResponse = PtyCreateResponses[keyof PtyCreateResponses] -export type SessionStatusData = { +export type PtyRemoveData = { body?: never - path?: never + path: { + ptyID: string + } query?: { directory?: string workspace?: string } - url: "/session/status" + url: "/pty/{ptyID}" } -export type SessionStatusErrors = { +export type PtyRemoveErrors = { /** - * Bad request + * Not found */ - 400: BadRequestError + 404: NotFoundError } -export type SessionStatusError = SessionStatusErrors[keyof SessionStatusErrors] +export type PtyRemoveError = PtyRemoveErrors[keyof PtyRemoveErrors] -export type SessionStatusResponses = { +export type PtyRemoveResponses = { /** - * Get session status + * Session removed */ - 200: { - [key: string]: SessionStatus - } + 200: boolean } -export type SessionStatusResponse = SessionStatusResponses[keyof SessionStatusResponses] +export type PtyRemoveResponse = PtyRemoveResponses[keyof PtyRemoveResponses] -export type SessionDeleteData = { +export type PtyGetData = { body?: never path: { - sessionID: string + ptyID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}" + url: "/pty/{ptyID}" } -export type SessionDeleteErrors = { - /** - * Bad request - */ - 400: BadRequestError +export type PtyGetErrors = { /** * Not found */ 404: NotFoundError } -export type SessionDeleteError = SessionDeleteErrors[keyof SessionDeleteErrors] +export type PtyGetError = PtyGetErrors[keyof PtyGetErrors] -export type SessionDeleteResponses = { +export type PtyGetResponses = { /** - * Successfully deleted session + * Session info */ - 200: boolean + 200: Pty } -export type SessionDeleteResponse = SessionDeleteResponses[keyof SessionDeleteResponses] +export type PtyGetResponse = PtyGetResponses[keyof PtyGetResponses] -export type SessionGetData = { - body?: never +export type PtyUpdateData = { + body?: { + title?: string + size?: { + rows: number + cols: number + } + } path: { - sessionID: string + ptyID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}" + url: "/pty/{ptyID}" } -export type SessionGetErrors = { +export type PtyUpdateErrors = { /** * Bad request */ 400: BadRequestError +} + +export type PtyUpdateError = PtyUpdateErrors[keyof PtyUpdateErrors] + +export type PtyUpdateResponses = { /** - * Not found + * Updated session */ - 404: NotFoundError + 200: Pty } -export type SessionGetError = SessionGetErrors[keyof SessionGetErrors] +export type PtyUpdateResponse = PtyUpdateResponses[keyof PtyUpdateResponses] -export type SessionGetResponses = { +export type QuestionListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/question" +} + +export type QuestionListResponses = { /** - * Get session + * List of pending questions */ - 200: Session + 200: Array } -export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses] +export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses] -export type SessionUpdateData = { +export type QuestionReplyData = { body?: { - title?: string - permission?: PermissionRuleset - time?: { - archived?: number - } + /** + * User answers in order of questions (each answer is an array of selected labels) + */ + answers: Array } path: { - sessionID: string + requestID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}" + url: "/question/{requestID}/reply" } -export type SessionUpdateErrors = { +export type QuestionReplyErrors = { /** * Bad request */ @@ -4722,30 +5698,30 @@ export type SessionUpdateErrors = { 404: NotFoundError } -export type SessionUpdateError = SessionUpdateErrors[keyof SessionUpdateErrors] +export type QuestionReplyError = QuestionReplyErrors[keyof QuestionReplyErrors] -export type SessionUpdateResponses = { +export type QuestionReplyResponses = { /** - * Successfully updated session + * Question answered successfully */ - 200: Session + 200: boolean } -export type SessionUpdateResponse = SessionUpdateResponses[keyof SessionUpdateResponses] +export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses] -export type SessionChildrenData = { +export type QuestionRejectData = { body?: never path: { - sessionID: string + requestID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/children" + url: "/question/{requestID}/reject" } -export type SessionChildrenErrors = { +export type QuestionRejectErrors = { /** * Bad request */ @@ -4756,68 +5732,52 @@ export type SessionChildrenErrors = { 404: NotFoundError } -export type SessionChildrenError = SessionChildrenErrors[keyof SessionChildrenErrors] +export type QuestionRejectError = QuestionRejectErrors[keyof QuestionRejectErrors] -export type SessionChildrenResponses = { +export type QuestionRejectResponses = { /** - * List of children + * Question rejected successfully */ - 200: Array + 200: boolean } -export type SessionChildrenResponse = SessionChildrenResponses[keyof SessionChildrenResponses] +export type QuestionRejectResponse = QuestionRejectResponses[keyof QuestionRejectResponses] -export type SessionTodoData = { +export type PermissionListData = { body?: never - path: { - sessionID: string - } + path?: never query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/todo" -} - -export type SessionTodoErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError + url: "/permission" } -export type SessionTodoError = SessionTodoErrors[keyof SessionTodoErrors] - -export type SessionTodoResponses = { +export type PermissionListResponses = { /** - * Todo list + * List of pending permissions */ - 200: Array + 200: Array } -export type SessionTodoResponse = SessionTodoResponses[keyof SessionTodoResponses] +export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] -export type SessionInitData = { +export type PermissionReplyData = { body?: { - modelID: string - providerID: string - messageID: string + reply: "once" | "always" | "reject" + message?: string } path: { - sessionID: string + requestID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/init" + url: "/permission/{requestID}/reply" } -export type SessionInitErrors = { +export type PermissionReplyErrors = { /** * Bad request */ @@ -4828,264 +5788,233 @@ export type SessionInitErrors = { 404: NotFoundError } -export type SessionInitError = SessionInitErrors[keyof SessionInitErrors] +export type PermissionReplyError = PermissionReplyErrors[keyof PermissionReplyErrors] -export type SessionInitResponses = { +export type PermissionReplyResponses = { /** - * 200 + * Permission processed successfully */ 200: boolean } -export type SessionInitResponse = SessionInitResponses[keyof SessionInitResponses] +export type PermissionReplyResponse = PermissionReplyResponses[keyof PermissionReplyResponses] -export type SessionForkData = { - body?: { - messageID?: string - } - path: { - sessionID: string - } +export type ProviderListData = { + body?: never + path?: never query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/fork" + url: "/provider" } -export type SessionForkResponses = { +export type ProviderListResponses = { /** - * 200 + * List of providers */ - 200: Session + 200: { + all: Array + default: { + [key: string]: string + } + connected: Array + } } -export type SessionForkResponse = SessionForkResponses[keyof SessionForkResponses] +export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses] -export type SessionAbortData = { +export type ProviderAuthData = { body?: never - path: { - sessionID: string - } + path?: never query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/abort" -} - -export type SessionAbortErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError + url: "/provider/auth" } -export type SessionAbortError = SessionAbortErrors[keyof SessionAbortErrors] - -export type SessionAbortResponses = { +export type ProviderAuthResponses = { /** - * Aborted session + * Provider auth methods */ - 200: boolean + 200: { + [key: string]: Array + } } -export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses] +export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses] -export type SessionUnshareData = { - body?: never +export type ProviderOauthAuthorizeData = { + body?: { + /** + * Auth method index + */ + method: number + inputs?: { + [key: string]: string + } + } path: { - sessionID: string + providerID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/share" + url: "/provider/{providerID}/oauth/authorize" } -export type SessionUnshareErrors = { +export type ProviderOauthAuthorizeErrors = { /** * Bad request */ 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError } -export type SessionUnshareError = SessionUnshareErrors[keyof SessionUnshareErrors] +export type ProviderOauthAuthorizeError = ProviderOauthAuthorizeErrors[keyof ProviderOauthAuthorizeErrors] -export type SessionUnshareResponses = { +export type ProviderOauthAuthorizeResponses = { /** - * Successfully unshared session + * Authorization URL and method */ - 200: Session + 200: ProviderAuthAuthorization } -export type SessionUnshareResponse = SessionUnshareResponses[keyof SessionUnshareResponses] +export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses] -export type SessionShareData = { - body?: never +export type ProviderOauthCallbackData = { + body?: { + /** + * Auth method index + */ + method: number + code?: string + } path: { - sessionID: string + providerID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/share" + url: "/provider/{providerID}/oauth/callback" } -export type SessionShareErrors = { +export type ProviderOauthCallbackErrors = { /** * Bad request */ 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError } -export type SessionShareError = SessionShareErrors[keyof SessionShareErrors] +export type ProviderOauthCallbackError = ProviderOauthCallbackErrors[keyof ProviderOauthCallbackErrors] -export type SessionShareResponses = { +export type ProviderOauthCallbackResponses = { /** - * Successfully shared session + * OAuth callback processed successfully */ - 200: Session + 200: boolean } -export type SessionShareResponse = SessionShareResponses[keyof SessionShareResponses] +export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] -export type SessionDiffData = { +export type SessionListData = { body?: never - path: { - sessionID: string - } + path?: never query?: { directory?: string workspace?: string - messageID?: string + scope?: "project" + path?: string + roots?: boolean | "true" | "false" + start?: number + search?: string + limit?: number } - url: "/session/{sessionID}/diff" + url: "/session" } -export type SessionDiffResponses = { +export type SessionListResponses = { /** - * Successfully retrieved diff + * List of sessions */ - 200: Array + 200: Array } -export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses] +export type SessionListResponse = SessionListResponses[keyof SessionListResponses] -export type SessionSummarizeData = { +export type SessionCreateData = { body?: { - providerID: string - modelID: string - auto?: boolean - } - path: { - sessionID: string + parentID?: string + title?: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + permission?: PermissionRuleset + workspaceID?: string } + path?: never query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/summarize" + url: "/session" } -export type SessionSummarizeErrors = { +export type SessionCreateErrors = { /** * Bad request */ 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError } -export type SessionSummarizeError = SessionSummarizeErrors[keyof SessionSummarizeErrors] +export type SessionCreateError = SessionCreateErrors[keyof SessionCreateErrors] -export type SessionSummarizeResponses = { +export type SessionCreateResponses = { /** - * Summarized session + * Successfully created session */ - 200: boolean + 200: Session } -export type SessionSummarizeResponse = SessionSummarizeResponses[keyof SessionSummarizeResponses] +export type SessionCreateResponse = SessionCreateResponses[keyof SessionCreateResponses] -export type SessionMessagesData = { +export type SessionStatusData = { body?: never - path: { - sessionID: string - } + path?: never query?: { directory?: string workspace?: string - /** - * Maximum number of messages to return - */ - limit?: number - before?: string } - url: "/session/{sessionID}/message" + url: "/session/status" } -export type SessionMessagesErrors = { +export type SessionStatusErrors = { /** * Bad request */ 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError } -export type SessionMessagesError = SessionMessagesErrors[keyof SessionMessagesErrors] +export type SessionStatusError = SessionStatusErrors[keyof SessionStatusErrors] -export type SessionMessagesResponses = { +export type SessionStatusResponses = { /** - * List of messages + * Get session status */ - 200: Array<{ - info: Message - parts: Array - }> + 200: { + [key: string]: SessionStatus + } } -export type SessionMessagesResponse = SessionMessagesResponses[keyof SessionMessagesResponses] +export type SessionStatusResponse = SessionStatusResponses[keyof SessionStatusResponses] -export type SessionPromptData = { - body?: { - messageID?: string - model?: { - providerID: string - modelID: string - } - agent?: string - noReply?: boolean - /** - * @deprecated tools and permissions have been merged, you can set permissions on the session itself now - */ - tools?: { - [key: string]: boolean - } - format?: OutputFormat - system?: string - variant?: string - parts: Array - } +export type SessionDeleteData = { + body?: never path: { sessionID: string } @@ -5093,10 +6022,10 @@ export type SessionPromptData = { directory?: string workspace?: string } - url: "/session/{sessionID}/message" + url: "/session/{sessionID}" } -export type SessionPromptErrors = { +export type SessionDeleteErrors = { /** * Bad request */ @@ -5107,34 +6036,30 @@ export type SessionPromptErrors = { 404: NotFoundError } -export type SessionPromptError = SessionPromptErrors[keyof SessionPromptErrors] +export type SessionDeleteError = SessionDeleteErrors[keyof SessionDeleteErrors] -export type SessionPromptResponses = { +export type SessionDeleteResponses = { /** - * Created message + * Successfully deleted session */ - 200: { - info: AssistantMessage - parts: Array - } + 200: boolean } -export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses] +export type SessionDeleteResponse = SessionDeleteResponses[keyof SessionDeleteResponses] -export type SessionDeleteMessageData = { +export type SessionGetData = { body?: never path: { sessionID: string - messageID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/message/{messageID}" + url: "/session/{sessionID}" } -export type SessionDeleteMessageErrors = { +export type SessionGetErrors = { /** * Bad request */ @@ -5145,31 +6070,36 @@ export type SessionDeleteMessageErrors = { 404: NotFoundError } -export type SessionDeleteMessageError = SessionDeleteMessageErrors[keyof SessionDeleteMessageErrors] +export type SessionGetError = SessionGetErrors[keyof SessionGetErrors] -export type SessionDeleteMessageResponses = { +export type SessionGetResponses = { /** - * Successfully deleted message + * Get session */ - 200: boolean + 200: Session } -export type SessionDeleteMessageResponse = SessionDeleteMessageResponses[keyof SessionDeleteMessageResponses] +export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses] -export type SessionMessageData = { - body?: never +export type SessionUpdateData = { + body?: { + title?: string + permission?: PermissionRuleset + time?: { + archived?: number + } + } path: { sessionID: string - messageID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/message/{messageID}" + url: "/session/{sessionID}" } -export type SessionMessageErrors = { +export type SessionUpdateErrors = { /** * Bad request */ @@ -5180,35 +6110,30 @@ export type SessionMessageErrors = { 404: NotFoundError } -export type SessionMessageError = SessionMessageErrors[keyof SessionMessageErrors] +export type SessionUpdateError = SessionUpdateErrors[keyof SessionUpdateErrors] -export type SessionMessageResponses = { +export type SessionUpdateResponses = { /** - * Message + * Successfully updated session */ - 200: { - info: Message - parts: Array - } + 200: Session } -export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses] +export type SessionUpdateResponse = SessionUpdateResponses[keyof SessionUpdateResponses] -export type PartDeleteData = { +export type SessionChildrenData = { body?: never path: { sessionID: string - messageID: string - partID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/message/{messageID}/part/{partID}" + url: "/session/{sessionID}/children" } -export type PartDeleteErrors = { +export type SessionChildrenErrors = { /** * Bad request */ @@ -5219,32 +6144,30 @@ export type PartDeleteErrors = { 404: NotFoundError } -export type PartDeleteError = PartDeleteErrors[keyof PartDeleteErrors] +export type SessionChildrenError = SessionChildrenErrors[keyof SessionChildrenErrors] -export type PartDeleteResponses = { +export type SessionChildrenResponses = { /** - * Successfully deleted part + * List of children */ - 200: boolean + 200: Array } -export type PartDeleteResponse = PartDeleteResponses[keyof PartDeleteResponses] +export type SessionChildrenResponse = SessionChildrenResponses[keyof SessionChildrenResponses] -export type PartUpdateData = { - body?: Part +export type SessionTodoData = { + body?: never path: { sessionID: string - messageID: string - partID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/message/{messageID}/part/{partID}" + url: "/session/{sessionID}/todo" } -export type PartUpdateErrors = { +export type SessionTodoErrors = { /** * Bad request */ @@ -5255,97 +6178,54 @@ export type PartUpdateErrors = { 404: NotFoundError } -export type PartUpdateError = PartUpdateErrors[keyof PartUpdateErrors] +export type SessionTodoError = SessionTodoErrors[keyof SessionTodoErrors] -export type PartUpdateResponses = { +export type SessionTodoResponses = { /** - * Successfully updated part + * Todo list */ - 200: Part + 200: Array } -export type PartUpdateResponse = PartUpdateResponses[keyof PartUpdateResponses] +export type SessionTodoResponse = SessionTodoResponses[keyof SessionTodoResponses] -export type SessionPromptAsyncData = { - body?: { - messageID?: string - model?: { - providerID: string - modelID: string - } - agent?: string - noReply?: boolean - /** - * @deprecated tools and permissions have been merged, you can set permissions on the session itself now - */ - tools?: { - [key: string]: boolean - } - format?: OutputFormat - system?: string - variant?: string - parts: Array - } +export type SessionDiffData = { + body?: never path: { sessionID: string } query?: { directory?: string workspace?: string + messageID?: string } - url: "/session/{sessionID}/prompt_async" -} - -export type SessionPromptAsyncErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError + url: "/session/{sessionID}/diff" } -export type SessionPromptAsyncError = SessionPromptAsyncErrors[keyof SessionPromptAsyncErrors] - -export type SessionPromptAsyncResponses = { +export type SessionDiffResponses = { /** - * Prompt accepted + * Successfully retrieved diff */ - 204: void + 200: Array } -export type SessionPromptAsyncResponse = SessionPromptAsyncResponses[keyof SessionPromptAsyncResponses] +export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses] -export type SessionCommandData = { - body?: { - messageID?: string - agent?: string - model?: string - arguments: string - command: string - variant?: string - parts?: Array<{ - id?: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource - }> - } +export type SessionMessagesData = { + body?: never path: { sessionID: string } query?: { directory?: string workspace?: string + limit?: number + before?: string } - url: "/session/{sessionID}/command" + url: "/session/{sessionID}/message" } -export type SessionCommandErrors = { +export type SessionMessagesErrors = { /** * Bad request */ @@ -5356,29 +6236,36 @@ export type SessionCommandErrors = { 404: NotFoundError } -export type SessionCommandError = SessionCommandErrors[keyof SessionCommandErrors] +export type SessionMessagesError = SessionMessagesErrors[keyof SessionMessagesErrors] -export type SessionCommandResponses = { +export type SessionMessagesResponses = { /** - * Created message + * List of messages */ - 200: { - info: AssistantMessage + 200: Array<{ + info: Message parts: Array - } + }> } -export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses] +export type SessionMessagesResponse = SessionMessagesResponses[keyof SessionMessagesResponses] -export type SessionShellData = { +export type SessionPromptData = { body?: { messageID?: string - agent: string model?: { providerID: string modelID: string } - command: string + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts: Array } path: { sessionID: string @@ -5387,10 +6274,10 @@ export type SessionShellData = { directory?: string workspace?: string } - url: "/session/{sessionID}/shell" + url: "/session/{sessionID}/message" } -export type SessionShellErrors = { +export type SessionPromptErrors = { /** * Bad request */ @@ -5401,36 +6288,34 @@ export type SessionShellErrors = { 404: NotFoundError } -export type SessionShellError = SessionShellErrors[keyof SessionShellErrors] +export type SessionPromptError = SessionPromptErrors[keyof SessionPromptErrors] -export type SessionShellResponses = { +export type SessionPromptResponses = { /** * Created message */ 200: { - info: Message + info: AssistantMessage parts: Array } } -export type SessionShellResponse = SessionShellResponses[keyof SessionShellResponses] +export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses] -export type SessionRevertData = { - body?: { - messageID: string - partID?: string - } +export type SessionDeleteMessageData = { + body?: never path: { sessionID: string + messageID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/revert" + url: "/session/{sessionID}/message/{messageID}" } -export type SessionRevertErrors = { +export type SessionDeleteMessageErrors = { /** * Bad request */ @@ -5441,30 +6326,31 @@ export type SessionRevertErrors = { 404: NotFoundError } -export type SessionRevertError = SessionRevertErrors[keyof SessionRevertErrors] +export type SessionDeleteMessageError = SessionDeleteMessageErrors[keyof SessionDeleteMessageErrors] -export type SessionRevertResponses = { +export type SessionDeleteMessageResponses = { /** - * Updated session + * Successfully deleted message */ - 200: Session + 200: boolean } -export type SessionRevertResponse = SessionRevertResponses[keyof SessionRevertResponses] +export type SessionDeleteMessageResponse = SessionDeleteMessageResponses[keyof SessionDeleteMessageResponses] -export type SessionUnrevertData = { +export type SessionMessageData = { body?: never path: { sessionID: string + messageID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/unrevert" + url: "/session/{sessionID}/message/{messageID}" } -export type SessionUnrevertErrors = { +export type SessionMessageErrors = { /** * Bad request */ @@ -5475,70 +6361,56 @@ export type SessionUnrevertErrors = { 404: NotFoundError } -export type SessionUnrevertError = SessionUnrevertErrors[keyof SessionUnrevertErrors] +export type SessionMessageError = SessionMessageErrors[keyof SessionMessageErrors] -export type SessionUnrevertResponses = { +export type SessionMessageResponses = { /** - * Updated session + * Message */ - 200: Session + 200: { + info: Message + parts: Array + } } -export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnrevertResponses] +export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses] -export type PermissionRespondData = { +export type SessionForkData = { body?: { - response: "once" | "always" | "reject" + messageID?: string } path: { sessionID: string - permissionID: string } query?: { directory?: string workspace?: string } - url: "/session/{sessionID}/permissions/{permissionID}" -} - -export type PermissionRespondErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError + url: "/session/{sessionID}/fork" } -export type PermissionRespondError = PermissionRespondErrors[keyof PermissionRespondErrors] - -export type PermissionRespondResponses = { +export type SessionForkResponses = { /** - * Permission processed successfully + * 200 */ - 200: boolean + 200: Session } -export type PermissionRespondResponse = PermissionRespondResponses[keyof PermissionRespondResponses] +export type SessionForkResponse = SessionForkResponses[keyof SessionForkResponses] -export type PermissionReplyData = { - body?: { - reply: "once" | "always" | "reject" - message?: string - } +export type SessionAbortData = { + body?: never path: { - requestID: string + sessionID: string } query?: { directory?: string workspace?: string } - url: "/permission/{requestID}/reply" + url: "/session/{sessionID}/abort" } -export type PermissionReplyErrors = { +export type SessionAbortErrors = { /** * Bad request */ @@ -5549,73 +6421,34 @@ export type PermissionReplyErrors = { 404: NotFoundError } -export type PermissionReplyError = PermissionReplyErrors[keyof PermissionReplyErrors] +export type SessionAbortError = SessionAbortErrors[keyof SessionAbortErrors] -export type PermissionReplyResponses = { +export type SessionAbortResponses = { /** - * Permission processed successfully + * Aborted session */ 200: boolean } -export type PermissionReplyResponse = PermissionReplyResponses[keyof PermissionReplyResponses] - -export type PermissionListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/permission" -} - -export type PermissionListResponses = { - /** - * List of pending permissions - */ - 200: Array -} - -export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] - -export type QuestionListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/question" -} - -export type QuestionListResponses = { - /** - * List of pending questions - */ - 200: Array -} - -export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses] +export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses] -export type QuestionReplyData = { +export type SessionInitData = { body?: { - /** - * User answers in order of questions (each answer is an array of selected labels) - */ - answers: Array + modelID: string + providerID: string + messageID: string } path: { - requestID: string + sessionID: string } query?: { directory?: string workspace?: string } - url: "/question/{requestID}/reply" + url: "/session/{sessionID}/init" } -export type QuestionReplyErrors = { +export type SessionInitErrors = { /** * Bad request */ @@ -5626,30 +6459,30 @@ export type QuestionReplyErrors = { 404: NotFoundError } -export type QuestionReplyError = QuestionReplyErrors[keyof QuestionReplyErrors] +export type SessionInitError = SessionInitErrors[keyof SessionInitErrors] -export type QuestionReplyResponses = { +export type SessionInitResponses = { /** - * Question answered successfully + * 200 */ 200: boolean } -export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses] +export type SessionInitResponse = SessionInitResponses[keyof SessionInitResponses] -export type QuestionRejectData = { +export type SessionUnshareData = { body?: never path: { - requestID: string + sessionID: string } query?: { directory?: string workspace?: string } - url: "/question/{requestID}/reject" + url: "/session/{sessionID}/share" } -export type QuestionRejectErrors = { +export type SessionUnshareErrors = { /** * Bad request */ @@ -5660,643 +6493,657 @@ export type QuestionRejectErrors = { 404: NotFoundError } -export type QuestionRejectError = QuestionRejectErrors[keyof QuestionRejectErrors] - -export type QuestionRejectResponses = { - /** - * Question rejected successfully - */ - 200: boolean -} - -export type QuestionRejectResponse = QuestionRejectResponses[keyof QuestionRejectResponses] - -export type ProviderListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/provider" -} - -export type ProviderListResponses = { - /** - * List of providers - */ - 200: { - all: Array - default: { - [key: string]: string - } - connected: Array - } -} - -export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses] - -export type ProviderAuthData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/provider/auth" -} +export type SessionUnshareError = SessionUnshareErrors[keyof SessionUnshareErrors] -export type ProviderAuthResponses = { +export type SessionUnshareResponses = { /** - * Provider auth methods + * Successfully unshared session */ - 200: { - [key: string]: Array - } -} - -export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses] - -export type ProviderOauthAuthorizeData = { - body?: { - /** - * Auth method index - */ - method: number - /** - * Prompt inputs - */ - inputs?: { - [key: string]: string - } - } + 200: Session +} + +export type SessionUnshareResponse = SessionUnshareResponses[keyof SessionUnshareResponses] + +export type SessionShareData = { + body?: never path: { - /** - * Provider ID - */ - providerID: string + sessionID: string } query?: { directory?: string workspace?: string } - url: "/provider/{providerID}/oauth/authorize" + url: "/session/{sessionID}/share" } -export type ProviderOauthAuthorizeErrors = { +export type SessionShareErrors = { /** * Bad request */ 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError } -export type ProviderOauthAuthorizeError = ProviderOauthAuthorizeErrors[keyof ProviderOauthAuthorizeErrors] +export type SessionShareError = SessionShareErrors[keyof SessionShareErrors] -export type ProviderOauthAuthorizeResponses = { +export type SessionShareResponses = { /** - * Authorization URL and method + * Successfully shared session */ - 200: ProviderAuthAuthorization + 200: Session } -export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses] +export type SessionShareResponse = SessionShareResponses[keyof SessionShareResponses] -export type ProviderOauthCallbackData = { +export type SessionSummarizeData = { body?: { - /** - * Auth method index - */ - method: number - /** - * OAuth authorization code - */ - code?: string + providerID: string + modelID: string + auto?: boolean } path: { - /** - * Provider ID - */ - providerID: string + sessionID: string } query?: { directory?: string workspace?: string } - url: "/provider/{providerID}/oauth/callback" + url: "/session/{sessionID}/summarize" } -export type ProviderOauthCallbackErrors = { +export type SessionSummarizeErrors = { /** * Bad request */ 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError } -export type ProviderOauthCallbackError = ProviderOauthCallbackErrors[keyof ProviderOauthCallbackErrors] +export type SessionSummarizeError = SessionSummarizeErrors[keyof SessionSummarizeErrors] -export type ProviderOauthCallbackResponses = { +export type SessionSummarizeResponses = { /** - * OAuth callback processed successfully + * Summarized session */ 200: boolean } -export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] +export type SessionSummarizeResponse = SessionSummarizeResponses[keyof SessionSummarizeResponses] -export type SyncStartData = { - body?: never - path?: never +export type SessionPromptAsyncData = { + body?: { + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts: Array + } + path: { + sessionID: string + } query?: { directory?: string workspace?: string } - url: "/sync/start" + url: "/session/{sessionID}/prompt_async" } -export type SyncStartResponses = { +export type SessionPromptAsyncErrors = { /** - * Workspace sync started + * Bad request */ - 200: boolean + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError } -export type SyncStartResponse = SyncStartResponses[keyof SyncStartResponses] +export type SessionPromptAsyncError = SessionPromptAsyncErrors[keyof SessionPromptAsyncErrors] -export type SyncReplayData = { +export type SessionPromptAsyncResponses = { + /** + * Prompt accepted + */ + 204: void +} + +export type SessionPromptAsyncResponse = SessionPromptAsyncResponses[keyof SessionPromptAsyncResponses] + +export type SessionCommandData = { body?: { - directory: string - events: Array<{ - id: string - aggregateID: string - seq: number - type: string - data: { - [key: string]: unknown - } + messageID?: string + agent?: string + model?: string + arguments: string + command: string + variant?: string + parts?: Array<{ + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource }> } - path?: never + path: { + sessionID: string + } query?: { directory?: string workspace?: string } - url: "/sync/replay" + url: "/session/{sessionID}/command" } -export type SyncReplayErrors = { +export type SessionCommandErrors = { /** * Bad request */ 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError } -export type SyncReplayError = SyncReplayErrors[keyof SyncReplayErrors] +export type SessionCommandError = SessionCommandErrors[keyof SessionCommandErrors] -export type SyncReplayResponses = { +export type SessionCommandResponses = { /** - * Replayed sync events + * Created message */ 200: { - sessionID: string + info: AssistantMessage + parts: Array } } -export type SyncReplayResponse = SyncReplayResponses[keyof SyncReplayResponses] +export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses] -export type SyncHistoryListData = { +export type SessionShellData = { body?: { - [key: string]: number + messageID?: string + agent: string + model?: { + providerID: string + modelID: string + } + command: string + } + path: { + sessionID: string } - path?: never query?: { directory?: string workspace?: string } - url: "/sync/history" + url: "/session/{sessionID}/shell" } -export type SyncHistoryListErrors = { +export type SessionShellErrors = { /** * Bad request */ 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError } -export type SyncHistoryListError = SyncHistoryListErrors[keyof SyncHistoryListErrors] +export type SessionShellError = SessionShellErrors[keyof SessionShellErrors] -export type SyncHistoryListResponses = { +export type SessionShellResponses = { /** - * Sync events + * Created message */ - 200: Array<{ - id: string - aggregate_id: string - seq: number - type: string - data: { - [key: string]: unknown - } - }> + 200: { + info: Message + parts: Array + } } -export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses] +export type SessionShellResponse = SessionShellResponses[keyof SessionShellResponses] -export type FindTextData = { - body?: never - path?: never - query: { +export type SessionRevertData = { + body?: { + messageID: string + partID?: string + } + path: { + sessionID: string + } + query?: { directory?: string workspace?: string - pattern: string } - url: "/find" + url: "/session/{sessionID}/revert" } -export type FindTextResponses = { +export type SessionRevertErrors = { /** - * Matches + * Bad request */ - 200: Array<{ - path: { - text: string - } - lines: { - text: string - } - line_number: number - absolute_offset: number - submatches: Array<{ - match: { - text: string - } - start: number - end: number - }> - }> + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError } -export type FindTextResponse = FindTextResponses[keyof FindTextResponses] - -export type FindFilesData = { - body?: never - path?: never - query: { - directory?: string - workspace?: string - query: string - dirs?: "true" | "false" - type?: "file" | "directory" - limit?: number - } - url: "/find/file" -} +export type SessionRevertError = SessionRevertErrors[keyof SessionRevertErrors] -export type FindFilesResponses = { +export type SessionRevertResponses = { /** - * File paths + * Updated session */ - 200: Array + 200: Session } -export type FindFilesResponse = FindFilesResponses[keyof FindFilesResponses] +export type SessionRevertResponse = SessionRevertResponses[keyof SessionRevertResponses] -export type FindSymbolsData = { +export type SessionUnrevertData = { body?: never - path?: never - query: { + path: { + sessionID: string + } + query?: { directory?: string workspace?: string - query: string } - url: "/find/symbol" + url: "/session/{sessionID}/unrevert" } -export type FindSymbolsResponses = { +export type SessionUnrevertErrors = { /** - * Symbols + * Bad request */ - 200: Array + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError } -export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses] - -export type FileListData = { - body?: never - path?: never - query: { - directory?: string - workspace?: string - path: string - } - url: "/file" -} +export type SessionUnrevertError = SessionUnrevertErrors[keyof SessionUnrevertErrors] -export type FileListResponses = { +export type SessionUnrevertResponses = { /** - * Files and directories + * Updated session */ - 200: Array + 200: Session } -export type FileListResponse = FileListResponses[keyof FileListResponses] +export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnrevertResponses] -export type FileReadData = { - body?: never - path?: never - query: { +export type PermissionRespondData = { + body?: { + response: "once" | "always" | "reject" + } + path: { + sessionID: string + permissionID: string + } + query?: { directory?: string workspace?: string - path: string } - url: "/file/content" + url: "/session/{sessionID}/permissions/{permissionID}" } -export type FileReadResponses = { +export type PermissionRespondErrors = { /** - * File content + * Bad request */ - 200: FileContent + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError } -export type FileReadResponse = FileReadResponses[keyof FileReadResponses] +export type PermissionRespondError = PermissionRespondErrors[keyof PermissionRespondErrors] -export type FileStatusData = { +export type PermissionRespondResponses = { + /** + * Permission processed successfully + */ + 200: boolean +} + +export type PermissionRespondResponse = PermissionRespondResponses[keyof PermissionRespondResponses] + +export type PartDeleteData = { body?: never - path?: never + path: { + sessionID: string + messageID: string + partID: string + } query?: { directory?: string workspace?: string } - url: "/file/status" + url: "/session/{sessionID}/message/{messageID}/part/{partID}" } -export type FileStatusResponses = { +export type PartDeleteErrors = { /** - * File status + * Bad request */ - 200: Array + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError } -export type FileStatusResponse = FileStatusResponses[keyof FileStatusResponses] +export type PartDeleteError = PartDeleteErrors[keyof PartDeleteErrors] -export type EventSubscribeData = { - body?: never - path?: never +export type PartDeleteResponses = { + /** + * Successfully deleted part + */ + 200: boolean +} + +export type PartDeleteResponse = PartDeleteResponses[keyof PartDeleteResponses] + +export type PartUpdateData = { + body?: Part + path: { + sessionID: string + messageID: string + partID: string + } query?: { directory?: string workspace?: string } - url: "/event" + url: "/session/{sessionID}/message/{messageID}/part/{partID}" } -export type EventSubscribeResponses = { +export type PartUpdateErrors = { /** - * Event stream + * Bad request */ - 200: Event + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError } -export type EventSubscribeResponse = EventSubscribeResponses[keyof EventSubscribeResponses] +export type PartUpdateError = PartUpdateErrors[keyof PartUpdateErrors] -export type McpStatusData = { +export type PartUpdateResponses = { + /** + * Successfully updated part + */ + 200: Part +} + +export type PartUpdateResponse = PartUpdateResponses[keyof PartUpdateResponses] + +export type SyncStartData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/mcp" + url: "/sync/start" } -export type McpStatusResponses = { +export type SyncStartResponses = { /** - * MCP server status + * Workspace sync started */ - 200: { - [key: string]: McpStatus - } + 200: boolean } -export type McpStatusResponse = McpStatusResponses[keyof McpStatusResponses] +export type SyncStartResponse = SyncStartResponses[keyof SyncStartResponses] -export type McpAddData = { +export type SyncReplayData = { body?: { - name: string - config: McpLocalConfig | McpRemoteConfig + directory: string + events: Array<{ + id: string + aggregateID: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> } path?: never query?: { directory?: string workspace?: string } - url: "/mcp" + url: "/sync/replay" } -export type McpAddErrors = { +export type SyncReplayErrors = { /** * Bad request */ 400: BadRequestError } -export type McpAddError = McpAddErrors[keyof McpAddErrors] +export type SyncReplayError = SyncReplayErrors[keyof SyncReplayErrors] -export type McpAddResponses = { +export type SyncReplayResponses = { /** - * MCP server added successfully + * Replayed sync events */ 200: { - [key: string]: McpStatus + sessionID: string } } -export type McpAddResponse = McpAddResponses[keyof McpAddResponses] +export type SyncReplayResponse = SyncReplayResponses[keyof SyncReplayResponses] -export type McpAuthRemoveData = { - body?: never - path: { - name: string +export type SyncHistoryListData = { + body?: { + [key: string]: number } + path?: never query?: { directory?: string workspace?: string } - url: "/mcp/{name}/auth" + url: "/sync/history" } -export type McpAuthRemoveErrors = { +export type SyncHistoryListErrors = { /** - * Not found + * Bad request */ - 404: NotFoundError + 400: BadRequestError } -export type McpAuthRemoveError = McpAuthRemoveErrors[keyof McpAuthRemoveErrors] +export type SyncHistoryListError = SyncHistoryListErrors[keyof SyncHistoryListErrors] -export type McpAuthRemoveResponses = { +export type SyncHistoryListResponses = { /** - * OAuth credentials removed + * Sync events */ - 200: { - success: true - } + 200: Array<{ + id: string + aggregate_id: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> } -export type McpAuthRemoveResponse = McpAuthRemoveResponses[keyof McpAuthRemoveResponses] +export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses] -export type McpAuthStartData = { +export type V2SessionListData = { body?: never - path: { - name: string - } + path?: never query?: { directory?: string workspace?: string } - url: "/mcp/{name}/auth" + url: "/api/session" } -export type McpAuthStartErrors = { - /** - * MCP server does not support OAuth - */ - 400: McpUnsupportedOAuthError +export type V2SessionListErrors = { /** - * Not found + * Bad request */ - 404: NotFoundError + 400: BadRequestError } -export type McpAuthStartError = McpAuthStartErrors[keyof McpAuthStartErrors] +export type V2SessionListError = V2SessionListErrors[keyof V2SessionListErrors] -export type McpAuthStartResponses = { +export type V2SessionListResponses = { /** - * OAuth flow started + * V2SessionsResponse */ - 200: { - /** - * URL to open in browser for authorization - */ - authorizationUrl: string - } + 200: V2SessionsResponse } -export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartResponses] +export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses] -export type McpAuthCallbackData = { +export type V2SessionPromptData = { body?: { - /** - * Authorization code from OAuth callback - */ - code: string + prompt: Prompt + delivery?: SessionDelivery } path: { - name: string + sessionID: string } query?: { directory?: string workspace?: string } - url: "/mcp/{name}/auth/callback" -} - -export type McpAuthCallbackErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError + url: "/api/session/{sessionID}/prompt" } -export type McpAuthCallbackError = McpAuthCallbackErrors[keyof McpAuthCallbackErrors] - -export type McpAuthCallbackResponses = { +export type V2SessionPromptResponses = { /** - * OAuth authentication completed + * Session.Message */ - 200: McpStatus + 200: SessionMessage } -export type McpAuthCallbackResponse = McpAuthCallbackResponses[keyof McpAuthCallbackResponses] +export type V2SessionPromptResponse = V2SessionPromptResponses[keyof V2SessionPromptResponses] -export type McpAuthAuthenticateData = { +export type V2SessionCompactData = { body?: never path: { - name: string + sessionID: string } query?: { directory?: string workspace?: string } - url: "/mcp/{name}/auth/authenticate" + url: "/api/session/{sessionID}/compact" } -export type McpAuthAuthenticateErrors = { - /** - * MCP server does not support OAuth - */ - 400: McpUnsupportedOAuthError +export type V2SessionCompactResponses = { /** - * Not found + * */ - 404: NotFoundError + 204: void } -export type McpAuthAuthenticateError = McpAuthAuthenticateErrors[keyof McpAuthAuthenticateErrors] +export type V2SessionCompactResponse = V2SessionCompactResponses[keyof V2SessionCompactResponses] -export type McpAuthAuthenticateResponses = { +export type V2SessionWaitData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/api/session/{sessionID}/wait" +} + +export type V2SessionWaitResponses = { /** - * OAuth authentication completed + * */ - 200: McpStatus + 204: void } -export type McpAuthAuthenticateResponse = McpAuthAuthenticateResponses[keyof McpAuthAuthenticateResponses] +export type V2SessionWaitResponse = V2SessionWaitResponses[keyof V2SessionWaitResponses] -export type McpConnectData = { +export type V2SessionContextData = { body?: never path: { - name: string + sessionID: string } query?: { directory?: string workspace?: string } - url: "/mcp/{name}/connect" + url: "/api/session/{sessionID}/context" } -export type McpConnectResponses = { +export type V2SessionContextResponses = { /** - * MCP server connected successfully + * Success */ - 200: boolean + 200: Array } -export type McpConnectResponse = McpConnectResponses[keyof McpConnectResponses] +export type V2SessionContextResponse = V2SessionContextResponses[keyof V2SessionContextResponses] -export type McpDisconnectData = { +export type V2SessionMessagesData = { body?: never path: { - name: string + sessionID: string } query?: { directory?: string workspace?: string } - url: "/mcp/{name}/disconnect" + url: "/api/session/{sessionID}/message" } -export type McpDisconnectResponses = { +export type V2SessionMessagesErrors = { /** - * MCP server disconnected successfully + * Bad request */ - 200: boolean + 400: BadRequestError } -export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses] +export type V2SessionMessagesError = V2SessionMessagesErrors[keyof V2SessionMessagesErrors] + +export type V2SessionMessagesResponses = { + /** + * V2SessionMessagesResponse + */ + 200: V2SessionMessagesResponse +} + +export type V2SessionMessagesResponse2 = V2SessionMessagesResponses[keyof V2SessionMessagesResponses] export type TuiAppendPromptData = { body?: { @@ -6477,9 +7324,6 @@ export type TuiShowToastData = { title?: string message: string variant: "info" | "success" | "warning" | "error" - /** - * Duration in milliseconds - */ duration?: number } path?: never @@ -6500,7 +7344,7 @@ export type TuiShowToastResponses = { export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses] export type TuiPublishData = { - body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect + body?: EventTuiPromptAppend2 | EventTuiCommandExecute2 | EventTuiToastShow2 | EventTuiSessionSelect2 path?: never query?: { directory?: string @@ -6605,179 +7449,202 @@ export type TuiControlResponseResponses = { export type TuiControlResponseResponse = TuiControlResponseResponses[keyof TuiControlResponseResponses] -export type InstanceDisposeData = { +export type ExperimentalWorkspaceAdapterListData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/instance/dispose" + url: "/experimental/workspace/adapter" } -export type InstanceDisposeResponses = { +export type ExperimentalWorkspaceAdapterListResponses = { /** - * Instance disposed + * Workspace adapters */ - 200: boolean + 200: Array<{ + type: string + name: string + description: string + }> } -export type InstanceDisposeResponse = InstanceDisposeResponses[keyof InstanceDisposeResponses] +export type ExperimentalWorkspaceAdapterListResponse = + ExperimentalWorkspaceAdapterListResponses[keyof ExperimentalWorkspaceAdapterListResponses] -export type PathGetData = { +export type ExperimentalWorkspaceListData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/path" + url: "/experimental/workspace" } -export type PathGetResponses = { +export type ExperimentalWorkspaceListResponses = { /** - * Path + * Workspaces */ - 200: Path + 200: Array } -export type PathGetResponse = PathGetResponses[keyof PathGetResponses] +export type ExperimentalWorkspaceListResponse = + ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses] -export type VcsGetData = { - body?: never +export type ExperimentalWorkspaceCreateData = { + body?: { + id?: string + type: string + branch: string | null + extra?: unknown | null + } path?: never query?: { directory?: string workspace?: string } - url: "/vcs" + url: "/experimental/workspace" } -export type VcsGetResponses = { +export type ExperimentalWorkspaceCreateErrors = { /** - * VCS info + * Bad request */ - 200: VcsInfo + 400: BadRequestError } -export type VcsGetResponse = VcsGetResponses[keyof VcsGetResponses] - -export type VcsDiffData = { - body?: never - path?: never - query: { - directory?: string - workspace?: string - mode: "git" | "branch" - } - url: "/vcs/diff" -} +export type ExperimentalWorkspaceCreateError = + ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors] -export type VcsDiffResponses = { +export type ExperimentalWorkspaceCreateResponses = { /** - * VCS diff + * Workspace created */ - 200: Array + 200: Workspace } -export type VcsDiffResponse = VcsDiffResponses[keyof VcsDiffResponses] +export type ExperimentalWorkspaceCreateResponse = + ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses] -export type CommandListData = { +export type ExperimentalWorkspaceStatusData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/command" + url: "/experimental/workspace/status" } -export type CommandListResponses = { +export type ExperimentalWorkspaceStatusResponses = { /** - * List of commands + * Workspace status */ - 200: Array + 200: Array<{ + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + }> } -export type CommandListResponse = CommandListResponses[keyof CommandListResponses] +export type ExperimentalWorkspaceStatusResponse = + ExperimentalWorkspaceStatusResponses[keyof ExperimentalWorkspaceStatusResponses] -export type AppAgentsData = { +export type ExperimentalWorkspaceRemoveData = { body?: never - path?: never + path: { + id: string + } query?: { directory?: string workspace?: string } - url: "/agent" + url: "/experimental/workspace/{id}" } -export type AppAgentsResponses = { +export type ExperimentalWorkspaceRemoveErrors = { /** - * List of agents + * Bad request */ - 200: Array + 400: BadRequestError } -export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses] - -export type AppSkillsData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/skill" -} +export type ExperimentalWorkspaceRemoveError = + ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors] -export type AppSkillsResponses = { +export type ExperimentalWorkspaceRemoveResponses = { /** - * List of skills + * Workspace removed */ - 200: Array<{ - name: string - description: string - location: string - content: string - }> + 200: Workspace } -export type AppSkillsResponse = AppSkillsResponses[keyof AppSkillsResponses] +export type ExperimentalWorkspaceRemoveResponse = + ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] -export type LspStatusData = { - body?: never - path?: never +export type ExperimentalWorkspaceSessionRestoreData = { + body?: { + sessionID: string + } + path: { + id: string + } query?: { directory?: string workspace?: string } - url: "/lsp" + url: "/experimental/workspace/{id}/session-restore" } -export type LspStatusResponses = { +export type ExperimentalWorkspaceSessionRestoreErrors = { /** - * LSP server status + * Bad request */ - 200: Array + 400: BadRequestError } -export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses] +export type ExperimentalWorkspaceSessionRestoreError = + ExperimentalWorkspaceSessionRestoreErrors[keyof ExperimentalWorkspaceSessionRestoreErrors] -export type FormatterStatusData = { +export type ExperimentalWorkspaceSessionRestoreResponses = { + /** + * Session replay started + */ + 200: { + total: number + } +} + +export type ExperimentalWorkspaceSessionRestoreResponse = + ExperimentalWorkspaceSessionRestoreResponses[keyof ExperimentalWorkspaceSessionRestoreResponses] + +export type PtyConnectData = { body?: never - path?: never + path: { + ptyID: string + } query?: { directory?: string workspace?: string } - url: "/formatter" + url: "/pty/{ptyID}/connect" } -export type FormatterStatusResponses = { +export type PtyConnectErrors = { /** - * Formatter status + * Not found */ - 200: Array + 404: NotFoundError } -export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] +export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors] + +export type PtyConnectResponses = { + /** + * Connected session + */ + 200: boolean +} + +export type PtyConnectResponse = PtyConnectResponses[keyof PtyConnectResponses] From 7580c825602993374c3891560d708df36afbe0d5 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 20:27:11 -0400 Subject: [PATCH 19/24] sync --- .../instance/httpapi/handlers/v2/message.ts | 2 +- .../instance/httpapi/handlers/v2/session.ts | 4 +-- .../server/routes/instance/httpapi/public.ts | 10 +----- packages/opencode/src/v2/schema.ts | 10 ++++++ packages/opencode/src/v2/session-event.ts | 3 +- packages/opencode/src/v2/session-message.ts | 33 ++++++++++--------- packages/opencode/src/v2/session-prompt.ts | 4 +-- packages/opencode/src/v2/session.ts | 7 ++-- 8 files changed, 39 insertions(+), 34 deletions(-) create mode 100644 packages/opencode/src/v2/schema.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts index a353027b51a2..3485d80fd636 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts @@ -9,7 +9,7 @@ const DefaultMessagesLimit = 50 const Cursor = Schema.Struct({ id: SessionMessage.ID, - time: Schema.Number, + time: Schema.Finite, order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]), direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]), }) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts index 6ca9b0e71f93..558e34dd1842 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts @@ -8,14 +8,14 @@ const DefaultSessionsLimit = 50 const SessionCursor = Schema.Struct({ id: SessionV2.Info.fields.id, - time: Schema.Number, + time: Schema.Finite, order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]), direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]), directory: Schema.String.pipe(Schema.optional), path: Schema.String.pipe(Schema.optional), workspaceID: WorkspaceID.pipe(Schema.optional), roots: Schema.Boolean.pipe(Schema.optional), - start: Schema.Number.pipe(Schema.optional), + start: Schema.Finite.pipe(Schema.optional), search: Schema.String.pipe(Schema.optional), }) type SessionCursor = typeof SessionCursor.Type diff --git a/packages/opencode/src/server/routes/instance/httpapi/public.ts b/packages/opencode/src/server/routes/instance/httpapi/public.ts index fc930551326a..c9668336ae92 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/public.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/public.ts @@ -446,7 +446,7 @@ function stripOptionalNull(schema: OpenApiSchema): OpenApiSchema { if (isEmptyObjectUnion(schema)) return { type: "object", properties: {} } const options = flattenOptions(schema.anyOf ?? schema.oneOf) if (options) { - const withoutNull = stripSpecialNumberStrings(options).filter((item) => item.type !== "null") + const withoutNull = options.filter((item) => item.type !== "null") if (withoutNull.length === 1) return stripOptionalNull(withoutNull[0]) if (schema.anyOf) schema.anyOf = withoutNull.map(stripOptionalNull) if (schema.oneOf) schema.oneOf = withoutNull.map(stripOptionalNull) @@ -490,14 +490,6 @@ function flattenOptions(options: OpenApiSchema[] | undefined): OpenApiSchema[] | return options?.flatMap((item) => flattenOptions(item.anyOf ?? item.oneOf) ?? [item]) } -function stripSpecialNumberStrings(options: OpenApiSchema[]) { - if (!options.some((item) => item.type === "number" || item.type === "integer")) return options - return options.filter( - (item) => - item.type !== "string" || !item.enum?.every((value) => ["NaN", "Infinity", "-Infinity"].includes(String(value))), - ) -} - function normalizeParameter(param: OpenApiParameter, route: string) { if (!param.schema || typeof param.schema !== "object") return if (param.in === "path") { diff --git a/packages/opencode/src/v2/schema.ts b/packages/opencode/src/v2/schema.ts new file mode 100644 index 000000000000..44587b838a43 --- /dev/null +++ b/packages/opencode/src/v2/schema.ts @@ -0,0 +1,10 @@ +import { DateTime, Schema, SchemaGetter } from "effect" + +export const DateTimeUtcFromMillis = Schema.Finite.pipe( + Schema.decodeTo(Schema.DateTimeUtc, { + decode: SchemaGetter.transform((value) => DateTime.makeUnsafe(value)), + encode: SchemaGetter.transform((value) => DateTime.toEpochMillis(value)), + }), +) + +export * as V2Schema from "./schema" diff --git a/packages/opencode/src/v2/session-event.ts b/packages/opencode/src/v2/session-event.ts index 62814686e61d..3af5932f0d24 100644 --- a/packages/opencode/src/v2/session-event.ts +++ b/packages/opencode/src/v2/session-event.ts @@ -6,6 +6,7 @@ import { Schema } from "effect" export { FileAttachment } import { ToolOutput } from "./tool-output" import { ModelID, ProviderID } from "@/provider/schema" +import { V2Schema } from "./schema" export const Source = Schema.Struct({ start: NonNegativeInt, @@ -17,7 +18,7 @@ export const Source = Schema.Struct({ export type Source = Schema.Schema.Type const Base = { - timestamp: Schema.DateTimeUtcFromMillis, + timestamp: V2Schema.DateTimeUtcFromMillis, sessionID: SessionID, } diff --git a/packages/opencode/src/v2/session-message.ts b/packages/opencode/src/v2/session-message.ts index d18edfce6150..8ec99bc200be 100644 --- a/packages/opencode/src/v2/session-message.ts +++ b/packages/opencode/src/v2/session-message.ts @@ -3,6 +3,7 @@ import { Prompt } from "./session-prompt" import { SessionEvent } from "./session-event" import { EventV2 } from "./event" import { ToolOutput } from "./tool-output" +import { V2Schema } from "./schema" export const ID = EventV2.ID export type ID = Schema.Schema.Type @@ -11,7 +12,7 @@ const Base = { id: ID, metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), time: Schema.Struct({ - created: Schema.DateTimeUtcFromMillis, + created: V2Schema.DateTimeUtcFromMillis, }), } @@ -38,7 +39,7 @@ export class User extends Schema.Class("Session.Message.User")({ agents: Prompt.fields.agents, type: Schema.Literal("user"), time: Schema.Struct({ - created: Schema.DateTimeUtcFromMillis, + created: V2Schema.DateTimeUtcFromMillis, }), }) {} @@ -56,8 +57,8 @@ export class Shell extends Schema.Class("Session.Message.Shell")({ command: SessionEvent.Shell.Started.fields.data.fields.command, output: Schema.String, time: Schema.Struct({ - created: Schema.DateTimeUtcFromMillis, - completed: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + created: V2Schema.DateTimeUtcFromMillis, + completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), }), }) {} @@ -107,10 +108,10 @@ export class AssistantTool extends Schema.Class("Session.Message. }).pipe(Schema.optional), state: ToolState, time: Schema.Struct({ - created: Schema.DateTimeUtcFromMillis, - ran: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - completed: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - pruned: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + created: V2Schema.DateTimeUtcFromMillis, + ran: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + pruned: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), }), }) {} @@ -141,20 +142,20 @@ export class Assistant extends Schema.Class("Session.Message.Assistan end: Schema.String.pipe(Schema.optional), }).pipe(Schema.optional), finish: Schema.String.pipe(Schema.optional), - cost: Schema.Number.pipe(Schema.optional), + cost: Schema.Finite.pipe(Schema.optional), tokens: Schema.Struct({ - input: Schema.Number, - output: Schema.Number, - reasoning: Schema.Number, + input: Schema.Finite, + output: Schema.Finite, + reasoning: Schema.Finite, cache: Schema.Struct({ - read: Schema.Number, - write: Schema.Number, + read: Schema.Finite, + write: Schema.Finite, }), }).pipe(Schema.optional), error: Schema.String.pipe(Schema.optional), time: Schema.Struct({ - created: Schema.DateTimeUtcFromMillis, - completed: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + created: V2Schema.DateTimeUtcFromMillis, + completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), }), }) {} diff --git a/packages/opencode/src/v2/session-prompt.ts b/packages/opencode/src/v2/session-prompt.ts index e7068e409233..86d8e52eb78d 100644 --- a/packages/opencode/src/v2/session-prompt.ts +++ b/packages/opencode/src/v2/session-prompt.ts @@ -1,8 +1,8 @@ import * as Schema from "effect/Schema" export class Source extends Schema.Class("Prompt.Source")({ - start: Schema.Number, - end: Schema.Number, + start: Schema.Finite, + end: Schema.Finite, text: Schema.String, }) {} diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index a28c44c0157a..1777b875aa8c 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -10,6 +10,7 @@ import { EventV2 } from "./event" import { ProjectID } from "@/project/schema" import { ModelID, ProviderID } from "@/provider/schema" import { SessionEvent } from "./session-event" +import { V2Schema } from "./schema" export const Delivery = Schema.Union([Schema.Literal("immediate"), Schema.Literal("deferred")]).annotate({ identifier: "Session.Delivery", @@ -31,9 +32,9 @@ export class Info extends Schema.Class("Session.Info")({ variant: Schema.String.pipe(Schema.optional), }).pipe(Schema.optional), time: Schema.Struct({ - created: Schema.DateTimeUtcFromMillis, - updated: Schema.DateTimeUtcFromMillis, - archived: Schema.DateTimeUtcFromMillis.pipe(Schema.optional), + created: V2Schema.DateTimeUtcFromMillis, + updated: V2Schema.DateTimeUtcFromMillis, + archived: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), }), title: Schema.String, /* From 10ff807c9a619c60c9c89649573b5ec9297794c9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 20:49:33 -0400 Subject: [PATCH 20/24] sync --- .../test/server/httpapi-bridge.test.ts | 1 - .../test/server/httpapi-event.test.ts | 15 ++++++- .../test/server/httpapi-session.test.ts | 42 +------------------ .../opencode/test/session/compaction.test.ts | 10 ----- packages/opencode/test/session/prompt.test.ts | 41 ------------------ packages/opencode/test/sync/index.test.ts | 2 +- 6 files changed, 15 insertions(+), 96 deletions(-) diff --git a/packages/opencode/test/server/httpapi-bridge.test.ts b/packages/opencode/test/server/httpapi-bridge.test.ts index 352fb2e2faf9..8d5dfed279b4 100644 --- a/packages/opencode/test/server/httpapi-bridge.test.ts +++ b/packages/opencode/test/server/httpapi-bridge.test.ts @@ -226,7 +226,6 @@ describe("HttpApi server", () => { const effectRoutes = openApiRouteKeys(effectOpenApi()) expect(honoRoutes.filter((route) => !effectRoutes.includes(route))).toEqual([]) - expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([]) }) test("matches generated OpenAPI route parameters", async () => { diff --git a/packages/opencode/test/server/httpapi-event.test.ts b/packages/opencode/test/server/httpapi-event.test.ts index d7e48240a9c9..940efed9c359 100644 --- a/packages/opencode/test/server/httpapi-event.test.ts +++ b/packages/opencode/test/server/httpapi-event.test.ts @@ -27,6 +27,14 @@ async function readFirstChunk(response: Response) { return new TextDecoder().decode(result.value) } +async function readFirstEvent(response: Response) { + return JSON.parse((await readFirstChunk(response)).replace(/^data: /, "")) as { + id?: string + type: string + properties: Record + } +} + afterEach(async () => { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original await disposeAllInstances() @@ -43,7 +51,7 @@ describe("event HttpApi bridge", () => { expect(response.headers.get("cache-control")).toBe("no-cache, no-transform") expect(response.headers.get("x-accel-buffering")).toBe("no") expect(response.headers.get("x-content-type-options")).toBe("nosniff") - expect(await readFirstChunk(response)).toContain('data: {"type":"server.connected","properties":{}}\n\n') + expect(await readFirstEvent(response)).toMatchObject({ type: "server.connected", properties: {} }) }) test("matches legacy first event frame", async () => { @@ -52,6 +60,9 @@ describe("event HttpApi bridge", () => { const legacy = await app(false).request(EventPaths.event, { headers }) const effect = await app(true).request(EventPaths.event, { headers }) - expect(await readFirstChunk(effect)).toBe(await readFirstChunk(legacy)) + const legacyEvent = await readFirstEvent(legacy) + const effectEvent = await readFirstEvent(effect) + expect(effectEvent.type).toBe(legacyEvent.type) + expect(effectEvent.properties).toEqual(legacyEvent.properties) }) }) diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index 5af93deb02cf..5af7df7b7671 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -17,9 +17,7 @@ import { Session } from "@/session/session" import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { MessageV2 } from "../../src/session/message-v2" import { Database } from "@/storage/db" -import { SessionMessageTable, SessionTable } from "@/session/session.sql" -import { SessionMessage } from "../../src/v2/session-message" -import * as DateTime from "effect/DateTime" +import { SessionTable } from "@/session/session.sql" import * as Log from "@opencode-ai/core/util/log" import { eq } from "drizzle-orm" import { resetDatabase } from "../fixture/db" @@ -206,44 +204,6 @@ describe("session HttpApi", () => { ), ).toMatchObject({ info: { id: message.info.id } }) - yield* Effect.promise(() => - Instance.provide({ - directory: tmp.path, - fn: async () => { - const message = new SessionMessage.Assistant({ - id: SessionMessage.ID.create(), - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - time: { created: DateTime.makeUnsafe(1) }, - content: [], - }) - Database.use((db) => - db - .insert(SessionMessageTable) - .values([ - { - id: message.id, - session_id: parent.id, - type: message.type, - time_created: 1, - data: { - time: { created: 1 }, - agent: message.agent, - model: message.model, - content: message.content, - } as NonNullable, - }, - ]) - .run(), - ) - }, - }), - ) - - expect(yield* requestJson(`/api/session/${parent.id}/message`, { headers })).toMatchObject([ - { type: "assistant" }, - ]) }), ), ) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 0d02d9918a29..df83adb8d40e 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -20,7 +20,6 @@ import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" -import { SessionV2 } from "../../src/v2/session" import { ModelID, ProviderID } from "../../src/provider/schema" import type { Provider } from "@/provider/provider" import * as SessionProcessorModule from "../../src/session/processor" @@ -598,15 +597,6 @@ describe("session.compaction.create", () => { auto: true, overflow: true, }) - - const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe( - Effect.provide(SessionV2.defaultLayer), - ) - expect(v2.at(-1)).toMatchObject({ - type: "compaction", - reason: "auto", - summary: "", - }) }), ), ) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index b49d6c9d9d92..53305694018c 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -19,7 +19,6 @@ import { ModelID, ProviderID } from "../../src/provider/schema" import { Question } from "../../src/question" import { Todo } from "../../src/session/todo" import { Session } from "@/session/session" -import { SessionMessageTable } from "../../src/session/session.sql" import { LLM } from "../../src/session/llm" import { MessageV2 } from "../../src/session/message-v2" import { AppFileSystem } from "@opencode-ai/core/filesystem" @@ -32,7 +31,6 @@ import { SessionRevert } from "../../src/session/revert" import { SessionRunState } from "../../src/session/run-state" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" -import { SessionV2 } from "../../src/v2/session" import { Skill } from "../../src/skill" import { SystemPrompt } from "../../src/session/system" import { Shell } from "../../src/shell/shell" @@ -41,7 +39,6 @@ import { ToolRegistry } from "@/tool/registry" import { Truncate } from "@/tool/truncate" import * as Log from "@opencode-ai/core/util/log" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" -import * as Database from "../../src/storage/db" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" @@ -374,44 +371,6 @@ it.live("loop calls LLM and returns assistant message", () => ), ) -it.live("prompt emits v2 prompted and synthetic events", () => - provideTmpdirServer( - Effect.fnUntraced(function* () { - const prompt = yield* SessionPrompt.Service - const sessions = yield* Session.Service - const chat = yield* sessions.create({ title: "Pinned" }) - - yield* prompt.prompt({ - sessionID: chat.id, - agent: "build", - noReply: true, - parts: [ - { type: "text", text: "hello v2" }, - { - type: "file", - mime: "text/plain", - filename: "note.txt", - url: "data:text/plain;base64,bm90ZSBjb250ZW50", - }, - ], - }) - - const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe( - Effect.provide(SessionV2.layer), - ) - const row = Database.use((db) => - db.select().from(SessionMessageTable).where(Database.eq(SessionMessageTable.session_id, chat.id)).get(), - ) - expect(messages).toHaveLength(3) - expect(messages[0]).toMatchObject({ type: "user", text: "hello v2" }) - expect(typeof row?.data.time.created).toBe("number") - expect(messages[1]).toMatchObject({ type: "synthetic", text: expect.stringContaining("Called the Read tool") }) - expect(messages[2]).toMatchObject({ type: "synthetic", text: "note content" }) - }), - { git: true, config: providerCfg }, - ), -) - it.live("static loop returns assistant text through local provider", () => provideTmpdirServer( Effect.fnUntraced(function* ({ llm }) { diff --git a/packages/opencode/test/sync/index.test.ts b/packages/opencode/test/sync/index.test.ts index 0afbb1831757..234c5246eeee 100644 --- a/packages/opencode/test/sync/index.test.ts +++ b/packages/opencode/test/sync/index.test.ts @@ -124,7 +124,7 @@ describe("SyncEvent", () => { yield* SyncEvent.use.run(Created, { id: "evt_1", name: "test" }) yield* Effect.promise(() => received) expect(events).toHaveLength(1) - expect(events[0]).toEqual({ + expect(events[0]).toMatchObject({ type: "item.created", properties: { id: "evt_1", From 315e907a8ff5763618dc2004142b4ab473701c7b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 20:53:40 -0400 Subject: [PATCH 21/24] Revert "sync" This reverts commit 485e26d226998aa8559422b6747a34e16cce3592. --- packages/opencode/test/preload.ts | 1 + .../test/server/httpapi-bridge.test.ts | 1 + .../test/server/httpapi-event.test.ts | 15 +------ .../test/server/httpapi-session.test.ts | 42 ++++++++++++++++++- .../opencode/test/session/compaction.test.ts | 10 +++++ packages/opencode/test/session/prompt.test.ts | 41 ++++++++++++++++++ packages/opencode/test/sync/index.test.ts | 2 +- 7 files changed, 97 insertions(+), 15 deletions(-) diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index aca0170bd8ec..a40a69bcee6e 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -34,6 +34,7 @@ process.env["XDG_CACHE_HOME"] = path.join(dir, "cache") process.env["XDG_CONFIG_HOME"] = path.join(dir, "config") process.env["XDG_STATE_HOME"] = path.join(dir, "state") process.env["OPENCODE_MODELS_PATH"] = path.join(import.meta.dir, "tool", "fixtures", "models-api.json") +process.env["OPENCODE_EXPERIMENTAL"] = "true" // Set test home directory to isolate tests from user's actual home directory // This prevents tests from picking up real user configs/skills from ~/.claude/skills diff --git a/packages/opencode/test/server/httpapi-bridge.test.ts b/packages/opencode/test/server/httpapi-bridge.test.ts index 8d5dfed279b4..352fb2e2faf9 100644 --- a/packages/opencode/test/server/httpapi-bridge.test.ts +++ b/packages/opencode/test/server/httpapi-bridge.test.ts @@ -226,6 +226,7 @@ describe("HttpApi server", () => { const effectRoutes = openApiRouteKeys(effectOpenApi()) expect(honoRoutes.filter((route) => !effectRoutes.includes(route))).toEqual([]) + expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([]) }) test("matches generated OpenAPI route parameters", async () => { diff --git a/packages/opencode/test/server/httpapi-event.test.ts b/packages/opencode/test/server/httpapi-event.test.ts index 940efed9c359..d7e48240a9c9 100644 --- a/packages/opencode/test/server/httpapi-event.test.ts +++ b/packages/opencode/test/server/httpapi-event.test.ts @@ -27,14 +27,6 @@ async function readFirstChunk(response: Response) { return new TextDecoder().decode(result.value) } -async function readFirstEvent(response: Response) { - return JSON.parse((await readFirstChunk(response)).replace(/^data: /, "")) as { - id?: string - type: string - properties: Record - } -} - afterEach(async () => { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original await disposeAllInstances() @@ -51,7 +43,7 @@ describe("event HttpApi bridge", () => { expect(response.headers.get("cache-control")).toBe("no-cache, no-transform") expect(response.headers.get("x-accel-buffering")).toBe("no") expect(response.headers.get("x-content-type-options")).toBe("nosniff") - expect(await readFirstEvent(response)).toMatchObject({ type: "server.connected", properties: {} }) + expect(await readFirstChunk(response)).toContain('data: {"type":"server.connected","properties":{}}\n\n') }) test("matches legacy first event frame", async () => { @@ -60,9 +52,6 @@ describe("event HttpApi bridge", () => { const legacy = await app(false).request(EventPaths.event, { headers }) const effect = await app(true).request(EventPaths.event, { headers }) - const legacyEvent = await readFirstEvent(legacy) - const effectEvent = await readFirstEvent(effect) - expect(effectEvent.type).toBe(legacyEvent.type) - expect(effectEvent.properties).toEqual(legacyEvent.properties) + expect(await readFirstChunk(effect)).toBe(await readFirstChunk(legacy)) }) }) diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index 5af7df7b7671..5af93deb02cf 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -17,7 +17,9 @@ import { Session } from "@/session/session" import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { MessageV2 } from "../../src/session/message-v2" import { Database } from "@/storage/db" -import { SessionTable } from "@/session/session.sql" +import { SessionMessageTable, SessionTable } from "@/session/session.sql" +import { SessionMessage } from "../../src/v2/session-message" +import * as DateTime from "effect/DateTime" import * as Log from "@opencode-ai/core/util/log" import { eq } from "drizzle-orm" import { resetDatabase } from "../fixture/db" @@ -204,6 +206,44 @@ describe("session HttpApi", () => { ), ).toMatchObject({ info: { id: message.info.id } }) + yield* Effect.promise(() => + Instance.provide({ + directory: tmp.path, + fn: async () => { + const message = new SessionMessage.Assistant({ + id: SessionMessage.ID.create(), + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + time: { created: DateTime.makeUnsafe(1) }, + content: [], + }) + Database.use((db) => + db + .insert(SessionMessageTable) + .values([ + { + id: message.id, + session_id: parent.id, + type: message.type, + time_created: 1, + data: { + time: { created: 1 }, + agent: message.agent, + model: message.model, + content: message.content, + } as NonNullable, + }, + ]) + .run(), + ) + }, + }), + ) + + expect(yield* requestJson(`/api/session/${parent.id}/message`, { headers })).toMatchObject([ + { type: "assistant" }, + ]) }), ), ) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index df83adb8d40e..0d02d9918a29 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -20,6 +20,7 @@ import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" +import { SessionV2 } from "../../src/v2/session" import { ModelID, ProviderID } from "../../src/provider/schema" import type { Provider } from "@/provider/provider" import * as SessionProcessorModule from "../../src/session/processor" @@ -597,6 +598,15 @@ describe("session.compaction.create", () => { auto: true, overflow: true, }) + + const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe( + Effect.provide(SessionV2.defaultLayer), + ) + expect(v2.at(-1)).toMatchObject({ + type: "compaction", + reason: "auto", + summary: "", + }) }), ), ) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 53305694018c..b49d6c9d9d92 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -19,6 +19,7 @@ import { ModelID, ProviderID } from "../../src/provider/schema" import { Question } from "../../src/question" import { Todo } from "../../src/session/todo" import { Session } from "@/session/session" +import { SessionMessageTable } from "../../src/session/session.sql" import { LLM } from "../../src/session/llm" import { MessageV2 } from "../../src/session/message-v2" import { AppFileSystem } from "@opencode-ai/core/filesystem" @@ -31,6 +32,7 @@ import { SessionRevert } from "../../src/session/revert" import { SessionRunState } from "../../src/session/run-state" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" +import { SessionV2 } from "../../src/v2/session" import { Skill } from "../../src/skill" import { SystemPrompt } from "../../src/session/system" import { Shell } from "../../src/shell/shell" @@ -39,6 +41,7 @@ import { ToolRegistry } from "@/tool/registry" import { Truncate } from "@/tool/truncate" import * as Log from "@opencode-ai/core/util/log" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import * as Database from "../../src/storage/db" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" @@ -371,6 +374,44 @@ it.live("loop calls LLM and returns assistant message", () => ), ) +it.live("prompt emits v2 prompted and synthetic events", () => + provideTmpdirServer( + Effect.fnUntraced(function* () { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ title: "Pinned" }) + + yield* prompt.prompt({ + sessionID: chat.id, + agent: "build", + noReply: true, + parts: [ + { type: "text", text: "hello v2" }, + { + type: "file", + mime: "text/plain", + filename: "note.txt", + url: "data:text/plain;base64,bm90ZSBjb250ZW50", + }, + ], + }) + + const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe( + Effect.provide(SessionV2.layer), + ) + const row = Database.use((db) => + db.select().from(SessionMessageTable).where(Database.eq(SessionMessageTable.session_id, chat.id)).get(), + ) + expect(messages).toHaveLength(3) + expect(messages[0]).toMatchObject({ type: "user", text: "hello v2" }) + expect(typeof row?.data.time.created).toBe("number") + expect(messages[1]).toMatchObject({ type: "synthetic", text: expect.stringContaining("Called the Read tool") }) + expect(messages[2]).toMatchObject({ type: "synthetic", text: "note content" }) + }), + { git: true, config: providerCfg }, + ), +) + it.live("static loop returns assistant text through local provider", () => provideTmpdirServer( Effect.fnUntraced(function* ({ llm }) { diff --git a/packages/opencode/test/sync/index.test.ts b/packages/opencode/test/sync/index.test.ts index 234c5246eeee..0afbb1831757 100644 --- a/packages/opencode/test/sync/index.test.ts +++ b/packages/opencode/test/sync/index.test.ts @@ -124,7 +124,7 @@ describe("SyncEvent", () => { yield* SyncEvent.use.run(Created, { id: "evt_1", name: "test" }) yield* Effect.promise(() => received) expect(events).toHaveLength(1) - expect(events[0]).toMatchObject({ + expect(events[0]).toEqual({ type: "item.created", properties: { id: "evt_1", From 4aa62b73549a4aad319abb2c47f4dc40e194a8ae Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 21:04:24 -0400 Subject: [PATCH 22/24] sync --- packages/sdk/js/src/v2/gen/types.gen.ts | 1201 ++--------------------- 1 file changed, 93 insertions(+), 1108 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index d48c70e14c1e..caa3d4c76770 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4,13 +4,10 @@ export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}) } -<<<<<<< HEAD -<<<<<<< HEAD -======= -======= export type Event = - | EventProjectUpdated | EventServerInstanceDisposed + | EventFileEdited + | EventFileWatcherUpdated | EventLspClientDiagnostics | EventLspUpdated | EventMessagePartDelta @@ -20,12 +17,6 @@ export type Event = | EventSessionError | EventInstallationUpdated | EventInstallationUpdateAvailable - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceRestore - | EventWorkspaceStatus - | EventFileEdited - | EventFileWatcherUpdated | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected @@ -40,7 +31,12 @@ export type Event = | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted + | EventProjectUpdated | EventVcsBranchUpdated + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceRestore + | EventWorkspaceStatus | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated @@ -107,140 +103,6 @@ export type WellKnownAuth = { export type Auth = OAuth | ApiAuth | WellKnownAuth ->>>>>>> eb048016e (sync) -export type Project = { - id: string - worktree: string - vcs?: "git" - name?: string - icon?: { - url?: string - override?: string - color?: string - } - commands?: { - /** - * Startup script to run when creating a new workspace (worktree) - */ - start?: string - } - time: { - created: number - updated: number - initialized?: number - } - sandboxes: Array -} - -<<<<<<< HEAD -export type EventProjectUpdated = { - id: string - type: "project.updated" - properties: Project -} - ->>>>>>> cbb60d3a5 (core: add unique IDs to all events for reliable tracking and debugging) -export type EventServerInstanceDisposed = { - id: string - type: "server.instance.disposed" - properties: { - directory: string - } -} - -<<<<<<< HEAD -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= -export type EventServerConnected = { - id: string - type: "server.connected" - properties: { - [key: string]: unknown - } -} - -export type EventGlobalDisposed = { - id: string - type: "global.disposed" - properties: { - [key: string]: unknown - } -} - -<<<<<<< HEAD -<<<<<<< HEAD ->>>>>>> 9a1d0085d (sync) -======= ->>>>>>> 7b56f08df (fix) -export type EventFileEdited = { - id: string - type: "file.edited" - properties: { - file: string - } -} - -export type EventFileWatcherUpdated = { - id: string - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" -<<<<<<< HEAD -<<<<<<< HEAD - } -} - -======= -======= -export type EventServerInstanceDisposed = { - id: string - type: "server.instance.disposed" - properties: { - directory: string ->>>>>>> 1a2dc3e57 (sync) -======= ->>>>>>> 7b56f08df (fix) - } -} - ->>>>>>> d0ca805a7 (sync) ->>>>>>> 9a1d0085d (sync) -======= ->>>>>>> b967ee6dd (generate types) -export type EventLspClientDiagnostics = { - id: string - type: "lsp.client.diagnostics" - properties: { - serverID: string - path: string - } -} - -export type EventLspUpdated = { - id: string - type: "lsp.updated" - properties: { - [key: string]: unknown - } -} - -export type EventMessagePartDelta = { - id: string - type: "message.part.delta" - properties: { - sessionID: string - messageID: string - partID: string - field: string - delta: string - } -} - -======= ->>>>>>> eb048016e (sync) export type PermissionRequest = { id: string sessionID: string @@ -325,120 +187,6 @@ export type ApiError = { } } -<<<<<<< HEAD -export type EventSessionError = { - id: string - type: "session.error" - properties: { - sessionID?: string - error?: - | ProviderAuthError - | UnknownError - | MessageOutputLengthError - | MessageAbortedError - | StructuredOutputError - | ContextOverflowError - | ApiError - } -} - -export type EventInstallationUpdated = { - id: string - type: "installation.updated" - properties: { - version: string - } -} - -export type EventInstallationUpdateAvailable = { - id: string - type: "installation.update-available" - properties: { - version: string - } -} - -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -======= -======= ->>>>>>> 7b56f08df (fix) -<<<<<<< HEAD -export type EventWorkspaceReady = { -======= -<<<<<<< HEAD -======= -======= ->>>>>>> b967ee6dd (generate types) -export type EventWorkspaceReady = { - id: string - type: "workspace.ready" - properties: { - name: string - } -} - -export type EventWorkspaceFailed = { - id: string - type: "workspace.failed" - properties: { - message: string - } -} - -export type EventWorkspaceRestore = { - id: string - type: "workspace.restore" - properties: { - workspaceID: string - sessionID: string - total: number - step: number - } -} - -export type EventWorkspaceStatus = { - id: string - type: "workspace.status" - properties: { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - } -} - -export type EventFileEdited = { - id: string - type: "file.edited" - properties: { - file: string - } -} - -export type EventFileWatcherUpdated = { - id: string - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) -<<<<<<< HEAD ->>>>>>> 9a1d0085d (sync) -======= -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) -======= ->>>>>>> b967ee6dd (generate types) -======= ->>>>>>> eb048016e (sync) export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -579,35 +327,6 @@ export type EventTuiSessionSelect = { } } -<<<<<<< HEAD -export type EventMcpToolsChanged = { - id: string - type: "mcp.tools.changed" - properties: { - server: string - } -} - -export type EventMcpBrowserOpenFailed = { - id: string - type: "mcp.browser.open.failed" - properties: { - mcpName: string - url: string - } -} - -export type EventCommandExecuted = { - id: string - type: "command.executed" - properties: { - name: string - sessionID: string - arguments: string - messageID: string - } -} - export type Project = { id: string worktree: string @@ -632,104 +351,18 @@ export type Project = { sandboxes: Array } -export type EventProjectUpdated = { - type: "project.updated" - properties: Project +export type Pty = { + id: string + title: string + command: string + args: Array + cwd: string + status: "running" | "exited" + pid: number } -export type EventVcsBranchUpdated = { - id: string - type: "vcs.branch.updated" - properties: { - branch?: string - } -} - -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= -<<<<<<< HEAD -<<<<<<< HEAD ->>>>>>> 9a1d0085d (sync) -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) -export type EventWorkspaceReady = { - id: string - type: "workspace.ready" - properties: { - name: string - } -} - -export type EventWorkspaceFailed = { - id: string - type: "workspace.failed" - properties: { - message: string - } -} - -export type EventWorkspaceRestore = { - id: string - type: "workspace.restore" - properties: { - workspaceID: string - sessionID: string - total: number - step: number - } -} - -export type EventWorkspaceStatus = { - id: string - type: "workspace.status" - properties: { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - } -} - -<<<<<<< HEAD -======= ->>>>>>> d0ca805a7 (sync) ->>>>>>> 9a1d0085d (sync) -======= ->>>>>>> b967ee6dd (generate types) -export type EventWorktreeReady = { - id: string - type: "worktree.ready" - properties: { - name: string - branch: string - } -} - -export type EventWorktreeFailed = { - id: string - type: "worktree.failed" - properties: { - message: string - } -} - -======= ->>>>>>> eb048016e (sync) -export type Pty = { - id: string - title: string - command: string - args: Array - cwd: string - status: "running" | "exited" - pid: number -} - -export type OutputFormatText = { - type: "text" +export type OutputFormatText = { + type: "text" } export type JsonSchema = { @@ -1137,8 +770,9 @@ export type GlobalEvent = { project?: string workspace?: string payload: - | EventProjectUpdated | EventServerInstanceDisposed + | EventFileEdited + | EventFileWatcherUpdated | EventLspClientDiagnostics | EventLspUpdated | EventMessagePartDelta @@ -1148,12 +782,6 @@ export type GlobalEvent = { | EventSessionError | EventInstallationUpdated | EventInstallationUpdateAvailable - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceRestore - | EventWorkspaceStatus - | EventFileEdited - | EventFileWatcherUpdated | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected @@ -1168,7 +796,12 @@ export type GlobalEvent = { | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted + | EventProjectUpdated | EventVcsBranchUpdated + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceRestore + | EventWorkspaceStatus | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated @@ -2601,208 +2234,6 @@ export type SyncEventSessionNextCompactionEnded = { } } -<<<<<<< HEAD -export type GlobalEvent = { - directory: string - project?: string - workspace?: string - payload: -<<<<<<< HEAD - | EventServerInstanceDisposed - | EventFileEdited - | EventFileWatcherUpdated -======= - | EventProjectUpdated - | EventServerInstanceDisposed -<<<<<<< HEAD -<<<<<<< HEAD -======= - | EventServerConnected - | EventGlobalDisposed - | EventFileEdited - | EventFileWatcherUpdated -<<<<<<< HEAD -======= - | EventServerConnected - | EventGlobalDisposed - | EventServerInstanceDisposed ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) -<<<<<<< HEAD ->>>>>>> 9a1d0085d (sync) -======= -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) -======= ->>>>>>> b967ee6dd (generate types) - | EventLspClientDiagnostics - | EventLspUpdated - | EventMessagePartDelta - | EventPermissionAsked - | EventPermissionReplied - | EventSessionDiff - | EventSessionError - | EventInstallationUpdated - | EventInstallationUpdateAvailable -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= ->>>>>>> d0ca805a7 (sync) -======= ->>>>>>> b967ee6dd (generate types) - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceRestore - | EventWorkspaceStatus - | EventFileEdited - | EventFileWatcherUpdated -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) -<<<<<<< HEAD ->>>>>>> 9a1d0085d (sync) -======= -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) -======= ->>>>>>> b967ee6dd (generate types) - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventTodoUpdated - | EventSessionStatus - | EventSessionIdle - | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventMcpBrowserOpenFailed - | EventCommandExecuted - | EventProjectUpdated - | EventVcsBranchUpdated -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= -<<<<<<< HEAD -<<<<<<< HEAD ->>>>>>> 9a1d0085d (sync) -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceRestore - | EventWorkspaceStatus -<<<<<<< HEAD -<<<<<<< HEAD -======= -======= ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) ->>>>>>> 9a1d0085d (sync) -======= -======= ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) -======= ->>>>>>> b967ee6dd (generate types) - | EventWorktreeReady - | EventWorktreeFailed - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - | EventSessionNextAgentSwitched - | EventSessionNextModelSwitched - | EventSessionNextPrompted - | EventSessionNextSynthetic - | EventSessionNextShellStarted - | EventSessionNextShellEnded - | EventSessionNextStepStarted - | EventSessionNextStepEnded - | EventSessionNextTextStarted - | EventSessionNextTextDelta - | EventSessionNextTextEnded - | EventSessionNextReasoningStarted - | EventSessionNextReasoningDelta - | EventSessionNextReasoningEnded - | EventSessionNextToolInputStarted - | EventSessionNextToolInputDelta - | EventSessionNextToolInputEnded - | EventSessionNextToolCalled - | EventSessionNextToolProgress - | EventSessionNextToolSuccess - | EventSessionNextToolError - | EventSessionNextRetried - | EventSessionNextCompactionStarted - | EventSessionNextCompactionDelta - | EventSessionNextCompactionEnded - | EventServerConnected - | EventGlobalDisposed - | SyncEventMessageUpdated - | SyncEventMessageRemoved - | SyncEventMessagePartUpdated - | SyncEventMessagePartRemoved - | SyncEventSessionCreated - | SyncEventSessionUpdated - | SyncEventSessionDeleted - | SyncEventSessionNextAgentSwitched - | SyncEventSessionNextModelSwitched - | SyncEventSessionNextPrompted - | SyncEventSessionNextSynthetic - | SyncEventSessionNextShellStarted - | SyncEventSessionNextShellEnded - | SyncEventSessionNextStepStarted - | SyncEventSessionNextStepEnded - | SyncEventSessionNextTextStarted - | SyncEventSessionNextTextDelta - | SyncEventSessionNextTextEnded - | SyncEventSessionNextReasoningStarted - | SyncEventSessionNextReasoningDelta - | SyncEventSessionNextReasoningEnded - | SyncEventSessionNextToolInputStarted - | SyncEventSessionNextToolInputDelta - | SyncEventSessionNextToolInputEnded - | SyncEventSessionNextToolCalled - | SyncEventSessionNextToolProgress - | SyncEventSessionNextToolSuccess - | SyncEventSessionNextToolError - | SyncEventSessionNextRetried - | SyncEventSessionNextCompactionStarted - | SyncEventSessionNextCompactionDelta - | SyncEventSessionNextCompactionEnded -======= -export type EventProjectUpdated = { - id: string - type: "project.updated" - properties: Project ->>>>>>> eb048016e (sync) -} - export type EventServerInstanceDisposed = { id: string type: "server.instance.disposed" @@ -2811,6 +2242,23 @@ export type EventServerInstanceDisposed = { } } +export type EventFileEdited = { + id: string + type: "file.edited" + properties: { + file: string + } +} + +export type EventFileWatcherUpdated = { + id: string + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + export type EventLspClientDiagnostics = { id: string type: "lsp.client.diagnostics" @@ -2897,78 +2345,25 @@ export type EventInstallationUpdateAvailable = { } } -export type EventWorkspaceReady = { +export type EventQuestionAsked = { id: string - type: "workspace.ready" - properties: { - name: string - } + type: "question.asked" + properties: QuestionRequest } -export type EventWorkspaceFailed = { +export type EventQuestionReplied = { id: string - type: "workspace.failed" - properties: { - message: string - } + type: "question.replied" + properties: QuestionReplied } -export type EventWorkspaceRestore = { +export type EventQuestionRejected = { id: string - type: "workspace.restore" - properties: { - workspaceID: string - sessionID: string - total: number - step: number - } + type: "question.rejected" + properties: QuestionRejected } -export type EventWorkspaceStatus = { - id: string - type: "workspace.status" - properties: { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - } -} - -export type EventFileEdited = { - id: string - type: "file.edited" - properties: { - file: string - } -} - -export type EventFileWatcherUpdated = { - id: string - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - -export type EventQuestionAsked = { - id: string - type: "question.asked" - properties: QuestionRequest -} - -export type EventQuestionReplied = { - id: string - type: "question.replied" - properties: QuestionReplied -} - -export type EventQuestionRejected = { - id: string - type: "question.rejected" - properties: QuestionRejected -} - -export type EventTodoUpdated = { +export type EventTodoUpdated = { id: string type: "todo.updated" properties: { @@ -3030,6 +2425,12 @@ export type EventCommandExecuted = { } } +export type EventProjectUpdated = { + id: string + type: "project.updated" + properties: Project +} + export type EventVcsBranchUpdated = { id: string type: "vcs.branch.updated" @@ -3038,6 +2439,42 @@ export type EventVcsBranchUpdated = { } } +export type EventWorkspaceReady = { + id: string + type: "workspace.ready" + properties: { + name: string + } +} + +export type EventWorkspaceFailed = { + id: string + type: "workspace.failed" + properties: { + message: string + } +} + +export type EventWorkspaceRestore = { + id: string + type: "workspace.restore" + properties: { + workspaceID: string + sessionID: string + total: number + step: number + } +} + +export type EventWorkspaceStatus = { + id: string + type: "workspace.status" + properties: { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + } +} + export type EventWorktreeReady = { id: string type: "worktree.ready" @@ -3873,459 +3310,7 @@ export type AppLogResponses = { 200: boolean } -<<<<<<< HEAD -export type WorktreeResetInput = { - directory: string -} - -export type ProjectSummary = { - id: string - name?: string - worktree: string -} - -export type GlobalSession = { - id: string - slug: string - projectID: string - workspaceID?: string - directory: string - path?: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - share?: { - url: string - } - title: string - agent?: string - model?: { - id: string - providerID: string - variant?: string - } - version: string - time: { - created: number - updated: number - compacting?: number - archived?: number - } - permission?: PermissionRuleset - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } - project: ProjectSummary | null -} - -export type McpResource = { - name: string - uri: string - description?: string - mimeType?: string - client: string -} - -export type TextPartInput = { - id?: string - type: "text" - text: string - synthetic?: boolean - ignored?: boolean - time?: { - start: number - end?: number - } - metadata?: { - [key: string]: unknown - } -} - -export type FilePartInput = { - id?: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource -} - -export type AgentPartInput = { - id?: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number - } -} - -export type SubtaskPartInput = { - id?: string - type: "subtask" - prompt: string - description: string - agent: string - model?: { - providerID: string - modelID: string - } - command?: string -} - -export type ProviderAuthMethod = { - type: "oauth" | "api" - label: string - prompts?: Array< - | { - type: "text" - key: string - message: string - placeholder?: string - when?: { - key: string - op: "eq" | "neq" - value: string - } - } - | { - type: "select" - key: string - message: string - options: Array<{ - label: string - value: string - hint?: string - }> - when?: { - key: string - op: "eq" | "neq" - value: string - } - } - > -} - -export type ProviderAuthAuthorization = { - url: string - method: "auto" | "code" - instructions: string -} - -export type Symbol = { - name: string - kind: number - location: { - uri: string - range: Range - } -} - -export type FileNode = { - name: string - path: string - absolute: string - type: "file" | "directory" - ignored: boolean -} - -export type FileContent = { - type: "text" | "binary" - content: string - diff?: string - patch?: { - oldFileName: string - newFileName: string - oldHeader?: string - newHeader?: string - hunks: Array<{ - oldStart: number - oldLines: number - newStart: number - newLines: number - lines: Array - }> - index?: string - } - encoding?: "base64" - mimeType?: string -} - -export type File = { - path: string - added: number - removed: number - status: "added" | "deleted" | "modified" -} - -export type Event = -<<<<<<< HEAD - | EventServerInstanceDisposed - | EventFileEdited - | EventFileWatcherUpdated -======= - | EventProjectUpdated - | EventServerInstanceDisposed -<<<<<<< HEAD -<<<<<<< HEAD -======= - | EventServerConnected - | EventGlobalDisposed - | EventFileEdited - | EventFileWatcherUpdated -<<<<<<< HEAD -======= - | EventServerConnected - | EventGlobalDisposed - | EventServerInstanceDisposed ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) -<<<<<<< HEAD ->>>>>>> 9a1d0085d (sync) -======= -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) -======= ->>>>>>> b967ee6dd (generate types) - | EventLspClientDiagnostics - | EventLspUpdated - | EventMessagePartDelta - | EventPermissionAsked - | EventPermissionReplied - | EventSessionDiff - | EventSessionError - | EventInstallationUpdated - | EventInstallationUpdateAvailable -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= ->>>>>>> d0ca805a7 (sync) -======= ->>>>>>> b967ee6dd (generate types) - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceRestore - | EventWorkspaceStatus - | EventFileEdited - | EventFileWatcherUpdated -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) -<<<<<<< HEAD ->>>>>>> 9a1d0085d (sync) -======= -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) -======= ->>>>>>> b967ee6dd (generate types) - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventTodoUpdated - | EventSessionStatus - | EventSessionIdle - | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventMcpBrowserOpenFailed - | EventCommandExecuted - | EventProjectUpdated - | EventVcsBranchUpdated -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= -<<<<<<< HEAD -<<<<<<< HEAD ->>>>>>> 9a1d0085d (sync) -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceRestore - | EventWorkspaceStatus -<<<<<<< HEAD -<<<<<<< HEAD -======= -======= ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) ->>>>>>> 9a1d0085d (sync) -======= -======= ->>>>>>> 1a2dc3e57 (sync) ->>>>>>> d0ca805a7 (sync) -======= ->>>>>>> 3e6a37422 (fix) ->>>>>>> 7b56f08df (fix) -======= ->>>>>>> b967ee6dd (generate types) - | EventWorktreeReady - | EventWorktreeFailed - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - | EventSessionNextAgentSwitched - | EventSessionNextModelSwitched - | EventSessionNextPrompted - | EventSessionNextSynthetic - | EventSessionNextShellStarted - | EventSessionNextShellEnded - | EventSessionNextStepStarted - | EventSessionNextStepEnded - | EventSessionNextTextStarted - | EventSessionNextTextDelta - | EventSessionNextTextEnded - | EventSessionNextReasoningStarted - | EventSessionNextReasoningDelta - | EventSessionNextReasoningEnded - | EventSessionNextToolInputStarted - | EventSessionNextToolInputDelta - | EventSessionNextToolInputEnded - | EventSessionNextToolCalled - | EventSessionNextToolProgress - | EventSessionNextToolSuccess - | EventSessionNextToolError - | EventSessionNextRetried - | EventSessionNextCompactionStarted - | EventSessionNextCompactionDelta - | EventSessionNextCompactionEnded - | EventServerConnected - | EventGlobalDisposed - -export type McpStatusConnected = { - status: "connected" -} - -export type McpStatusDisabled = { - status: "disabled" -} - -export type McpStatusFailed = { - status: "failed" - error: string -} - -export type McpStatusNeedsAuth = { - status: "needs_auth" -} - -export type McpStatusNeedsClientRegistration = { - status: "needs_client_registration" - error: string -} - -export type McpStatus = - | McpStatusConnected - | McpStatusDisabled - | McpStatusFailed - | McpStatusNeedsAuth - | McpStatusNeedsClientRegistration - -export type McpUnsupportedOAuthError = { - error: string -} - -export type Path = { - home: string - state: string - config: string - worktree: string - directory: string -} - -export type VcsInfo = { - branch?: string - default_branch?: string -} - -export type VcsFileDiff = { - file: string - patch: string - additions: number - deletions: number - status?: "added" | "deleted" | "modified" -} - -export type Command = { - name: string - description?: string - agent?: string - model?: string - source?: "command" | "mcp" | "skill" - template: string - subtask?: boolean - hints: Array -} - -export type Agent = { - name: string - description?: string - mode: "subagent" | "primary" | "all" - native?: boolean - hidden?: boolean - topP?: number - temperature?: number - color?: string - permission: PermissionRuleset - model?: { - modelID: string - providerID: string - } - variant?: string - prompt?: string - options: { - [key: string]: unknown - } - steps?: number -} - -export type LspStatus = { - id: string - name: string - root: string - status: "connected" | "error" -} - -export type FormatterStatus = { - name: string - extensions: Array - enabled: boolean -} -======= export type AppLogResponse = AppLogResponses[keyof AppLogResponses] ->>>>>>> eb048016e (sync) export type GlobalHealthData = { body?: never From 60fd27c78b05d08d4fee98613cda72dfd2003f65 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 21:05:02 -0400 Subject: [PATCH 23/24] sync --- packages/opencode/test/server/httpapi-session.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index 5af93deb02cf..d2f4cd28b892 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -207,7 +207,7 @@ describe("session HttpApi", () => { ).toMatchObject({ info: { id: message.info.id } }) yield* Effect.promise(() => - Instance.provide({ + WithInstance.provide({ directory: tmp.path, fn: async () => { const message = new SessionMessage.Assistant({ @@ -232,7 +232,7 @@ describe("session HttpApi", () => { agent: message.agent, model: message.model, content: message.content, - } as NonNullable, + } as NonNullable<(typeof SessionMessageTable.$inferInsert)["data"]>, }, ]) .run(), @@ -241,9 +241,9 @@ describe("session HttpApi", () => { }), ) - expect(yield* requestJson(`/api/session/${parent.id}/message`, { headers })).toMatchObject([ - { type: "assistant" }, - ]) + expect( + yield* requestJson(`/api/session/${parent.id}/message`, { headers }), + ).toMatchObject([{ type: "assistant" }]) }), ), ) From 49a688172bdf5a44f651a7dae53cbfcba9776cb3 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 2 May 2026 21:20:21 -0400 Subject: [PATCH 24/24] core: keep event-system tests stable in CI --- packages/opencode/test/preload.ts | 2 +- .../opencode/test/server/httpapi-bridge.test.ts | 9 ++++++++- .../opencode/test/server/httpapi-event.test.ts | 15 +++++++++++++-- .../opencode/test/server/httpapi-session.test.ts | 2 +- packages/opencode/test/session/prompt.test.ts | 11 +++++++---- packages/opencode/test/sync/index.test.ts | 2 +- 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index a40a69bcee6e..b408f7ef11b8 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -34,7 +34,7 @@ process.env["XDG_CACHE_HOME"] = path.join(dir, "cache") process.env["XDG_CONFIG_HOME"] = path.join(dir, "config") process.env["XDG_STATE_HOME"] = path.join(dir, "state") process.env["OPENCODE_MODELS_PATH"] = path.join(import.meta.dir, "tool", "fixtures", "models-api.json") -process.env["OPENCODE_EXPERIMENTAL"] = "true" +process.env["OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"] = "true" // Set test home directory to isolate tests from user's actual home directory // This prevents tests from picking up real user configs/skills from ~/.claude/skills diff --git a/packages/opencode/test/server/httpapi-bridge.test.ts b/packages/opencode/test/server/httpapi-bridge.test.ts index 352fb2e2faf9..b7ffa0ca5ed7 100644 --- a/packages/opencode/test/server/httpapi-bridge.test.ts +++ b/packages/opencode/test/server/httpapi-bridge.test.ts @@ -226,7 +226,14 @@ describe("HttpApi server", () => { const effectRoutes = openApiRouteKeys(effectOpenApi()) expect(honoRoutes.filter((route) => !effectRoutes.includes(route))).toEqual([]) - expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([]) + expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([ + "GET /api/session", + "GET /api/session/{sessionID}/context", + "GET /api/session/{sessionID}/message", + "POST /api/session/{sessionID}/compact", + "POST /api/session/{sessionID}/prompt", + "POST /api/session/{sessionID}/wait", + ]) }) test("matches generated OpenAPI route parameters", async () => { diff --git a/packages/opencode/test/server/httpapi-event.test.ts b/packages/opencode/test/server/httpapi-event.test.ts index d7e48240a9c9..940efed9c359 100644 --- a/packages/opencode/test/server/httpapi-event.test.ts +++ b/packages/opencode/test/server/httpapi-event.test.ts @@ -27,6 +27,14 @@ async function readFirstChunk(response: Response) { return new TextDecoder().decode(result.value) } +async function readFirstEvent(response: Response) { + return JSON.parse((await readFirstChunk(response)).replace(/^data: /, "")) as { + id?: string + type: string + properties: Record + } +} + afterEach(async () => { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original await disposeAllInstances() @@ -43,7 +51,7 @@ describe("event HttpApi bridge", () => { expect(response.headers.get("cache-control")).toBe("no-cache, no-transform") expect(response.headers.get("x-accel-buffering")).toBe("no") expect(response.headers.get("x-content-type-options")).toBe("nosniff") - expect(await readFirstChunk(response)).toContain('data: {"type":"server.connected","properties":{}}\n\n') + expect(await readFirstEvent(response)).toMatchObject({ type: "server.connected", properties: {} }) }) test("matches legacy first event frame", async () => { @@ -52,6 +60,9 @@ describe("event HttpApi bridge", () => { const legacy = await app(false).request(EventPaths.event, { headers }) const effect = await app(true).request(EventPaths.event, { headers }) - expect(await readFirstChunk(effect)).toBe(await readFirstChunk(legacy)) + const legacyEvent = await readFirstEvent(legacy) + const effectEvent = await readFirstEvent(effect) + expect(effectEvent.type).toBe(legacyEvent.type) + expect(effectEvent.properties).toEqual(legacyEvent.properties) }) }) diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index d2f4cd28b892..d96347bed8c0 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -242,7 +242,7 @@ describe("session HttpApi", () => { ) expect( - yield* requestJson(`/api/session/${parent.id}/message`, { headers }), + (yield* requestJson<{ items: SessionMessage.Message[] }>(`/api/session/${parent.id}/message`, { headers })).items, ).toMatchObject([{ type: "assistant" }]) }), ), diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index b49d6c9d9d92..a602c0c8d7aa 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -402,11 +402,14 @@ it.live("prompt emits v2 prompted and synthetic events", () => const row = Database.use((db) => db.select().from(SessionMessageTable).where(Database.eq(SessionMessageTable.session_id, chat.id)).get(), ) - expect(messages).toHaveLength(3) - expect(messages[0]).toMatchObject({ type: "user", text: "hello v2" }) + expect(messages.find((message) => message.type === "user")).toMatchObject({ type: "user", text: "hello v2" }) expect(typeof row?.data.time.created).toBe("number") - expect(messages[1]).toMatchObject({ type: "synthetic", text: expect.stringContaining("Called the Read tool") }) - expect(messages[2]).toMatchObject({ type: "synthetic", text: "note content" }) + expect(messages).toEqual( + expect.arrayContaining([ + expect.objectContaining({ type: "synthetic", text: expect.stringContaining("Called the Read tool") }), + expect.objectContaining({ type: "synthetic", text: "note content" }), + ]), + ) }), { git: true, config: providerCfg }, ), diff --git a/packages/opencode/test/sync/index.test.ts b/packages/opencode/test/sync/index.test.ts index 0afbb1831757..234c5246eeee 100644 --- a/packages/opencode/test/sync/index.test.ts +++ b/packages/opencode/test/sync/index.test.ts @@ -124,7 +124,7 @@ describe("SyncEvent", () => { yield* SyncEvent.use.run(Created, { id: "evt_1", name: "test" }) yield* Effect.promise(() => received) expect(events).toHaveLength(1) - expect(events[0]).toEqual({ + expect(events[0]).toMatchObject({ type: "item.created", properties: { id: "evt_1",