summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax <[email protected]>2026-05-02 22:09:48 -0400
committerGitHub <[email protected]>2026-05-02 22:09:48 -0400
commita3bc5d35b0f8f542d4531193b8816bc8b55363e3 (patch)
tree8a305b985e2fc0a1e05060760fd67d890dace443
parent1409a0715cd9f0bd92b9c1b736055791f336324c (diff)
downloadopencode-a3bc5d35b0f8f542d4531193b8816bc8b55363e3.tar.gz
opencode-a3bc5d35b0f8f542d4531193b8816bc8b55363e3.zip
Refactor v2 session events as schemas (#24512)
-rw-r--r--packages/core/src/flag/flag.ts1
-rw-r--r--packages/core/src/util/log.ts2
-rw-r--r--packages/opencode/migration/20260427172553_slow_nightmare/migration.sql17
-rw-r--r--packages/opencode/migration/20260427172553_slow_nightmare/snapshot.json1481
-rw-r--r--packages/opencode/migration/20260428004200_add_session_path/snapshot.json176
-rw-r--r--packages/opencode/migration/20260501142318_next_venus/migration.sql2
-rw-r--r--packages/opencode/migration/20260501142318_next_venus/snapshot.json1511
-rw-r--r--packages/opencode/src/bus/bus-event.ts2
-rw-r--r--packages/opencode/src/bus/global.ts14
-rw-r--r--packages/opencode/src/bus/index.ts25
-rw-r--r--packages/opencode/src/cli/cmd/tui/app.tsx45
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx12
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx298
-rw-r--r--packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx1087
-rw-r--r--packages/opencode/src/cli/cmd/tui/plugin/internal.ts3
-rw-r--r--packages/opencode/src/server/routes/global.ts3
-rw-r--r--packages/opencode/src/server/routes/instance/event.ts8
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/api.ts2
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/event.ts4
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts14
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts69
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts140
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts5
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts6
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts60
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts115
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/server.ts2
-rw-r--r--packages/opencode/src/server/routes/instance/index.ts128
-rw-r--r--packages/opencode/src/session/compaction.ts24
-rw-r--r--packages/opencode/src/session/processor.ts143
-rw-r--r--packages/opencode/src/session/projectors-next.ts204
-rw-r--r--packages/opencode/src/session/projectors.ts5
-rw-r--r--packages/opencode/src/session/prompt.ts119
-rw-r--r--packages/opencode/src/session/session.sql.ts25
-rw-r--r--packages/opencode/src/session/session.ts29
-rw-r--r--packages/opencode/src/sync/index.ts8
-rw-r--r--packages/opencode/src/util/effect-zod.ts2
-rw-r--r--packages/opencode/src/v2/event.ts53
-rw-r--r--packages/opencode/src/v2/schema.ts10
-rw-r--r--packages/opencode/src/v2/session-entry-stepper.ts261
-rw-r--r--packages/opencode/src/v2/session-entry.ts220
-rw-r--r--packages/opencode/src/v2/session-event.ts701
-rw-r--r--packages/opencode/src/v2/session-message-updater.ts411
-rw-r--r--packages/opencode/src/v2/session-message.ts178
-rw-r--r--packages/opencode/src/v2/session-prompt.ts36
-rw-r--r--packages/opencode/src/v2/session.ts302
-rw-r--r--packages/opencode/src/v2/tool-output.ts18
-rw-r--r--packages/opencode/test/acp/event-subscription.test.ts1
-rw-r--r--packages/opencode/test/cli/tui/use-event.test.tsx2
-rw-r--r--packages/opencode/test/preload.ts3
-rw-r--r--packages/opencode/test/server/httpapi-bridge.test.ts9
-rw-r--r--packages/opencode/test/server/httpapi-event.test.ts15
-rw-r--r--packages/opencode/test/server/httpapi-session.test.ts43
-rw-r--r--packages/opencode/test/session/compaction.test.ts10
-rw-r--r--packages/opencode/test/session/prompt.test.ts44
-rw-r--r--packages/opencode/test/session/session-entry-stepper.test.ts916
-rw-r--r--packages/opencode/test/sync/index.test.ts2
-rw-r--r--packages/opencode/test/v2/session-message-updater.test.ts203
-rwxr-xr-xpackages/sdk/js/script/build.ts2
-rw-r--r--packages/sdk/js/src/v2/gen/sdk.gen.ts2763
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts5209
-rw-r--r--specs/v2/session-concepts-gap.md131
62 files changed, 12060 insertions, 5274 deletions
diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts
index ed52f90e6..0daae5580 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/core/src/util/log.ts b/packages/core/src/util/log.ts
index a61c15f7a..e1962aed4 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 000000000..d5efe5f9e
--- /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 000000000..bb6d06237
--- /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/migration/20260428004200_add_session_path/snapshot.json b/packages/opencode/migration/20260428004200_add_session_path/snapshot.json
index d79324fed..1f3bc493c 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 000000000..e0ffe7823
--- /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 000000000..e594de2f0
--- /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/bus/bus-event.ts b/packages/opencode/src/bus/bus-event.ts
index cf9fcfbee..3250c166a 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 b5392a81b..3cfd45362 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 9ee8e6fb0..449694a53 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<D extends BusEvent.Definition = BusEvent.Definition> = {
+ id: string
type: D["type"]
properties: BusProperties<D>
}
@@ -28,7 +30,11 @@ type State = {
}
export interface Interface {
- readonly publish: <D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) => Effect.Effect<void>
+ readonly publish: <D extends BusEvent.Definition>(
+ def: D,
+ properties: BusProperties<D>,
+ options?: { id?: string },
+ ) => Effect.Effect<void>
readonly subscribe: <D extends BusEvent.Definition>(def: D) => Stream.Stream<Payload<D>>
readonly subscribeAll: () => Stream.Stream<Payload>
readonly subscribeCallback: <D extends BusEvent.Definition>(
@@ -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<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) {
+ function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>, 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<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) {
- return runPromise((svc) => svc.publish(def, properties))
+export function createID() {
+ return Identifier.create("evt", "ascending")
+}
+
+export async function publish<D extends BusEvent.Definition>(
+ def: D,
+ properties: BusProperties<D>,
+ options?: { id?: string },
+) {
+ return runPromise((svc) => svc.publish(def, properties, options))
}
export function subscribe<D extends BusEvent.Definition>(def: D, callback: (event: Payload<D>) => unknown) {
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 7117ae7d1..ea742f699 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: {
>
<ProjectProvider>
<SyncProvider>
- <ThemeProvider mode={mode}>
- <LocalProvider>
- <KeybindProvider>
- <PromptStashProvider>
- <DialogProvider>
- <CommandProvider>
- <FrecencyProvider>
- <PromptHistoryProvider>
- <PromptRefProvider>
- <EditorContextProvider>
- <App onSnapshot={input.onSnapshot} />
- </EditorContextProvider>
- </PromptRefProvider>
- </PromptHistoryProvider>
- </FrecencyProvider>
- </CommandProvider>
- </DialogProvider>
- </PromptStashProvider>
- </KeybindProvider>
- </LocalProvider>
- </ThemeProvider>
+ <SyncProviderV2>
+ <ThemeProvider mode={mode}>
+ <LocalProvider>
+ <KeybindProvider>
+ <PromptStashProvider>
+ <DialogProvider>
+ <CommandProvider>
+ <FrecencyProvider>
+ <PromptHistoryProvider>
+ <PromptRefProvider>
+ <EditorContextProvider>
+ <App onSnapshot={input.onSnapshot} />
+ </EditorContextProvider>
+ </PromptRefProvider>
+ </PromptHistoryProvider>
+ </FrecencyProvider>
+ </CommandProvider>
+ </DialogProvider>
+ </PromptStashProvider>
+ </KeybindProvider>
+ </LocalProvider>
+ </ThemeProvider>
+ </SyncProviderV2>
</SyncProvider>
</ProjectProvider>
</SDKProvider>
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 79034a01b..a6ba797f3 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
new file mode 100644
index 000000000..f82bb4d96
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx
@@ -0,0 +1,298 @@
+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 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),
+ )
+}
+
+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.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.id,
+ type: "synthetic",
+ sessionID: event.properties.sessionID,
+ text: event.properties.text,
+ time: { created: event.properties.timestamp },
+ })
+ })
+ 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)
+ if (currentAssistant) currentAssistant.time.completed = event.properties.timestamp
+ draft.push({
+ id: event.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.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?.items ?? []))
+ },
+ 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 000000000..7270a9c3b
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx
@@ -0,0 +1,1087 @@
+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, useRenderer, 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,
+ SessionMessageAgentSwitched,
+ SessionMessageAssistant,
+ SessionMessageAssistantReasoning,
+ SessionMessageAssistantText,
+ SessionMessageAssistantTool,
+ SessionMessageCompaction,
+ SessionMessageModelSwitched,
+ SessionMessageShell,
+ SessionMessageSynthetic,
+ SessionMessageUser,
+ ToolFileContent,
+ ToolTextContent,
+} from "@opencode-ai/sdk/v2"
+import { createEffect, createMemo, createSignal, 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 renderedMessages = createMemo(() => messages().toReversed())
+ const lastAssistant = createMemo(() => renderedMessages().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 (
+ <box width={dimensions().width} height={dimensions().height} backgroundColor={theme.background}>
+ <box flexDirection="row">
+ <box flexGrow={1} paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1}>
+ <scrollbox
+ viewportOptions={{ paddingRight: 0 }}
+ verticalScrollbarOptions={{ visible: false }}
+ stickyScroll={true}
+ stickyStart="bottom"
+ flexGrow={1}
+ >
+ <box height={1} />
+ <Show when={messages().length === 0}>
+ <MissingData label="Messages" detail="No v2 messages loaded from useSyncV2 yet." />
+ </Show>
+ <For each={renderedMessages()}>
+ {(message, index) => (
+ <Switch>
+ <Match when={message.type === "user"}>
+ <UserMessage message={message as SessionMessageUser} index={index()} />
+ </Match>
+ <Match when={message.type === "assistant"}>
+ <AssistantMessage
+ message={message as SessionMessageAssistant}
+ last={lastAssistant()?.id === message.id}
+ syntax={syntax()}
+ subtleSyntax={subtleSyntax()}
+ />
+ </Match>
+ <Match when={message.type === "synthetic"}>
+ <SyntheticMessage message={message as SessionMessageSynthetic} index={index()} />
+ </Match>
+ <Match when={message.type === "shell"}>
+ <ShellMessage message={message as SessionMessageShell} />
+ </Match>
+ <Match when={message.type === "compaction"}>
+ <CompactionMessage message={message as SessionMessageCompaction} />
+ </Match>
+ <Match when={message.type === "agent-switched"}>
+ <AgentSwitchedMessage message={message as SessionMessageAgentSwitched} />
+ </Match>
+ <Match when={message.type === "model-switched"}>
+ <ModelSwitchedMessage message={message as SessionMessageModelSwitched} />
+ </Match>
+ <Match when={true}>
+ <UnknownMessage message={message} />
+ </Match>
+ </Switch>
+ )}
+ </For>
+ </scrollbox>
+ <MissingData
+ label="Session prompt, permission prompt, question prompt, sidebar"
+ detail="The v2 message endpoint only exposes messages, so these session UI regions cannot be rendered here. Press Esc to return to the live session."
+ />
+ </box>
+ </box>
+ </box>
+ )
+}
+
+function MissingData(props: { label: string; detail: string }) {
+ const { theme } = useTheme()
+ return (
+ <box
+ border={["left"]}
+ customBorderChars={SplitBorder.customBorderChars}
+ borderColor={theme.warning}
+ backgroundColor={theme.backgroundPanel}
+ paddingLeft={2}
+ paddingTop={1}
+ paddingBottom={1}
+ marginTop={1}
+ flexShrink={0}
+ >
+ <text fg={theme.text}>
+ <span style={{ bg: theme.warning, fg: theme.background, bold: true }}> MISSING DATA </span> {props.label}
+ </text>
+ <text fg={theme.textMuted}>{props.detail}</text>
+ </box>
+ )
+}
+
+function UserMessage(props: { message: SessionMessageUser; index: number }) {
+ const { theme } = useTheme()
+ const attachments = createMemo(() => [...(props.message.files ?? []), ...(props.message.agents ?? [])])
+ return (
+ <box
+ id={props.message.id}
+ border={["left"]}
+ borderColor={theme.primary}
+ customBorderChars={SplitBorder.customBorderChars}
+ marginTop={props.index === 0 ? 0 : 1}
+ flexShrink={0}
+ >
+ <box paddingTop={1} paddingBottom={1} paddingLeft={2} backgroundColor={theme.backgroundPanel}>
+ <Show
+ when={props.message.text.trim()}
+ fallback={
+ <MissingData label="User message text" detail={`Message ${props.message.id} has no text field content.`} />
+ }
+ >
+ <text fg={theme.text}>{props.message.text}</text>
+ </Show>
+ <Show when={attachments().length}>
+ <box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
+ <For each={props.message.files ?? []}>
+ {(file) => (
+ <text fg={theme.text}>
+ <span style={{ bg: theme.secondary, fg: theme.background }}> {file.mime} </span>
+ <span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.name ?? file.uri} </span>
+ </text>
+ )}
+ </For>
+ <For each={props.message.agents ?? []}>
+ {(agent) => (
+ <text fg={theme.text}>
+ <span style={{ bg: theme.accent, fg: theme.background }}> agent </span>
+ <span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {agent.name} </span>
+ </text>
+ )}
+ </For>
+ </box>
+ </Show>
+ <text fg={theme.textMuted}>{Locale.todayTimeOrDateTime(props.message.time.created)}</text>
+ </box>
+ </box>
+ )
+}
+
+function SyntheticMessage(props: { message: SessionMessageSynthetic; index: number }) {
+ const { theme } = useTheme()
+ return (
+ <box
+ id={props.message.id}
+ border={["left"]}
+ borderColor={theme.backgroundElement}
+ customBorderChars={SplitBorder.customBorderChars}
+ marginTop={props.index === 0 ? 0 : 1}
+ paddingLeft={2}
+ paddingTop={1}
+ paddingBottom={1}
+ backgroundColor={theme.backgroundPanel}
+ flexShrink={0}
+ >
+ <text fg={theme.textMuted}>Synthetic</text>
+ <text fg={theme.text}>{props.message.text}</text>
+ </box>
+ )
+}
+
+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 (
+ <BlockTool
+ title="# Shell"
+ spinner={!props.message.time.completed}
+ onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
+ >
+ <box gap={1}>
+ <text fg={theme.text}>$ {props.message.command}</text>
+ <Show when={output()}>
+ <text fg={theme.text}>{limited()}</text>
+ </Show>
+ <Show when={overflow()}>
+ <text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
+ </Show>
+ </box>
+ </BlockTool>
+ )
+}
+
+function CompactionMessage(props: { message: SessionMessageCompaction }) {
+ const { theme } = useTheme()
+ return (
+ <box
+ marginTop={1}
+ border={["top"]}
+ title={props.message.reason === "auto" ? " Auto Compaction " : " Compaction "}
+ titleAlignment="center"
+ borderColor={theme.borderActive}
+ flexShrink={0}
+ >
+ <Show when={props.message.summary}>
+ <text fg={theme.textMuted}>{props.message.summary}</text>
+ </Show>
+ </box>
+ )
+}
+
+function AgentSwitchedMessage(props: { message: SessionMessageAgentSwitched }) {
+ const { theme } = useTheme()
+ const local = useLocal()
+ return (
+ <box paddingLeft={3} marginTop={1} flexShrink={0}>
+ <text>
+ <span style={{ fg: local.agent.color(props.message.agent) }}>▣ </span>
+ <span style={{ fg: theme.textMuted }}>Switched agent to </span>
+ <span style={{ fg: theme.text }}>{Locale.titlecase(props.message.agent)}</span>
+ </text>
+ </box>
+ )
+}
+
+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 (
+ <box paddingLeft={3} marginTop={1} flexShrink={0}>
+ <text>
+ <span style={{ fg: theme.secondary }}>◇ </span>
+ <span style={{ fg: theme.textMuted }}>Switched model to </span>
+ <span style={{ fg: theme.text }}>{model()}</span>
+ </text>
+ </box>
+ )
+}
+
+function UnknownMessage(props: { message: SessionMessage }) {
+ return <MissingData label="Unknown message type" detail={JSON.stringify(props.message)} />
+}
+
+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 (
+ <>
+ <For each={props.message.content}>
+ {(part) => (
+ <Switch>
+ <Match when={part.type === "text"}>
+ <AssistantText part={part as SessionMessageAssistantText} syntax={props.syntax} />
+ </Match>
+ <Match when={part.type === "reasoning"}>
+ <AssistantReasoning part={part as SessionMessageAssistantReasoning} subtleSyntax={props.subtleSyntax} />
+ </Match>
+ <Match when={part.type === "tool"}>
+ <AssistantTool part={part as SessionMessageAssistantTool} />
+ </Match>
+ </Switch>
+ )}
+ </For>
+ <Show when={props.message.content.length === 0}>
+ <MissingData label="Assistant content" detail={`Assistant message ${props.message.id} has no content items.`} />
+ </Show>
+ <Show when={props.message.error}>
+ <box
+ border={["left"]}
+ paddingTop={1}
+ paddingBottom={1}
+ paddingLeft={2}
+ marginTop={1}
+ backgroundColor={theme.backgroundPanel}
+ customBorderChars={SplitBorder.customBorderChars}
+ borderColor={theme.error}
+ flexShrink={0}
+ >
+ <text fg={theme.textMuted}>{props.message.error}</text>
+ </box>
+ </Show>
+ <Show when={props.last || final() || props.message.error}>
+ <box paddingLeft={3} flexShrink={0}>
+ <text marginTop={1}>
+ <span style={{ fg: local.agent.color(props.message.agent) }}>▣ </span>
+ <span style={{ fg: theme.text }}>{Locale.titlecase(props.message.agent)}</span>
+ <span style={{ fg: theme.textMuted }}> · {model()}</span>
+ <Show when={duration()}>
+ <span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
+ </Show>
+ </text>
+ </box>
+ </Show>
+ </>
+ )
+}
+
+function AssistantText(props: { part: SessionMessageAssistantText; syntax: SyntaxStyle }) {
+ const { theme } = useTheme()
+ return (
+ <Show when={props.part.text.trim()}>
+ <box paddingLeft={3} marginTop={1} flexShrink={0}>
+ <code
+ filetype="markdown"
+ drawUnstyledText={false}
+ streaming={true}
+ syntaxStyle={props.syntax}
+ content={props.part.text.trim()}
+ conceal={true}
+ fg={theme.text}
+ />
+ </box>
+ </Show>
+ )
+}
+
+function AssistantReasoning(props: { part: SessionMessageAssistantReasoning; subtleSyntax: SyntaxStyle }) {
+ const { theme } = useTheme()
+ const content = createMemo(() => props.part.text.replace("[REDACTED]", "").trim())
+ return (
+ <Show when={content()}>
+ <box
+ paddingLeft={2}
+ marginTop={1}
+ flexDirection="column"
+ border={["left"]}
+ customBorderChars={SplitBorder.customBorderChars}
+ borderColor={theme.backgroundElement}
+ flexShrink={0}
+ >
+ <code
+ filetype="markdown"
+ drawUnstyledText={false}
+ streaming={true}
+ syntaxStyle={props.subtleSyntax}
+ content={"_Thinking:_ " + content()}
+ conceal={true}
+ fg={theme.textMuted}
+ />
+ </box>
+ </Show>
+ )
+}
+
+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 (
+ <Switch>
+ <Match when={props.part.name === "bash"}>
+ <Bash {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "glob"}>
+ <Glob {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "read"}>
+ <Read {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "grep"}>
+ <Grep {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "webfetch"}>
+ <WebFetch {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "codesearch"}>
+ <CodeSearch {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "websearch"}>
+ <WebSearch {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "write"}>
+ <Write {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "edit"}>
+ <Edit {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "apply_patch"}>
+ <ApplyPatch {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "todowrite"}>
+ <TodoWrite {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "question"}>
+ <Question {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "skill"}>
+ <Skill {...toolprops} />
+ </Match>
+ <Match when={props.part.name === "task"}>
+ <Task {...toolprops} />
+ </Match>
+ <Match when={true}>
+ <GenericTool {...toolprops} />
+ </Match>
+ </Switch>
+ )
+}
+
+type ToolProps = {
+ input: Record<string, unknown>
+ metadata: Record<string, unknown>
+ output?: string
+ part: SessionMessageAssistantTool
+}
+
+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 (
+ <Show
+ when={output()}
+ fallback={
+ <InlineTool icon="⚙" pending="Writing command..." complete={toolComplete(props.part)} part={props.part}>
+ {props.part.name} {input(props.input)}
+ </InlineTool>
+ }
+ >
+ <BlockTool
+ title={`# ${props.part.name} ${input(props.input)}`}
+ part={props.part}
+ onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
+ >
+ <box gap={1}>
+ <text fg={theme.text}>{limited()}</text>
+ <Show when={overflow()}>
+ <text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
+ </Show>
+ </box>
+ </BlockTool>
+ </Show>
+ )
+}
+
+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 (
+ <box marginTop={1} paddingLeft={3} flexShrink={0}>
+ <Switch>
+ <Match when={props.spinner}>
+ <Spinner color={theme.text}>{props.children}</Spinner>
+ </Match>
+ <Match when={true}>
+ <text paddingLeft={3} fg={props.complete ? theme.textMuted : theme.text}>
+ <Show fallback={<>~ {props.pending}</>} when={props.complete}>
+ {props.icon} {props.children}
+ </Show>
+ </text>
+ </Match>
+ </Switch>
+ <Show when={error() && !denied()}>
+ <text fg={theme.error}>{error()}</text>
+ </Show>
+ </box>
+ )
+}
+
+function BlockTool(props: {
+ title: string
+ children: JSX.Element
+ part?: SessionMessageAssistantTool
+ onClick?: () => void
+ spinner?: boolean
+}) {
+ const { theme } = useTheme()
+ const renderer = useRenderer()
+ const [hover, setHover] = createSignal(false)
+ const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error.message : undefined))
+ return (
+ <box
+ border={["left"]}
+ paddingTop={1}
+ paddingBottom={1}
+ paddingLeft={2}
+ marginTop={1}
+ gap={1}
+ backgroundColor={hover() ? theme.backgroundMenu : theme.backgroundPanel}
+ customBorderChars={SplitBorder.customBorderChars}
+ borderColor={theme.background}
+ onMouseOver={() => props.onClick && setHover(true)}
+ onMouseOut={() => setHover(false)}
+ onMouseUp={() => {
+ if (renderer.getSelection()?.getSelectedText()) return
+ props.onClick?.()
+ }}
+ flexShrink={0}
+ >
+ <Show
+ when={props.spinner}
+ fallback={
+ <text paddingLeft={3} fg={theme.textMuted}>
+ {props.title}
+ </text>
+ }
+ >
+ <Spinner color={theme.textMuted}>{props.title.replace(/^# /, "")}</Spinner>
+ </Show>
+ {props.children}
+ <Show when={error()}>
+ <text fg={theme.error}>{error()}</text>
+ </Show>
+ </box>
+ )
+}
+
+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"}`)
+ 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 (
+ <Switch>
+ <Match when={output()}>
+ <BlockTool
+ title={title()}
+ part={props.part}
+ spinner={props.part.state.status === "running"}
+ onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
+ >
+ <box gap={1}>
+ <text fg={theme.text}>$ {command()}</text>
+ <text fg={theme.text}>{limited()}</text>
+ <Show when={overflow()}>
+ <text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
+ </Show>
+ </box>
+ </BlockTool>
+ </Match>
+ <Match when={true}>
+ <InlineTool icon="$" pending="Writing command..." complete={command()} part={props.part}>
+ {command()}
+ </InlineTool>
+ </Match>
+ </Switch>
+ )
+}
+
+function Glob(props: ToolProps) {
+ return (
+ <InlineTool icon="✱" pending="Finding files..." complete={toolComplete(props.part)} part={props.part}>
+ Glob "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "}
+ <Show when={stringValue(props.input.path)}>in {normalizePath(stringValue(props.input.path))} </Show>
+ <Show when={numberValue(props.metadata.count)}>
+ {(count) => (
+ <>
+ ({count()} {count() === 1 ? "match" : "matches"})
+ </>
+ )}
+ </Show>
+ </InlineTool>
+ )
+}
+
+function Read(props: ToolProps) {
+ const { theme } = useTheme()
+ const loaded = createMemo(() =>
+ arrayValue(props.metadata.loaded).filter((item): item is string => typeof item === "string"),
+ )
+ return (
+ <>
+ <InlineTool
+ icon="→"
+ pending="Reading file..."
+ complete={stringValue(props.input.filePath) ?? pendingInput(props.part)}
+ spinner={props.part.state.status === "running"}
+ part={props.part}
+ >
+ Read {normalizePath(stringValue(props.input.filePath) ?? pendingInput(props.part))}{" "}
+ {input(props.input, ["filePath"])}
+ </InlineTool>
+ <For each={loaded()}>
+ {(filepath) => (
+ <box paddingLeft={3} flexShrink={0}>
+ <text paddingLeft={3} fg={theme.textMuted}>
+ ↳ Loaded {normalizePath(filepath)}
+ </text>
+ </box>
+ )}
+ </For>
+ </>
+ )
+}
+
+function Grep(props: ToolProps) {
+ return (
+ <InlineTool icon="✱" pending="Searching content..." complete={toolComplete(props.part)} part={props.part}>
+ Grep "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "}
+ <Show when={stringValue(props.input.path)}>in {normalizePath(stringValue(props.input.path))} </Show>
+ <Show when={numberValue(props.metadata.matches)}>
+ {(matches) => (
+ <>
+ ({matches()} {matches() === 1 ? "match" : "matches"})
+ </>
+ )}
+ </Show>
+ </InlineTool>
+ )
+}
+
+function WebFetch(props: ToolProps) {
+ return (
+ <InlineTool icon="%" pending="Fetching from the web..." complete={toolComplete(props.part)} part={props.part}>
+ WebFetch {stringValue(props.input.url) ?? pendingInput(props.part)}
+ </InlineTool>
+ )
+}
+
+function CodeSearch(props: ToolProps) {
+ return (
+ <InlineTool icon="◇" pending="Searching code..." complete={toolComplete(props.part)} part={props.part}>
+ Exa Code Search "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "}
+ <Show when={numberValue(props.metadata.results)}>{(results) => <>({results()} results)</>}</Show>
+ </InlineTool>
+ )
+}
+
+function WebSearch(props: ToolProps) {
+ return (
+ <InlineTool icon="◈" pending="Searching web..." complete={toolComplete(props.part)} part={props.part}>
+ Exa Web Search "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "}
+ <Show when={numberValue(props.metadata.numResults)}>{(results) => <>({results()} results)</>}</Show>
+ </InlineTool>
+ )
+}
+
+function Write(props: ToolProps) {
+ const { theme, syntax } = useTheme()
+ const filePath = createMemo(() => stringValue(props.input.filePath) ?? "")
+ const content = createMemo(() => stringValue(props.input.content) ?? "")
+ return (
+ <Switch>
+ <Match when={content() && props.part.state.status === "completed"}>
+ <BlockTool title={"# Wrote " + normalizePath(filePath())} part={props.part}>
+ <line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
+ <code
+ conceal={false}
+ fg={theme.text}
+ filetype={filetype(filePath())}
+ syntaxStyle={syntax()}
+ content={content()}
+ />
+ </line_number>
+ <Diagnostics diagnostics={props.metadata.diagnostics} filePath={filePath()} />
+ </BlockTool>
+ </Match>
+ <Match when={true}>
+ <InlineTool icon="←" pending="Preparing write..." complete={filePath()} part={props.part}>
+ Write {normalizePath(filePath())}
+ </InlineTool>
+ </Match>
+ </Switch>
+ )
+}
+
+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 (
+ <Switch>
+ <Match when={diff()}>
+ {(diff) => (
+ <BlockTool title={"← Edit " + normalizePath(filePath())} part={props.part}>
+ <box paddingLeft={1}>
+ <diff
+ diff={diff()}
+ view={dimensions().width > 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}
+ />
+ </box>
+ <Diagnostics diagnostics={props.metadata.diagnostics} filePath={filePath()} />
+ </BlockTool>
+ )}
+ </Match>
+ <Match when={true}>
+ <InlineTool icon="←" pending="Preparing edit..." complete={filePath()} part={props.part}>
+ Edit {normalizePath(filePath())} {input({ replaceAll: props.input.replaceAll })}
+ </InlineTool>
+ </Match>
+ </Switch>
+ )
+}
+
+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<string, unknown>) => {
+ 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 (
+ <Switch>
+ <Match when={files().length > 0}>
+ <For each={files()}>
+ {(file) => (
+ <BlockTool title={fileTitle(file)} part={props.part}>
+ <Show
+ when={stringValue(file.patch)}
+ fallback={
+ <text fg={theme.diffRemoved}>
+ -{numberValue(file.deletions) ?? 0} line{numberValue(file.deletions) === 1 ? "" : "s"}
+ </text>
+ }
+ >
+ {(patch) => (
+ <box paddingLeft={1}>
+ <diff
+ diff={patch()}
+ view={dimensions().width > 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}
+ />
+ </box>
+ )}
+ </Show>
+ </BlockTool>
+ )}
+ </For>
+ </Match>
+ <Match when={true}>
+ <InlineTool icon="%" pending="Preparing patch..." complete={false} part={props.part}>
+ Patch
+ </InlineTool>
+ </Match>
+ </Switch>
+ )
+}
+
+function TodoWrite(props: ToolProps) {
+ const { theme } = useTheme()
+ const todos = createMemo(() => arrayValue(props.input.todos).flatMap((item) => (isRecord(item) ? [item] : [])))
+ return (
+ <Switch>
+ <Match when={todos().length > 0 && props.part.state.status === "completed"}>
+ <BlockTool title="# Todos" part={props.part}>
+ <box>
+ <For each={todos()}>
+ {(todo) => (
+ <text fg={theme.text}>
+ {todoIcon(stringValue(todo.status))} {stringValue(todo.content)}
+ </text>
+ )}
+ </For>
+ </box>
+ </BlockTool>
+ </Match>
+ <Match when={true}>
+ <InlineTool icon="⚙" pending="Updating todos..." complete={false} part={props.part}>
+ Updating todos...
+ </InlineTool>
+ </Match>
+ </Switch>
+ )
+}
+
+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 (
+ <Switch>
+ <Match when={answers().length > 0}>
+ <BlockTool title="# Questions" part={props.part}>
+ <box gap={1}>
+ <For each={questions()}>
+ {(question, index) => (
+ <box>
+ <text fg={theme.textMuted}>{stringValue(question.question)}</text>
+ <text fg={theme.text}>{formatAnswer(answers()[index()])}</text>
+ </box>
+ )}
+ </For>
+ </box>
+ </BlockTool>
+ </Match>
+ <Match when={true}>
+ <InlineTool icon="→" pending="Asking questions..." complete={questions().length} part={props.part}>
+ Asked {questions().length} question{questions().length === 1 ? "" : "s"}
+ </InlineTool>
+ </Match>
+ </Switch>
+ )
+}
+
+function Skill(props: ToolProps) {
+ return (
+ <InlineTool icon="→" pending="Loading skill..." complete={toolComplete(props.part)} part={props.part}>
+ Skill "{stringValue(props.input.name) ?? pendingInput(props.part)}"
+ </InlineTool>
+ )
+}
+
+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 (
+ <InlineTool
+ icon="│"
+ spinner={props.part.state.status === "running"}
+ complete={toolComplete(props.part)}
+ pending="Delegating..."
+ part={props.part}
+ >
+ {content()}
+ </InlineTool>
+ )
+}
+
+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 (
+ <Show when={errors().length}>
+ <box>
+ <For each={errors()}>
+ {(diagnostic) => <text fg={theme.error}>Error {stringValue(diagnostic.message)}</text>}
+ </For>
+ </box>
+ </Show>
+ )
+}
+
+function toolOutput(content?: Array<ToolTextContent | ToolFileContent>) {
+ 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<string, unknown>) {
+ 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<string, unknown> {
+ return !!value && typeof value === "object" && !Array.isArray(value)
+}
+
+function input(input: Record<string, unknown>, 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 <text fg={api.theme.current.error}>Missing sessionID</text>
+ }
+ return <View api={api} sessionID={sessionID} />
+ },
+ },
+ ])
+
+ 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 })
+ api.ui.dialog.clear()
+ },
+ },
+ ])
+}
+
+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 856ee0ebb..2b0d85919 100644
--- a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts
+++ b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts
@@ -7,7 +7,9 @@ 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"
+import { Flag } from "@opencode-ai/core/flag/flag"
export type InternalTuiPlugin = TuiPluginModule & {
id: string
@@ -24,4 +26,5 @@ export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
SidebarFiles,
SidebarFooter,
PluginManager,
+ ...(Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM ? [SessionV2Debug] : []),
]
diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts
index 4a491d95b..da3614d22 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<string | null>
q.push(
JSON.stringify({
payload: {
+ id: Bus.createID(),
type: "server.connected",
properties: {},
},
@@ -37,6 +39,7 @@ async function streamEvents(c: Context, subscribe: (q: AsyncQueue<string | null>
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 474d92b31..52e9bc196 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/api.ts b/packages/opencode/src/server/routes/instance/httpapi/api.ts
index 81ea2394c..1cf1584e3 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 "./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.
@@ -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/event.ts b/packages/opencode/src/server/routes/instance/httpapi/event.ts
index 25e810753..a5c328ac0 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/groups/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts
new file mode 100644
index 000000000..05da5b720
--- /dev/null
+++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts
@@ -0,0 +1,14 @@
+import { HttpApi, OpenApi } from "effect/unstable/httpapi"
+import { MessageGroup } from "./v2/message"
+import { SessionGroup } from "./v2/session"
+
+export const V2Api = HttpApi.make("v2")
+ .add(SessionGroup)
+ .add(MessageGroup)
+ .annotateMerge(
+ OpenApi.annotations({
+ title: "opencode experimental HttpApi",
+ version: "0.0.1",
+ description: "Experimental HttpApi surface for selected instance routes.",
+ }),
+ )
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 000000000..3b0b2fa5b
--- /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 000000000..17ddcaeda
--- /dev/null
+++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts
@@ -0,0 +1,140 @@
+import { WorkspaceID } from "@/control-plane/schema"
+import { SessionID } from "@/session/schema"
+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(SessionV2.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.",
+ }),
+ ),
+ )
+ .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",
+ description: "Experimental v2 routes.",
+ }),
+ )
+ .middleware(Authorization)
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 f9be57f4f..f80869b64 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/httpapi/handlers/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts
new file mode 100644
index 000000000..55cb53458
--- /dev/null
+++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts
@@ -0,0 +1,6 @@
+import { SessionV2 } from "@/v2/session"
+import { Layer } from "effect"
+import { messageHandlers } from "./v2/message"
+import { sessionHandlers } from "./v2/session"
+
+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 000000000..3485d80fd
--- /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.Finite,
+ 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 000000000..558e34dd1
--- /dev/null
+++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts
@@ -0,0 +1,115 @@
+import { WorkspaceID } from "@/control-plane/schema"
+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: SessionV2.Info.fields.id,
+ 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.Finite.pipe(Schema.optional),
+ search: Schema.String.pipe(Schema.optional),
+})
+type SessionCursor = typeof SessionCursor.Type
+
+const decodeCursor = Schema.decodeUnknownSync(SessionCursor)
+
+const sessionCursor = {
+ encode(
+ session: SessionV2.Info,
+ order: "asc" | "desc",
+ direction: "previous" | "next",
+ filters: Pick<SessionCursor, "directory" | "path" | "workspaceID" | "roots" | "start" | "search">,
+ ) {
+ 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()
+ }),
+ )
+ .handle(
+ "context",
+ Effect.fn(function* (ctx) {
+ return yield* session.context(ctx.params.sessionID)
+ }),
+ )
+ }),
+)
diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts
index 0b4bc252c..e53eca3ef 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/index.ts b/packages/opencode/src/server/routes/instance/index.ts
index f0da2f3d8..3f9f3f660 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,10 +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()
+ const handler = ExperimentalHttpApiServer.webHandler().handler
+ const context = Context.empty() as Context.Context<unknown>
+
+ app.all("/api/*", (c) => handler(c.req.raw, context))
+
+ if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
+ 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.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))
+ 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())
diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts
index aaee2be2f..067d43da2 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 { EventV2 } from "@/v2/event"
+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: [],
+ },
+ )
+ EventV2.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,11 @@ export const layer: Layer.Layer<
auto: input.auto,
overflow: input.overflow,
})
+ EventV2.run(SessionEvent.Compaction.Started.Sync, {
+ 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 b475ec1c5..1a32a656d 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 { EventV2 } from "@/v2/event"
+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.
+ EventV2.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,
@@ -248,6 +257,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.
+ EventV2.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 +276,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.
+ EventV2.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 +304,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.
+ EventV2.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.
+ EventV2.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 +375,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.
+ EventV2.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.
+ EventV2.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 +426,20 @@ 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.
+ EventV2.run(SessionEvent.Step.Started.Sync, {
+ 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 +450,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.
+ EventV2.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 +511,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.
+ EventV2.run(SessionEvent.Text.Started.Sync, {
+ sessionID: ctx.sessionID,
+ timestamp: DateTime.makeUnsafe(Date.now()),
+ })
+ }
ctx.currentText = {
id: PartID.ascending(),
messageID: ctx.assistantMessage.id,
@@ -442,6 +556,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.
+ EventV2.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 +690,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.
+ EventV2.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 000000000..951e3e874
--- /dev/null
+++ b/packages/opencode/src/session/projectors-next.ts
@@ -0,0 +1,204 @@
+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, SessionTable } from "./session.sql"
+import type { SessionID } from "./schema"
+import { Schema } from "effect"
+
+const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
+type SessionMessageData = NonNullable<(typeof SessionMessageTable.$inferInsert)["data"]>
+
+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<void> {
+ 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")
+ },
+ 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)
+ .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()
+ },
+ 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)
+ .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.AgentSwitched.Sync, (db, data, event) => {
+ db.update(SessionTable)
+ .set({
+ agent: data.agent,
+ time_updated: DateTime.toEpochMillis(data.timestamp),
+ })
+ .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, event) => {
+ 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()
+ update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.model.switched", 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, 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 })
+ }),
+ 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, 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, event) => {
+ update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.text.ended", 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, event) => {
+ update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.input.ended", 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, event) => {
+ update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.success", 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, 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, event) => {
+ update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.reasoning.ended", 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, event) => {
+ update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.started", 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 })
+ }),
+]
diff --git a/packages/opencode/src/session/projectors.ts b/packages/opencode/src/session/projectors.ts
index a3832ebe6..9819ad810 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 9f1420388..0590fc382 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -54,6 +54,13 @@ 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 * 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
@@ -785,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(),
@@ -794,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)
+ EventV2.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))
@@ -813,14 +828,21 @@ NOTE: At any point in time through this workflow you should feel free to ask the
if (aborted) {
output += "\n\n" + ["<metadata>", "User aborted the command", "</metadata>"].join("\n")
}
+ const completed = Date.now()
+ EventV2.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: "" },
@@ -934,6 +956,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) {
+ EventV2.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
+ ) {
+ EventV2.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> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never
@@ -1250,6 +1300,69 @@ 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.
+ EventV2.run(SessionEvent.Prompted.Sync, {
+ 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.
+ EventV2.run(SessionEvent.Synthetic.Sync, {
+ 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 863fb21d6..421fa6869 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<MessageV2.Part, "id" | "sessionID" | "messageID">
type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
+type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id">
export const SessionTable = sqliteTable(
"session",
@@ -34,6 +35,12 @@ export const SessionTable = sqliteTable(
summary_diffs: text({ mode: "json" }).$type<Snapshot.FileDiff[]>(),
revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: PartID; snapshot?: string; diff?: string }>(),
permission: text({ mode: "json" }).$type<Permission.Ruleset>(),
+ agent: text(),
+ model: text({ mode: "json" }).$type<{
+ id: string
+ providerID: string
+ variant?: string
+ }>(),
...Timestamps,
time_compacting: integer(),
time_archived: integer(),
@@ -96,22 +103,22 @@ export const TodoTable = sqliteTable(
],
)
-export const SessionEntryTable = sqliteTable(
- "session_entry",
+export const SessionMessageTable = sqliteTable(
+ "session_message",
{
- id: text().$type<SessionEntry.ID>().primaryKey(),
+ id: text().$type<SessionMessage.ID>().primaryKey(),
session_id: text()
.$type<SessionID>()
.notNull()
.references(() => SessionTable.id, { onDelete: "cascade" }),
- type: text().$type<SessionEntry.Type>().notNull(),
+ type: text().$type<SessionMessage.Type>().notNull(),
...Timestamps,
- data: text({ mode: "json" }).notNull().$type<Omit<SessionEntry.Entry, "type" | "id">>(),
+ data: text({ mode: "json" }).notNull().$type<SessionMessageData>(),
},
(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/session/session.ts b/packages/opencode/src/session/session.ts
index e1d0c527a..fedfa8996 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<typeof Model>
permission?: Permission.Ruleset
workspaceID?: WorkspaceID
}) => Effect.Effect<Info>
@@ -464,6 +485,8 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
const createNext = Effect.fn("Session.createNext")(function* (input: {
id?: SessionID
title?: string
+ agent?: string
+ model?: Schema.Schema.Type<typeof Model>
parentID?: SessionID
workspaceID?: WorkspaceID
directory: string
@@ -481,6 +504,8 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
workspaceID: input.workspaceID,
parentID: input.parentID,
title: input.title ?? createDefaultTitle(!!input.parentID),
+ agent: input.agent,
+ model: input.model,
permission: input.permission,
time: {
created: Date.now(),
@@ -591,6 +616,8 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
const create = Effect.fn("Session.create")(function* (input?: {
parentID?: SessionID
title?: string
+ agent?: string
+ model?: Schema.Schema.Type<typeof Model>
permission?: Permission.Ruleset
workspaceID?: WorkspaceID
}) {
@@ -601,6 +628,8 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
directory: ctx.directory,
path: sessionPath(ctx.worktree, ctx.directory),
title: input?.title,
+ agent: input?.agent,
+ model: input?.model,
permission: input?.permission,
workspaceID: input?.workspaceID ?? workspace,
})
diff --git a/packages/opencode/src/sync/index.ts b/packages/opencode/src/sync/index.ts
index 324c4ec45..2654767e9 100644
--- a/packages/opencode/src/sync/index.ts
+++ b/packages/opencode/src/sync/index.ts
@@ -46,7 +46,7 @@ export type Properties<Def extends Definition = Definition> = EffectSchema.Schem
export type SerializedEvent<Def extends Definition = Definition> = Event<Def> & { 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<unknown>
type PublishContext = {
instance?: InstanceContext
@@ -255,7 +255,7 @@ export function define<
export function project<Def extends Definition>(
def: Def,
- func: (db: Database.TxOrDb, data: Event<Def>["data"]) => void,
+ func: (db: Database.TxOrDb, data: Event<Def>["data"], event: Event<Def>) => void,
): [Definition, ProjectorFunc] {
return [def, func as ProjectorFunc]
}
@@ -277,7 +277,7 @@ function process<Def extends Definition>(
// 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<Def extends Definition>(
}
const result = convertEvent(def.type, event.data)
- const publish = (data: unknown) => ProjectBus.publish(def, data as Properties<Def>)
+ const publish = (data: unknown) => ProjectBus.publish(def, data as Properties<Def>, { id: event.id })
if (result instanceof Promise) {
void result.then(publish)
} else {
diff --git a/packages/opencode/src/util/effect-zod.ts b/packages/opencode/src/util/effect-zod.ts
index 332a5c76e..1c88712d7 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 000000000..fde8d4326
--- /dev/null
+++ b/packages/opencode/src/v2/event.ts
@@ -0,0 +1,53 @@
+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(
+ Schema.brand("Event.ID"),
+ withStatics((s) => ({
+ create: () => s.make(Identifier.create("evt", "ascending")),
+ })),
+)
+export type ID = Schema.Schema.Type<typeof ID>
+
+export function define<const Type extends string, Fields extends Schema.Struct.Fields>(input: {
+ type: Type
+ schema: Fields
+ aggregate: string
+ version?: number
+}) {
+ const Payload = Schema.Struct({
+ id: ID,
+ 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 function run<Def extends SyncEvent.Definition>(
+ def: Def,
+ data: SyncEvent.Event<Def>["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/schema.ts b/packages/opencode/src/v2/schema.ts
new file mode 100644
index 000000000..44587b838
--- /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-entry-stepper.ts b/packages/opencode/src/v2/session-entry-stepper.ts
deleted file mode 100644
index 3fe4266c0..000000000
--- 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<Result> {
- 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<MemoryState> {
- 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<Result>(adapter: Adapter<Result>, event: SessionEvent.Event): Result {
- const currentAssistant = adapter.getCurrentAssistant()
- type DraftAssistant = WritableDraft<SessionEntry.Assistant>
- type DraftTool = WritableDraft<SessionEntry.AssistantTool>
- type DraftText = WritableDraft<SessionEntry.AssistantText>
- type DraftReasoning = WritableDraft<SessionEntry.AssistantReasoning>
-
- 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 66576a688..000000000
--- 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<typeof ID>
-
-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<User>("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<Synthetic>("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<ToolStatePending>("Session.Entry.ToolState.Pending")({
- status: Schema.Literal("pending"),
- input: Schema.String,
-}) {}
-
-export class ToolStateRunning extends Schema.Class<ToolStateRunning>("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<ToolStateCompleted>("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<ToolStateError>("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<typeof ToolState>
-
-export class AssistantTool extends Schema.Class<AssistantTool>("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<AssistantText>("Session.Entry.Assistant.Text")({
- type: Schema.Literal("text"),
- text: Schema.String,
-}) {}
-
-export class AssistantReasoning extends Schema.Class<AssistantReasoning>("Session.Entry.Assistant.Reasoning")({
- type: Schema.Literal("reasoning"),
- text: Schema.String,
-}) {}
-
-export class AssistantRetry extends Schema.Class<AssistantRetry>("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<typeof AssistantContent>
-
-export class Assistant extends Schema.Class<Assistant>("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<Compaction>("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<typeof Entry>
-
-export type Type = Entry["type"]
-
-/*
-export interface Interface {
- readonly decode: (row: typeof SessionEntryTable.$inferSelect) => Entry
- readonly fromSession: (sessionID: SessionID) => Effect.Effect<Entry[], never>
-}
-
-export class Service extends Context.Service<Service, Interface>()("@opencode/SessionEntry") {}
-
-export const layer: Layer.Layer<Service, never, never> = 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 aaf71c8dc..3af5932f0 100644
--- a/packages/opencode/src/v2/session-event.ts
+++ b/packages/opencode/src/v2/session-event.ts
@@ -1,128 +1,119 @@
-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 { EventV2 } from "./event"
+import { FileAttachment, Prompt } from "./session-prompt"
import { Schema } from "effect"
+export { FileAttachment }
+import { ToolOutput } from "./tool-output"
+import { ModelID, ProviderID } from "@/provider/schema"
+import { V2Schema } from "./schema"
-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<typeof ID>
- type Stamp = Schema.Schema.Type<typeof Schema.DateTimeUtc>
- type BaseInput = {
- id?: ID
- metadata?: Record<string, unknown>
- timestamp?: Stamp
- }
+export const Source = Schema.Struct({
+ start: NonNegativeInt,
+ end: NonNegativeInt,
+ text: Schema.String,
+}).annotate({
+ identifier: "session.next.event.source",
+})
+export type Source = Schema.Schema.Type<typeof Source>
- const Base = {
- id: ID,
- metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
- timestamp: Schema.DateTimeUtc,
- }
+const Base = {
+ timestamp: V2Schema.DateTimeUtcFromMillis,
+ sessionID: SessionID,
+}
- export class Source extends Schema.Class<Source>("Session.Event.Source")({
- start: NonNegativeInt,
- end: NonNegativeInt,
- text: Schema.String,
- }) {}
-
- export class FileAttachment extends Schema.Class<FileAttachment>("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 AgentSwitched = EventV2.define({
+ type: "session.next.agent.switched",
+ aggregate: "sessionID",
+ version: 1,
+ schema: {
+ ...Base,
+ agent: Schema.String,
+ },
+})
+export type AgentSwitched = Schema.Schema.Type<typeof AgentSwitched>
- export class AgentAttachment extends Schema.Class<AgentAttachment>("Session.Event.AgentAttachment")({
- name: Schema.String,
- source: Source.pipe(Schema.optional),
- }) {}
-
- export class RetryError extends Schema.Class<RetryError>("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),
- }) {}
-
- export class Prompt extends Schema.Class<Prompt>("Session.Event.Prompt")({
+export const ModelSwitched = EventV2.define({
+ type: "session.next.model.switched",
+ 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: ModelID,
+ providerID: ProviderID,
+ variant: Schema.String.pipe(Schema.optional),
+ },
+})
+export type ModelSwitched = Schema.Schema.Type<typeof ModelSwitched>
+
+export const Prompted = EventV2.define({
+ type: "session.next.prompted",
+ aggregate: "sessionID",
+ version: 1,
+ schema: {
+ ...Base,
+ prompt: Prompt,
+ },
+})
+export type Prompted = Schema.Schema.Type<typeof Prompted>
- export class Synthetic extends Schema.Class<Synthetic>("Session.Event.Synthetic")({
+export const Synthetic = EventV2.define({
+ type: "session.next.synthetic",
+ aggregate: "sessionID",
+ schema: {
...Base,
- type: Schema.Literal("synthetic"),
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<typeof Synthetic>
+
+export namespace Shell {
+ export const Started = EventV2.define({
+ type: "session.next.shell.started",
+ aggregate: "sessionID",
+ schema: {
+ ...Base,
+ callID: Schema.String,
+ command: Schema.String,
+ },
+ })
+ export type Started = Schema.Schema.Type<typeof Started>
+
+ export const Ended = EventV2.define({
+ type: "session.next.shell.ended",
+ aggregate: "sessionID",
+ schema: {
+ ...Base,
+ callID: Schema.String,
+ output: Schema.String,
+ },
+ })
+ export type Ended = Schema.Schema.Type<typeof Ended>
+}
- export namespace Step {
- export class Started extends Schema.Class<Started>("Session.Event.Step.Started")({
+export namespace Step {
+ export const Started = EventV2.define({
+ type: "session.next.step.started",
+ aggregate: "sessionID",
+ schema: {
...Base,
- type: Schema.Literal("step.started"),
+ 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,
- })
- }
- }
-
- export class Ended extends Schema.Class<Ended>("Session.Event.Step.Ended")({
+ snapshot: Schema.String.pipe(Schema.optional),
+ },
+ })
+ export type Started = Schema.Schema.Type<typeof Started>
+
+ export const Ended = EventV2.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 +124,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<typeof Ended>
+}
- export namespace Text {
- export class Started extends Schema.Class<Started>("Session.Event.Text.Started")({
+export namespace Text {
+ export const Started = EventV2.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 class Delta extends Schema.Class<Delta>("Session.Event.Text.Delta")({
+ },
+ })
+ export type Started = Schema.Schema.Type<typeof Started>
+
+ export const Delta = EventV2.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 class Ended extends Schema.Class<Ended>("Session.Event.Text.Ended")({
+ },
+ })
+ export type Delta = Schema.Schema.Type<typeof Delta>
+
+ export const Ended = EventV2.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<typeof Ended>
+}
- export namespace Reasoning {
- export class Started extends Schema.Class<Started>("Session.Event.Reasoning.Started")({
+export namespace Reasoning {
+ export const Started = EventV2.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,
- })
- }
- }
-
- export class Delta extends Schema.Class<Delta>("Session.Event.Reasoning.Delta")({
+ reasoningID: Schema.String,
+ },
+ })
+ export type Started = Schema.Schema.Type<typeof Started>
+
+ export const Delta = EventV2.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 class Ended extends Schema.Class<Ended>("Session.Event.Reasoning.Ended")({
+ },
+ })
+ export type Delta = Schema.Schema.Type<typeof Delta>
+
+ export const Ended = EventV2.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<typeof Ended>
+}
- export namespace Tool {
- export namespace Input {
- export class Started extends Schema.Class<Started>("Session.Event.Tool.Input.Started")({
+export namespace Tool {
+ export namespace Input {
+ export const Started = EventV2.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 class Delta extends Schema.Class<Delta>("Session.Event.Tool.Input.Delta")({
+ },
+ })
+ export type Started = Schema.Schema.Type<typeof Started>
+
+ export const Delta = EventV2.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 class Ended extends Schema.Class<Ended>("Session.Event.Tool.Input.Ended")({
+ },
+ })
+ export type Delta = Schema.Schema.Type<typeof Delta>
+
+ export const Ended = EventV2.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 class Called extends Schema.Class<Called>("Session.Event.Tool.Called")({
+ },
+ })
+ export type Ended = Schema.Schema.Type<typeof Ended>
+ }
+
+ export const Called = EventV2.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 +243,155 @@ 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<string, unknown>
- 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 class Success extends Schema.Class<Success>("Session.Event.Tool.Success")({
+ },
+ })
+ export type Called = Schema.Schema.Type<typeof Called>
+
+ export const Progress = EventV2.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<typeof Progress>
+
+ export const Success = EventV2.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 class Error extends Schema.Class<Error>("Session.Event.Tool.Error")({
+ },
+ })
+ export type Success = Schema.Schema.Type<typeof Success>
+
+ export const Error = EventV2.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<typeof Error>
+}
+
+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<typeof RetryError>
- export class Retried extends Schema.Class<Retried>("Session.Event.Retried")({
+export const Retried = EventV2.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<typeof Retried>
- export class Compacted extends Schema.Class<Compacted>("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 = EventV2.define({
+ type: "session.next.compaction.started",
+ aggregate: "sessionID",
+ schema: {
+ ...Base,
+ reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
+ },
+ })
+ export type Started = Schema.Schema.Type<typeof Started>
- 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 = EventV2.define({
+ type: "session.next.compaction.delta",
+ aggregate: "sessionID",
+ schema: {
+ ...Base,
+ text: Schema.String,
},
- ).pipe(Schema.toTaggedUnion("type"))
- export type Event = Schema.Schema.Type<typeof Event>
- export type Type = Event["type"]
+ })
+
+ export const Ended = EventV2.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<typeof Ended>
}
+
+export const All = Schema.Union(
+ [
+ AgentSwitched,
+ ModelSwitched,
+ Prompted,
+ Synthetic,
+ Shell.Started,
+ Shell.Ended,
+ 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<typeof All>
+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 000000000..844f6fe2d
--- /dev/null
+++ b/packages/opencode/src/v2/session-message-updater.ts
@@ -0,0 +1,411 @@
+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<Result> {
+ 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
+}
+
+export function memory(state: MemoryState): Adapter<MemoryState> {
+ 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() {
+ 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
+ },
+ 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
+ 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
+ },
+ 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)
+ },
+ finish() {
+ return state
+ },
+ }
+}
+
+export function update<Result>(adapter: Adapter<Result>, event: SessionEvent.Event): Result {
+ const currentAssistant = adapter.getCurrentAssistant()
+ type DraftAssistant = WritableDraft<SessionMessage.Assistant>
+ type DraftTool = WritableDraft<SessionMessage.AssistantTool>
+ type DraftText = WritableDraft<SessionMessage.AssistantText>
+ type DraftReasoning = WritableDraft<SessionMessage.AssistantReasoning>
+
+ 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.agent.switched": (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(
+ 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(
+ 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(
+ 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) {
+ adapter.updateAssistant(
+ produce(currentAssistant, (draft) => {
+ draft.time.completed = event.data.timestamp
+ }),
+ )
+ }
+ 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) {
+ 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(
+ 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()
+ 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 000000000..8ec99bc20
--- /dev/null
+++ b/packages/opencode/src/v2/session-message.ts
@@ -0,0 +1,178 @@
+import { Schema } from "effect"
+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<typeof ID>
+
+const Base = {
+ id: ID,
+ metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
+ time: Schema.Struct({
+ created: V2Schema.DateTimeUtcFromMillis,
+ }),
+}
+
+export class AgentSwitched extends Schema.Class<AgentSwitched>("Session.Message.AgentSwitched")({
+ ...Base,
+ type: Schema.Literal("agent-switched"),
+ agent: SessionEvent.AgentSwitched.fields.data.fields.agent,
+}) {}
+
+export class ModelSwitched extends Schema.Class<ModelSwitched>("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,
+ }),
+}) {}
+
+export class User extends Schema.Class<User>("Session.Message.User")({
+ ...Base,
+ text: Prompt.fields.text,
+ files: Prompt.fields.files,
+ agents: Prompt.fields.agents,
+ type: Schema.Literal("user"),
+ time: Schema.Struct({
+ created: V2Schema.DateTimeUtcFromMillis,
+ }),
+}) {}
+
+export class Synthetic extends Schema.Class<Synthetic>("Session.Message.Synthetic")({
+ ...Base,
+ sessionID: SessionEvent.Synthetic.fields.data.fields.sessionID,
+ text: SessionEvent.Synthetic.fields.data.fields.text,
+ type: Schema.Literal("synthetic"),
+}) {}
+
+export class Shell extends Schema.Class<Shell>("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: V2Schema.DateTimeUtcFromMillis,
+ completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional),
+ }),
+}) {}
+
+export class ToolStatePending extends Schema.Class<ToolStatePending>("Session.Message.ToolState.Pending")({
+ status: Schema.Literal("pending"),
+ input: Schema.String,
+}) {}
+
+export class ToolStateRunning extends Schema.Class<ToolStateRunning>("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<ToolStateCompleted>("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<ToolStateError>("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<typeof ToolState>
+
+export class AssistantTool extends Schema.Class<AssistantTool>("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: V2Schema.DateTimeUtcFromMillis,
+ ran: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional),
+ completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional),
+ pruned: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional),
+ }),
+}) {}
+
+export class AssistantText extends Schema.Class<AssistantText>("Session.Message.Assistant.Text")({
+ type: Schema.Literal("text"),
+ text: Schema.String,
+}) {}
+
+export class AssistantReasoning extends Schema.Class<AssistantReasoning>("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<typeof AssistantContent>
+
+export class Assistant extends Schema.Class<Assistant>("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.Finite.pipe(Schema.optional),
+ tokens: Schema.Struct({
+ input: Schema.Finite,
+ output: Schema.Finite,
+ reasoning: Schema.Finite,
+ cache: Schema.Struct({
+ read: Schema.Finite,
+ write: Schema.Finite,
+ }),
+ }).pipe(Schema.optional),
+ error: Schema.String.pipe(Schema.optional),
+ time: Schema.Struct({
+ created: V2Schema.DateTimeUtcFromMillis,
+ completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional),
+ }),
+}) {}
+
+export class Compaction extends Schema.Class<Compaction>("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,
+}) {}
+
+export const Message = Schema.Union([AgentSwitched, ModelSwitched, User, Synthetic, Shell, Assistant, Compaction])
+ .pipe(Schema.toTaggedUnion("type"))
+ .annotate({ identifier: "Session.Message" })
+
+export type Message = Schema.Schema.Type<typeof Message>
+
+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 000000000..86d8e52eb
--- /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<Source>("Prompt.Source")({
+ start: Schema.Finite,
+ end: Schema.Finite,
+ text: Schema.String,
+}) {}
+
+export class FileAttachment extends Schema.Class<FileAttachment>("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<AgentAttachment>("Prompt.AgentAttachment")({
+ name: Schema.String,
+ source: Source.pipe(Schema.optional),
+}) {}
+
+export class Prompt extends Schema.Class<Prompt>("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 2bac11f4f..1777b875a 100644
--- a/packages/opencode/src/v2/session.ts
+++ b/packages/opencode/src/v2/session.ts
@@ -1,69 +1,279 @@
-import { Context, Layer, Schema, Effect } from "effect"
-import { SessionEntry } from "./session-entry"
-import { Struct } from "effect"
-import { Session } from "@/session/session"
+import { SessionMessageTable, SessionTable } from "@/session/session.sql"
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, DateTime, Effect, Layer, Schema } from "effect"
+import { SessionMessage } from "./session-message"
+import type { Prompt } from "./session-prompt"
+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 ID = SessionID
+export const Delivery = Schema.Union([Schema.Literal("immediate"), Schema.Literal("deferred")]).annotate({
+ identifier: "Session.Delivery",
+})
+export type Delivery = Schema.Schema.Type<typeof Delivery>
-export type ID = Schema.Schema.Type<typeof ID>
-
-export class PromptInput extends Schema.Class<PromptInput>("Session.PromptInput")({
- ...Struct.omit(SessionEntry.User.fields, ["time", "type"]),
- id: Schema.optionalKey(SessionEntry.ID),
- sessionID: ID,
-}) {}
-
-export class CreateInput extends Schema.Class<CreateInput>("Session.CreateInput")({
- id: Schema.optionalKey(ID),
-}) {}
+export const DefaultDelivery = "immediate" satisfies Delivery
export class Info extends Schema.Class<Info>("Session.Info")({
- id: ID,
+ 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: Schema.String,
- providerID: Schema.String,
- modelID: Schema.String,
+ id: ModelID,
+ providerID: ProviderID,
+ variant: Schema.String.pipe(Schema.optional),
}).pipe(Schema.optional),
+ time: Schema.Struct({
+ created: V2Schema.DateTimeUtcFromMillis,
+ updated: V2Schema.DateTimeUtcFromMillis,
+ archived: V2Schema.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 {
- fromID: (id: ID) => Effect.Effect<Info>
- create: (input: CreateInput) => Effect.Effect<Info>
- prompt: (input: PromptInput) => Effect.Effect<SessionEntry.User>
+ 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<Info[], never>
+ readonly messages: (input: {
+ sessionID: SessionID
+ limit?: number
+ order?: "asc" | "desc"
+ cursor?: {
+ id: SessionMessage.ID
+ time: number
+ direction: "previous" | "next"
+ }
+ }) => Effect.Effect<SessionMessage.Message[], never>
+ readonly context: (sessionID: SessionID) => Effect.Effect<SessionMessage.Message[], never>
+ readonly prompt: (input: {
+ id?: EventV2.ID
+ sessionID: SessionID
+ prompt: Prompt
+ delivery?: Delivery
+ }) => Effect.Effect<SessionMessage.User, never>
+ readonly shell: (input: { id?: EventV2.ID; sessionID: SessionID; command: string }) => Effect.Effect<void, never>
+ readonly skill: (input: { id?: EventV2.ID; sessionID: SessionID; skill: string }) => Effect.Effect<void, never>
+ readonly switchAgent: (input: { sessionID: SessionID; agent: string }) => Effect.Effect<void, never>
+ readonly switchModel: (input: {
+ sessionID: SessionID
+ id: ModelID
+ providerID: ProviderID
+ variant?: string
+ }) => Effect.Effect<void, never>
+ readonly compact: (sessionID: SessionID) => Effect.Effect<void, never>
+ readonly wait: (sessionID: SessionID) => Effect.Effect<void, never>
}
-export class Service extends Context.Service<Service, Interface>()("Session.Service") {}
+export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Session") {}
-export const layer = Layer.effect(Service)(
+export const layer = Layer.effect(
+ Service,
Effect.gen(function* () {
- const session = yield* Session.Service
+ const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
- const create: Interface["create"] = Effect.fn("Session.create")(function* (_input) {
- throw new Error("Not implemented")
- })
+ const decode = (row: typeof SessionMessageTable.$inferSelect) =>
+ decodeMessage({ ...row.data, id: row.id, type: row.type })
- const prompt: Interface["prompt"] = Effect.fn("Session.prompt")(function* (_input) {
- throw new Error("Not implemented")
- })
+ 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 fromID: Interface["fromID"] = Effect.fn("Session.fromID")(function* (id) {
- const match = yield* session.get(id)
- return fromV1(match)
- })
+ 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),
+ )
- return Service.of({
- create,
- prompt,
- fromID,
- })
+ const rows = input.limit === undefined ? query.all() : query.limit(input.limit).all()
+ return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row))
+ }),
+ messages: Effect.fn("V2Session.messages")(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 boundary = input.cursor
+ ? order === "asc"
+ ? 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) => {
+ const query = db
+ .select()
+ .from(SessionMessageTable)
+ .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))
+ }),
+ 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,
+ timestamp: DateTime.makeUnsafe(Date.now()),
+ agent: input.agent,
+ })
+ }),
+ switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
+ EventV2.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)
}),
)
-function fromV1(input: Session.Info): Info {
- return new Info({
- id: ID.make(input.id),
- })
-}
+export const defaultLayer = layer
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 000000000..dee2bb11e
--- /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<TextContent>("Tool.TextContent")({
+ type: Schema.Literal("text"),
+ text: Schema.String,
+}) {}
+
+export class FileContent extends Schema.Class<FileContent>("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/acp/event-subscription.test.ts b/packages/opencode/test/acp/event-subscription.test.ts
index 9a92fc507..2722757ab 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 5b0fcad3c..78253361b 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/preload.ts b/packages/opencode/test/preload.ts
index 479da7f51..b408f7ef1 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_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
@@ -79,7 +80,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-bridge.test.ts b/packages/opencode/test/server/httpapi-bridge.test.ts
index 352fb2e2f..b7ffa0ca5 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 d7e48240a..940efed9c 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<string, unknown>
+ }
+}
+
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 70fe2d81b..d96347bed 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(() =>
+ WithInstance.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<(typeof SessionMessageTable.$inferInsert)["data"]>,
+ },
+ ])
+ .run(),
+ )
+ },
+ }),
+ )
+
+ expect(
+ (yield* requestJson<{ items: SessionMessage.Message[] }>(`/api/session/${parent.id}/message`, { headers })).items,
+ ).toMatchObject([{ type: "assistant" }])
}),
),
)
diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts
index df83adb8d..0d02d9918 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 533056940..a602c0c8d 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,47 @@ 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.find((message) => message.type === "user")).toMatchObject({ type: "user", text: "hello v2" })
+ expect(typeof row?.data.time.created).toBe("number")
+ 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 },
+ ),
+)
+
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 defce40c1..000000000
--- 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<A>(arb: FastCheck.Arbitrary<A>) {
- 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<SessionEntryStepper.MemoryState>((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<typeof adapterStore>): SessionEntryStepper.Adapter<typeof store> {
- 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/sync/index.test.ts b/packages/opencode/test/sync/index.test.ts
index 0afbb1831..234c5246e 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",
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 000000000..128177167
--- /dev/null
+++ b/packages/opencode/test/v2/session-message-updater.test.ts
@@ -0,0 +1,203 @@
+import { expect, test } from "bun:test"
+import * as DateTime from "effect/DateTime"
+import { SessionID } from "../../src/session/schema"
+import { EventV2 } from "../../src/v2/event"
+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), {
+ id: EventV2.ID.create(),
+ type: "session.next.step.started",
+ data: {
+ sessionID,
+ timestamp: DateTime.makeUnsafe(1),
+ agent: "build",
+ model: { id: "model", providerID: "provider" },
+ snapshot: "before",
+ },
+ } satisfies SessionEvent.Event)
+
+ SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
+ id: EventV2.ID.create(),
+ 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), {
+ id: EventV2.ID.create(),
+ type: "session.next.step.started",
+ data: {
+ sessionID,
+ timestamp: DateTime.makeUnsafe(1),
+ agent: "build",
+ model: { id: "model", providerID: "provider" },
+ },
+ } satisfies SessionEvent.Event)
+
+ SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
+ id: EventV2.ID.create(),
+ type: "session.next.text.started",
+ data: {
+ sessionID,
+ timestamp: DateTime.makeUnsafe(2),
+ },
+ } satisfies SessionEvent.Event)
+
+ SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
+ id: EventV2.ID.create(),
+ 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), {
+ id: EventV2.ID.create(),
+ type: "session.next.step.started",
+ data: {
+ sessionID,
+ timestamp: DateTime.makeUnsafe(1),
+ agent: "build",
+ model: { id: "model", providerID: "provider" },
+ },
+ } satisfies SessionEvent.Event)
+
+ SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
+ id: EventV2.ID.create(),
+ type: "session.next.tool.input.started",
+ data: {
+ sessionID,
+ timestamp: DateTime.makeUnsafe(2),
+ callID,
+ name: "bash",
+ },
+ } satisfies SessionEvent.Event)
+
+ SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
+ id: EventV2.ID.create(),
+ 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), {
+ id: EventV2.ID.create(),
+ 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 = EventV2.ID.create()
+
+ SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
+ id,
+ type: "session.next.compaction.started",
+ data: {
+ sessionID,
+ timestamp: DateTime.makeUnsafe(1),
+ reason: "auto",
+ },
+ } satisfies SessionEvent.Event)
+
+ SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
+ id: EventV2.ID.create(),
+ type: "session.next.compaction.delta",
+ data: {
+ sessionID,
+ timestamp: DateTime.makeUnsafe(2),
+ text: "hello ",
+ },
+ } satisfies SessionEvent.Event)
+
+ SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
+ id: EventV2.ID.create(),
+ type: "session.next.compaction.delta",
+ data: {
+ sessionID,
+ timestamp: DateTime.makeUnsafe(3),
+ text: "summary",
+ },
+ } satisfies SessionEvent.Event)
+
+ SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
+ id: EventV2.ID.create(),
+ 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/script/build.ts b/packages/sdk/js/script/build.ts
index e920cc0fd..c490a0be7 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 67261d749..74c584462 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,6 +254,169 @@ class HeyApiRegistry<T> {
}
}
+export class Auth extends HeyApiClient {
+ /**
+ * Remove auth credentials
+ *
+ * Remove authentication credentials
+ */
+ public remove<ThrowOnError extends boolean = false>(
+ parameters: {
+ providerID: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }])
+ return (options?.client ?? this.client).delete<AuthRemoveResponses, AuthRemoveErrors, ThrowOnError>({
+ url: "/auth/{providerID}",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * Set auth credentials
+ *
+ * Set authentication credentials
+ */
+ public set<ThrowOnError extends boolean = false>(
+ parameters: {
+ providerID: string
+ auth?: Auth3
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "providerID" },
+ { key: "auth", map: "body" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).put<AuthSetResponses, AuthSetErrors, ThrowOnError>({
+ url: "/auth/{providerID}",
+ ...options,
+ ...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
+ })
+ }
+}
+
+export class App extends HeyApiClient {
+ /**
+ * Write log
+ *
+ * Write a log entry to the server logs with specified level and metadata.
+ */
+ public log<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ service?: string
+ level?: "debug" | "info" | "error" | "warn"
+ message?: string
+ extra?: {
+ [key: string]: unknown
+ }
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ { in: "body", key: "service" },
+ { in: "body", key: "level" },
+ { in: "body", key: "message" },
+ { in: "body", key: "extra" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post<AppLogResponses, AppLogErrors, ThrowOnError>({
+ url: "/log",
+ ...options,
+ ...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
+ })
+ }
+
+ /**
+ * List agents
+ *
+ * Get a list of all available AI agents in the OpenCode system.
+ */
+ public agents<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).get<AppAgentsResponses, unknown, ThrowOnError>({
+ url: "/agent",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * List skills
+ *
+ * Get a list of all available skills in the OpenCode system.
+ */
+ public skills<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).get<AppSkillsResponses, unknown, ThrowOnError>({
+ url: "/skill",
+ ...options,
+ ...params,
+ })
+ }
+}
+
export class Config extends HeyApiClient {
/**
* Get global configuration
@@ -349,35 +522,48 @@ export class Global extends HeyApiClient {
}
}
-export class Auth extends HeyApiClient {
+export class Event extends HeyApiClient {
/**
- * Remove auth credentials
+ * Subscribe to events
*
- * Remove authentication credentials
+ * Get events
*/
- public remove<ThrowOnError extends boolean = false>(
- parameters: {
- providerID: string
+ public subscribe<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
- const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }])
- return (options?.client ?? this.client).delete<AuthRemoveResponses, AuthRemoveErrors, ThrowOnError>({
- url: "/auth/{providerID}",
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).sse.get<EventSubscribeResponses, unknown, ThrowOnError>({
+ url: "/event",
...options,
...params,
})
}
+}
+export class Config2 extends HeyApiClient {
/**
- * Set auth credentials
+ * Get configuration
*
- * Set authentication credentials
+ * Retrieve the current OpenCode configuration settings and preferences.
*/
- public set<ThrowOnError extends boolean = false>(
- parameters: {
- providerID: string
- auth?: Auth3
+ public get<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -386,14 +572,46 @@ export class Auth extends HeyApiClient {
[
{
args: [
- { in: "path", key: "providerID" },
- { key: "auth", map: "body" },
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).put<AuthSetResponses, AuthSetErrors, ThrowOnError>({
- url: "/auth/{providerID}",
+ return (options?.client ?? this.client).get<ConfigGetResponses, unknown, ThrowOnError>({
+ url: "/config",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * Update configuration
+ *
+ * Update OpenCode configuration settings and preferences.
+ */
+ public update<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ config?: Config3
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ { key: "config", map: "body" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).patch<ConfigUpdateResponses, ConfigUpdateErrors, ThrowOnError>({
+ url: "/config",
...options,
...params,
headers: {
@@ -403,24 +621,48 @@ export class Auth extends HeyApiClient {
},
})
}
+
+ /**
+ * List config providers
+ *
+ * Get a list of all configured AI providers and their default models.
+ */
+ public providers<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).get<ConfigProvidersResponses, unknown, ThrowOnError>({
+ url: "/config/providers",
+ ...options,
+ ...params,
+ })
+ }
}
-export class App extends HeyApiClient {
+export class Console extends HeyApiClient {
/**
- * Write log
+ * Get active Console provider metadata
*
- * Write a log entry to the server logs with specified level and metadata.
+ * Get the active Console org name and the set of provider IDs managed by that Console org.
*/
- public log<ThrowOnError extends boolean = false>(
+ public get<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- service?: string
- level?: "debug" | "info" | "error" | "warn"
- message?: string
- extra?: {
- [key: string]: unknown
- }
},
options?: Options<never, ThrowOnError>,
) {
@@ -431,16 +673,76 @@ export class App extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { in: "body", key: "service" },
- { in: "body", key: "level" },
- { in: "body", key: "message" },
- { in: "body", key: "extra" },
],
},
],
)
- return (options?.client ?? this.client).post<AppLogResponses, AppLogErrors, ThrowOnError>({
- url: "/log",
+ return (options?.client ?? this.client).get<ExperimentalConsoleGetResponses, unknown, ThrowOnError>({
+ url: "/experimental/console",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * List switchable Console orgs
+ *
+ * Get the available Console orgs across logged-in accounts, including the current active org.
+ */
+ public listOrgs<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).get<ExperimentalConsoleListOrgsResponses, unknown, ThrowOnError>({
+ url: "/experimental/console/orgs",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * Switch active Console org
+ *
+ * Persist a new active Console account/org selection for the current local OpenCode state.
+ */
+ public switchOrg<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ accountID?: string
+ orgID?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ { in: "body", key: "accountID" },
+ { in: "body", key: "orgID" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post<ExperimentalConsoleSwitchOrgResponses, unknown, ThrowOnError>({
+ url: "/experimental/console/switch",
...options,
...params,
headers: {
@@ -450,16 +752,24 @@ export class App extends HeyApiClient {
},
})
}
+}
+export class Session extends HeyApiClient {
/**
- * List agents
+ * List sessions
*
- * Get a list of all available AI agents in the OpenCode system.
+ * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.
*/
- public agents<ThrowOnError extends boolean = false>(
+ public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
+ roots?: boolean | "true" | "false"
+ start?: number
+ cursor?: number
+ search?: string
+ limit?: number
+ archived?: boolean | "true" | "false"
},
options?: Options<never, ThrowOnError>,
) {
@@ -470,23 +780,31 @@ export class App extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { in: "query", key: "roots" },
+ { in: "query", key: "start" },
+ { in: "query", key: "cursor" },
+ { in: "query", key: "search" },
+ { in: "query", key: "limit" },
+ { in: "query", key: "archived" },
],
},
],
)
- return (options?.client ?? this.client).get<AppAgentsResponses, unknown, ThrowOnError>({
- url: "/agent",
+ return (options?.client ?? this.client).get<ExperimentalSessionListResponses, unknown, ThrowOnError>({
+ url: "/experimental/session",
...options,
...params,
})
}
+}
+export class Resource extends HeyApiClient {
/**
- * List skills
+ * Get MCP resources
*
- * Get a list of all available skills in the OpenCode system.
+ * Get all available MCP resources from connected servers. Optionally filter by name.
*/
- public skills<ThrowOnError extends boolean = false>(
+ public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
@@ -504,8 +822,8 @@ export class App extends HeyApiClient {
},
],
)
- return (options?.client ?? this.client).get<AppSkillsResponses, unknown, ThrowOnError>({
- url: "/skill",
+ return (options?.client ?? this.client).get<ExperimentalResourceListResponses, unknown, ThrowOnError>({
+ url: "/experimental/resource",
...options,
...params,
})
@@ -737,16 +1055,40 @@ export class Workspace extends HeyApiClient {
}
}
-export class Console extends HeyApiClient {
+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 {
/**
- * Get active Console provider metadata
+ * List tools
*
- * Get the active Console org name and the set of provider IDs managed by that Console org.
+ * Get a list of available tools with their JSON schema parameters for a specific provider and model combination.
*/
- public get<ThrowOnError extends boolean = false>(
- parameters?: {
+ public list<ThrowOnError extends boolean = false>(
+ parameters: {
directory?: string
workspace?: string
+ provider: string
+ model: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -757,23 +1099,25 @@ export class Console extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { in: "query", key: "provider" },
+ { in: "query", key: "model" },
],
},
],
)
- return (options?.client ?? this.client).get<ExperimentalConsoleGetResponses, unknown, ThrowOnError>({
- url: "/experimental/console",
+ return (options?.client ?? this.client).get<ToolListResponses, ToolListErrors, ThrowOnError>({
+ url: "/experimental/tool",
...options,
...params,
})
}
/**
- * List switchable Console orgs
+ * List tool IDs
*
- * Get the available Console orgs across logged-in accounts, including the current active org.
+ * Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.
*/
- public listOrgs<ThrowOnError extends boolean = false>(
+ public ids<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
@@ -791,24 +1135,25 @@ export class Console extends HeyApiClient {
},
],
)
- return (options?.client ?? this.client).get<ExperimentalConsoleListOrgsResponses, unknown, ThrowOnError>({
- url: "/experimental/console/orgs",
+ return (options?.client ?? this.client).get<ToolIdsResponses, ToolIdsErrors, ThrowOnError>({
+ url: "/experimental/tool/ids",
...options,
...params,
})
}
+}
+export class Worktree extends HeyApiClient {
/**
- * Switch active Console org
+ * Remove worktree
*
- * Persist a new active Console account/org selection for the current local OpenCode state.
+ * Remove a git worktree and delete its branch.
*/
- public switchOrg<ThrowOnError extends boolean = false>(
+ public remove<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- accountID?: string
- orgID?: string
+ worktreeRemoveInput?: WorktreeRemoveInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -819,14 +1164,13 @@ export class Console extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { in: "body", key: "accountID" },
- { in: "body", key: "orgID" },
+ { key: "worktreeRemoveInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).post<ExperimentalConsoleSwitchOrgResponses, unknown, ThrowOnError>({
- url: "/experimental/console/switch",
+ return (options?.client ?? this.client).delete<WorktreeRemoveResponses, WorktreeRemoveErrors, ThrowOnError>({
+ url: "/experimental/worktree",
...options,
...params,
headers: {
@@ -836,24 +1180,16 @@ export class Console extends HeyApiClient {
},
})
}
-}
-export class Session extends HeyApiClient {
/**
- * List sessions
+ * List worktrees
*
- * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.
+ * List all sandbox worktrees for the current project.
*/
public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- roots?: boolean | "true" | "false"
- start?: number
- cursor?: number
- search?: string
- limit?: number
- archived?: boolean | "true" | "false"
},
options?: Options<never, ThrowOnError>,
) {
@@ -864,34 +1200,27 @@ export class Session extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { in: "query", key: "roots" },
- { in: "query", key: "start" },
- { in: "query", key: "cursor" },
- { in: "query", key: "search" },
- { in: "query", key: "limit" },
- { in: "query", key: "archived" },
],
},
],
)
- return (options?.client ?? this.client).get<ExperimentalSessionListResponses, unknown, ThrowOnError>({
- url: "/experimental/session",
+ return (options?.client ?? this.client).get<WorktreeListResponses, unknown, ThrowOnError>({
+ url: "/experimental/worktree",
...options,
...params,
})
}
-}
-export class Resource extends HeyApiClient {
/**
- * Get MCP resources
+ * Create worktree
*
- * Get all available MCP resources from connected servers. Optionally filter by name.
+ * Create a new git worktree for the current project and run any configured startup scripts.
*/
- public list<ThrowOnError extends boolean = false>(
+ public create<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
+ worktreeCreateInput?: WorktreeCreateInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -902,50 +1231,33 @@ export class Resource extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { key: "worktreeCreateInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).get<ExperimentalResourceListResponses, unknown, ThrowOnError>({
- url: "/experimental/resource",
+ return (options?.client ?? this.client).post<WorktreeCreateResponses, WorktreeCreateErrors, ThrowOnError>({
+ url: "/experimental/worktree",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
-}
-
-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 {
/**
- * List all projects
+ * Reset worktree
*
- * Get a list of projects that have been opened with OpenCode.
+ * Reset a worktree branch to the primary default branch.
*/
- public list<ThrowOnError extends boolean = false>(
+ public reset<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
+ worktreeResetInput?: WorktreeResetInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -956,26 +1268,35 @@ export class Project extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { key: "worktreeResetInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).get<ProjectListResponses, unknown, ThrowOnError>({
- url: "/project",
+ return (options?.client ?? this.client).post<WorktreeResetResponses, WorktreeResetErrors, ThrowOnError>({
+ url: "/experimental/worktree/reset",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
+}
+export class Find extends HeyApiClient {
/**
- * Get current project
+ * Find text
*
- * Retrieve the currently active project that OpenCode is working with.
+ * Search for text patterns across files in the project using ripgrep.
*/
- public current<ThrowOnError extends boolean = false>(
- parameters?: {
+ public text<ThrowOnError extends boolean = false>(
+ parameters: {
directory?: string
workspace?: string
+ pattern: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -986,26 +1307,31 @@ export class Project extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { in: "query", key: "pattern" },
],
},
],
)
- return (options?.client ?? this.client).get<ProjectCurrentResponses, unknown, ThrowOnError>({
- url: "/project/current",
+ return (options?.client ?? this.client).get<FindTextResponses, unknown, ThrowOnError>({
+ url: "/find",
...options,
...params,
})
}
/**
- * Initialize git repository
+ * Find files
*
- * Create a git repository for the current project and return the refreshed project info.
+ * Search for files or directories by name or pattern in the project directory.
*/
- public initGit<ThrowOnError extends boolean = false>(
- parameters?: {
+ public files<ThrowOnError extends boolean = false>(
+ parameters: {
directory?: string
workspace?: string
+ query: string
+ dirs?: "true" | "false"
+ type?: "file" | "directory"
+ limit?: number
},
options?: Options<never, ThrowOnError>,
) {
@@ -1016,39 +1342,31 @@ export class Project 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).post<ProjectInitGitResponses, unknown, ThrowOnError>({
- url: "/project/git/init",
+ return (options?.client ?? this.client).get<FindFilesResponses, unknown, ThrowOnError>({
+ url: "/find/file",
...options,
...params,
})
}
/**
- * Update project
+ * Find symbols
*
- * Update project properties such as name, icon, and commands.
+ * Search for workspace symbols like functions, classes, and variables using LSP.
*/
- public update<ThrowOnError extends boolean = false>(
+ public symbols<ThrowOnError extends boolean = false>(
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
- }
+ query: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -1057,39 +1375,32 @@ 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" },
+ { in: "query", key: "query" },
],
},
],
)
- return (options?.client ?? this.client).patch<ProjectUpdateResponses, ProjectUpdateErrors, ThrowOnError>({
- url: "/project/{projectID}",
+ return (options?.client ?? this.client).get<FindSymbolsResponses, unknown, ThrowOnError>({
+ url: "/find/symbol",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
}
-export class Pty extends HeyApiClient {
+export class File extends HeyApiClient {
/**
- * List available shells
+ * List files
*
- * Get a list of available shells on the system.
+ * List files and directories in a specified path.
*/
- public shells<ThrowOnError extends boolean = false>(
- parameters?: {
+ public list<ThrowOnError extends boolean = false>(
+ parameters: {
directory?: string
workspace?: string
+ path: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -1100,26 +1411,28 @@ export class Pty extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { in: "query", key: "path" },
],
},
],
)
- return (options?.client ?? this.client).get<PtyShellsResponses, unknown, ThrowOnError>({
- url: "/pty/shells",
+ return (options?.client ?? this.client).get<FileListResponses, unknown, ThrowOnError>({
+ url: "/file",
...options,
...params,
})
}
/**
- * List PTY sessions
+ * Read file
*
- * Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.
+ * Read the content of a specified file.
*/
- public list<ThrowOnError extends boolean = false>(
- parameters?: {
+ public read<ThrowOnError extends boolean = false>(
+ parameters: {
directory?: string
workspace?: string
+ path: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -1130,33 +1443,27 @@ export class Pty extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { in: "query", key: "path" },
],
},
],
)
- return (options?.client ?? this.client).get<PtyListResponses, unknown, ThrowOnError>({
- url: "/pty",
+ return (options?.client ?? this.client).get<FileReadResponses, unknown, ThrowOnError>({
+ url: "/file/content",
...options,
...params,
})
}
/**
- * Create PTY session
+ * Get file status
*
- * Create a new pseudo-terminal (PTY) session for running shell commands and processes.
+ * Get the git status of all files in the project.
*/
- public create<ThrowOnError extends boolean = false>(
+ public status<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- command?: string
- args?: Array<string>
- cwd?: string
- title?: string
- env?: {
- [key: string]: string
- }
},
options?: Options<never, ThrowOnError>,
) {
@@ -1167,35 +1474,26 @@ export class Pty extends HeyApiClient {
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<PtyCreateResponses, PtyCreateErrors, ThrowOnError>({
- url: "/pty",
+ return (options?.client ?? this.client).get<FileStatusResponses, unknown, ThrowOnError>({
+ url: "/file/status",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
+}
+export class Instance extends HeyApiClient {
/**
- * Remove PTY session
+ * Dispose instance
*
- * Remove and terminate a specific pseudo-terminal (PTY) session.
+ * Clean up and dispose the current OpenCode instance, releasing all resources.
*/
- public remove<ThrowOnError extends boolean = false>(
- parameters: {
- ptyID: string
+ public dispose<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
},
@@ -1206,28 +1504,28 @@ export class Pty extends HeyApiClient {
[
{
args: [
- { in: "path", key: "ptyID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).delete<PtyRemoveResponses, PtyRemoveErrors, ThrowOnError>({
- url: "/pty/{ptyID}",
+ return (options?.client ?? this.client).post<InstanceDisposeResponses, unknown, ThrowOnError>({
+ url: "/instance/dispose",
...options,
...params,
})
}
+}
+export class Path extends HeyApiClient {
/**
- * Get PTY session
+ * Get paths
*
- * Retrieve detailed information about a specific pseudo-terminal (PTY) session.
+ * Retrieve the current working directory and related path information for the OpenCode instance.
*/
public get<ThrowOnError extends boolean = false>(
- parameters: {
- ptyID: string
+ parameters?: {
directory?: string
workspace?: string
},
@@ -1238,35 +1536,30 @@ export class Pty extends HeyApiClient {
[
{
args: [
- { in: "path", key: "ptyID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).get<PtyGetResponses, PtyGetErrors, ThrowOnError>({
- url: "/pty/{ptyID}",
+ return (options?.client ?? this.client).get<PathGetResponses, unknown, ThrowOnError>({
+ url: "/path",
...options,
...params,
})
}
+}
+export class Vcs extends HeyApiClient {
/**
- * Update PTY session
+ * Get VCS info
*
- * Update properties of an existing pseudo-terminal (PTY) session.
+ * Retrieve version control system (VCS) information for the current project, such as git branch.
*/
- public update<ThrowOnError extends boolean = false>(
- parameters: {
- ptyID: string
+ public get<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
- title?: string
- size?: {
- rows: number
- cols: number
- }
},
options?: Options<never, ThrowOnError>,
) {
@@ -1275,37 +1568,29 @@ 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" },
],
},
],
)
- return (options?.client ?? this.client).put<PtyUpdateResponses, PtyUpdateErrors, ThrowOnError>({
- url: "/pty/{ptyID}",
+ return (options?.client ?? this.client).get<VcsGetResponses, unknown, ThrowOnError>({
+ url: "/vcs",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
/**
- * Connect to PTY session
+ * Get VCS diff
*
- * Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.
+ * Retrieve the current git diff for the working tree or against the default branch.
*/
- public connect<ThrowOnError extends boolean = false>(
+ public diff<ThrowOnError extends boolean = false>(
parameters: {
- ptyID: string
directory?: string
workspace?: string
+ mode: "git" | "branch"
},
options?: Options<never, ThrowOnError>,
) {
@@ -1314,28 +1599,28 @@ export class Pty extends HeyApiClient {
[
{
args: [
- { in: "path", key: "ptyID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { in: "query", key: "mode" },
],
},
],
)
- return (options?.client ?? this.client).get<PtyConnectResponses, PtyConnectErrors, ThrowOnError>({
- url: "/pty/{ptyID}/connect",
+ return (options?.client ?? this.client).get<VcsDiffResponses, unknown, ThrowOnError>({
+ url: "/vcs/diff",
...options,
...params,
})
}
}
-export class Config2 extends HeyApiClient {
+export class Command extends HeyApiClient {
/**
- * Get configuration
+ * List commands
*
- * Retrieve the current OpenCode configuration settings and preferences.
+ * Get a list of all available commands in the OpenCode system.
*/
- public get<ThrowOnError extends boolean = false>(
+ public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
@@ -1353,23 +1638,24 @@ export class Config2 extends HeyApiClient {
},
],
)
- return (options?.client ?? this.client).get<ConfigGetResponses, unknown, ThrowOnError>({
- url: "/config",
+ return (options?.client ?? this.client).get<CommandListResponses, unknown, ThrowOnError>({
+ url: "/command",
...options,
...params,
})
}
+}
+export class Lsp extends HeyApiClient {
/**
- * Update configuration
+ * Get LSP status
*
- * Update OpenCode configuration settings and preferences.
+ * Get LSP server status
*/
- public update<ThrowOnError extends boolean = false>(
+ public status<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- config?: Config3
},
options?: Options<never, ThrowOnError>,
) {
@@ -1380,29 +1666,25 @@ export class Config2 extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { key: "config", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).patch<ConfigUpdateResponses, ConfigUpdateErrors, ThrowOnError>({
- url: "/config",
+ return (options?.client ?? this.client).get<LspStatusResponses, unknown, ThrowOnError>({
+ url: "/lsp",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
+}
+export class Formatter extends HeyApiClient {
/**
- * List config providers
+ * Get formatter status
*
- * Get a list of all configured AI providers and their default models.
+ * Get formatter status
*/
- public providers<ThrowOnError extends boolean = false>(
+ public status<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
@@ -1420,22 +1702,23 @@ export class Config2 extends HeyApiClient {
},
],
)
- return (options?.client ?? this.client).get<ConfigProvidersResponses, unknown, ThrowOnError>({
- url: "/config/providers",
+ return (options?.client ?? this.client).get<FormatterStatusResponses, unknown, ThrowOnError>({
+ url: "/formatter",
...options,
...params,
})
}
}
-export class Tool extends HeyApiClient {
+export class Auth2 extends HeyApiClient {
/**
- * List tool IDs
+ * Remove MCP OAuth
*
- * Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.
+ * Remove OAuth credentials for an MCP server.
*/
- public ids<ThrowOnError extends boolean = false>(
- parameters?: {
+ public remove<ThrowOnError extends boolean = false>(
+ parameters: {
+ name: string
directory?: string
workspace?: string
},
@@ -1446,30 +1729,30 @@ export class Tool extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "name" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).get<ToolIdsResponses, ToolIdsErrors, ThrowOnError>({
- url: "/experimental/tool/ids",
+ return (options?.client ?? this.client).delete<McpAuthRemoveResponses, McpAuthRemoveErrors, ThrowOnError>({
+ url: "/mcp/{name}/auth",
...options,
...params,
})
}
/**
- * List tools
+ * Start MCP OAuth
*
- * Get a list of available tools with their JSON schema parameters for a specific provider and model combination.
+ * Start OAuth authentication flow for a Model Context Protocol (MCP) server.
*/
- public list<ThrowOnError extends boolean = false>(
+ public start<ThrowOnError extends boolean = false>(
parameters: {
+ name: string
directory?: string
workspace?: string
- provider: string
- model: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -1478,33 +1761,31 @@ export class Tool extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "name" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { in: "query", key: "provider" },
- { in: "query", key: "model" },
],
},
],
)
- return (options?.client ?? this.client).get<ToolListResponses, ToolListErrors, ThrowOnError>({
- url: "/experimental/tool",
+ return (options?.client ?? this.client).post<McpAuthStartResponses, McpAuthStartErrors, ThrowOnError>({
+ url: "/mcp/{name}/auth",
...options,
...params,
})
}
-}
-export class Worktree extends HeyApiClient {
/**
- * Remove worktree
+ * Complete MCP OAuth
*
- * Remove a git worktree and delete its branch.
+ * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.
*/
- public remove<ThrowOnError extends boolean = false>(
- parameters?: {
+ public callback<ThrowOnError extends boolean = false>(
+ parameters: {
+ name: string
directory?: string
workspace?: string
- worktreeRemoveInput?: WorktreeRemoveInput
+ code?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -1513,15 +1794,16 @@ export class Worktree extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "name" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { key: "worktreeRemoveInput", map: "body" },
+ { in: "body", key: "code" },
],
},
],
)
- return (options?.client ?? this.client).delete<WorktreeRemoveResponses, WorktreeRemoveErrors, ThrowOnError>({
- url: "/experimental/worktree",
+ return (options?.client ?? this.client).post<McpAuthCallbackResponses, McpAuthCallbackErrors, ThrowOnError>({
+ url: "/mcp/{name}/auth/callback",
...options,
...params,
headers: {
@@ -1533,11 +1815,47 @@ export class Worktree extends HeyApiClient {
}
/**
- * List worktrees
+ * Authenticate MCP OAuth
*
- * List all sandbox worktrees for the current project.
+ * Start OAuth flow and wait for callback (opens browser).
*/
- public list<ThrowOnError extends boolean = false>(
+ public authenticate<ThrowOnError extends boolean = false>(
+ parameters: {
+ name: string
+ directory?: string
+ workspace?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "name" },
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post<McpAuthAuthenticateResponses, McpAuthAuthenticateErrors, ThrowOnError>(
+ {
+ url: "/mcp/{name}/auth/authenticate",
+ ...options,
+ ...params,
+ },
+ )
+ }
+}
+
+export class Mcp extends HeyApiClient {
+ /**
+ * Get MCP status
+ *
+ * Get the status of all Model Context Protocol (MCP) servers.
+ */
+ public status<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
@@ -1555,23 +1873,24 @@ export class Worktree extends HeyApiClient {
},
],
)
- return (options?.client ?? this.client).get<WorktreeListResponses, unknown, ThrowOnError>({
- url: "/experimental/worktree",
+ return (options?.client ?? this.client).get<McpStatusResponses, unknown, ThrowOnError>({
+ url: "/mcp",
...options,
...params,
})
}
/**
- * Create worktree
+ * Add MCP server
*
- * Create a new git worktree for the current project and run any configured startup scripts.
+ * Dynamically add a new Model Context Protocol (MCP) server to the system.
*/
- public create<ThrowOnError extends boolean = false>(
+ public add<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- worktreeCreateInput?: WorktreeCreateInput
+ name?: string
+ config?: McpLocalConfig | McpRemoteConfig
},
options?: Options<never, ThrowOnError>,
) {
@@ -1582,13 +1901,14 @@ export class Worktree extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { key: "worktreeCreateInput", map: "body" },
+ { in: "body", key: "name" },
+ { in: "body", key: "config" },
],
},
],
)
- return (options?.client ?? this.client).post<WorktreeCreateResponses, WorktreeCreateErrors, ThrowOnError>({
- url: "/experimental/worktree",
+ return (options?.client ?? this.client).post<McpAddResponses, McpAddErrors, ThrowOnError>({
+ url: "/mcp",
...options,
...params,
headers: {
@@ -1600,15 +1920,13 @@ export class Worktree extends HeyApiClient {
}
/**
- * Reset worktree
- *
- * Reset a worktree branch to the primary default branch.
+ * Connect an MCP server.
*/
- public reset<ThrowOnError extends boolean = false>(
- parameters?: {
+ public connect<ThrowOnError extends boolean = false>(
+ parameters: {
+ name: string
directory?: string
workspace?: string
- worktreeResetInput?: WorktreeResetInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -1617,42 +1935,66 @@ export class Worktree extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "name" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { key: "worktreeResetInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).post<WorktreeResetResponses, WorktreeResetErrors, ThrowOnError>({
- url: "/experimental/worktree/reset",
+ return (options?.client ?? this.client).post<McpConnectResponses, unknown, ThrowOnError>({
+ url: "/mcp/{name}/connect",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * Disconnect an MCP server.
+ */
+ public disconnect<ThrowOnError extends boolean = false>(
+ parameters: {
+ name: string
+ directory?: string
+ workspace?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "name" },
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post<McpDisconnectResponses, unknown, ThrowOnError>({
+ url: "/mcp/{name}/disconnect",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
+
+ private _auth?: Auth2
+ get auth(): Auth2 {
+ return (this._auth ??= new Auth2({ client: this.client }))
+ }
}
-export class Session2 extends HeyApiClient {
+export class Project extends HeyApiClient {
/**
- * List sessions
+ * List all projects
*
- * Get a list of all OpenCode sessions, sorted by most recently updated.
+ * Get a list of projects that have been opened with OpenCode.
*/
public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- scope?: "project"
- path?: string
- roots?: boolean | "true" | "false"
- start?: number
- search?: string
- limit?: number
},
options?: Options<never, ThrowOnError>,
) {
@@ -1663,36 +2005,26 @@ 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<SessionListResponses, unknown, ThrowOnError>({
- url: "/session",
+ return (options?.client ?? this.client).get<ProjectListResponses, unknown, ThrowOnError>({
+ url: "/project",
...options,
...params,
})
}
/**
- * Create session
+ * Get current project
*
- * Create a new OpenCode session for interacting with AI assistants and managing conversations.
+ * Retrieve the currently active project that OpenCode is working with.
*/
- public create<ThrowOnError extends boolean = false>(
+ public current<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- parentID?: string
- title?: string
- permission?: PermissionRuleset
- workspaceID?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -1703,32 +2035,23 @@ 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: "permission" },
- { in: "body", key: "workspaceID" },
],
},
],
)
- return (options?.client ?? this.client).post<SessionCreateResponses, SessionCreateErrors, ThrowOnError>({
- url: "/session",
+ return (options?.client ?? this.client).get<ProjectCurrentResponses, unknown, ThrowOnError>({
+ url: "/project/current",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
/**
- * Get session status
+ * Initialize git repository
*
- * Retrieve the current status of all sessions, including active, idle, and completed states.
+ * Create a git repository for the current project and return the refreshed project info.
*/
- public status<ThrowOnError extends boolean = false>(
+ public initGit<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
@@ -1746,23 +2069,35 @@ export class Session2 extends HeyApiClient {
},
],
)
- return (options?.client ?? this.client).get<SessionStatusResponses, SessionStatusErrors, ThrowOnError>({
- url: "/session/status",
+ return (options?.client ?? this.client).post<ProjectInitGitResponses, unknown, ThrowOnError>({
+ url: "/project/git/init",
...options,
...params,
})
}
/**
- * Delete session
+ * Update project
*
- * Delete a session and permanently remove all associated data, including messages and history.
+ * Update project properties such as name, icon, and commands.
*/
- public delete<ThrowOnError extends boolean = false>(
+ public update<ThrowOnError extends boolean = false>(
parameters: {
- sessionID: string
+ 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<never, ThrowOnError>,
) {
@@ -1771,28 +2106,37 @@ export class Session2 extends HeyApiClient {
[
{
args: [
- { in: "path", key: "sessionID" },
+ { 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).delete<SessionDeleteResponses, SessionDeleteErrors, ThrowOnError>({
- url: "/session/{sessionID}",
+ return (options?.client ?? this.client).patch<ProjectUpdateResponses, ProjectUpdateErrors, ThrowOnError>({
+ url: "/project/{projectID}",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
+}
+export class Pty extends HeyApiClient {
/**
- * Get session
+ * List available shells
*
- * Retrieve detailed information about a specific OpenCode session.
+ * Get a list of available shells on the system.
*/
- public get<ThrowOnError extends boolean = false>(
- parameters: {
- sessionID: string
+ public shells<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
},
@@ -1803,34 +2147,64 @@ export class Session2 extends HeyApiClient {
[
{
args: [
- { in: "path", key: "sessionID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).get<SessionGetResponses, SessionGetErrors, ThrowOnError>({
- url: "/session/{sessionID}",
+ return (options?.client ?? this.client).get<PtyShellsResponses, unknown, ThrowOnError>({
+ url: "/pty/shells",
...options,
...params,
})
}
/**
- * Update session
+ * List PTY sessions
*
- * Update properties of an existing session, such as title or other metadata.
+ * Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.
*/
- public update<ThrowOnError extends boolean = false>(
- parameters: {
- sessionID: string
+ public list<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).get<PtyListResponses, unknown, ThrowOnError>({
+ url: "/pty",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * Create PTY session
+ *
+ * Create a new pseudo-terminal (PTY) session for running shell commands and processes.
+ */
+ public create<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ command?: string
+ args?: Array<string>
+ cwd?: string
title?: string
- permission?: PermissionRuleset
- time?: {
- archived?: number
+ env?: {
+ [key: string]: string
}
},
options?: Options<never, ThrowOnError>,
@@ -1840,18 +2214,19 @@ export class Session2 extends HeyApiClient {
[
{
args: [
- { in: "path", key: "sessionID" },
{ 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: "permission" },
- { in: "body", key: "time" },
+ { in: "body", key: "env" },
],
},
],
)
- return (options?.client ?? this.client).patch<SessionUpdateResponses, SessionUpdateErrors, ThrowOnError>({
- url: "/session/{sessionID}",
+ return (options?.client ?? this.client).post<PtyCreateResponses, PtyCreateErrors, ThrowOnError>({
+ url: "/pty",
...options,
...params,
headers: {
@@ -1863,13 +2238,13 @@ export class Session2 extends HeyApiClient {
}
/**
- * Get session children
+ * Remove PTY session
*
- * Retrieve all child sessions that were forked from the specified parent session.
+ * Remove and terminate a specific pseudo-terminal (PTY) session.
*/
- public children<ThrowOnError extends boolean = false>(
+ public remove<ThrowOnError extends boolean = false>(
parameters: {
- sessionID: string
+ ptyID: string
directory?: string
workspace?: string
},
@@ -1880,28 +2255,28 @@ 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).get<SessionChildrenResponses, SessionChildrenErrors, ThrowOnError>({
- url: "/session/{sessionID}/children",
+ return (options?.client ?? this.client).delete<PtyRemoveResponses, PtyRemoveErrors, ThrowOnError>({
+ url: "/pty/{ptyID}",
...options,
...params,
})
}
/**
- * Get session todos
+ * Get PTY session
*
- * Retrieve the todo list associated with a specific session, showing tasks and action items.
+ * Retrieve detailed information about a specific pseudo-terminal (PTY) session.
*/
- public todo<ThrowOnError extends boolean = false>(
+ public get<ThrowOnError extends boolean = false>(
parameters: {
- sessionID: string
+ ptyID: string
directory?: string
workspace?: string
},
@@ -1912,33 +2287,35 @@ 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).get<SessionTodoResponses, SessionTodoErrors, ThrowOnError>({
- url: "/session/{sessionID}/todo",
+ return (options?.client ?? this.client).get<PtyGetResponses, PtyGetErrors, ThrowOnError>({
+ url: "/pty/{ptyID}",
...options,
...params,
})
}
/**
- * Initialize session
+ * Update PTY session
*
- * Analyze the current application and create an AGENTS.md file with project-specific agent configurations.
+ * Update properties of an existing pseudo-terminal (PTY) session.
*/
- public init<ThrowOnError extends boolean = false>(
+ public update<ThrowOnError extends boolean = false>(
parameters: {
- sessionID: string
+ ptyID: string
directory?: string
workspace?: string
- modelID?: string
- providerID?: string
- messageID?: string
+ title?: string
+ size?: {
+ rows: number
+ cols: number
+ }
},
options?: Options<never, ThrowOnError>,
) {
@@ -1947,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: "modelID" },
- { in: "body", key: "providerID" },
- { in: "body", key: "messageID" },
+ { in: "body", key: "title" },
+ { in: "body", key: "size" },
],
},
],
)
- return (options?.client ?? this.client).post<SessionInitResponses, SessionInitErrors, ThrowOnError>({
- url: "/session/{sessionID}/init",
+ return (options?.client ?? this.client).put<PtyUpdateResponses, PtyUpdateErrors, ThrowOnError>({
+ url: "/pty/{ptyID}",
...options,
...params,
headers: {
@@ -1970,16 +2346,15 @@ export class Session2 extends HeyApiClient {
}
/**
- * Fork session
+ * Connect to PTY session
*
- * Create a new session by forking an existing session at a specific message point.
+ * Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.
*/
- public fork<ThrowOnError extends boolean = false>(
+ public connect<ThrowOnError extends boolean = false>(
parameters: {
- sessionID: string
+ ptyID: string
directory?: string
workspace?: string
- messageID?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -1988,34 +2363,29 @@ 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: "messageID" },
],
},
],
)
- return (options?.client ?? this.client).post<SessionForkResponses, unknown, ThrowOnError>({
- url: "/session/{sessionID}/fork",
+ return (options?.client ?? this.client).get<PtyConnectResponses, PtyConnectErrors, ThrowOnError>({
+ url: "/pty/{ptyID}/connect",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
+}
+export class Question extends HeyApiClient {
/**
- * Abort session
+ * List pending questions
*
- * Abort an active session and stop any ongoing AI processing or command execution.
+ * Get all pending question requests across all sessions.
*/
- public abort<ThrowOnError extends boolean = false>(
- parameters: {
- sessionID: string
+ public list<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
},
@@ -2026,30 +2396,30 @@ export class Session2 extends HeyApiClient {
[
{
args: [
- { in: "path", key: "sessionID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).post<SessionAbortResponses, SessionAbortErrors, ThrowOnError>({
- url: "/session/{sessionID}/abort",
+ return (options?.client ?? this.client).get<QuestionListResponses, unknown, ThrowOnError>({
+ url: "/question",
...options,
...params,
})
}
/**
- * Unshare session
+ * Reply to question request
*
- * Remove the shareable link for a session, making it private again.
+ * Provide answers to a question request from the AI assistant.
*/
- public unshare<ThrowOnError extends boolean = false>(
+ public reply<ThrowOnError extends boolean = false>(
parameters: {
- sessionID: string
+ requestID: string
directory?: string
workspace?: string
+ answers?: Array<QuestionAnswer>
},
options?: Options<never, ThrowOnError>,
) {
@@ -2058,28 +2428,34 @@ 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: "answers" },
],
},
],
)
- return (options?.client ?? this.client).delete<SessionUnshareResponses, SessionUnshareErrors, ThrowOnError>({
- url: "/session/{sessionID}/share",
+ return (options?.client ?? this.client).post<QuestionReplyResponses, QuestionReplyErrors, ThrowOnError>({
+ url: "/question/{requestID}/reply",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
/**
- * Share session
+ * Reject question request
*
- * Create a shareable link for a session, allowing others to view the conversation.
+ * Reject a question request from the AI assistant.
*/
- public share<ThrowOnError extends boolean = false>(
+ public reject<ThrowOnError extends boolean = false>(
parameters: {
- sessionID: string
+ requestID: string
directory?: string
workspace?: string
},
@@ -2090,31 +2466,31 @@ export class Session2 extends HeyApiClient {
[
{
args: [
- { in: "path", key: "sessionID" },
+ { in: "path", key: "requestID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).post<SessionShareResponses, SessionShareErrors, ThrowOnError>({
- url: "/session/{sessionID}/share",
+ return (options?.client ?? this.client).post<QuestionRejectResponses, QuestionRejectErrors, ThrowOnError>({
+ url: "/question/{requestID}/reject",
...options,
...params,
})
}
+}
+export class Permission extends HeyApiClient {
/**
- * Get message diff
+ * List pending permissions
*
- * Get the file changes (diff) that resulted from a specific user message in the session.
+ * Get all pending permission requests across all sessions.
*/
- public diff<ThrowOnError extends boolean = false>(
- parameters: {
- sessionID: string
+ public list<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
- messageID?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -2123,34 +2499,31 @@ export class Session2 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<SessionDiffResponses, unknown, ThrowOnError>({
- url: "/session/{sessionID}/diff",
+ return (options?.client ?? this.client).get<PermissionListResponses, unknown, ThrowOnError>({
+ url: "/permission",
...options,
...params,
})
}
/**
- * Summarize session
+ * Respond to permission request
*
- * Generate a concise summary of the session using AI compaction to preserve key information.
+ * Approve or deny a permission request from the AI assistant.
*/
- public summarize<ThrowOnError extends boolean = false>(
+ public reply<ThrowOnError extends boolean = false>(
parameters: {
- sessionID: string
+ requestID: string
directory?: string
workspace?: string
- providerID?: string
- modelID?: string
- auto?: boolean
+ reply?: "once" | "always" | "reject"
+ message?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -2159,18 +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: "providerID" },
- { in: "body", key: "modelID" },
- { in: "body", key: "auto" },
+ { in: "body", key: "reply" },
+ { in: "body", key: "message" },
],
},
],
)
- return (options?.client ?? this.client).post<SessionSummarizeResponses, SessionSummarizeErrors, ThrowOnError>({
- url: "/session/{sessionID}/summarize",
+ return (options?.client ?? this.client).post<PermissionReplyResponses, PermissionReplyErrors, ThrowOnError>({
+ url: "/permission/{requestID}/reply",
...options,
...params,
headers: {
@@ -2182,17 +2554,19 @@ export class Session2 extends HeyApiClient {
}
/**
- * Get session messages
+ * Respond to permission
*
- * Retrieve all messages in a session, including user prompts and AI responses.
+ * Approve or deny a permission request from the AI assistant.
+ *
+ * @deprecated
*/
- public messages<ThrowOnError extends boolean = false>(
+ public respond<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
+ permissionID: string
directory?: string
workspace?: string
- limit?: number
- before?: string
+ response?: "once" | "always" | "reject"
},
options?: Options<never, ThrowOnError>,
) {
@@ -2202,45 +2576,42 @@ export class Session2 extends HeyApiClient {
{
args: [
{ in: "path", key: "sessionID" },
+ { in: "path", key: "permissionID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { in: "query", key: "limit" },
- { in: "query", key: "before" },
+ { in: "body", key: "response" },
],
},
],
)
- return (options?.client ?? this.client).get<SessionMessagesResponses, SessionMessagesErrors, ThrowOnError>({
- url: "/session/{sessionID}/message",
+ return (options?.client ?? this.client).post<PermissionRespondResponses, PermissionRespondErrors, ThrowOnError>({
+ url: "/session/{sessionID}/permissions/{permissionID}",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
+}
+export class Oauth extends HeyApiClient {
/**
- * Send message
+ * Start OAuth authorization
*
- * Create and send a new message to a session, streaming the AI response.
+ * Start the OAuth authorization flow for a provider.
*/
- public prompt<ThrowOnError extends boolean = false>(
+ public authorize<ThrowOnError extends boolean = false>(
parameters: {
- sessionID: string
+ providerID: string
directory?: string
workspace?: string
- messageID?: string
- model?: {
- providerID: string
- modelID: string
- }
- agent?: string
- noReply?: boolean
- tools?: {
- [key: string]: boolean
+ method?: number
+ inputs?: {
+ [key: string]: string
}
- format?: OutputFormat
- system?: string
- variant?: string
- parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
},
options?: Options<never, ThrowOnError>,
) {
@@ -2249,24 +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: "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" },
+ { in: "body", key: "method" },
+ { in: "body", key: "inputs" },
],
},
],
)
- return (options?.client ?? this.client).post<SessionPromptResponses, SessionPromptErrors, ThrowOnError>({
- url: "/session/{sessionID}/message",
+ return (options?.client ?? this.client).post<
+ ProviderOauthAuthorizeResponses,
+ ProviderOauthAuthorizeErrors,
+ ThrowOnError
+ >({
+ url: "/provider/{providerID}/oauth/authorize",
...options,
...params,
headers: {
@@ -2278,16 +2646,17 @@ export class Session2 extends HeyApiClient {
}
/**
- * Delete message
+ * Handle OAuth callback
*
- * 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.
+ * Handle the OAuth callback from a provider after user authorization.
*/
- public deleteMessage<ThrowOnError extends boolean = false>(
+ public callback<ThrowOnError extends boolean = false>(
parameters: {
- sessionID: string
- messageID: string
+ providerID: string
directory?: string
workspace?: string
+ method?: number
+ code?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -2296,34 +2665,40 @@ export class Session2 extends HeyApiClient {
[
{
args: [
- { in: "path", key: "sessionID" },
- { in: "path", key: "messageID" },
+ { 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).delete<
- SessionDeleteMessageResponses,
- SessionDeleteMessageErrors,
+ return (options?.client ?? this.client).post<
+ ProviderOauthCallbackResponses,
+ ProviderOauthCallbackErrors,
ThrowOnError
>({
- url: "/session/{sessionID}/message/{messageID}",
+ url: "/provider/{providerID}/oauth/callback",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
+}
+export class Provider extends HeyApiClient {
/**
- * Get message
+ * List providers
*
- * Retrieve a specific message from a session by its message ID.
+ * Get a list of all available AI providers, including both available and connected ones.
*/
- public message<ThrowOnError extends boolean = false>(
- parameters: {
- sessionID: string
- messageID: string
+ public list<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
},
@@ -2334,45 +2709,28 @@ export class Session2 extends HeyApiClient {
[
{
args: [
- { in: "path", key: "sessionID" },
- { in: "path", key: "messageID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).get<SessionMessageResponses, SessionMessageErrors, ThrowOnError>({
- url: "/session/{sessionID}/message/{messageID}",
+ return (options?.client ?? this.client).get<ProviderListResponses, unknown, ThrowOnError>({
+ url: "/provider",
...options,
...params,
})
}
/**
- * Send async message
+ * Get provider auth methods
*
- * Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.
+ * Retrieve available authentication methods for all AI providers.
*/
- public promptAsync<ThrowOnError extends boolean = false>(
- parameters: {
- sessionID: string
+ public auth<ThrowOnError extends boolean = false>(
+ 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<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
},
options?: Options<never, ThrowOnError>,
) {
@@ -2381,58 +2739,41 @@ 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<SessionPromptAsyncResponses, SessionPromptAsyncErrors, ThrowOnError>({
- url: "/session/{sessionID}/prompt_async",
+ return (options?.client ?? this.client).get<ProviderAuthResponses, unknown, ThrowOnError>({
+ 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 Session2 extends HeyApiClient {
/**
- * Send command
+ * List sessions
*
- * Send a new command to a session for execution by the AI assistant.
+ * Get a list of all OpenCode sessions, sorted by most recently updated.
*/
- public command<ThrowOnError extends boolean = false>(
- parameters: {
- sessionID: string
+ public list<ThrowOnError extends boolean = false>(
+ parameters?: {
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
- }>
+ scope?: "project"
+ path?: string
+ roots?: boolean | "true" | "false"
+ start?: number
+ search?: string
+ limit?: number
},
options?: Options<never, ThrowOnError>,
) {
@@ -2441,49 +2782,44 @@ 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: "agent" },
- { in: "body", key: "model" },
- { in: "body", key: "arguments" },
- { in: "body", key: "command" },
- { in: "body", key: "variant" },
- { in: "body", key: "parts" },
+ { 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<SessionCommandResponses, SessionCommandErrors, ThrowOnError>({
- url: "/session/{sessionID}/command",
+ return (options?.client ?? this.client).get<SessionListResponses, unknown, ThrowOnError>({
+ url: "/session",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
/**
- * Run shell command
+ * Create session
*
- * Execute a shell command within the session context and return the AI's response.
+ * Create a new OpenCode session for interacting with AI assistants and managing conversations.
*/
- public shell<ThrowOnError extends boolean = false>(
- parameters: {
- sessionID: string
+ public create<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
- messageID?: string
+ parentID?: string
+ title?: string
agent?: string
model?: {
+ id: string
providerID: string
- modelID: string
+ variant?: string
}
- command?: string
+ permission?: PermissionRuleset
+ workspaceID?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -2492,19 +2828,20 @@ 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: "parentID" },
+ { in: "body", key: "title" },
{ in: "body", key: "agent" },
{ in: "body", key: "model" },
- { in: "body", key: "command" },
+ { in: "body", key: "permission" },
+ { in: "body", key: "workspaceID" },
],
},
],
)
- return (options?.client ?? this.client).post<SessionShellResponses, SessionShellErrors, ThrowOnError>({
- url: "/session/{sessionID}/shell",
+ return (options?.client ?? this.client).post<SessionCreateResponses, SessionCreateErrors, ThrowOnError>({
+ url: "/session",
...options,
...params,
headers: {
@@ -2516,17 +2853,14 @@ export class Session2 extends HeyApiClient {
}
/**
- * Revert message
+ * Get session status
*
- * Revert a specific message in a session, undoing its effects and restoring the previous state.
+ * Retrieve the current status of all sessions, including active, idle, and completed states.
*/
- public revert<ThrowOnError extends boolean = false>(
- parameters: {
- sessionID: string
+ public status<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
- messageID?: string
- partID?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -2535,33 +2869,25 @@ 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: "partID" },
],
},
],
)
- return (options?.client ?? this.client).post<SessionRevertResponses, SessionRevertErrors, ThrowOnError>({
- url: "/session/{sessionID}/revert",
+ return (options?.client ?? this.client).get<SessionStatusResponses, SessionStatusErrors, ThrowOnError>({
+ url: "/session/status",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
/**
- * Restore reverted messages
+ * Delete session
*
- * Restore all previously reverted messages in a session.
+ * Delete a session and permanently remove all associated data, including messages and history.
*/
- public unrevert<ThrowOnError extends boolean = false>(
+ public delete<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
directory?: string
@@ -2581,23 +2907,21 @@ export class Session2 extends HeyApiClient {
},
],
)
- return (options?.client ?? this.client).post<SessionUnrevertResponses, SessionUnrevertErrors, ThrowOnError>({
- url: "/session/{sessionID}/unrevert",
+ return (options?.client ?? this.client).delete<SessionDeleteResponses, SessionDeleteErrors, ThrowOnError>({
+ url: "/session/{sessionID}",
...options,
...params,
})
}
-}
-export class Part extends HeyApiClient {
/**
- * Delete a part from a message
+ * Get session
+ *
+ * Retrieve detailed information about a specific OpenCode session.
*/
- public delete<ThrowOnError extends boolean = false>(
+ public get<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
- messageID: string
- partID: string
directory?: string
workspace?: string
},
@@ -2609,32 +2933,34 @@ 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<PartDeleteResponses, PartDeleteErrors, ThrowOnError>({
- url: "/session/{sessionID}/message/{messageID}/part/{partID}",
+ return (options?.client ?? this.client).get<SessionGetResponses, SessionGetErrors, ThrowOnError>({
+ url: "/session/{sessionID}",
...options,
...params,
})
}
/**
- * Update a part in a message
+ * Update session
+ *
+ * Update properties of an existing session, such as title or other metadata.
*/
public update<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
- messageID: string
- partID: string
directory?: string
workspace?: string
- part?: Part2
+ title?: string
+ permission?: PermissionRuleset
+ time?: {
+ archived?: number
+ }
},
options?: Options<never, ThrowOnError>,
) {
@@ -2644,17 +2970,17 @@ 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" },
+ { in: "body", key: "title" },
+ { in: "body", key: "permission" },
+ { in: "body", key: "time" },
],
},
],
)
- return (options?.client ?? this.client).patch<PartUpdateResponses, PartUpdateErrors, ThrowOnError>({
- url: "/session/{sessionID}/message/{messageID}/part/{partID}",
+ return (options?.client ?? this.client).patch<SessionUpdateResponses, SessionUpdateErrors, ThrowOnError>({
+ url: "/session/{sessionID}",
...options,
...params,
headers: {
@@ -2664,23 +2990,17 @@ export class Part extends HeyApiClient {
},
})
}
-}
-export class Permission extends HeyApiClient {
/**
- * Respond to permission
- *
- * Approve or deny a permission request from the AI assistant.
+ * Get session children
*
- * @deprecated
+ * Retrieve all child sessions that were forked from the specified parent session.
*/
- public respond<ThrowOnError extends boolean = false>(
+ public children<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
- permissionID: string
directory?: string
workspace?: string
- response?: "once" | "always" | "reject"
},
options?: Options<never, ThrowOnError>,
) {
@@ -2690,38 +3010,29 @@ 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" },
],
},
],
)
- return (options?.client ?? this.client).post<PermissionRespondResponses, PermissionRespondErrors, ThrowOnError>({
- url: "/session/{sessionID}/permissions/{permissionID}",
+ return (options?.client ?? this.client).get<SessionChildrenResponses, SessionChildrenErrors, ThrowOnError>({
+ url: "/session/{sessionID}/children",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
/**
- * Respond to permission request
+ * Get session todos
*
- * Approve or deny a permission request from the AI assistant.
+ * Retrieve the todo list associated with a specific session, showing tasks and action items.
*/
- public reply<ThrowOnError extends boolean = false>(
+ public todo<ThrowOnError extends boolean = false>(
parameters: {
- requestID: string
+ sessionID: string
directory?: string
workspace?: string
- reply?: "once" | "always" | "reject"
- message?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -2730,36 +3041,31 @@ export class Permission extends HeyApiClient {
[
{
args: [
- { in: "path", key: "requestID" },
+ { in: "path", key: "sessionID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { in: "body", key: "reply" },
- { in: "body", key: "message" },
],
},
],
)
- return (options?.client ?? this.client).post<PermissionReplyResponses, PermissionReplyErrors, ThrowOnError>({
- url: "/permission/{requestID}/reply",
+ return (options?.client ?? this.client).get<SessionTodoResponses, SessionTodoErrors, ThrowOnError>({
+ url: "/session/{sessionID}/todo",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
/**
- * List pending permissions
+ * Get message diff
*
- * Get all pending permission requests across all sessions.
+ * Get the file changes (diff) that resulted from a specific user message in the session.
*/
- public list<ThrowOnError extends boolean = false>(
- parameters?: {
+ public diff<ThrowOnError extends boolean = false>(
+ parameters: {
+ sessionID: string
directory?: string
workspace?: string
+ messageID?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -2768,30 +3074,33 @@ export class Permission 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<PermissionListResponses, unknown, ThrowOnError>({
- url: "/permission",
+ return (options?.client ?? this.client).get<SessionDiffResponses, unknown, ThrowOnError>({
+ url: "/session/{sessionID}/diff",
...options,
...params,
})
}
-}
-export class Question extends HeyApiClient {
/**
- * List pending questions
+ * Get session messages
*
- * Get all pending question requests across all sessions.
+ * Retrieve all messages in a session, including user prompts and AI responses.
*/
- public list<ThrowOnError extends boolean = false>(
- parameters?: {
+ public messages<ThrowOnError extends boolean = false>(
+ parameters: {
+ sessionID: string
directory?: string
workspace?: string
+ limit?: number
+ before?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -2800,30 +3109,46 @@ export class Question 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<QuestionListResponses, unknown, ThrowOnError>({
- url: "/question",
+ return (options?.client ?? this.client).get<SessionMessagesResponses, SessionMessagesErrors, ThrowOnError>({
+ url: "/session/{sessionID}/message",
...options,
...params,
})
}
/**
- * Reply to question request
+ * Send message
*
- * Provide answers to a question request from the AI assistant.
+ * Create and send a new message to a session, streaming the AI response.
*/
- public reply<ThrowOnError extends boolean = false>(
+ public prompt<ThrowOnError extends boolean = false>(
parameters: {
- requestID: string
+ sessionID: string
directory?: string
workspace?: string
- answers?: Array<QuestionAnswer>
+ messageID?: string
+ model?: {
+ providerID: string
+ modelID: string
+ }
+ agent?: string
+ noReply?: boolean
+ tools?: {
+ [key: string]: boolean
+ }
+ format?: OutputFormat
+ system?: string
+ variant?: string
+ parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
},
options?: Options<never, ThrowOnError>,
) {
@@ -2832,16 +3157,24 @@ 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" },
+ { 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<QuestionReplyResponses, QuestionReplyErrors, ThrowOnError>({
- url: "/question/{requestID}/reply",
+ return (options?.client ?? this.client).post<SessionPromptResponses, SessionPromptErrors, ThrowOnError>({
+ url: "/session/{sessionID}/message",
...options,
...params,
headers: {
@@ -2853,13 +3186,14 @@ export class Question extends HeyApiClient {
}
/**
- * Reject question request
+ * Delete message
*
- * Reject a question request from the AI assistant.
+ * Permanently delete a specific message and all of its parts from a session without reverting file changes.
*/
- public reject<ThrowOnError extends boolean = false>(
+ public deleteMessage<ThrowOnError extends boolean = false>(
parameters: {
- requestID: string
+ sessionID: string
+ messageID: string
directory?: string
workspace?: string
},
@@ -2870,36 +3204,36 @@ export class Question extends HeyApiClient {
[
{
args: [
- { in: "path", key: "requestID" },
+ { in: "path", key: "sessionID" },
+ { in: "path", key: "messageID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).post<QuestionRejectResponses, QuestionRejectErrors, ThrowOnError>({
- url: "/question/{requestID}/reject",
+ return (options?.client ?? this.client).delete<
+ SessionDeleteMessageResponses,
+ SessionDeleteMessageErrors,
+ ThrowOnError
+ >({
+ url: "/session/{sessionID}/message/{messageID}",
...options,
...params,
})
}
-}
-export class Oauth extends HeyApiClient {
/**
- * OAuth authorize
+ * Get message
*
- * Initiate OAuth authorization for a specific AI provider to get an authorization URL.
+ * Retrieve a specific message from a session by its message ID.
*/
- public authorize<ThrowOnError extends boolean = false>(
+ public message<ThrowOnError extends boolean = false>(
parameters: {
- providerID: string
+ sessionID: string
+ messageID: string
directory?: string
workspace?: string
- method?: number
- inputs?: {
- [key: string]: string
- }
},
options?: Options<never, ThrowOnError>,
) {
@@ -2908,43 +3242,32 @@ export class Oauth extends HeyApiClient {
[
{
args: [
- { in: "path", key: "providerID" },
+ { in: "path", key: "sessionID" },
+ { in: "path", key: "messageID" },
{ 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<SessionMessageResponses, SessionMessageErrors, ThrowOnError>({
+ url: "/session/{sessionID}/message/{messageID}",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
/**
- * OAuth callback
+ * Fork session
*
- * Handle the OAuth callback from a provider after user authorization.
+ * Create a new session by forking an existing session at a specific message point.
*/
- public callback<ThrowOnError extends boolean = false>(
+ public fork<ThrowOnError extends boolean = false>(
parameters: {
- providerID: string
+ sessionID: string
directory?: string
workspace?: string
- method?: number
- code?: string
+ messageID?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -2953,21 +3276,16 @@ 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" },
+ { in: "body", key: "messageID" },
],
},
],
)
- return (options?.client ?? this.client).post<
- ProviderOauthCallbackResponses,
- ProviderOauthCallbackErrors,
- ThrowOnError
- >({
- url: "/provider/{providerID}/oauth/callback",
+ return (options?.client ?? this.client).post<SessionForkResponses, unknown, ThrowOnError>({
+ url: "/session/{sessionID}/fork",
...options,
...params,
headers: {
@@ -2977,16 +3295,15 @@ export class Oauth extends HeyApiClient {
},
})
}
-}
-export class Provider extends HeyApiClient {
/**
- * List providers
+ * Abort session
*
- * Get a list of all available AI providers, including both available and connected ones.
+ * Abort an active session and stop any ongoing AI processing or command execution.
*/
- public list<ThrowOnError extends boolean = false>(
- parameters?: {
+ public abort<ThrowOnError extends boolean = false>(
+ parameters: {
+ sessionID: string
directory?: string
workspace?: string
},
@@ -2997,28 +3314,33 @@ export class Provider extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "sessionID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).get<ProviderListResponses, unknown, ThrowOnError>({
- url: "/provider",
+ return (options?.client ?? this.client).post<SessionAbortResponses, SessionAbortErrors, ThrowOnError>({
+ url: "/session/{sessionID}/abort",
...options,
...params,
})
}
/**
- * Get provider auth methods
+ * Initialize session
*
- * Retrieve available authentication methods for all AI providers.
+ * Analyze the current application and create an AGENTS.md file with project-specific agent configurations.
*/
- public auth<ThrowOnError extends boolean = false>(
- parameters?: {
+ public init<ThrowOnError extends boolean = false>(
+ parameters: {
+ sessionID: string
directory?: string
workspace?: string
+ modelID?: string
+ providerID?: string
+ messageID?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -3027,38 +3349,38 @@ export class Provider extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "sessionID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { in: "body", key: "modelID" },
+ { in: "body", key: "providerID" },
+ { in: "body", key: "messageID" },
],
},
],
)
- return (options?.client ?? this.client).get<ProviderAuthResponses, unknown, ThrowOnError>({
- url: "/provider/auth",
+ return (options?.client ?? this.client).post<SessionInitResponses, SessionInitErrors, ThrowOnError>({
+ url: "/session/{sessionID}/init",
...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 History extends HeyApiClient {
/**
- * List sync events
+ * Unshare session
*
- * 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.
+ * Remove the shareable link for a session, making it private again.
*/
- public list<ThrowOnError extends boolean = false>(
- parameters?: {
+ public unshare<ThrowOnError extends boolean = false>(
+ parameters: {
+ sessionID: string
directory?: string
workspace?: string
- body?: {
- [key: string]: number
- }
},
options?: Options<never, ThrowOnError>,
) {
@@ -3067,34 +3389,28 @@ export class History extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "sessionID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { key: "body", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).post<SyncHistoryListResponses, SyncHistoryListErrors, ThrowOnError>({
- url: "/sync/history",
+ return (options?.client ?? this.client).delete<SessionUnshareResponses, SessionUnshareErrors, ThrowOnError>({
+ url: "/session/{sessionID}/share",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
-}
-export class Sync extends HeyApiClient {
/**
- * Start workspace sync
+ * Share session
*
- * Start sync loops for workspaces in the current project that have active sessions.
+ * Create a shareable link for a session, allowing others to view the conversation.
*/
- public start<ThrowOnError extends boolean = false>(
- parameters?: {
+ public share<ThrowOnError extends boolean = false>(
+ parameters: {
+ sessionID: string
directory?: string
workspace?: string
},
@@ -3105,38 +3421,33 @@ export class Sync extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "sessionID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).post<SyncStartResponses, unknown, ThrowOnError>({
- url: "/sync/start",
+ return (options?.client ?? this.client).post<SessionShareResponses, SessionShareErrors, ThrowOnError>({
+ url: "/session/{sessionID}/share",
...options,
...params,
})
}
/**
- * Replay sync events
+ * Summarize session
*
- * Validate and replay a complete sync event history.
+ * Generate a concise summary of the session using AI compaction to preserve key information.
*/
- public replay<ThrowOnError extends boolean = false>(
- parameters?: {
- query_directory?: string
+ public summarize<ThrowOnError extends boolean = false>(
+ parameters: {
+ sessionID: string
+ directory?: string
workspace?: string
- body_directory?: string
- events?: Array<{
- id: string
- aggregateID: string
- seq: number
- type: string
- data: {
- [key: string]: unknown
- }
- }>
+ providerID?: string
+ modelID?: string
+ auto?: boolean
},
options?: Options<never, ThrowOnError>,
) {
@@ -3145,24 +3456,18 @@ export class Sync extends HeyApiClient {
[
{
args: [
- {
- in: "query",
- key: "query_directory",
- map: "directory",
- },
+ { in: "path", key: "sessionID" },
+ { in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- {
- in: "body",
- key: "body_directory",
- map: "directory",
- },
- { in: "body", key: "events" },
+ { in: "body", key: "providerID" },
+ { in: "body", key: "modelID" },
+ { in: "body", key: "auto" },
],
},
],
)
- return (options?.client ?? this.client).post<SyncReplayResponses, SyncReplayErrors, ThrowOnError>({
- url: "/sync/replay",
+ return (options?.client ?? this.client).post<SessionSummarizeResponses, SessionSummarizeErrors, ThrowOnError>({
+ url: "/session/{sessionID}/summarize",
...options,
...params,
headers: {
@@ -3173,23 +3478,30 @@ export class Sync extends HeyApiClient {
})
}
- private _history?: History
- get history(): History {
- return (this._history ??= new History({ client: this.client }))
- }
-}
-
-export class Find extends HeyApiClient {
/**
- * Find text
+ * Send async message
*
- * Search for text patterns across files in the project using ripgrep.
+ * Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.
*/
- public text<ThrowOnError extends boolean = false>(
+ public promptAsync<ThrowOnError extends boolean = false>(
parameters: {
+ sessionID: string
directory?: string
workspace?: string
- pattern: string
+ messageID?: string
+ model?: {
+ providerID: string
+ modelID: string
+ }
+ agent?: string
+ noReply?: boolean
+ tools?: {
+ [key: string]: boolean
+ }
+ format?: OutputFormat
+ system?: string
+ variant?: string
+ parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
},
options?: Options<never, ThrowOnError>,
) {
@@ -3198,33 +3510,58 @@ 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" },
+ { 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).get<FindTextResponses, unknown, ThrowOnError>({
- url: "/find",
+ return (options?.client ?? this.client).post<SessionPromptAsyncResponses, SessionPromptAsyncErrors, ThrowOnError>({
+ url: "/session/{sessionID}/prompt_async",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
/**
- * Find files
+ * Send command
*
- * Search for files or directories by name or pattern in the project directory.
+ * Send a new command to a session for execution by the AI assistant.
*/
- public files<ThrowOnError extends boolean = false>(
+ public command<ThrowOnError extends boolean = false>(
parameters: {
+ sessionID: string
directory?: string
workspace?: string
- query: string
- dirs?: "true" | "false"
- type?: "file" | "directory"
- limit?: number
+ 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<never, ThrowOnError>,
) {
@@ -3233,33 +3570,49 @@ 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" },
+ { 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).get<FindFilesResponses, unknown, ThrowOnError>({
- url: "/find/file",
+ return (options?.client ?? this.client).post<SessionCommandResponses, SessionCommandErrors, ThrowOnError>({
+ url: "/session/{sessionID}/command",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
/**
- * Find symbols
+ * Run shell command
*
- * Search for workspace symbols like functions, classes, and variables using LSP.
+ * Execute a shell command within the session context and return the AI's response.
*/
- public symbols<ThrowOnError extends boolean = false>(
+ public shell<ThrowOnError extends boolean = false>(
parameters: {
+ sessionID: string
directory?: string
workspace?: string
- query: string
+ messageID?: string
+ agent?: string
+ model?: {
+ providerID: string
+ modelID: string
+ }
+ command?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -3268,32 +3621,41 @@ 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: "messageID" },
+ { in: "body", key: "agent" },
+ { in: "body", key: "model" },
+ { in: "body", key: "command" },
],
},
],
)
- return (options?.client ?? this.client).get<FindSymbolsResponses, unknown, ThrowOnError>({
- url: "/find/symbol",
+ return (options?.client ?? this.client).post<SessionShellResponses, SessionShellErrors, ThrowOnError>({
+ url: "/session/{sessionID}/shell",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
-}
-export class File extends HeyApiClient {
/**
- * List files
+ * Revert message
*
- * List files and directories in a specified path.
+ * Revert a specific message in a session, undoing its effects and restoring the previous state.
*/
- public list<ThrowOnError extends boolean = false>(
+ public revert<ThrowOnError extends boolean = false>(
parameters: {
+ sessionID: string
directory?: string
workspace?: string
- path: string
+ messageID?: string
+ partID?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -3302,30 +3664,37 @@ export class File extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "sessionID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { in: "query", key: "path" },
+ { in: "body", key: "messageID" },
+ { in: "body", key: "partID" },
],
},
],
)
- return (options?.client ?? this.client).get<FileListResponses, unknown, ThrowOnError>({
- url: "/file",
+ return (options?.client ?? this.client).post<SessionRevertResponses, SessionRevertErrors, ThrowOnError>({
+ url: "/session/{sessionID}/revert",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
/**
- * Read file
+ * Restore reverted messages
*
- * Read the content of a specified file.
+ * Restore all previously reverted messages in a session.
*/
- public read<ThrowOnError extends boolean = false>(
+ public unrevert<ThrowOnError extends boolean = false>(
parameters: {
+ sessionID: string
directory?: string
workspace?: string
- path: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -3334,27 +3703,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<FileReadResponses, unknown, ThrowOnError>({
- url: "/file/content",
+ return (options?.client ?? this.client).post<SessionUnrevertResponses, SessionUnrevertErrors, ThrowOnError>({
+ url: "/session/{sessionID}/unrevert",
...options,
...params,
})
}
+}
+export class Part extends HeyApiClient {
/**
- * Get file status
- *
- * Get the git status of all files in the project.
+ * Delete a part from a message.
*/
- public status<ThrowOnError extends boolean = false>(
- parameters?: {
+ public delete<ThrowOnError extends boolean = false>(
+ parameters: {
+ sessionID: string
+ messageID: string
+ partID: string
directory?: string
workspace?: string
},
@@ -3365,30 +3737,33 @@ export class File 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<FileStatusResponses, unknown, ThrowOnError>({
- url: "/file/status",
+ return (options?.client ?? this.client).delete<PartDeleteResponses, PartDeleteErrors, ThrowOnError>({
+ url: "/session/{sessionID}/message/{messageID}/part/{partID}",
...options,
...params,
})
}
-}
-export class Event extends HeyApiClient {
/**
- * Subscribe to events
- *
- * Get events
+ * Update a part in a message.
*/
- public subscribe<ThrowOnError extends boolean = false>(
- parameters?: {
+ public update<ThrowOnError extends boolean = false>(
+ parameters: {
+ sessionID: string
+ messageID: string
+ partID: string
directory?: string
workspace?: string
+ part?: Part2
},
options?: Options<never, ThrowOnError>,
) {
@@ -3397,31 +3772,42 @@ export class Event 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).sse.get<EventSubscribeResponses, unknown, ThrowOnError>({
- url: "/event",
+ return (options?.client ?? this.client).patch<PartUpdateResponses, PartUpdateErrors, ThrowOnError>({
+ url: "/session/{sessionID}/message/{messageID}/part/{partID}",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
}
-export class Auth2 extends HeyApiClient {
+export class History extends HeyApiClient {
/**
- * Remove MCP OAuth
+ * List sync events
*
- * Remove OAuth credentials for an MCP server
+ * 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 remove<ThrowOnError extends boolean = false>(
- parameters: {
- name: string
+ public list<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
+ body?: {
+ [key: string]: number
+ }
},
options?: Options<never, ThrowOnError>,
) {
@@ -3430,28 +3816,34 @@ export class Auth2 extends HeyApiClient {
[
{
args: [
- { in: "path", key: "name" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { key: "body", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).delete<McpAuthRemoveResponses, McpAuthRemoveErrors, ThrowOnError>({
- url: "/mcp/{name}/auth",
+ return (options?.client ?? this.client).post<SyncHistoryListResponses, SyncHistoryListErrors, ThrowOnError>({
+ url: "/sync/history",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
+}
+export class Sync extends HeyApiClient {
/**
- * Start MCP OAuth
+ * Start workspace sync
*
- * Start OAuth authentication flow for a Model Context Protocol (MCP) server.
+ * Start sync loops for workspaces in the current project that have active sessions.
*/
public start<ThrowOnError extends boolean = false>(
- parameters: {
- name: string
+ parameters?: {
directory?: string
workspace?: string
},
@@ -3462,31 +3854,38 @@ export class Auth2 extends HeyApiClient {
[
{
args: [
- { in: "path", key: "name" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).post<McpAuthStartResponses, McpAuthStartErrors, ThrowOnError>({
- url: "/mcp/{name}/auth",
+ return (options?.client ?? this.client).post<SyncStartResponses, unknown, ThrowOnError>({
+ url: "/sync/start",
...options,
...params,
})
}
/**
- * Complete MCP OAuth
+ * Replay sync events
*
- * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.
+ * Validate and replay a complete sync event history.
*/
- public callback<ThrowOnError extends boolean = false>(
- parameters: {
- name: string
- directory?: string
+ public replay<ThrowOnError extends boolean = false>(
+ parameters?: {
+ query_directory?: string
workspace?: string
- code?: string
+ body_directory?: string
+ events?: Array<{
+ id: string
+ aggregateID: string
+ seq: number
+ type: string
+ data: {
+ [key: string]: unknown
+ }
+ }>
},
options?: Options<never, ThrowOnError>,
) {
@@ -3495,16 +3894,24 @@ export class Auth2 extends HeyApiClient {
[
{
args: [
- { in: "path", key: "name" },
- { in: "query", key: "directory" },
+ {
+ in: "query",
+ key: "query_directory",
+ map: "directory",
+ },
{ in: "query", key: "workspace" },
- { in: "body", key: "code" },
+ {
+ in: "body",
+ key: "body_directory",
+ map: "directory",
+ },
+ { in: "body", key: "events" },
],
},
],
)
- return (options?.client ?? this.client).post<McpAuthCallbackResponses, McpAuthCallbackErrors, ThrowOnError>({
- url: "/mcp/{name}/auth/callback",
+ return (options?.client ?? this.client).post<SyncReplayResponses, SyncReplayErrors, ThrowOnError>({
+ url: "/sync/replay",
...options,
...params,
headers: {
@@ -3515,16 +3922,55 @@ export class Auth2 extends HeyApiClient {
})
}
+ private _history?: History
+ get history(): History {
+ return (this._history ??= new History({ client: this.client }))
+ }
+}
+
+export class Session3 extends HeyApiClient {
/**
- * Authenticate MCP OAuth
+ * List v2 sessions
*
- * Start OAuth flow and wait for callback (opens browser)
+ * 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 authenticate<ThrowOnError extends boolean = false>(
+ public list<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).get<V2SessionListResponses, V2SessionListErrors, ThrowOnError>({
+ url: "/api/session",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * Send v2 message
+ *
+ * Create a v2 session message and queue it for the agent loop.
+ */
+ public prompt<ThrowOnError extends boolean = false>(
parameters: {
- name: string
+ sessionID: string
directory?: string
workspace?: string
+ prompt?: Prompt
+ delivery?: SessionDelivery
},
options?: Options<never, ThrowOnError>,
) {
@@ -3533,31 +3979,35 @@ 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: "prompt" },
+ { in: "body", key: "delivery" },
],
},
],
)
- return (options?.client ?? this.client).post<McpAuthAuthenticateResponses, McpAuthAuthenticateErrors, ThrowOnError>(
- {
- url: "/mcp/{name}/auth/authenticate",
- ...options,
- ...params,
+ return (options?.client ?? this.client).post<V2SessionPromptResponses, unknown, ThrowOnError>({
+ url: "/api/session/{sessionID}/prompt",
+ ...options,
+ ...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
},
- )
+ })
}
-}
-export class Mcp extends HeyApiClient {
/**
- * Get MCP status
+ * Compact v2 session
*
- * Get the status of all Model Context Protocol (MCP) servers.
+ * Compact a v2 session conversation.
*/
- public status<ThrowOnError extends boolean = false>(
- parameters?: {
+ public compact<ThrowOnError extends boolean = false>(
+ parameters: {
+ sessionID: string
directory?: string
workspace?: string
},
@@ -3568,30 +4018,30 @@ export class Mcp extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "sessionID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).get<McpStatusResponses, unknown, ThrowOnError>({
- url: "/mcp",
+ return (options?.client ?? this.client).post<V2SessionCompactResponses, unknown, ThrowOnError>({
+ url: "/api/session/{sessionID}/compact",
...options,
...params,
})
}
/**
- * Add MCP server
+ * Wait for v2 session
*
- * Dynamically add a new Model Context Protocol (MCP) server to the system.
+ * Wait for a v2 session agent loop to become idle.
*/
- public add<ThrowOnError extends boolean = false>(
- parameters?: {
+ public wait<ThrowOnError extends boolean = false>(
+ parameters: {
+ sessionID: string
directory?: string
workspace?: string
- name?: string
- config?: McpLocalConfig | McpRemoteConfig
},
options?: Options<never, ThrowOnError>,
) {
@@ -3600,32 +4050,28 @@ export class Mcp extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "sessionID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { in: "body", key: "name" },
- { in: "body", key: "config" },
],
},
],
)
- return (options?.client ?? this.client).post<McpAddResponses, McpAddErrors, ThrowOnError>({
- url: "/mcp",
+ return (options?.client ?? this.client).post<V2SessionWaitResponses, unknown, ThrowOnError>({
+ url: "/api/session/{sessionID}/wait",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
/**
- * Connect an MCP server
+ * Get v2 session context
+ *
+ * Retrieve the active context messages for a v2 session (all messages after the last compaction).
*/
- public connect<ThrowOnError extends boolean = false>(
+ public context<ThrowOnError extends boolean = false>(
parameters: {
- name: string
+ sessionID: string
directory?: string
workspace?: string
},
@@ -3636,26 +4082,28 @@ export class Mcp 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<McpConnectResponses, unknown, ThrowOnError>({
- url: "/mcp/{name}/connect",
+ return (options?.client ?? this.client).get<V2SessionContextResponses, unknown, ThrowOnError>({
+ url: "/api/session/{sessionID}/context",
...options,
...params,
})
}
/**
- * Disconnect an MCP server
+ * Get v2 session messages
+ *
+ * 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 disconnect<ThrowOnError extends boolean = false>(
+ public messages<ThrowOnError extends boolean = false>(
parameters: {
- name: string
+ sessionID: string
directory?: string
workspace?: string
},
@@ -3666,23 +4114,25 @@ export class Mcp 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<McpDisconnectResponses, unknown, ThrowOnError>({
- url: "/mcp/{name}/disconnect",
+ return (options?.client ?? this.client).get<V2SessionMessagesResponses, V2SessionMessagesErrors, ThrowOnError>({
+ url: "/api/session/{sessionID}/message",
...options,
...params,
})
}
+}
- private _auth?: Auth2
- get auth(): Auth2 {
- return (this._auth ??= new Auth2({ client: this.client }))
+export class V2 extends HeyApiClient {
+ private _session?: Session3
+ get session(): Session3 {
+ return (this._session ??= new Session3({ client: this.client }))
}
}
@@ -3690,7 +4140,7 @@ export class Control extends HeyApiClient {
/**
* Get next TUI request
*
- * Retrieve the next TUI (Terminal User Interface) request from the queue for processing.
+ * Retrieve the next TUI request from the queue for processing.
*/
public next<ThrowOnError extends boolean = false>(
parameters?: {
@@ -3759,7 +4209,7 @@ export class Tui extends HeyApiClient {
/**
* Append TUI prompt
*
- * Append prompt to the TUI
+ * Append prompt to the TUI.
*/
public appendPrompt<ThrowOnError extends boolean = false>(
parameters?: {
@@ -3826,7 +4276,7 @@ export class Tui extends HeyApiClient {
/**
* Open sessions dialog
*
- * Open the session dialog
+ * Open the session dialog.
*/
public openSessions<ThrowOnError extends boolean = false>(
parameters?: {
@@ -3856,7 +4306,7 @@ export class Tui extends HeyApiClient {
/**
* Open themes dialog
*
- * Open the theme dialog
+ * Open the theme dialog.
*/
public openThemes<ThrowOnError extends boolean = false>(
parameters?: {
@@ -3886,7 +4336,7 @@ export class Tui extends HeyApiClient {
/**
* Open models dialog
*
- * Open the model dialog
+ * Open the model dialog.
*/
public openModels<ThrowOnError extends boolean = false>(
parameters?: {
@@ -3916,7 +4366,7 @@ export class Tui extends HeyApiClient {
/**
* Submit TUI prompt
*
- * Submit the prompt
+ * Submit the prompt.
*/
public submitPrompt<ThrowOnError extends boolean = false>(
parameters?: {
@@ -3946,7 +4396,7 @@ export class Tui extends HeyApiClient {
/**
* Clear TUI prompt
*
- * Clear the prompt
+ * Clear the prompt.
*/
public clearPrompt<ThrowOnError extends boolean = false>(
parameters?: {
@@ -3976,7 +4426,7 @@ export class Tui extends HeyApiClient {
/**
* Execute TUI command
*
- * Execute a TUI command (e.g. agent_cycle)
+ * Execute a TUI command.
*/
public executeCommand<ThrowOnError extends boolean = false>(
parameters?: {
@@ -4013,7 +4463,7 @@ export class Tui extends HeyApiClient {
/**
* Show TUI toast
*
- * Show a toast notification in the TUI
+ * Show a toast notification in the TUI.
*/
public showToast<ThrowOnError extends boolean = false>(
parameters?: {
@@ -4056,13 +4506,13 @@ export class Tui extends HeyApiClient {
/**
* Publish TUI event
*
- * Publish a TUI event
+ * Publish a TUI event.
*/
public publish<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect
+ body?: EventTuiPromptAppend2 | EventTuiCommandExecute2 | EventTuiToastShow2 | EventTuiSessionSelect2
},
options?: Options<never, ThrowOnError>,
) {
@@ -4133,230 +4583,6 @@ export class Tui extends HeyApiClient {
}
}
-export class Instance extends HeyApiClient {
- /**
- * Dispose instance
- *
- * Clean up and dispose the current OpenCode instance, releasing all resources.
- */
- public dispose<ThrowOnError extends boolean = false>(
- parameters?: {
- directory?: string
- workspace?: string
- },
- options?: Options<never, ThrowOnError>,
- ) {
- const params = buildClientParams(
- [parameters],
- [
- {
- args: [
- { in: "query", key: "directory" },
- { in: "query", key: "workspace" },
- ],
- },
- ],
- )
- return (options?.client ?? this.client).post<InstanceDisposeResponses, unknown, ThrowOnError>({
- url: "/instance/dispose",
- ...options,
- ...params,
- })
- }
-}
-
-export class Path extends HeyApiClient {
- /**
- * Get paths
- *
- * Retrieve the current working directory and related path information for the OpenCode instance.
- */
- public get<ThrowOnError extends boolean = false>(
- parameters?: {
- directory?: string
- workspace?: string
- },
- options?: Options<never, ThrowOnError>,
- ) {
- const params = buildClientParams(
- [parameters],
- [
- {
- args: [
- { in: "query", key: "directory" },
- { in: "query", key: "workspace" },
- ],
- },
- ],
- )
- return (options?.client ?? this.client).get<PathGetResponses, unknown, ThrowOnError>({
- url: "/path",
- ...options,
- ...params,
- })
- }
-}
-
-export class Vcs extends HeyApiClient {
- /**
- * Get VCS info
- *
- * Retrieve version control system (VCS) information for the current project, such as git branch.
- */
- public get<ThrowOnError extends boolean = false>(
- parameters?: {
- directory?: string
- workspace?: string
- },
- options?: Options<never, ThrowOnError>,
- ) {
- const params = buildClientParams(
- [parameters],
- [
- {
- args: [
- { in: "query", key: "directory" },
- { in: "query", key: "workspace" },
- ],
- },
- ],
- )
- return (options?.client ?? this.client).get<VcsGetResponses, unknown, ThrowOnError>({
- url: "/vcs",
- ...options,
- ...params,
- })
- }
-
- /**
- * Get VCS diff
- *
- * Retrieve the current git diff for the working tree or against the default branch.
- */
- public diff<ThrowOnError extends boolean = false>(
- parameters: {
- directory?: string
- workspace?: string
- mode: "git" | "branch"
- },
- options?: Options<never, ThrowOnError>,
- ) {
- const params = buildClientParams(
- [parameters],
- [
- {
- args: [
- { in: "query", key: "directory" },
- { in: "query", key: "workspace" },
- { in: "query", key: "mode" },
- ],
- },
- ],
- )
- return (options?.client ?? this.client).get<VcsDiffResponses, unknown, ThrowOnError>({
- url: "/vcs/diff",
- ...options,
- ...params,
- })
- }
-}
-
-export class Command extends HeyApiClient {
- /**
- * List commands
- *
- * Get a list of all available commands in the OpenCode system.
- */
- public list<ThrowOnError extends boolean = false>(
- parameters?: {
- directory?: string
- workspace?: string
- },
- options?: Options<never, ThrowOnError>,
- ) {
- const params = buildClientParams(
- [parameters],
- [
- {
- args: [
- { in: "query", key: "directory" },
- { in: "query", key: "workspace" },
- ],
- },
- ],
- )
- return (options?.client ?? this.client).get<CommandListResponses, unknown, ThrowOnError>({
- url: "/command",
- ...options,
- ...params,
- })
- }
-}
-
-export class Lsp extends HeyApiClient {
- /**
- * Get LSP status
- *
- * Get LSP server status
- */
- public status<ThrowOnError extends boolean = false>(
- parameters?: {
- directory?: string
- workspace?: string
- },
- options?: Options<never, ThrowOnError>,
- ) {
- const params = buildClientParams(
- [parameters],
- [
- {
- args: [
- { in: "query", key: "directory" },
- { in: "query", key: "workspace" },
- ],
- },
- ],
- )
- return (options?.client ?? this.client).get<LspStatusResponses, unknown, ThrowOnError>({
- url: "/lsp",
- ...options,
- ...params,
- })
- }
-}
-
-export class Formatter extends HeyApiClient {
- /**
- * Get formatter status
- *
- * Get formatter status
- */
- public status<ThrowOnError extends boolean = false>(
- parameters?: {
- directory?: string
- workspace?: string
- },
- options?: Options<never, ThrowOnError>,
- ) {
- const params = buildClientParams(
- [parameters],
- [
- {
- args: [
- { in: "query", key: "directory" },
- { in: "query", key: "workspace" },
- ],
- },
- ],
- )
- return (options?.client ?? this.client).get<FormatterStatusResponses, unknown, ThrowOnError>({
- url: "/formatter",
- ...options,
- ...params,
- })
- }
-}
-
export class OpencodeClient extends HeyApiClient {
public static readonly __registry = new HeyApiRegistry<OpencodeClient>()
@@ -4365,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 }))
@@ -4380,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
@@ -4400,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 }))
@@ -4410,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 }))
@@ -4450,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 }))
@@ -4494,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 31bd40ab4..caa3d4c76 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -4,53 +4,104 @@ export type ClientOptions = {
baseUrl: `${string}://${string}` | (string & {})
}
-export type EventServerInstanceDisposed = {
- type: "server.instance.disposed"
- properties: {
- directory: string
- }
-}
-
-export type EventFileEdited = {
- type: "file.edited"
- properties: {
- file: string
- }
-}
+export type Event =
+ | EventServerInstanceDisposed
+ | EventFileEdited
+ | EventFileWatcherUpdated
+ | EventLspClientDiagnostics
+ | EventLspUpdated
+ | EventMessagePartDelta
+ | EventPermissionAsked
+ | EventPermissionReplied
+ | EventSessionDiff
+ | EventSessionError
+ | EventInstallationUpdated
+ | EventInstallationUpdateAvailable
+ | EventQuestionAsked
+ | EventQuestionReplied
+ | EventQuestionRejected
+ | EventTodoUpdated
+ | EventSessionStatus
+ | EventSessionIdle
+ | EventSessionCompacted
+ | EventTuiPromptAppend
+ | EventTuiCommandExecute
+ | EventTuiToastShow1
+ | EventTuiSessionSelect
+ | EventMcpToolsChanged
+ | EventMcpBrowserOpenFailed
+ | EventCommandExecuted
+ | EventProjectUpdated
+ | EventVcsBranchUpdated
+ | EventWorkspaceReady
+ | EventWorkspaceFailed
+ | EventWorkspaceRestore
+ | EventWorkspaceStatus
+ | 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 EventFileWatcherUpdated = {
- type: "file.watcher.updated"
- properties: {
- file: string
- event: "add" | "change" | "unlink"
- }
+export type OAuth = {
+ type: "oauth"
+ refresh: string
+ access: string
+ expires: number
+ accountId?: string
+ enterpriseUrl?: string
}
-export type EventLspClientDiagnostics = {
- type: "lsp.client.diagnostics"
- properties: {
- serverID: string
- path: string
+export type ApiAuth = {
+ type: "api"
+ key: string
+ metadata?: {
+ [key: string]: string
}
}
-export type EventLspUpdated = {
- type: "lsp.updated"
- properties: {
- [key: string]: unknown
- }
+export type WellKnownAuth = {
+ type: "wellknown"
+ key: string
+ token: string
}
-export type EventMessagePartDelta = {
- type: "message.part.delta"
- properties: {
- sessionID: string
- messageID: string
- partID: string
- field: string
- delta: string
- }
-}
+export type Auth = OAuth | ApiAuth | WellKnownAuth
export type PermissionRequest = {
id: string
@@ -67,20 +118,6 @@ export type PermissionRequest = {
}
}
-export type EventPermissionAsked = {
- type: "permission.asked"
- properties: PermissionRequest
-}
-
-export type EventPermissionReplied = {
- type: "permission.replied"
- properties: {
- sessionID: string
- requestID: string
- reply: "once" | "always" | "reject"
- }
-}
-
export type SnapshotFileDiff = {
file: string
patch: string
@@ -89,14 +126,6 @@ export type SnapshotFileDiff = {
status?: "added" | "deleted" | "modified"
}
-export type EventSessionDiff = {
- type: "session.diff"
- properties: {
- sessionID: string
- diff: Array<SnapshotFileDiff>
- }
-}
-
export type ProviderAuthError = {
name: "ProviderAuthError"
data: {
@@ -158,35 +187,6 @@ export type ApiError = {
}
}
-export type EventSessionError = {
- type: "session.error"
- properties: {
- sessionID?: string
- error?:
- | ProviderAuthError
- | UnknownError
- | MessageOutputLengthError
- | MessageAbortedError
- | StructuredOutputError
- | ContextOverflowError
- | ApiError
- }
-}
-
-export type EventInstallationUpdated = {
- type: "installation.updated"
- properties: {
- version: string
- }
-}
-
-export type EventInstallationUpdateAvailable = {
- type: "installation.update-available"
- properties: {
- version: string
- }
-}
-
export type QuestionOption = {
/**
* Display text (1-5 words, concise)
@@ -211,13 +211,7 @@ export type QuestionInfo = {
* Available choices
*/
options: Array<QuestionOption>
- /**
- * Allow selecting multiple choices
- */
multiple?: boolean
- /**
- * Allow typing a custom answer (default: true)
- */
custom?: boolean
}
@@ -236,11 +230,6 @@ export type QuestionRequest = {
tool?: QuestionTool
}
-export type EventQuestionAsked = {
- type: "question.asked"
- properties: QuestionRequest
-}
-
export type QuestionAnswer = Array<string>
export type QuestionReplied = {
@@ -249,21 +238,11 @@ export type QuestionReplied = {
answers: Array<QuestionAnswer>
}
-export type EventQuestionReplied = {
- type: "question.replied"
- properties: QuestionReplied
-}
-
export type QuestionRejected = {
sessionID: string
requestID: string
}
-export type EventQuestionRejected = {
- type: "question.rejected"
- properties: QuestionRejected
-}
-
export type Todo = {
/**
* Brief description of the task
@@ -279,14 +258,6 @@ export type Todo = {
priority: string
}
-export type EventTodoUpdated = {
- type: "todo.updated"
- properties: {
- sessionID: string
- todos: Array<Todo>
- }
-}
-
export type SessionStatus =
| {
type: "idle"
@@ -301,29 +272,8 @@ export type SessionStatus =
type: "busy"
}
-export type EventSessionStatus = {
- type: "session.status"
- properties: {
- sessionID: string
- status: SessionStatus
- }
-}
-
-export type EventSessionIdle = {
- type: "session.idle"
- properties: {
- sessionID: string
- }
-}
-
-export type EventSessionCompacted = {
- type: "session.compacted"
- properties: {
- sessionID: string
- }
-}
-
export type EventTuiPromptAppend = {
+ id: string
type: "tui.prompt.append"
properties: {
text: string
@@ -331,6 +281,7 @@ export type EventTuiPromptAppend = {
}
export type EventTuiCommandExecute = {
+ id: string
type: "tui.command.execute"
properties: {
command:
@@ -355,19 +306,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: {
/**
@@ -377,31 +327,6 @@ export type EventTuiSessionSelect = {
}
}
-export type EventMcpToolsChanged = {
- type: "mcp.tools.changed"
- properties: {
- server: string
- }
-}
-
-export type EventMcpBrowserOpenFailed = {
- type: "mcp.browser.open.failed"
- properties: {
- mcpName: string
- url: string
- }
-}
-
-export type EventCommandExecuted = {
- type: "command.executed"
- properties: {
- name: string
- sessionID: string
- arguments: string
- messageID: string
- }
-}
-
export type Project = {
id: string
worktree: string
@@ -426,65 +351,6 @@ export type Project = {
sandboxes: Array<string>
}
-export type EventProjectUpdated = {
- type: "project.updated"
- properties: Project
-}
-
-export type EventVcsBranchUpdated = {
- type: "vcs.branch.updated"
- properties: {
- branch?: string
- }
-}
-
-export type EventWorkspaceReady = {
- type: "workspace.ready"
- properties: {
- name: string
- }
-}
-
-export type EventWorkspaceFailed = {
- type: "workspace.failed"
- properties: {
- message: string
- }
-}
-
-export type EventWorkspaceRestore = {
- type: "workspace.restore"
- properties: {
- workspaceID: string
- sessionID: string
- total: number
- step: number
- }
-}
-
-export type EventWorkspaceStatus = {
- type: "workspace.status"
- properties: {
- workspaceID: string
- status: "connected" | "connecting" | "disconnected" | "error"
- }
-}
-
-export type EventWorktreeReady = {
- type: "worktree.ready"
- properties: {
- name: string
- branch: string
- }
-}
-
-export type EventWorktreeFailed = {
- type: "worktree.failed"
- properties: {
- message: string
- }
-}
-
export type Pty = {
id: string
title: string
@@ -495,35 +361,6 @@ export type Pty = {
pid: number
}
-export type EventPtyCreated = {
- type: "pty.created"
- properties: {
- info: Pty
- }
-}
-
-export type EventPtyUpdated = {
- type: "pty.updated"
- properties: {
- info: Pty
- }
-}
-
-export type EventPtyExited = {
- type: "pty.exited"
- properties: {
- id: string
- exitCode: number
- }
-}
-
-export type EventPtyDeleted = {
- type: "pty.deleted"
- properties: {
- id: string
- }
-}
-
export type OutputFormatText = {
type: "text"
}
@@ -609,22 +446,6 @@ export type AssistantMessage = {
export type Message = UserMessage | AssistantMessage
-export type EventMessageUpdated = {
- type: "message.updated"
- properties: {
- sessionID: string
- info: Message
- }
-}
-
-export type EventMessageRemoved = {
- type: "message.removed"
- properties: {
- sessionID: string
- messageID: string
- }
-}
-
export type TextPart = {
id: string
sessionID: string
@@ -888,24 +709,6 @@ export type Part =
| RetryPart
| CompactionPart
-export type EventMessagePartUpdated = {
- type: "message.part.updated"
- properties: {
- sessionID: string
- part: Part
- time: number
- }
-}
-
-export type EventMessagePartRemoved = {
- type: "message.part.removed"
- properties: {
- sessionID: string
- messageID: string
- partID: string
- }
-}
-
export type PermissionAction = "allow" | "deny" | "ask"
export type PermissionRule = {
@@ -934,6 +737,12 @@ export type Session = {
url: string
}
title: string
+ agent?: string
+ model?: {
+ id: string
+ providerID: string
+ variant?: string
+ }
version: string
time: {
created: number
@@ -950,160 +759,10 @@ export type Session = {
}
}
-export type EventSessionCreated = {
- type: "session.created"
- properties: {
- sessionID: string
- info: Session
- }
-}
-
-export type EventSessionUpdated = {
- type: "session.updated"
- properties: {
- sessionID: string
- info: Session
- }
-}
-
-export type EventSessionDeleted = {
- type: "session.deleted"
- properties: {
- sessionID: string
- info: Session
- }
-}
-
-export type EventServerConnected = {
- type: "server.connected"
- properties: {
- [key: string]: unknown
- }
-}
-
-export type EventGlobalDisposed = {
- type: "global.disposed"
- properties: {
- [key: string]: unknown
- }
-}
-
-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<SnapshotFileDiff>
- } | 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 Prompt = {
+ text: string
+ files?: Array<PromptFileAttachment>
+ agents?: Array<PromptAgentAttachment>
}
export type GlobalEvent = {
@@ -1156,6 +815,31 @@ export type GlobalEvent = {
| 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
@@ -1165,6 +849,31 @@ export type GlobalEvent = {
| 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
}
/**
@@ -1176,25 +885,10 @@ export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR"
* 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<string>
}
@@ -1229,28 +923,16 @@ export type PermissionConfig =
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
}
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
@@ -1259,13 +941,7 @@ export type AgentConfig = {
* 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]:
@@ -1306,21 +982,12 @@ export type ProviderConfig = {
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
}
@@ -1377,9 +1044,6 @@ export type ProviderConfig = {
*/
variants?: {
[key: string]: {
- /**
- * Disable this variant for the model
- */
disabled?: boolean
[key: string]: unknown | boolean | undefined
}
@@ -1397,38 +1061,17 @@ export type McpLocalConfig = {
* Command and arguments to run the MCP server
*/
command: Array<string>
- /**
- * Environment variables to set when running the MCP server
- */
environment?: {
[key: string]: 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
}
@@ -1441,13 +1084,7 @@ export type McpRemoteConfig = {
* 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
}
@@ -1455,9 +1092,6 @@ export type McpRemoteConfig = {
* 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
}
@@ -1467,19 +1101,10 @@ export type McpRemoteConfig = {
export type LayoutConfig = "auto" | "stretch"
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
@@ -1489,25 +1114,13 @@ export type Config = {
subtask?: boolean
}
}
- /**
- * Additional skill folder paths
- */
skills?: {
- /**
- * Additional paths to skill folders
- */
paths?: Array<string>
- /**
- * URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)
- */
urls?: Array<string>
}
watcher?: {
ignore?: Array<string>
}
- /**
- * 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
@@ -1518,53 +1131,23 @@ export type Config = {
},
]
>
- /**
- * 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<string>
- /**
- * When set, ONLY these providers will be enabled. All other providers will be ignored
- */
enabled_providers?: Array<string>
- /**
- * 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
}
- /**
- * Agent configuration, see https://opencode.ai/docs/agents
- */
agent?: {
plan?: AgentConfig
build?: AgentConfig
@@ -1575,15 +1158,9 @@ export type Config = {
compaction?: AgentConfig
[key: string]: AgentConfig | undefined
}
- /**
- * Custom provider configurations and model overrides
- */
provider?: {
[key: string]: ProviderConfig
}
- /**
- * MCP (Model Context Protocol) server configurations
- */
mcp?: {
[key: string]:
| McpLocalConfig
@@ -1629,9 +1206,6 @@ export type Config = {
}
}
}
- /**
- * Additional instruction files or patterns to include
- */
instructions?: Array<string>
layout?: LayoutConfig
permission?: PermissionConfig
@@ -1639,121 +1213,29 @@ export type Config = {
[key: string]: boolean
}
enterprise?: {
- /**
- * Enterprise URL
- */
url?: 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
}
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
}
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<string>
- /**
- * 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 BadRequestError = {
- data: unknown
- errors: Array<{
- [key: string]: unknown
- }>
- success: false
-}
-
-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
-
-export type Workspace = {
- id: string
- type: string
- name: string
- branch: string | null
- directory: string | null
- extra: unknown | null
- projectID: string
-}
-
-export type NotFoundError = {
- name: "NotFoundError"
- data: {
- message: string
- }
-}
-
export type Model = {
id: string
providerID: string
@@ -1845,8 +1327,6 @@ export type ConsoleState = {
switchableOrgCount: number
}
-export type ToolIds = Array<string>
-
export type ToolListItem = {
id: string
description: string
@@ -1855,11 +1335,7 @@ export type ToolListItem = {
export type ToolList = Array<ToolListItem>
-export type Worktree = {
- name: string
- branch: string
- directory: string
-}
+export type ToolIds = Array<string>
export type WorktreeCreateInput = {
name?: string
@@ -1869,6 +1345,12 @@ export type WorktreeCreateInput = {
startCommand?: string
}
+export type Worktree = {
+ name: string
+ branch: string
+ directory: string
+}
+
export type WorktreeRemoveInput = {
directory: string
}
@@ -1901,6 +1383,12 @@ export type GlobalSession = {
url: string
}
title: string
+ agent?: string
+ model?: {
+ id: string
+ providerID: string
+ variant?: string
+ }
version: string
time: {
created: number
@@ -1926,93 +1414,6 @@ export type McpResource = {
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
@@ -2059,88 +1460,6 @@ export type File = {
status: "added" | "deleted" | "modified"
}
-export type Event =
- | EventServerInstanceDisposed
- | EventFileEdited
- | EventFileWatcherUpdated
- | EventLspClientDiagnostics
- | EventLspUpdated
- | EventMessagePartDelta
- | EventPermissionAsked
- | EventPermissionReplied
- | EventSessionDiff
- | EventSessionError
- | EventInstallationUpdated
- | EventInstallationUpdateAvailable
- | EventQuestionAsked
- | EventQuestionReplied
- | EventQuestionRejected
- | EventTodoUpdated
- | EventSessionStatus
- | EventSessionIdle
- | EventSessionCompacted
- | EventTuiPromptAppend
- | EventTuiCommandExecute
- | EventTuiToastShow
- | EventTuiSessionSelect
- | EventMcpToolsChanged
- | EventMcpBrowserOpenFailed
- | EventCommandExecuted
- | EventProjectUpdated
- | EventVcsBranchUpdated
- | EventWorkspaceReady
- | EventWorkspaceFailed
- | EventWorkspaceRestore
- | EventWorkspaceStatus
- | EventWorktreeReady
- | EventWorktreeFailed
- | EventPtyCreated
- | EventPtyUpdated
- | EventPtyExited
- | EventPtyDeleted
- | EventMessageUpdated
- | EventMessageRemoved
- | EventMessagePartUpdated
- | EventMessagePartRemoved
- | EventSessionCreated
- | EventSessionUpdated
- | EventSessionDeleted
- | 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
@@ -2208,6 +1527,1791 @@ export type FormatterStatus = {
enabled: boolean
}
+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 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<SessionInfo>
+ cursor: {
+ previous?: string
+ next?: string
+ }
+}
+
+export type V2SessionMessagesResponse = {
+ items: Array<SessionMessage>
+ cursor: {
+ previous?: string
+ next?: string
+ }
+}
+
+export type EventTuiPromptAppend2 = {
+ type: "tui.prompt.append"
+ properties: {
+ text: string
+ }
+}
+
+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: "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<SnapshotFileDiff>
+ } | 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 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
+ }
+}
+
+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"
+ 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
+ }
+}
+
+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 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 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<ToolTextContent | ToolFileContent>
+ }
+}
+
+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<ToolTextContent | ToolFileContent>
+ 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
+ }
+}
+
+export type EventServerInstanceDisposed = {
+ id: string
+ type: "server.instance.disposed"
+ properties: {
+ directory: string
+ }
+}
+
+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"
+ 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
+ }
+}
+
+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 EventSessionDiff = {
+ id: string
+ type: "session.diff"
+ properties: {
+ sessionID: string
+ diff: Array<SnapshotFileDiff>
+ }
+}
+
+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
+ }
+}
+
+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<Todo>
+ }
+}
+
+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 EventProjectUpdated = {
+ id: string
+ type: "project.updated"
+ properties: Project
+}
+
+export type EventVcsBranchUpdated = {
+ id: string
+ type: "vcs.branch.updated"
+ properties: {
+ branch?: string
+ }
+}
+
+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"
+ 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<ToolTextContent | ToolFileContent>
+ }
+}
+
+export type EventSessionNextToolSuccess = {
+ id: string
+ type: "session.next.tool.success"
+ properties: {
+ timestamp: number
+ sessionID: string
+ callID: string
+ structured: {
+ [key: string]: unknown
+ }
+ content: Array<ToolTextContent | ToolFileContent>
+ 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
+ }
+ }
+ }
+}
+
+export type SessionNextRetryError = {
+ message: string
+ statusCode?: number
+ isRetryable: boolean
+ responseHeaders?: {
+ [key: string]: string
+ }
+ responseBody?: string
+ metadata?: {
+ [key: string]: string
+ }
+}
+
+export type EventSessionNextRetried = {
+ id: string
+ type: "session.next.retried"
+ properties: {
+ timestamp: number
+ sessionID: string
+ attempt: number
+ error: SessionNextRetryError
+ }
+}
+
+export type EventSessionNextCompactionStarted = {
+ id: string
+ type: "session.next.compaction.started"
+ properties: {
+ timestamp: number
+ sessionID: string
+ reason: "auto" | "manual"
+ }
+}
+
+export type EventSessionNextCompactionDelta = {
+ id: string
+ type: "session.next.compaction.delta"
+ properties: {
+ timestamp: number
+ sessionID: string
+ text: string
+ }
+}
+
+export type EventSessionNextCompactionEnded = {
+ id: string
+ type: "session.next.compaction.ended"
+ properties: {
+ timestamp: number
+ sessionID: string
+ text: string
+ include?: string
+ }
+}
+
+export type EventServerConnected = {
+ id: string
+ type: "server.connected"
+ properties: {
+ [key: string]: unknown
+ }
+}
+
+export type EventGlobalDisposed = {
+ id: string
+ type: "global.disposed"
+ properties: {
+ [key: string]: unknown
+ }
+}
+
+export type 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<PromptFileAttachment>
+ agents?: Array<PromptAgentAttachment>
+ 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<ToolTextContent | ToolFileContent>
+}
+
+export type SessionMessageToolStateCompleted = {
+ status: "completed"
+ input: {
+ [key: string]: unknown
+ }
+ attachments?: Array<PromptFileAttachment>
+ content: Array<ToolTextContent | ToolFileContent>
+ structured: {
+ [key: string]: unknown
+ }
+}
+
+export type SessionMessageToolStateError = {
+ status: "error"
+ input: {
+ [key: string]: unknown
+ }
+ content: Array<ToolTextContent | ToolFileContent>
+ 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<SessionMessageAssistantText | SessionMessageAssistantReasoning | SessionMessageAssistantTool>
+ 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 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
+ }
+}
+
+export type AuthRemoveData = {
+ body?: never
+ path: {
+ providerID: 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
+ }
+ }
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/log"
+}
+
+export type AppLogErrors = {
+ /**
+ * Bad request
+ */
+ 400: BadRequestError
+}
+
+export type AppLogError = AppLogErrors[keyof AppLogErrors]
+
+export type AppLogResponses = {
+ /**
+ * Log entry written successfully
+ */
+ 200: boolean
+}
+
+export type AppLogResponse = AppLogResponses[keyof AppLogResponses]
+
export type GlobalHealthData = {
body?: never
path?: never
@@ -2335,276 +3439,924 @@ 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<Provider>
+ 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 AuthSetError = AuthSetErrors[keyof AuthSetErrors]
+export type ToolListError = ToolListErrors[keyof ToolListErrors]
-export type AuthSetResponses = {
+export type ToolListResponses = {
/**
- * Successfully set authentication credentials
+ * 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 ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors]
+
+export type ToolIdsResponses = {
+ /**
+ * Tool IDs
+ */
+ 200: ToolIds
+}
+
+export type ToolIdsResponse = ToolIdsResponses[keyof ToolIdsResponses]
+
+export type WorktreeRemoveData = {
+ body?: WorktreeRemoveInput
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/experimental/worktree"
+}
+
+export type WorktreeRemoveErrors = {
+ /**
+ * Bad request
+ */
+ 400: BadRequestError
+}
+
+export type WorktreeRemoveError = WorktreeRemoveErrors[keyof WorktreeRemoveErrors]
+
+export type WorktreeRemoveResponses = {
+ /**
+ * Worktree removed
*/
200: boolean
}
-export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses]
+export type WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses]
-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 WorktreeListData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
}
+ url: "/experimental/worktree"
+}
+
+export type WorktreeListResponses = {
+ /**
+ * List of worktree directories
+ */
+ 200: Array<string>
+}
+
+export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses]
+
+export type WorktreeCreateData = {
+ body?: WorktreeCreateInput
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/log"
+ url: "/experimental/worktree"
}
-export type AppLogErrors = {
+export type WorktreeCreateErrors = {
/**
* Bad request
*/
400: BadRequestError
}
-export type AppLogError = AppLogErrors[keyof AppLogErrors]
+export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors]
-export type AppLogResponses = {
+export type WorktreeCreateResponses = {
/**
- * Log entry written successfully
+ * Worktree created
+ */
+ 200: Worktree
+}
+
+export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses]
+
+export type WorktreeResetData = {
+ body?: WorktreeResetInput
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/experimental/worktree/reset"
+}
+
+export type WorktreeResetErrors = {
+ /**
+ * Bad request
+ */
+ 400: BadRequestError
+}
+
+export type WorktreeResetError = WorktreeResetErrors[keyof WorktreeResetErrors]
+
+export type WorktreeResetResponses = {
+ /**
+ * Worktree reset
*/
200: boolean
}
-export type AppLogResponse = AppLogResponses[keyof AppLogResponses]
+export type WorktreeResetResponse = WorktreeResetResponses[keyof WorktreeResetResponses]
-export type ExperimentalWorkspaceAdapterListData = {
+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/session"
+}
+
+export type ExperimentalSessionListResponses = {
+ /**
+ * List of sessions
+ */
+ 200: Array<GlobalSession>
+}
+
+export type ExperimentalSessionListResponse = ExperimentalSessionListResponses[keyof ExperimentalSessionListResponses]
+
+export type ExperimentalResourceListData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/workspace/adapter"
+ url: "/experimental/resource"
}
-export type ExperimentalWorkspaceAdapterListResponses = {
+export type ExperimentalResourceListResponses = {
/**
- * Workspace adapters
+ * MCP resources
+ */
+ 200: {
+ [key: string]: McpResource
+ }
+}
+
+export type ExperimentalResourceListResponse =
+ ExperimentalResourceListResponses[keyof ExperimentalResourceListResponses]
+
+export type FindTextData = {
+ body?: never
+ path?: never
+ query: {
+ directory?: string
+ workspace?: string
+ pattern: string
+ }
+ url: "/find"
+}
+
+export type FindTextResponses = {
+ /**
+ * Matches
+ */
+ 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 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 FindFilesResponses = {
+ /**
+ * File paths
+ */
+ 200: Array<string>
+}
+
+export type FindFilesResponse = FindFilesResponses[keyof FindFilesResponses]
+
+export type FindSymbolsData = {
+ body?: never
+ path?: never
+ query: {
+ directory?: string
+ workspace?: string
+ query: string
+ }
+ url: "/find/symbol"
+}
+
+export type FindSymbolsResponses = {
+ /**
+ * Symbols
+ */
+ 200: Array<Symbol>
+}
+
+export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses]
+
+export type FileListData = {
+ body?: never
+ path?: never
+ query: {
+ directory?: string
+ workspace?: string
+ path: string
+ }
+ url: "/file"
+}
+
+export type FileListResponses = {
+ /**
+ * Files and directories
+ */
+ 200: Array<FileNode>
+}
+
+export type FileListResponse = FileListResponses[keyof FileListResponses]
+
+export type FileReadData = {
+ body?: never
+ path?: never
+ query: {
+ directory?: string
+ workspace?: string
+ path: string
+ }
+ url: "/file/content"
+}
+
+export type FileReadResponses = {
+ /**
+ * File content
+ */
+ 200: FileContent
+}
+
+export type FileReadResponse = FileReadResponses[keyof FileReadResponses]
+
+export type FileStatusData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/file/status"
+}
+
+export type FileStatusResponses = {
+ /**
+ * File status
+ */
+ 200: Array<File>
+}
+
+export type FileStatusResponse = FileStatusResponses[keyof FileStatusResponses]
+
+export type InstanceDisposeData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/instance/dispose"
+}
+
+export type InstanceDisposeResponses = {
+ /**
+ * Instance disposed
+ */
+ 200: boolean
+}
+
+export type InstanceDisposeResponse = InstanceDisposeResponses[keyof InstanceDisposeResponses]
+
+export type PathGetData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/path"
+}
+
+export type PathGetResponses = {
+ /**
+ * Path
+ */
+ 200: Path
+}
+
+export type PathGetResponse = PathGetResponses[keyof PathGetResponses]
+
+export type VcsGetData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/vcs"
+}
+
+export type VcsGetResponses = {
+ /**
+ * VCS info
+ */
+ 200: VcsInfo
+}
+
+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 VcsDiffResponses = {
+ /**
+ * VCS diff
+ */
+ 200: Array<VcsFileDiff>
+}
+
+export type VcsDiffResponse = VcsDiffResponses[keyof VcsDiffResponses]
+
+export type CommandListData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/command"
+}
+
+export type CommandListResponses = {
+ /**
+ * List of commands
+ */
+ 200: Array<Command>
+}
+
+export type CommandListResponse = CommandListResponses[keyof CommandListResponses]
+
+export type AppAgentsData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/agent"
+}
+
+export type AppAgentsResponses = {
+ /**
+ * List of agents
+ */
+ 200: Array<Agent>
+}
+
+export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses]
+
+export type AppSkillsData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/skill"
+}
+
+export type AppSkillsResponses = {
+ /**
+ * List of skills
*/
200: Array<{
- type: string
name: string
description: string
+ location: string
+ content: string
}>
}
-export type ExperimentalWorkspaceAdapterListResponse =
- ExperimentalWorkspaceAdapterListResponses[keyof ExperimentalWorkspaceAdapterListResponses]
+export type AppSkillsResponse = AppSkillsResponses[keyof AppSkillsResponses]
-export type ExperimentalWorkspaceListData = {
+export type LspStatusData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/workspace"
+ url: "/lsp"
}
-export type ExperimentalWorkspaceListResponses = {
+export type LspStatusResponses = {
/**
- * Workspaces
+ * LSP server status
*/
- 200: Array<Workspace>
+ 200: Array<LspStatus>
}
-export type ExperimentalWorkspaceListResponse =
- ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses]
+export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses]
-export type ExperimentalWorkspaceCreateData = {
+export type FormatterStatusData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/formatter"
+}
+
+export type FormatterStatusResponses = {
+ /**
+ * Formatter status
+ */
+ 200: Array<FormatterStatus>
+}
+
+export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses]
+
+export type McpStatusData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/mcp"
+}
+
+export type McpStatusResponses = {
+ /**
+ * MCP server status
+ */
+ 200: {
+ [key: string]: McpStatus
+ }
+}
+
+export type McpStatusResponse = McpStatusResponses[keyof McpStatusResponses]
+
+export type McpAddData = {
body?: {
- id?: string
- type: string
- branch: string | null
- extra: unknown | null
+ name: string
+ config: McpLocalConfig | McpRemoteConfig
}
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/workspace"
+ url: "/mcp"
}
-export type ExperimentalWorkspaceCreateErrors = {
+export type McpAddErrors = {
/**
* Bad request
*/
400: BadRequestError
}
-export type ExperimentalWorkspaceCreateError =
- ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors]
+export type McpAddError = McpAddErrors[keyof McpAddErrors]
-export type ExperimentalWorkspaceCreateResponses = {
+export type McpAddResponses = {
/**
- * Workspace created
+ * MCP server added successfully
*/
- 200: Workspace
+ 200: {
+ [key: string]: McpStatus
+ }
}
-export type ExperimentalWorkspaceCreateResponse =
- ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses]
+export type McpAddResponse = McpAddResponses[keyof McpAddResponses]
-export type ExperimentalWorkspaceStatusData = {
+export type McpAuthRemoveData = {
body?: never
- path?: never
+ path: {
+ name: string
+ }
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/workspace/status"
+ url: "/mcp/{name}/auth"
}
-export type ExperimentalWorkspaceStatusResponses = {
+export type McpAuthRemoveErrors = {
/**
- * Workspace status
+ * Not found
*/
- 200: Array<{
- workspaceID: string
- status: "connected" | "connecting" | "disconnected" | "error"
- }>
+ 404: NotFoundError
}
-export type ExperimentalWorkspaceStatusResponse =
- ExperimentalWorkspaceStatusResponses[keyof ExperimentalWorkspaceStatusResponses]
+export type McpAuthRemoveError = McpAuthRemoveErrors[keyof McpAuthRemoveErrors]
-export type ExperimentalWorkspaceRemoveData = {
+export type McpAuthRemoveResponses = {
+ /**
+ * OAuth credentials removed
+ */
+ 200: {
+ success: true
+ }
+}
+
+export type McpAuthRemoveResponse = McpAuthRemoveResponses[keyof McpAuthRemoveResponses]
+
+export type McpAuthStartData = {
body?: never
path: {
- id: string
+ name: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/workspace/{id}"
+ url: "/mcp/{name}/auth"
}
-export type ExperimentalWorkspaceRemoveErrors = {
+export type McpAuthStartErrors = {
/**
- * Bad request
+ * McpUnsupportedOAuthError
*/
- 400: BadRequestError
+ 400: McpUnsupportedOAuthError
+ /**
+ * Not found
+ */
+ 404: NotFoundError
}
-export type ExperimentalWorkspaceRemoveError =
- ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors]
+export type McpAuthStartError = McpAuthStartErrors[keyof McpAuthStartErrors]
-export type ExperimentalWorkspaceRemoveResponses = {
+export type McpAuthStartResponses = {
/**
- * Workspace removed
+ * OAuth flow started
*/
- 200: Workspace
+ 200: {
+ authorizationUrl: string
+ oauthState: string
+ }
}
-export type ExperimentalWorkspaceRemoveResponse =
- ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses]
+export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartResponses]
-export type ExperimentalWorkspaceSessionRestoreData = {
+export type McpAuthCallbackData = {
body?: {
- sessionID: string
+ code: string
}
path: {
- id: string
+ name: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/workspace/{id}/session-restore"
+ url: "/mcp/{name}/auth/callback"
}
-export type ExperimentalWorkspaceSessionRestoreErrors = {
+export type McpAuthCallbackErrors = {
/**
* Bad request
*/
400: BadRequestError
+ /**
+ * Not found
+ */
+ 404: NotFoundError
}
-export type ExperimentalWorkspaceSessionRestoreError =
- ExperimentalWorkspaceSessionRestoreErrors[keyof ExperimentalWorkspaceSessionRestoreErrors]
+export type McpAuthCallbackError = McpAuthCallbackErrors[keyof McpAuthCallbackErrors]
-export type ExperimentalWorkspaceSessionRestoreResponses = {
+export type McpAuthCallbackResponses = {
/**
- * Session replay started
+ * OAuth authentication completed
*/
- 200: {
- total: number
+ 200: McpStatus
+}
+
+export type McpAuthCallbackResponse = McpAuthCallbackResponses[keyof McpAuthCallbackResponses]
+
+export type McpAuthAuthenticateData = {
+ body?: never
+ path: {
+ name: string
+ }
+ query?: {
+ directory?: string
+ workspace?: string
}
+ url: "/mcp/{name}/auth/authenticate"
}
-export type ExperimentalWorkspaceSessionRestoreResponse =
- ExperimentalWorkspaceSessionRestoreResponses[keyof ExperimentalWorkspaceSessionRestoreResponses]
+export type McpAuthAuthenticateErrors = {
+ /**
+ * McpUnsupportedOAuthError
+ */
+ 400: McpUnsupportedOAuthError
+ /**
+ * Not found
+ */
+ 404: NotFoundError
+}
+
+export type McpAuthAuthenticateError = McpAuthAuthenticateErrors[keyof McpAuthAuthenticateErrors]
+
+export type McpAuthAuthenticateResponses = {
+ /**
+ * OAuth authentication completed
+ */
+ 200: McpStatus
+}
+
+export type McpAuthAuthenticateResponse = McpAuthAuthenticateResponses[keyof McpAuthAuthenticateResponses]
+
+export type McpConnectData = {
+ body?: never
+ path: {
+ name: string
+ }
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/mcp/{name}/connect"
+}
+
+export type McpConnectResponses = {
+ /**
+ * MCP server connected successfully
+ */
+ 200: boolean
+}
+
+export type McpConnectResponse = McpConnectResponses[keyof McpConnectResponses]
+
+export type McpDisconnectData = {
+ body?: never
+ path: {
+ name: string
+ }
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/mcp/{name}/disconnect"
+}
+
+export type McpDisconnectResponses = {
+ /**
+ * MCP server disconnected successfully
+ */
+ 200: boolean
+}
+
+export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses]
export type ProjectListData = {
body?: never
@@ -2884,439 +4636,285 @@ export type PtyUpdateResponses = {
export type PtyUpdateResponse = PtyUpdateResponses[keyof PtyUpdateResponses]
-export type PtyConnectData = {
- body?: never
- path: {
- ptyID: string
- }
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/pty/{ptyID}/connect"
-}
-
-export type PtyConnectErrors = {
- /**
- * Not found
- */
- 404: NotFoundError
-}
-
-export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors]
-
-export type PtyConnectResponses = {
- /**
- * Connected session
- */
- 200: boolean
-}
-
-export type PtyConnectResponse = PtyConnectResponses[keyof PtyConnectResponses]
-
-export type ConfigGetData = {
+export type QuestionListData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/config"
+ url: "/question"
}
-export type ConfigGetResponses = {
+export type QuestionListResponses = {
/**
- * Get config info
+ * List of pending questions
*/
- 200: Config
+ 200: Array<QuestionRequest>
}
-export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses]
+export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses]
-export type ConfigUpdateData = {
- body?: Config
- path?: never
+export type QuestionReplyData = {
+ body?: {
+ /**
+ * User answers in order of questions (each answer is an array of selected labels)
+ */
+ answers: Array<QuestionAnswer>
+ }
+ path: {
+ requestID: string
+ }
query?: {
directory?: string
workspace?: string
}
- url: "/config"
+ url: "/question/{requestID}/reply"
}
-export type ConfigUpdateErrors = {
+export type QuestionReplyErrors = {
/**
* Bad request
*/
400: BadRequestError
-}
-
-export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors]
-
-export type ConfigUpdateResponses = {
/**
- * Successfully updated config
+ * Not found
*/
- 200: Config
+ 404: NotFoundError
}
-export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses]
-
-export type ConfigProvidersData = {
- body?: never
- path?: never
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/config/providers"
-}
+export type QuestionReplyError = QuestionReplyErrors[keyof QuestionReplyErrors]
-export type ConfigProvidersResponses = {
+export type QuestionReplyResponses = {
/**
- * List of providers
+ * Question answered successfully
*/
- 200: {
- providers: Array<Provider>
- default: {
- [key: string]: string
- }
- }
+ 200: boolean
}
-export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses]
+export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses]
-export type ExperimentalConsoleGetData = {
+export type QuestionRejectData = {
body?: never
- path?: never
+ path: {
+ requestID: string
+ }
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/console"
+ url: "/question/{requestID}/reject"
}
-export type ExperimentalConsoleGetResponses = {
+export type QuestionRejectErrors = {
/**
- * Active Console provider metadata
+ * Bad request
*/
- 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 = {
+ 400: BadRequestError
/**
- * Switchable Console orgs
+ * Not found
*/
- 200: {
- orgs: Array<{
- accountID: string
- accountEmail: string
- accountUrl: string
- orgID: string
- orgName: string
- active: boolean
- }>
- }
+ 404: NotFoundError
}
-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 QuestionRejectError = QuestionRejectErrors[keyof QuestionRejectErrors]
-export type ExperimentalConsoleSwitchOrgResponses = {
+export type QuestionRejectResponses = {
/**
- * Switch success
+ * Question rejected successfully
*/
200: boolean
}
-export type ExperimentalConsoleSwitchOrgResponse =
- ExperimentalConsoleSwitchOrgResponses[keyof ExperimentalConsoleSwitchOrgResponses]
+export type QuestionRejectResponse = QuestionRejectResponses[keyof QuestionRejectResponses]
-export type ToolIdsData = {
+export type PermissionListData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/tool/ids"
-}
-
-export type ToolIdsErrors = {
- /**
- * Bad request
- */
- 400: BadRequestError
+ url: "/permission"
}
-export type ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors]
-
-export type ToolIdsResponses = {
+export type PermissionListResponses = {
/**
- * Tool IDs
+ * List of pending permissions
*/
- 200: ToolIds
+ 200: Array<PermissionRequest>
}
-export type ToolIdsResponse = ToolIdsResponses[keyof ToolIdsResponses]
+export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses]
-export type ToolListData = {
- body?: never
- path?: never
- query: {
- directory?: string
- workspace?: string
- provider: string
- model: string
+export type PermissionReplyData = {
+ body?: {
+ reply: "once" | "always" | "reject"
+ message?: string
+ }
+ path: {
+ requestID: string
}
- url: "/experimental/tool"
-}
-
-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 WorktreeRemoveData = {
- body?: WorktreeRemoveInput
- path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/worktree"
+ url: "/permission/{requestID}/reply"
}
-export type WorktreeRemoveErrors = {
+export type PermissionReplyErrors = {
/**
* Bad request
*/
400: BadRequestError
+ /**
+ * Not found
+ */
+ 404: NotFoundError
}
-export type WorktreeRemoveError = WorktreeRemoveErrors[keyof WorktreeRemoveErrors]
+export type PermissionReplyError = PermissionReplyErrors[keyof PermissionReplyErrors]
-export type WorktreeRemoveResponses = {
+export type PermissionReplyResponses = {
/**
- * Worktree removed
+ * Permission processed successfully
*/
200: boolean
}
-export type WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses]
+export type PermissionReplyResponse = PermissionReplyResponses[keyof PermissionReplyResponses]
-export type WorktreeListData = {
+export type ProviderListData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/worktree"
+ url: "/provider"
}
-export type WorktreeListResponses = {
+export type ProviderListResponses = {
/**
- * List of worktree directories
+ * List of providers
*/
- 200: Array<string>
+ 200: {
+ all: Array<Provider>
+ default: {
+ [key: string]: string
+ }
+ connected: Array<string>
+ }
}
-export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses]
+export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses]
-export type WorktreeCreateData = {
- body?: WorktreeCreateInput
+export type ProviderAuthData = {
+ body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/worktree"
-}
-
-export type WorktreeCreateErrors = {
- /**
- * Bad request
- */
- 400: BadRequestError
+ url: "/provider/auth"
}
-export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors]
-
-export type WorktreeCreateResponses = {
+export type ProviderAuthResponses = {
/**
- * Worktree created
+ * Provider auth methods
*/
- 200: Worktree
+ 200: {
+ [key: string]: Array<ProviderAuthMethod>
+ }
}
-export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses]
+export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses]
-export type WorktreeResetData = {
- body?: WorktreeResetInput
- path?: never
+export type ProviderOauthAuthorizeData = {
+ body?: {
+ /**
+ * Auth method index
+ */
+ method: number
+ inputs?: {
+ [key: string]: string
+ }
+ }
+ path: {
+ providerID: string
+ }
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/worktree/reset"
+ url: "/provider/{providerID}/oauth/authorize"
}
-export type WorktreeResetErrors = {
+export type ProviderOauthAuthorizeErrors = {
/**
* Bad request
*/
400: BadRequestError
}
-export type WorktreeResetError = WorktreeResetErrors[keyof WorktreeResetErrors]
+export type ProviderOauthAuthorizeError = ProviderOauthAuthorizeErrors[keyof ProviderOauthAuthorizeErrors]
-export type WorktreeResetResponses = {
+export type ProviderOauthAuthorizeResponses = {
/**
- * Worktree reset
+ * Authorization URL and method
*/
- 200: boolean
+ 200: ProviderAuthAuthorization
}
-export type WorktreeResetResponse = WorktreeResetResponses[keyof WorktreeResetResponses]
+export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses]
-export type ExperimentalSessionListData = {
- body?: never
- path?: never
- query?: {
+export type ProviderOauthCallbackData = {
+ body?: {
/**
- * Filter sessions by project directory
+ * Auth method index
*/
+ method: number
+ code?: string
+ }
+ path: {
+ providerID: string
+ }
+ query?: {
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"
+ url: "/provider/{providerID}/oauth/callback"
}
-export type ExperimentalSessionListResponses = {
+export type ProviderOauthCallbackErrors = {
/**
- * List of sessions
+ * Bad request
*/
- 200: Array<GlobalSession>
+ 400: BadRequestError
}
-export type ExperimentalSessionListResponse = ExperimentalSessionListResponses[keyof ExperimentalSessionListResponses]
-
-export type ExperimentalResourceListData = {
- body?: never
- path?: never
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/experimental/resource"
-}
+export type ProviderOauthCallbackError = ProviderOauthCallbackErrors[keyof ProviderOauthCallbackErrors]
-export type ExperimentalResourceListResponses = {
+export type ProviderOauthCallbackResponses = {
/**
- * MCP resources
+ * OAuth callback processed successfully
*/
- 200: {
- [key: string]: McpResource
- }
+ 200: boolean
}
-export type ExperimentalResourceListResponse =
- ExperimentalResourceListResponses[keyof ExperimentalResourceListResponses]
+export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses]
export type SessionListData = {
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"
@@ -3335,6 +4933,12 @@ export type SessionCreateData = {
body?: {
parentID?: string
title?: string
+ agent?: string
+ model?: {
+ id: string
+ providerID: string
+ variant?: string
+ }
permission?: PermissionRuleset
workspaceID?: string
}
@@ -3570,68 +5174,29 @@ export type SessionTodoResponses = {
export type SessionTodoResponse = SessionTodoResponses[keyof SessionTodoResponses]
-export type SessionInitData = {
- body?: {
- modelID: string
- providerID: string
- messageID: string
- }
+export type SessionDiffData = {
+ body?: never
path: {
sessionID: string
}
query?: {
directory?: string
workspace?: string
- }
- url: "/session/{sessionID}/init"
-}
-
-export type SessionInitErrors = {
- /**
- * Bad request
- */
- 400: BadRequestError
- /**
- * Not found
- */
- 404: NotFoundError
-}
-
-export type SessionInitError = SessionInitErrors[keyof SessionInitErrors]
-
-export type SessionInitResponses = {
- /**
- * 200
- */
- 200: boolean
-}
-
-export type SessionInitResponse = SessionInitResponses[keyof SessionInitResponses]
-
-export type SessionForkData = {
- body?: {
messageID?: string
}
- path: {
- sessionID: string
- }
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/session/{sessionID}/fork"
+ url: "/session/{sessionID}/diff"
}
-export type SessionForkResponses = {
+export type SessionDiffResponses = {
/**
- * 200
+ * Successfully retrieved diff
*/
- 200: Session
+ 200: Array<SnapshotFileDiff>
}
-export type SessionForkResponse = SessionForkResponses[keyof SessionForkResponses]
+export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses]
-export type SessionAbortData = {
+export type SessionMessagesData = {
body?: never
path: {
sessionID: string
@@ -3639,11 +5204,13 @@ export type SessionAbortData = {
query?: {
directory?: string
workspace?: string
+ limit?: number
+ before?: string
}
- url: "/session/{sessionID}/abort"
+ url: "/session/{sessionID}/message"
}
-export type SessionAbortErrors = {
+export type SessionMessagesErrors = {
/**
* Bad request
*/
@@ -3654,19 +5221,37 @@ export type SessionAbortErrors = {
404: NotFoundError
}
-export type SessionAbortError = SessionAbortErrors[keyof SessionAbortErrors]
+export type SessionMessagesError = SessionMessagesErrors[keyof SessionMessagesErrors]
-export type SessionAbortResponses = {
+export type SessionMessagesResponses = {
/**
- * Aborted session
+ * List of messages
*/
- 200: boolean
+ 200: Array<{
+ info: Message
+ parts: Array<Part>
+ }>
}
-export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses]
+export type SessionMessagesResponse = SessionMessagesResponses[keyof SessionMessagesResponses]
-export type SessionUnshareData = {
- body?: never
+export type SessionPromptData = {
+ body?: {
+ messageID?: string
+ model?: {
+ providerID: string
+ modelID: string
+ }
+ agent?: string
+ noReply?: boolean
+ tools?: {
+ [key: string]: boolean
+ }
+ format?: OutputFormat
+ system?: string
+ variant?: string
+ parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
+ }
path: {
sessionID: string
}
@@ -3674,10 +5259,10 @@ export type SessionUnshareData = {
directory?: string
workspace?: string
}
- url: "/session/{sessionID}/share"
+ url: "/session/{sessionID}/message"
}
-export type SessionUnshareErrors = {
+export type SessionPromptErrors = {
/**
* Bad request
*/
@@ -3688,30 +5273,34 @@ export type SessionUnshareErrors = {
404: NotFoundError
}
-export type SessionUnshareError = SessionUnshareErrors[keyof SessionUnshareErrors]
+export type SessionPromptError = SessionPromptErrors[keyof SessionPromptErrors]
-export type SessionUnshareResponses = {
+export type SessionPromptResponses = {
/**
- * Successfully unshared session
+ * Created message
*/
- 200: Session
+ 200: {
+ info: AssistantMessage
+ parts: Array<Part>
+ }
}
-export type SessionUnshareResponse = SessionUnshareResponses[keyof SessionUnshareResponses]
+export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses]
-export type SessionShareData = {
+export type SessionDeleteMessageData = {
body?: never
path: {
sessionID: string
+ messageID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/session/{sessionID}/share"
+ url: "/session/{sessionID}/message/{messageID}"
}
-export type SessionShareErrors = {
+export type SessionDeleteMessageErrors = {
/**
* Bad request
*/
@@ -3722,56 +5311,31 @@ export type SessionShareErrors = {
404: NotFoundError
}
-export type SessionShareError = SessionShareErrors[keyof SessionShareErrors]
+export type SessionDeleteMessageError = SessionDeleteMessageErrors[keyof SessionDeleteMessageErrors]
-export type SessionShareResponses = {
+export type SessionDeleteMessageResponses = {
/**
- * Successfully shared session
+ * Successfully deleted message
*/
- 200: Session
+ 200: boolean
}
-export type SessionShareResponse = SessionShareResponses[keyof SessionShareResponses]
+export type SessionDeleteMessageResponse = SessionDeleteMessageResponses[keyof SessionDeleteMessageResponses]
-export type SessionDiffData = {
+export type SessionMessageData = {
body?: never
path: {
sessionID: string
- }
- query?: {
- directory?: string
- workspace?: string
- messageID?: string
- }
- url: "/session/{sessionID}/diff"
-}
-
-export type SessionDiffResponses = {
- /**
- * Successfully retrieved diff
- */
- 200: Array<SnapshotFileDiff>
-}
-
-export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses]
-
-export type SessionSummarizeData = {
- body?: {
- providerID: string
- modelID: string
- auto?: boolean
- }
- path: {
- sessionID: string
+ messageID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/session/{sessionID}/summarize"
+ url: "/session/{sessionID}/message/{messageID}"
}
-export type SessionSummarizeErrors = {
+export type SessionMessageErrors = {
/**
* Bad request
*/
@@ -3782,79 +5346,45 @@ export type SessionSummarizeErrors = {
404: NotFoundError
}
-export type SessionSummarizeError = SessionSummarizeErrors[keyof SessionSummarizeErrors]
+export type SessionMessageError = SessionMessageErrors[keyof SessionMessageErrors]
-export type SessionSummarizeResponses = {
+export type SessionMessageResponses = {
/**
- * Summarized session
+ * Message
*/
- 200: boolean
+ 200: {
+ info: Message
+ parts: Array<Part>
+ }
}
-export type SessionSummarizeResponse = SessionSummarizeResponses[keyof SessionSummarizeResponses]
+export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses]
-export type SessionMessagesData = {
- body?: never
+export type SessionForkData = {
+ body?: {
+ messageID?: string
+ }
path: {
sessionID: string
}
query?: {
directory?: string
workspace?: string
- /**
- * Maximum number of messages to return
- */
- limit?: number
- before?: string
}
- url: "/session/{sessionID}/message"
-}
-
-export type SessionMessagesErrors = {
- /**
- * Bad request
- */
- 400: BadRequestError
- /**
- * Not found
- */
- 404: NotFoundError
+ url: "/session/{sessionID}/fork"
}
-export type SessionMessagesError = SessionMessagesErrors[keyof SessionMessagesErrors]
-
-export type SessionMessagesResponses = {
+export type SessionForkResponses = {
/**
- * List of messages
+ * 200
*/
- 200: Array<{
- info: Message
- parts: Array<Part>
- }>
+ 200: Session
}
-export type SessionMessagesResponse = SessionMessagesResponses[keyof SessionMessagesResponses]
+export type SessionForkResponse = SessionForkResponses[keyof SessionForkResponses]
-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<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
- }
+export type SessionAbortData = {
+ body?: never
path: {
sessionID: string
}
@@ -3862,10 +5392,10 @@ export type SessionPromptData = {
directory?: string
workspace?: string
}
- url: "/session/{sessionID}/message"
+ url: "/session/{sessionID}/abort"
}
-export type SessionPromptErrors = {
+export type SessionAbortErrors = {
/**
* Bad request
*/
@@ -3876,34 +5406,34 @@ export type SessionPromptErrors = {
404: NotFoundError
}
-export type SessionPromptError = SessionPromptErrors[keyof SessionPromptErrors]
+export type SessionAbortError = SessionAbortErrors[keyof SessionAbortErrors]
-export type SessionPromptResponses = {
+export type SessionAbortResponses = {
/**
- * Created message
+ * Aborted session
*/
- 200: {
- info: AssistantMessage
- parts: Array<Part>
- }
+ 200: boolean
}
-export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses]
+export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses]
-export type SessionDeleteMessageData = {
- body?: never
+export type SessionInitData = {
+ body?: {
+ modelID: string
+ providerID: string
+ messageID: string
+ }
path: {
sessionID: string
- messageID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/session/{sessionID}/message/{messageID}"
+ url: "/session/{sessionID}/init"
}
-export type SessionDeleteMessageErrors = {
+export type SessionInitErrors = {
/**
* Bad request
*/
@@ -3914,31 +5444,30 @@ export type SessionDeleteMessageErrors = {
404: NotFoundError
}
-export type SessionDeleteMessageError = SessionDeleteMessageErrors[keyof SessionDeleteMessageErrors]
+export type SessionInitError = SessionInitErrors[keyof SessionInitErrors]
-export type SessionDeleteMessageResponses = {
+export type SessionInitResponses = {
/**
- * Successfully deleted message
+ * 200
*/
200: boolean
}
-export type SessionDeleteMessageResponse = SessionDeleteMessageResponses[keyof SessionDeleteMessageResponses]
+export type SessionInitResponse = SessionInitResponses[keyof SessionInitResponses]
-export type SessionMessageData = {
+export type SessionUnshareData = {
body?: never
path: {
sessionID: string
- messageID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/session/{sessionID}/message/{messageID}"
+ url: "/session/{sessionID}/share"
}
-export type SessionMessageErrors = {
+export type SessionUnshareErrors = {
/**
* Bad request
*/
@@ -3949,35 +5478,30 @@ export type SessionMessageErrors = {
404: NotFoundError
}
-export type SessionMessageError = SessionMessageErrors[keyof SessionMessageErrors]
+export type SessionUnshareError = SessionUnshareErrors[keyof SessionUnshareErrors]
-export type SessionMessageResponses = {
+export type SessionUnshareResponses = {
/**
- * Message
+ * Successfully unshared session
*/
- 200: {
- info: Message
- parts: Array<Part>
- }
+ 200: Session
}
-export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses]
+export type SessionUnshareResponse = SessionUnshareResponses[keyof SessionUnshareResponses]
-export type PartDeleteData = {
+export type SessionShareData = {
body?: never
path: {
sessionID: string
- messageID: string
- partID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/session/{sessionID}/message/{messageID}/part/{partID}"
+ url: "/session/{sessionID}/share"
}
-export type PartDeleteErrors = {
+export type SessionShareErrors = {
/**
* Bad request
*/
@@ -3988,32 +5512,34 @@ export type PartDeleteErrors = {
404: NotFoundError
}
-export type PartDeleteError = PartDeleteErrors[keyof PartDeleteErrors]
+export type SessionShareError = SessionShareErrors[keyof SessionShareErrors]
-export type PartDeleteResponses = {
+export type SessionShareResponses = {
/**
- * Successfully deleted part
+ * Successfully shared session
*/
- 200: boolean
+ 200: Session
}
-export type PartDeleteResponse = PartDeleteResponses[keyof PartDeleteResponses]
+export type SessionShareResponse = SessionShareResponses[keyof SessionShareResponses]
-export type PartUpdateData = {
- body?: Part
+export type SessionSummarizeData = {
+ body?: {
+ providerID: string
+ modelID: string
+ auto?: boolean
+ }
path: {
sessionID: string
- messageID: string
- partID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/session/{sessionID}/message/{messageID}/part/{partID}"
+ url: "/session/{sessionID}/summarize"
}
-export type PartUpdateErrors = {
+export type SessionSummarizeErrors = {
/**
* Bad request
*/
@@ -4024,16 +5550,16 @@ export type PartUpdateErrors = {
404: NotFoundError
}
-export type PartUpdateError = PartUpdateErrors[keyof PartUpdateErrors]
+export type SessionSummarizeError = SessionSummarizeErrors[keyof SessionSummarizeErrors]
-export type PartUpdateResponses = {
+export type SessionSummarizeResponses = {
/**
- * Successfully updated part
+ * Summarized session
*/
- 200: Part
+ 200: boolean
}
-export type PartUpdateResponse = PartUpdateResponses[keyof PartUpdateResponses]
+export type SessionSummarizeResponse = SessionSummarizeResponses[keyof SessionSummarizeResponses]
export type SessionPromptAsyncData = {
body?: {
@@ -4044,9 +5570,6 @@ export type SessionPromptAsyncData = {
}
agent?: string
noReply?: boolean
- /**
- * @deprecated tools and permissions have been merged, you can set permissions on the session itself now
- */
tools?: {
[key: string]: boolean
}
@@ -4292,99 +5815,21 @@ export type PermissionRespondResponses = {
export type PermissionRespondResponse = PermissionRespondResponses[keyof PermissionRespondResponses]
-export type PermissionReplyData = {
- body?: {
- reply: "once" | "always" | "reject"
- message?: string
- }
- path: {
- requestID: string
- }
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/permission/{requestID}/reply"
-}
-
-export type PermissionReplyErrors = {
- /**
- * Bad request
- */
- 400: BadRequestError
- /**
- * Not found
- */
- 404: NotFoundError
-}
-
-export type PermissionReplyError = PermissionReplyErrors[keyof PermissionReplyErrors]
-
-export type PermissionReplyResponses = {
- /**
- * Permission processed successfully
- */
- 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<PermissionRequest>
-}
-
-export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses]
-
-export type QuestionListData = {
+export type PartDeleteData = {
body?: never
- path?: never
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/question"
-}
-
-export type QuestionListResponses = {
- /**
- * List of pending questions
- */
- 200: Array<QuestionRequest>
-}
-
-export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses]
-
-export type QuestionReplyData = {
- body?: {
- /**
- * User answers in order of questions (each answer is an array of selected labels)
- */
- answers: Array<QuestionAnswer>
- }
path: {
- requestID: string
+ sessionID: string
+ messageID: string
+ partID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/question/{requestID}/reply"
+ url: "/session/{sessionID}/message/{messageID}/part/{partID}"
}
-export type QuestionReplyErrors = {
+export type PartDeleteErrors = {
/**
* Bad request
*/
@@ -4395,30 +5840,32 @@ export type QuestionReplyErrors = {
404: NotFoundError
}
-export type QuestionReplyError = QuestionReplyErrors[keyof QuestionReplyErrors]
+export type PartDeleteError = PartDeleteErrors[keyof PartDeleteErrors]
-export type QuestionReplyResponses = {
+export type PartDeleteResponses = {
/**
- * Question answered successfully
+ * Successfully deleted part
*/
200: boolean
}
-export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses]
+export type PartDeleteResponse = PartDeleteResponses[keyof PartDeleteResponses]
-export type QuestionRejectData = {
- body?: never
+export type PartUpdateData = {
+ body?: Part
path: {
- requestID: string
+ sessionID: string
+ messageID: string
+ partID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/question/{requestID}/reject"
+ url: "/session/{sessionID}/message/{messageID}/part/{partID}"
}
-export type QuestionRejectErrors = {
+export type PartUpdateErrors = {
/**
* Bad request
*/
@@ -4429,148 +5876,16 @@ 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<Provider>
- default: {
- [key: string]: string
- }
- connected: Array<string>
- }
-}
-
-export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses]
-
-export type ProviderAuthData = {
- body?: never
- path?: never
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/provider/auth"
-}
-
-export type ProviderAuthResponses = {
- /**
- * Provider auth methods
- */
- 200: {
- [key: string]: Array<ProviderAuthMethod>
- }
-}
-
-export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses]
-
-export type ProviderOauthAuthorizeData = {
- body?: {
- /**
- * Auth method index
- */
- method: number
- /**
- * Prompt inputs
- */
- inputs?: {
- [key: string]: string
- }
- }
- path: {
- /**
- * Provider ID
- */
- providerID: string
- }
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/provider/{providerID}/oauth/authorize"
-}
-
-export type ProviderOauthAuthorizeErrors = {
- /**
- * Bad request
- */
- 400: BadRequestError
-}
-
-export type ProviderOauthAuthorizeError = ProviderOauthAuthorizeErrors[keyof ProviderOauthAuthorizeErrors]
-
-export type ProviderOauthAuthorizeResponses = {
- /**
- * Authorization URL and method
- */
- 200: ProviderAuthAuthorization
-}
-
-export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses]
-
-export type ProviderOauthCallbackData = {
- body?: {
- /**
- * Auth method index
- */
- method: number
- /**
- * OAuth authorization code
- */
- code?: string
- }
- path: {
- /**
- * Provider ID
- */
- providerID: string
- }
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/provider/{providerID}/oauth/callback"
-}
-
-export type ProviderOauthCallbackErrors = {
- /**
- * Bad request
- */
- 400: BadRequestError
-}
-
-export type ProviderOauthCallbackError = ProviderOauthCallbackErrors[keyof ProviderOauthCallbackErrors]
+export type PartUpdateError = PartUpdateErrors[keyof PartUpdateErrors]
-export type ProviderOauthCallbackResponses = {
+export type PartUpdateResponses = {
/**
- * OAuth callback processed successfully
+ * Successfully updated part
*/
- 200: boolean
+ 200: Part
}
-export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses]
+export type PartUpdateResponse = PartUpdateResponses[keyof PartUpdateResponses]
export type SyncStartData = {
body?: never
@@ -4670,402 +5985,150 @@ export type SyncHistoryListResponses = {
export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses]
-export type FindTextData = {
- body?: never
- path?: never
- query: {
- directory?: string
- workspace?: string
- pattern: string
- }
- url: "/find"
-}
-
-export type FindTextResponses = {
- /**
- * Matches
- */
- 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 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 FindFilesResponses = {
- /**
- * File paths
- */
- 200: Array<string>
-}
-
-export type FindFilesResponse = FindFilesResponses[keyof FindFilesResponses]
-
-export type FindSymbolsData = {
- body?: never
- path?: never
- query: {
- directory?: string
- workspace?: string
- query: string
- }
- url: "/find/symbol"
-}
-
-export type FindSymbolsResponses = {
- /**
- * Symbols
- */
- 200: Array<Symbol>
-}
-
-export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses]
-
-export type FileListData = {
- body?: never
- path?: never
- query: {
- directory?: string
- workspace?: string
- path: string
- }
- url: "/file"
-}
-
-export type FileListResponses = {
- /**
- * Files and directories
- */
- 200: Array<FileNode>
-}
-
-export type FileListResponse = FileListResponses[keyof FileListResponses]
-
-export type FileReadData = {
- body?: never
- path?: never
- query: {
- directory?: string
- workspace?: string
- path: string
- }
- url: "/file/content"
-}
-
-export type FileReadResponses = {
- /**
- * File content
- */
- 200: FileContent
-}
-
-export type FileReadResponse = FileReadResponses[keyof FileReadResponses]
-
-export type FileStatusData = {
- body?: never
- path?: never
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/file/status"
-}
-
-export type FileStatusResponses = {
- /**
- * File status
- */
- 200: Array<File>
-}
-
-export type FileStatusResponse = FileStatusResponses[keyof FileStatusResponses]
-
-export type EventSubscribeData = {
+export type V2SessionListData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/event"
+ url: "/api/session"
}
-export type EventSubscribeResponses = {
- /**
- * Event stream
- */
- 200: Event
-}
-
-export type EventSubscribeResponse = EventSubscribeResponses[keyof EventSubscribeResponses]
-
-export type McpStatusData = {
- body?: never
- path?: never
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/mcp"
-}
-
-export type McpStatusResponses = {
- /**
- * MCP server status
- */
- 200: {
- [key: string]: McpStatus
- }
-}
-
-export type McpStatusResponse = McpStatusResponses[keyof McpStatusResponses]
-
-export type McpAddData = {
- body?: {
- name: string
- config: McpLocalConfig | McpRemoteConfig
- }
- path?: never
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/mcp"
-}
-
-export type McpAddErrors = {
+export type V2SessionListErrors = {
/**
* Bad request
*/
400: BadRequestError
}
-export type McpAddError = McpAddErrors[keyof McpAddErrors]
+export type V2SessionListError = V2SessionListErrors[keyof V2SessionListErrors]
-export type McpAddResponses = {
+export type V2SessionListResponses = {
/**
- * MCP server added successfully
+ * V2SessionsResponse
*/
- 200: {
- [key: string]: McpStatus
- }
+ 200: V2SessionsResponse
}
-export type McpAddResponse = McpAddResponses[keyof McpAddResponses]
+export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses]
-export type McpAuthRemoveData = {
- body?: never
+export type V2SessionPromptData = {
+ body?: {
+ prompt: Prompt
+ delivery?: SessionDelivery
+ }
path: {
- name: string
+ sessionID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/mcp/{name}/auth"
-}
-
-export type McpAuthRemoveErrors = {
- /**
- * Not found
- */
- 404: NotFoundError
+ url: "/api/session/{sessionID}/prompt"
}
-export type McpAuthRemoveError = McpAuthRemoveErrors[keyof McpAuthRemoveErrors]
-
-export type McpAuthRemoveResponses = {
+export type V2SessionPromptResponses = {
/**
- * OAuth credentials removed
+ * Session.Message
*/
- 200: {
- success: true
- }
+ 200: SessionMessage
}
-export type McpAuthRemoveResponse = McpAuthRemoveResponses[keyof McpAuthRemoveResponses]
+export type V2SessionPromptResponse = V2SessionPromptResponses[keyof V2SessionPromptResponses]
-export type McpAuthStartData = {
+export type V2SessionCompactData = {
body?: never
path: {
- name: string
+ sessionID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/mcp/{name}/auth"
+ url: "/api/session/{sessionID}/compact"
}
-export type McpAuthStartErrors = {
+export type V2SessionCompactResponses = {
/**
- * MCP server does not support OAuth
+ * <No Content>
*/
- 400: McpUnsupportedOAuthError
- /**
- * Not found
- */
- 404: NotFoundError
-}
-
-export type McpAuthStartError = McpAuthStartErrors[keyof McpAuthStartErrors]
-
-export type McpAuthStartResponses = {
- /**
- * OAuth flow started
- */
- 200: {
- /**
- * URL to open in browser for authorization
- */
- authorizationUrl: string
- }
+ 204: void
}
-export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartResponses]
+export type V2SessionCompactResponse = V2SessionCompactResponses[keyof V2SessionCompactResponses]
-export type McpAuthCallbackData = {
- body?: {
- /**
- * Authorization code from OAuth callback
- */
- code: string
- }
+export type V2SessionWaitData = {
+ body?: never
path: {
- name: string
+ sessionID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/mcp/{name}/auth/callback"
+ url: "/api/session/{sessionID}/wait"
}
-export type McpAuthCallbackErrors = {
+export type V2SessionWaitResponses = {
/**
- * Bad request
+ * <No Content>
*/
- 400: BadRequestError
- /**
- * Not found
- */
- 404: NotFoundError
-}
-
-export type McpAuthCallbackError = McpAuthCallbackErrors[keyof McpAuthCallbackErrors]
-
-export type McpAuthCallbackResponses = {
- /**
- * OAuth authentication completed
- */
- 200: McpStatus
+ 204: void
}
-export type McpAuthCallbackResponse = McpAuthCallbackResponses[keyof McpAuthCallbackResponses]
+export type V2SessionWaitResponse = V2SessionWaitResponses[keyof V2SessionWaitResponses]
-export type McpAuthAuthenticateData = {
+export type V2SessionContextData = {
body?: never
path: {
- name: string
+ sessionID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/mcp/{name}/auth/authenticate"
+ url: "/api/session/{sessionID}/context"
}
-export type McpAuthAuthenticateErrors = {
+export type V2SessionContextResponses = {
/**
- * MCP server does not support OAuth
- */
- 400: McpUnsupportedOAuthError
- /**
- * Not found
+ * Success
*/
- 404: NotFoundError
+ 200: Array<SessionMessage>
}
-export type McpAuthAuthenticateError = McpAuthAuthenticateErrors[keyof McpAuthAuthenticateErrors]
+export type V2SessionContextResponse = V2SessionContextResponses[keyof V2SessionContextResponses]
-export type McpAuthAuthenticateResponses = {
- /**
- * OAuth authentication completed
- */
- 200: McpStatus
-}
-
-export type McpAuthAuthenticateResponse = McpAuthAuthenticateResponses[keyof McpAuthAuthenticateResponses]
-
-export type McpConnectData = {
+export type V2SessionMessagesData = {
body?: never
path: {
- name: string
+ sessionID: string
}
query?: {
directory?: string
workspace?: string
}
- url: "/mcp/{name}/connect"
+ url: "/api/session/{sessionID}/message"
}
-export type McpConnectResponses = {
+export type V2SessionMessagesErrors = {
/**
- * MCP server connected successfully
+ * Bad request
*/
- 200: boolean
+ 400: BadRequestError
}
-export type McpConnectResponse = McpConnectResponses[keyof McpConnectResponses]
+export type V2SessionMessagesError = V2SessionMessagesErrors[keyof V2SessionMessagesErrors]
-export type McpDisconnectData = {
- body?: never
- path: {
- name: string
- }
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/mcp/{name}/disconnect"
-}
-
-export type McpDisconnectResponses = {
+export type V2SessionMessagesResponses = {
/**
- * MCP server disconnected successfully
+ * V2SessionMessagesResponse
*/
- 200: boolean
+ 200: V2SessionMessagesResponse
}
-export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses]
+export type V2SessionMessagesResponse2 = V2SessionMessagesResponses[keyof V2SessionMessagesResponses]
export type TuiAppendPromptData = {
body?: {
@@ -5246,9 +6309,6 @@ export type TuiShowToastData = {
title?: string
message: string
variant: "info" | "success" | "warning" | "error"
- /**
- * Duration in milliseconds
- */
duration?: number
}
path?: never
@@ -5269,7 +6329,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
@@ -5374,179 +6434,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<Workspace>
}
-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<VcsFileDiff>
+ 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<Command>
+ 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<Agent>
+ 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<LspStatus>
+ 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<FormatterStatus>
+ 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]
diff --git a/specs/v2/session-concepts-gap.md b/specs/v2/session-concepts-gap.md
new file mode 100644
index 000000000..20d84c8f4
--- /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.