summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorJames Long <[email protected]>2026-03-03 21:35:38 -0500
committerGitHub <[email protected]>2026-03-03 21:35:38 -0500
commit7f37acdaaa89b358fb795ccd006d37ed7fa6672e (patch)
tree843595f6931d5de4daae43000cbf071c9bb192bf /packages
parente79d41c70ed3cd01b3a576dcf774c35cad6551c1 (diff)
downloadopencode-7f37acdaaa89b358fb795ccd006d37ed7fa6672e.tar.gz
opencode-7f37acdaaa89b358fb795ccd006d37ed7fa6672e.zip
feat(core): rework workspace integration and adaptor interface (#15895)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/migration/20260303231226_add_workspace_fields/migration.sql5
-rw-r--r--packages/opencode/migration/20260303231226_add_workspace_fields/snapshot.json1063
-rw-r--r--packages/opencode/src/cli/cmd/serve.ts7
-rw-r--r--packages/opencode/src/control-plane/adaptors/index.ts28
-rw-r--r--packages/opencode/src/control-plane/adaptors/types.ts7
-rw-r--r--packages/opencode/src/control-plane/adaptors/worktree.ts52
-rw-r--r--packages/opencode/src/control-plane/config.ts10
-rw-r--r--packages/opencode/src/control-plane/session-proxy-middleware.ts46
-rw-r--r--packages/opencode/src/control-plane/types.ts20
-rw-r--r--packages/opencode/src/control-plane/workspace-router-middleware.ts50
-rw-r--r--packages/opencode/src/control-plane/workspace-server/server.ts46
-rw-r--r--packages/opencode/src/control-plane/workspace.sql.ts6
-rw-r--r--packages/opencode/src/control-plane/workspace.ts108
-rw-r--r--packages/opencode/src/server/routes/experimental.ts2
-rw-r--r--packages/opencode/src/server/routes/session.ts2
-rw-r--r--packages/opencode/src/server/routes/workspace.ts18
-rw-r--r--packages/opencode/src/server/server.ts2
-rw-r--r--packages/opencode/src/worktree/index.ts24
-rw-r--r--packages/opencode/test/control-plane/session-proxy-middleware.test.ts114
-rw-r--r--packages/opencode/test/control-plane/workspace-server-sse.test.ts7
-rw-r--r--packages/opencode/test/control-plane/workspace-sync.test.ts60
-rw-r--r--packages/sdk/js/src/v2/gen/sdk.gen.ts252
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts159
23 files changed, 1614 insertions, 474 deletions
diff --git a/packages/opencode/migration/20260303231226_add_workspace_fields/migration.sql b/packages/opencode/migration/20260303231226_add_workspace_fields/migration.sql
new file mode 100644
index 000000000..185de5913
--- /dev/null
+++ b/packages/opencode/migration/20260303231226_add_workspace_fields/migration.sql
@@ -0,0 +1,5 @@
+ALTER TABLE `workspace` ADD `type` text NOT NULL;--> statement-breakpoint
+ALTER TABLE `workspace` ADD `name` text;--> statement-breakpoint
+ALTER TABLE `workspace` ADD `directory` text;--> statement-breakpoint
+ALTER TABLE `workspace` ADD `extra` text;--> statement-breakpoint
+ALTER TABLE `workspace` DROP COLUMN `config`; \ No newline at end of file
diff --git a/packages/opencode/migration/20260303231226_add_workspace_fields/snapshot.json b/packages/opencode/migration/20260303231226_add_workspace_fields/snapshot.json
new file mode 100644
index 000000000..d47bad579
--- /dev/null
+++ b/packages/opencode/migration/20260303231226_add_workspace_fields/snapshot.json
@@ -0,0 +1,1063 @@
+{
+ "version": "7",
+ "dialect": "sqlite",
+ "id": "4ec9de62-88a7-4bec-91cc-0a759e84db21",
+ "prevIds": [
+ "572fb732-56f4-4b1e-b981-77152c9980dd"
+ ],
+ "ddl": [
+ {
+ "name": "workspace",
+ "entityType": "tables"
+ },
+ {
+ "name": "control_account",
+ "entityType": "tables"
+ },
+ {
+ "name": "project",
+ "entityType": "tables"
+ },
+ {
+ "name": "message",
+ "entityType": "tables"
+ },
+ {
+ "name": "part",
+ "entityType": "tables"
+ },
+ {
+ "name": "permission",
+ "entityType": "tables"
+ },
+ {
+ "name": "session",
+ "entityType": "tables"
+ },
+ {
+ "name": "todo",
+ "entityType": "tables"
+ },
+ {
+ "name": "session_share",
+ "entityType": "tables"
+ },
+ {
+ "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": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "branch",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "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": 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": "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_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"
+ },
+ {
+ "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"
+ },
+ {
+ "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": [
+ "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": [
+ "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": "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_pk",
+ "table": "session",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "session_id"
+ ],
+ "nameExplicit": false,
+ "name": "session_share_pk",
+ "table": "session_share",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ {
+ "value": "session_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "message_session_idx",
+ "entityType": "indexes",
+ "table": "message"
+ },
+ {
+ "columns": [
+ {
+ "value": "message_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "part_message_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": "project_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "session_project_idx",
+ "entityType": "indexes",
+ "table": "session"
+ },
+ {
+ "columns": [
+ {
+ "value": "workspace_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "session_workspace_idx",
+ "entityType": "indexes",
+ "table": "session"
+ },
+ {
+ "columns": [
+ {
+ "value": "parent_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "session_parent_idx",
+ "entityType": "indexes",
+ "table": "session"
+ },
+ {
+ "columns": [
+ {
+ "value": "session_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "todo_session_idx",
+ "entityType": "indexes",
+ "table": "todo"
+ }
+ ],
+ "renames": []
+} \ No newline at end of file
diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts
index 8f4bb0144..ab51fe8c3 100644
--- a/packages/opencode/src/cli/cmd/serve.ts
+++ b/packages/opencode/src/cli/cmd/serve.ts
@@ -18,14 +18,7 @@ export const ServeCommand = cmd({
const server = Server.listen(opts)
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
- let workspaceSync: Array<ReturnType<typeof Workspace.startSyncing>> = []
- // Only available in development right now
- if (Installation.isLocal()) {
- workspaceSync = Project.list().map((project) => Workspace.startSyncing(project))
- }
-
await new Promise(() => {})
await server.stop()
- await Promise.all(workspaceSync.map((item) => item.stop()))
},
})
diff --git a/packages/opencode/src/control-plane/adaptors/index.ts b/packages/opencode/src/control-plane/adaptors/index.ts
index 77e1f53c6..a43fce248 100644
--- a/packages/opencode/src/control-plane/adaptors/index.ts
+++ b/packages/opencode/src/control-plane/adaptors/index.ts
@@ -1,10 +1,20 @@
-import { WorktreeAdaptor } from "./worktree"
-import type { Config } from "../config"
-import type { Adaptor } from "./types"
-
-export function getAdaptor(config: Config): Adaptor {
- switch (config.type) {
- case "worktree":
- return WorktreeAdaptor
- }
+import { lazy } from "@/util/lazy"
+import type { Adaptor } from "../types"
+
+const ADAPTORS: Record<string, () => Promise<Adaptor>> = {
+ worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor),
+}
+
+export function getAdaptor(type: string): Promise<Adaptor> {
+ return ADAPTORS[type]()
+}
+
+export function installAdaptor(type: string, adaptor: Adaptor) {
+ // This is experimental: mostly used for testing right now, but we
+ // will likely allow this in the future. Need to figure out the
+ // TypeScript story
+
+ // @ts-expect-error we force the builtin types right now, but we
+ // will implement a way to extend the types for custom adaptors
+ ADAPTORS[type] = () => adaptor
}
diff --git a/packages/opencode/src/control-plane/adaptors/types.ts b/packages/opencode/src/control-plane/adaptors/types.ts
deleted file mode 100644
index 47a0405a5..000000000
--- a/packages/opencode/src/control-plane/adaptors/types.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { Config } from "../config"
-
-export type Adaptor<T extends Config = Config> = {
- create(from: T, branch?: string | null): Promise<{ config: T; init: () => Promise<void> }>
- remove(from: T): Promise<void>
- request(from: T, method: string, url: string, data?: BodyInit, signal?: AbortSignal): Promise<Response | undefined>
-}
diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts
index e355bb770..f84890950 100644
--- a/packages/opencode/src/control-plane/adaptors/worktree.ts
+++ b/packages/opencode/src/control-plane/adaptors/worktree.ts
@@ -1,26 +1,46 @@
+import z from "zod"
import { Worktree } from "@/worktree"
-import type { Config } from "../config"
-import type { Adaptor } from "./types"
+import { type Adaptor, WorkspaceInfo } from "../types"
-type WorktreeConfig = Extract<Config, { type: "worktree" }>
+const Config = WorkspaceInfo.extend({
+ name: WorkspaceInfo.shape.name.unwrap(),
+ branch: WorkspaceInfo.shape.branch.unwrap(),
+ directory: WorkspaceInfo.shape.directory.unwrap(),
+})
-export const WorktreeAdaptor: Adaptor<WorktreeConfig> = {
- async create(_from: WorktreeConfig, _branch: string) {
- const next = await Worktree.create(undefined)
+type Config = z.infer<typeof Config>
+
+export const WorktreeAdaptor: Adaptor = {
+ async configure(info) {
+ const worktree = await Worktree.makeWorktreeInfo(info.name ?? undefined)
return {
- config: {
- type: "worktree",
- directory: next.directory,
- },
- // Hack for now: `Worktree.create` puts all its async code in a
- // `setTimeout` so it doesn't use this, but we should change that
- init: async () => {},
+ ...info,
+ name: worktree.name,
+ branch: worktree.branch,
+ directory: worktree.directory,
}
},
- async remove(config: WorktreeConfig) {
+ async create(info) {
+ const config = Config.parse(info)
+ const bootstrap = await Worktree.createFromInfo({
+ name: config.name,
+ directory: config.directory,
+ branch: config.branch,
+ })
+ return bootstrap()
+ },
+ async remove(info) {
+ const config = Config.parse(info)
await Worktree.remove({ directory: config.directory })
},
- async request(_from: WorktreeConfig, _method: string, _url: string, _data?: BodyInit, _signal?: AbortSignal) {
- throw new Error("worktree does not support request")
+ async fetch(info, input: RequestInfo | URL, init?: RequestInit) {
+ const config = Config.parse(info)
+ const { WorkspaceServer } = await import("../workspace-server/server")
+ const url = input instanceof Request || input instanceof URL ? input : new URL(input, "http://opencode.internal")
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined))
+ headers.set("x-opencode-directory", config.directory)
+
+ const request = new Request(url, { ...init, headers })
+ return WorkspaceServer.App().fetch(request)
},
}
diff --git a/packages/opencode/src/control-plane/config.ts b/packages/opencode/src/control-plane/config.ts
deleted file mode 100644
index 73dbc4bdb..000000000
--- a/packages/opencode/src/control-plane/config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import z from "zod"
-
-export const Config = z.discriminatedUnion("type", [
- z.object({
- directory: z.string(),
- type: z.literal("worktree"),
- }),
-])
-
-export type Config = z.infer<typeof Config>
diff --git a/packages/opencode/src/control-plane/session-proxy-middleware.ts b/packages/opencode/src/control-plane/session-proxy-middleware.ts
deleted file mode 100644
index df2591017..000000000
--- a/packages/opencode/src/control-plane/session-proxy-middleware.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Instance } from "@/project/instance"
-import type { MiddlewareHandler } from "hono"
-import { Installation } from "../installation"
-import { getAdaptor } from "./adaptors"
-import { Workspace } from "./workspace"
-
-// This middleware forwards all non-GET requests if the workspace is a
-// remote. The remote workspace needs to handle session mutations
-async function proxySessionRequest(req: Request) {
- if (req.method === "GET") return
- if (!Instance.directory.startsWith("wrk_")) return
-
- const workspace = await Workspace.get(Instance.directory)
- if (!workspace) {
- return new Response(`Workspace not found: ${Instance.directory}`, {
- status: 500,
- headers: {
- "content-type": "text/plain; charset=utf-8",
- },
- })
- }
- if (workspace.config.type === "worktree") return
-
- const url = new URL(req.url)
- const body = req.method === "HEAD" ? undefined : await req.arrayBuffer()
- return getAdaptor(workspace.config).request(
- workspace.config,
- req.method,
- `${url.pathname}${url.search}`,
- body,
- req.signal,
- )
-}
-
-export const SessionProxyMiddleware: MiddlewareHandler = async (c, next) => {
- // Only available in development for now
- if (!Installation.isLocal()) {
- return next()
- }
-
- const response = await proxySessionRequest(c.req.raw)
- if (response) {
- return response
- }
- return next()
-}
diff --git a/packages/opencode/src/control-plane/types.ts b/packages/opencode/src/control-plane/types.ts
new file mode 100644
index 000000000..3d27757fd
--- /dev/null
+++ b/packages/opencode/src/control-plane/types.ts
@@ -0,0 +1,20 @@
+import z from "zod"
+import { Identifier } from "@/id/id"
+
+export const WorkspaceInfo = z.object({
+ id: Identifier.schema("workspace"),
+ type: z.string(),
+ branch: z.string().nullable(),
+ name: z.string().nullable(),
+ directory: z.string().nullable(),
+ extra: z.unknown().nullable(),
+ projectID: z.string(),
+})
+export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>
+
+export type Adaptor = {
+ configure(input: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
+ create(input: WorkspaceInfo, from?: WorkspaceInfo): Promise<void>
+ remove(config: WorkspaceInfo): Promise<void>
+ fetch(config: WorkspaceInfo, input: RequestInfo | URL, init?: RequestInit): Promise<Response>
+}
diff --git a/packages/opencode/src/control-plane/workspace-router-middleware.ts b/packages/opencode/src/control-plane/workspace-router-middleware.ts
new file mode 100644
index 000000000..b48f2fd2b
--- /dev/null
+++ b/packages/opencode/src/control-plane/workspace-router-middleware.ts
@@ -0,0 +1,50 @@
+import { Instance } from "@/project/instance"
+import type { MiddlewareHandler } from "hono"
+import { Installation } from "../installation"
+import { getAdaptor } from "./adaptors"
+import { Workspace } from "./workspace"
+import { WorkspaceContext } from "./workspace-context"
+
+// This middleware forwards all non-GET requests if the workspace is a
+// remote. The remote workspace needs to handle session mutations
+async function routeRequest(req: Request) {
+ // Right now, we need to forward all requests to the workspace
+ // because we don't have syncing. In the future all GET requests
+ // which don't mutate anything will be handled locally
+ //
+ // if (req.method === "GET") return
+
+ if (!WorkspaceContext.workspaceID) return
+
+ const workspace = await Workspace.get(WorkspaceContext.workspaceID)
+ if (!workspace) {
+ return new Response(`Workspace not found: ${WorkspaceContext.workspaceID}`, {
+ status: 500,
+ headers: {
+ "content-type": "text/plain; charset=utf-8",
+ },
+ })
+ }
+
+ const adaptor = await getAdaptor(workspace.type)
+
+ return adaptor.fetch(workspace, `${new URL(req.url).pathname}${new URL(req.url).search}`, {
+ method: req.method,
+ body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),
+ signal: req.signal,
+ headers: req.headers,
+ })
+}
+
+export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c, next) => {
+ // Only available in development for now
+ if (!Installation.isLocal()) {
+ return next()
+ }
+
+ const response = await routeRequest(c.req.raw)
+ if (response) {
+ return response
+ }
+ return next()
+}
diff --git a/packages/opencode/src/control-plane/workspace-server/server.ts b/packages/opencode/src/control-plane/workspace-server/server.ts
index 716989942..fd7fd9308 100644
--- a/packages/opencode/src/control-plane/workspace-server/server.ts
+++ b/packages/opencode/src/control-plane/workspace-server/server.ts
@@ -1,17 +1,57 @@
import { Hono } from "hono"
+import { Instance } from "../../project/instance"
+import { InstanceBootstrap } from "../../project/bootstrap"
import { SessionRoutes } from "../../server/routes/session"
import { WorkspaceServerRoutes } from "./routes"
+import { WorkspaceContext } from "../workspace-context"
export namespace WorkspaceServer {
export function App() {
const session = new Hono()
- .use("*", async (c, next) => {
- if (c.req.method === "GET") return c.notFound()
+ .use(async (c, next) => {
+ // Right now, we need handle all requests because we don't
+ // have syncing. In the future all GET requests will handled
+ // by the control plane
+ //
+ // if (c.req.method === "GET") return c.notFound()
await next()
})
.route("/", SessionRoutes())
- return new Hono().route("/session", session).route("/", WorkspaceServerRoutes())
+ return new Hono()
+ .use(async (c, next) => {
+ const workspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
+ const raw = c.req.query("directory") || c.req.header("x-opencode-directory")
+ if (workspaceID == null) {
+ throw new Error("workspaceID parameter is required")
+ }
+ if (raw == null) {
+ throw new Error("directory parameter is required")
+ }
+
+ const directory = (() => {
+ try {
+ return decodeURIComponent(raw)
+ } catch {
+ return raw
+ }
+ })()
+
+ return WorkspaceContext.provide({
+ workspaceID,
+ async fn() {
+ return Instance.provide({
+ directory,
+ init: InstanceBootstrap,
+ async fn() {
+ return next()
+ },
+ })
+ },
+ })
+ })
+ .route("/session", session)
+ .route("/", WorkspaceServerRoutes())
}
export function Listen(opts: { hostname: string; port: number }) {
diff --git a/packages/opencode/src/control-plane/workspace.sql.ts b/packages/opencode/src/control-plane/workspace.sql.ts
index 1a2011982..1ba1605f8 100644
--- a/packages/opencode/src/control-plane/workspace.sql.ts
+++ b/packages/opencode/src/control-plane/workspace.sql.ts
@@ -1,12 +1,14 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "@/project/project.sql"
-import type { Config } from "./config"
export const WorkspaceTable = sqliteTable("workspace", {
id: text().primaryKey(),
+ type: text().notNull(),
branch: text(),
+ name: text(),
+ directory: text(),
+ extra: text({ mode: "json" }),
project_id: text()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
- config: text({ mode: "json" }).notNull().$type<Config>(),
})
diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts
index 5ce373b12..8c76fbdab 100644
--- a/packages/opencode/src/control-plane/workspace.ts
+++ b/packages/opencode/src/control-plane/workspace.ts
@@ -7,8 +7,8 @@ import { BusEvent } from "@/bus/bus-event"
import { GlobalBus } from "@/bus/global"
import { Log } from "@/util/log"
import { WorkspaceTable } from "./workspace.sql"
-import { Config } from "./config"
import { getAdaptor } from "./adaptors"
+import { WorkspaceInfo } from "./types"
import { parseSSE } from "./sse"
export namespace Workspace {
@@ -27,72 +27,64 @@ export namespace Workspace {
),
}
- export const Info = z
- .object({
- id: Identifier.schema("workspace"),
- branch: z.string().nullable(),
- projectID: z.string(),
- config: Config,
- })
- .meta({
- ref: "Workspace",
- })
+ export const Info = WorkspaceInfo.meta({
+ ref: "Workspace",
+ })
export type Info = z.infer<typeof Info>
function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
return {
id: row.id,
+ type: row.type,
branch: row.branch,
+ name: row.name,
+ directory: row.directory,
+ extra: row.extra,
projectID: row.project_id,
- config: row.config,
}
}
- export const create = fn(
- z.object({
- id: Identifier.schema("workspace").optional(),
- projectID: Info.shape.projectID,
- branch: Info.shape.branch,
- config: Info.shape.config,
- }),
- async (input) => {
- const id = Identifier.ascending("workspace", input.id)
-
- const { config, init } = await getAdaptor(input.config).create(input.config, input.branch)
-
- const info: Info = {
- id,
- projectID: input.projectID,
- branch: input.branch,
- config,
- }
+ const CreateInput = z.object({
+ id: Identifier.schema("workspace").optional(),
+ type: Info.shape.type,
+ branch: Info.shape.branch,
+ projectID: Info.shape.projectID,
+ extra: Info.shape.extra,
+ })
- setTimeout(async () => {
- await init()
-
- Database.use((db) => {
- db.insert(WorkspaceTable)
- .values({
- id: info.id,
- branch: info.branch,
- project_id: info.projectID,
- config: info.config,
- })
- .run()
- })
+ export const create = fn(CreateInput, async (input) => {
+ const id = Identifier.ascending("workspace", input.id)
+ const adaptor = await getAdaptor(input.type)
- GlobalBus.emit("event", {
- directory: id,
- payload: {
- type: Event.Ready.type,
- properties: {},
- },
+ const config = await adaptor.configure({ ...input, id, name: null, directory: null })
+
+ const info: Info = {
+ id,
+ type: config.type,
+ branch: config.branch ?? null,
+ name: config.name ?? null,
+ directory: config.directory ?? null,
+ extra: config.extra ?? null,
+ projectID: input.projectID,
+ }
+
+ Database.use((db) => {
+ db.insert(WorkspaceTable)
+ .values({
+ id: info.id,
+ type: info.type,
+ branch: info.branch,
+ name: info.name,
+ directory: info.directory,
+ extra: info.extra,
+ project_id: info.projectID,
})
- }, 0)
+ .run()
+ })
- return info
- },
- )
+ await adaptor.create(config)
+ return info
+ })
export function list(project: Project.Info) {
const rows = Database.use((db) =>
@@ -111,7 +103,8 @@ export namespace Workspace {
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
if (row) {
const info = fromRow(row)
- await getAdaptor(info.config).remove(info.config)
+ const adaptor = await getAdaptor(row.type)
+ adaptor.remove(info)
Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
return info
}
@@ -120,9 +113,8 @@ export namespace Workspace {
async function workspaceEventLoop(space: Info, stop: AbortSignal) {
while (!stop.aborted) {
- const res = await getAdaptor(space.config)
- .request(space.config, "GET", "/event", undefined, stop)
- .catch(() => undefined)
+ const adaptor = await getAdaptor(space.type)
+ const res = await adaptor.fetch(space, "/event", { method: "GET", signal: stop }).catch(() => undefined)
if (!res || !res.ok || !res.body) {
await Bun.sleep(1000)
continue
@@ -140,7 +132,7 @@ export namespace Workspace {
export function startSyncing(project: Project.Info) {
const stop = new AbortController()
- const spaces = list(project).filter((space) => space.config.type !== "worktree")
+ const spaces = list(project).filter((space) => space.type !== "worktree")
spaces.forEach((space) => {
void workspaceEventLoop(space, stop.signal).catch((error) => {
diff --git a/packages/opencode/src/server/routes/experimental.ts b/packages/opencode/src/server/routes/experimental.ts
index 892bca485..98c7ece10 100644
--- a/packages/opencode/src/server/routes/experimental.ts
+++ b/packages/opencode/src/server/routes/experimental.ts
@@ -88,6 +88,7 @@ export const ExperimentalRoutes = lazy(() =>
)
},
)
+ .route("/workspace", WorkspaceRoutes())
.post(
"/worktree",
describeRoute({
@@ -113,7 +114,6 @@ export const ExperimentalRoutes = lazy(() =>
return c.json(worktree)
},
)
- .route("/workspace", WorkspaceRoutes())
.get(
"/worktree",
describeRoute({
diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts
index a39197952..12938aeab 100644
--- a/packages/opencode/src/server/routes/session.ts
+++ b/packages/opencode/src/server/routes/session.ts
@@ -16,13 +16,11 @@ import { Log } from "../../util/log"
import { PermissionNext } from "@/permission/next"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
-import { SessionProxyMiddleware } from "../../control-plane/session-proxy-middleware"
const log = Log.create({ service: "server" })
export const SessionRoutes = lazy(() =>
new Hono()
- .use(SessionProxyMiddleware)
.get(
"/",
describeRoute({
diff --git a/packages/opencode/src/server/routes/workspace.ts b/packages/opencode/src/server/routes/workspace.ts
index 0c64c9cd4..cd2d844ae 100644
--- a/packages/opencode/src/server/routes/workspace.ts
+++ b/packages/opencode/src/server/routes/workspace.ts
@@ -9,7 +9,7 @@ import { lazy } from "../../util/lazy"
export const WorkspaceRoutes = lazy(() =>
new Hono()
.post(
- "/:id",
+ "/",
describeRoute({
summary: "Create workspace",
description: "Create a workspace for the current project.",
@@ -27,26 +27,16 @@ export const WorkspaceRoutes = lazy(() =>
},
}),
validator(
- "param",
- z.object({
- id: Workspace.Info.shape.id,
- }),
- ),
- validator(
"json",
- z.object({
- branch: Workspace.Info.shape.branch,
- config: Workspace.Info.shape.config,
+ Workspace.create.schema.omit({
+ projectID: true,
}),
),
async (c) => {
- const { id } = c.req.valid("param")
const body = c.req.valid("json")
const workspace = await Workspace.create({
- id,
projectID: Instance.project.id,
- branch: body.branch,
- config: body.config,
+ ...body,
})
return c.json(workspace)
},
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 85049650c..6ea66be98 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -22,6 +22,7 @@ import { Flag } from "../flag/flag"
import { Command } from "../command"
import { Global } from "../global"
import { WorkspaceContext } from "../control-plane/workspace-context"
+import { WorkspaceRouterMiddleware } from "../control-plane/workspace-router-middleware"
import { ProjectRoutes } from "./routes/project"
import { SessionRoutes } from "./routes/session"
import { PtyRoutes } from "./routes/pty"
@@ -218,6 +219,7 @@ export namespace Server {
},
})
})
+ .use(WorkspaceRouterMiddleware)
.get(
"/doc",
openAPIRouteHandler(app, {
diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts
index d85a0843f..226732249 100644
--- a/packages/opencode/src/worktree/index.ts
+++ b/packages/opencode/src/worktree/index.ts
@@ -331,7 +331,7 @@ export namespace Worktree {
}, 0)
}
- export const create = fn(CreateInput.optional(), async (input) => {
+ export async function makeWorktreeInfo(name?: string): Promise<Info> {
if (Instance.project.vcs !== "git") {
throw new NotGitError({ message: "Worktrees are only supported for git projects" })
}
@@ -339,9 +339,11 @@ export namespace Worktree {
const root = path.join(Global.Path.data, "worktree", Instance.project.id)
await fs.mkdir(root, { recursive: true })
- const base = input?.name ? slug(input.name) : ""
- const info = await candidate(root, base || undefined)
+ const base = name ? slug(name) : ""
+ return candidate(root, base || undefined)
+ }
+ export async function createFromInfo(info: Info, startCommand?: string) {
const created = await $`git worktree add --no-checkout -b ${info.branch} ${info.directory}`
.quiet()
.nothrow()
@@ -353,8 +355,9 @@ export namespace Worktree {
await Project.addSandbox(Instance.project.id, info.directory).catch(() => undefined)
const projectID = Instance.project.id
- const extra = input?.startCommand?.trim()
- setTimeout(() => {
+ const extra = startCommand?.trim()
+
+ return () => {
const start = async () => {
const populated = await $`git reset --hard`.quiet().nothrow().cwd(info.directory)
if (populated.exitCode !== 0) {
@@ -411,8 +414,17 @@ export namespace Worktree {
void start().catch((error) => {
log.error("worktree start task failed", { directory: info.directory, error })
})
- }, 0)
+ }
+ }
+ export const create = fn(CreateInput.optional(), async (input) => {
+ const info = await makeWorktreeInfo(input?.name)
+ const bootstrap = await createFromInfo(info, input?.startCommand)
+ // This is needed due to how worktrees currently work in the
+ // desktop app
+ setTimeout(() => {
+ bootstrap()
+ }, 0)
return info
})
diff --git a/packages/opencode/test/control-plane/session-proxy-middleware.test.ts b/packages/opencode/test/control-plane/session-proxy-middleware.test.ts
index 596e4761e..369b9152a 100644
--- a/packages/opencode/test/control-plane/session-proxy-middleware.test.ts
+++ b/packages/opencode/test/control-plane/session-proxy-middleware.test.ts
@@ -5,8 +5,11 @@ import { tmpdir } from "../fixture/fixture"
import { Project } from "../../src/project/project"
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
import { Instance } from "../../src/project/instance"
+import { WorkspaceContext } from "../../src/control-plane/workspace-context"
import { Database } from "../../src/storage/db"
import { resetDatabase } from "../fixture/db"
+import * as adaptors from "../../src/control-plane/adaptors"
+import type { Adaptor } from "../../src/control-plane/types"
afterEach(async () => {
mock.restore()
@@ -18,18 +21,35 @@ type State = {
calls: Array<{ method: string; url: string; body?: string }>
}
-const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
+const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert
async function setup(state: State) {
- mock.module("../../src/control-plane/adaptors", () => ({
- getAdaptor: () => ({
- request: async (_config: unknown, method: string, url: string, data?: BodyInit) => {
- const body = data ? await new Response(data).text() : undefined
- state.calls.push({ method, url, body })
- return new Response("proxied", { status: 202 })
- },
- }),
- }))
+ const TestAdaptor: Adaptor = {
+ configure(config) {
+ return config
+ },
+ async create() {
+ throw new Error("not used")
+ },
+ async remove() {},
+
+ async fetch(_config: unknown, input: RequestInfo | URL, init?: RequestInit) {
+ const url =
+ input instanceof Request || input instanceof URL
+ ? input.toString()
+ : new URL(input, "http://workspace.test").toString()
+ const request = new Request(url, init)
+ const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.text()
+ state.calls.push({
+ method: request.method,
+ url: `${new URL(request.url).pathname}${new URL(request.url).search}`,
+ body,
+ })
+ return new Response("proxied", { status: 202 })
+ },
+ }
+
+ adaptors.installAdaptor("testing", TestAdaptor)
await using tmp = await tmpdir({ git: true })
const { project } = await Project.fromDirectory(tmp.path)
@@ -45,20 +65,23 @@ async function setup(state: State) {
id: id1,
branch: "main",
project_id: project.id,
- config: remote,
+ type: remote.type,
+ name: remote.name,
},
{
id: id2,
branch: "main",
project_id: project.id,
- config: { type: "worktree", directory: tmp.path },
+ type: "worktree",
+ directory: tmp.path,
+ name: "local",
},
])
.run(),
)
- const { SessionProxyMiddleware } = await import("../../src/control-plane/session-proxy-middleware")
- const app = new Hono().use(SessionProxyMiddleware)
+ const { WorkspaceRouterMiddleware } = await import("../../src/control-plane/workspace-router-middleware")
+ const app = new Hono().use(WorkspaceRouterMiddleware)
return {
id1,
@@ -66,15 +89,19 @@ async function setup(state: State) {
app,
async request(input: RequestInfo | URL, init?: RequestInit) {
return Instance.provide({
- directory: state.workspace === "first" ? id1 : id2,
- fn: async () => app.request(input, init),
+ directory: tmp.path,
+ fn: async () =>
+ WorkspaceContext.provide({
+ workspaceID: state.workspace === "first" ? id1 : id2,
+ fn: () => app.request(input, init),
+ }),
})
},
}
}
describe("control-plane/session-proxy-middleware", () => {
- test("forwards non-GET session requests for remote workspaces", async () => {
+ test("forwards non-GET session requests for workspaces", async () => {
const state: State = {
workspace: "first",
calls: [],
@@ -102,46 +129,21 @@ describe("control-plane/session-proxy-middleware", () => {
])
})
- test("does not forward GET requests", async () => {
- const state: State = {
- workspace: "first",
- calls: [],
- }
+ // It will behave this way when we have syncing
+ //
+ // test("does not forward GET requests", async () => {
+ // const state: State = {
+ // workspace: "first",
+ // calls: [],
+ // }
- const ctx = await setup(state)
+ // const ctx = await setup(state)
- ctx.app.get("/session/foo", (c) => c.text("local", 200))
- const response = await ctx.request("http://workspace.test/session/foo?x=1")
+ // ctx.app.get("/session/foo", (c) => c.text("local", 200))
+ // const response = await ctx.request("http://workspace.test/session/foo?x=1")
- expect(response.status).toBe(200)
- expect(await response.text()).toBe("local")
- expect(state.calls).toEqual([])
- })
-
- test("does not forward GET or POST requests for worktree workspaces", async () => {
- const state: State = {
- workspace: "second",
- calls: [],
- }
-
- const ctx = await setup(state)
-
- ctx.app.get("/session/foo", (c) => c.text("local-get", 200))
- ctx.app.post("/session/foo", (c) => c.text("local-post", 200))
-
- const getResponse = await ctx.request("http://workspace.test/session/foo?x=1")
- const postResponse = await ctx.request("http://workspace.test/session/foo?x=1", {
- method: "POST",
- body: JSON.stringify({ hello: "world" }),
- headers: {
- "content-type": "application/json",
- },
- })
-
- expect(getResponse.status).toBe(200)
- expect(await getResponse.text()).toBe("local-get")
- expect(postResponse.status).toBe(200)
- expect(await postResponse.text()).toBe("local-post")
- expect(state.calls).toEqual([])
- })
+ // expect(response.status).toBe(200)
+ // expect(await response.text()).toBe("local")
+ // expect(state.calls).toEqual([])
+ // })
})
diff --git a/packages/opencode/test/control-plane/workspace-server-sse.test.ts b/packages/opencode/test/control-plane/workspace-server-sse.test.ts
index 91504af0f..7e7cddb14 100644
--- a/packages/opencode/test/control-plane/workspace-server-sse.test.ts
+++ b/packages/opencode/test/control-plane/workspace-server-sse.test.ts
@@ -4,6 +4,7 @@ import { WorkspaceServer } from "../../src/control-plane/workspace-server/server
import { parseSSE } from "../../src/control-plane/sse"
import { GlobalBus } from "../../src/bus/global"
import { resetDatabase } from "../fixture/db"
+import { tmpdir } from "../fixture/fixture"
afterEach(async () => {
await resetDatabase()
@@ -13,13 +14,17 @@ Log.init({ print: false })
describe("control-plane/workspace-server SSE", () => {
test("streams GlobalBus events and parseSSE reads them", async () => {
+ await using tmp = await tmpdir({ git: true })
const app = WorkspaceServer.App()
const stop = new AbortController()
const seen: unknown[] = []
-
try {
const response = await app.request("/event", {
signal: stop.signal,
+ headers: {
+ "x-opencode-workspace": "wrk_test_workspace",
+ "x-opencode-directory": tmp.path,
+ },
})
expect(response.status).toBe(200)
diff --git a/packages/opencode/test/control-plane/workspace-sync.test.ts b/packages/opencode/test/control-plane/workspace-sync.test.ts
index 2769c8a3b..899118920 100644
--- a/packages/opencode/test/control-plane/workspace-sync.test.ts
+++ b/packages/opencode/test/control-plane/workspace-sync.test.ts
@@ -7,6 +7,8 @@ import { Database } from "../../src/storage/db"
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
import { GlobalBus } from "../../src/bus/global"
import { resetDatabase } from "../fixture/db"
+import * as adaptors from "../../src/control-plane/adaptors"
+import type { Adaptor } from "../../src/control-plane/types"
afterEach(async () => {
mock.restore()
@@ -15,35 +17,34 @@ afterEach(async () => {
Log.init({ print: false })
-const seen: string[] = []
-const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
+const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert
-mock.module("../../src/control-plane/adaptors", () => ({
- getAdaptor: (config: { type: string }) => {
- seen.push(config.type)
- return {
- async create() {
- throw new Error("not used")
+const TestAdaptor: Adaptor = {
+ configure(config) {
+ return config
+ },
+ async create() {
+ throw new Error("not used")
+ },
+ async remove() {},
+ async fetch(_config: unknown, _input: RequestInfo | URL, _init?: RequestInit) {
+ const body = new ReadableStream<Uint8Array>({
+ start(controller) {
+ const encoder = new TextEncoder()
+ controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
+ controller.close()
},
- async remove() {},
- async request() {
- const body = new ReadableStream<Uint8Array>({
- start(controller) {
- const encoder = new TextEncoder()
- controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
- controller.close()
- },
- })
- return new Response(body, {
- status: 200,
- headers: {
- "content-type": "text/event-stream",
- },
- })
+ })
+ return new Response(body, {
+ status: 200,
+ headers: {
+ "content-type": "text/event-stream",
},
- }
+ })
},
-}))
+}
+
+adaptors.installAdaptor("testing", TestAdaptor)
describe("control-plane/workspace.startSyncing", () => {
test("syncs only remote workspaces and emits remote SSE events", async () => {
@@ -62,13 +63,16 @@ describe("control-plane/workspace.startSyncing", () => {
id: id1,
branch: "main",
project_id: project.id,
- config: remote,
+ type: remote.type,
+ name: remote.name,
},
{
id: id2,
branch: "main",
project_id: project.id,
- config: { type: "worktree", directory: tmp.path },
+ type: "worktree",
+ directory: tmp.path,
+ name: "local",
},
])
.run(),
@@ -91,7 +95,5 @@ describe("control-plane/workspace.startSyncing", () => {
])
await sync.stop()
- expect(seen).toContain("testing")
- expect(seen).not.toContain("worktree")
})
})
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index 49ebc8473..feabf7199 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -862,17 +862,16 @@ export class Tool extends HeyApiClient {
}
}
-export class Worktree extends HeyApiClient {
+export class Workspace extends HeyApiClient {
/**
- * Remove worktree
+ * List workspaces
*
- * Remove a git worktree and delete its branch.
+ * List all workspaces.
*/
- public remove<ThrowOnError extends boolean = false>(
+ public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- worktreeRemoveInput?: WorktreeRemoveInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -883,32 +882,32 @@ export class Worktree extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { key: "worktreeRemoveInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).delete<WorktreeRemoveResponses, WorktreeRemoveErrors, ThrowOnError>({
- url: "/experimental/worktree",
+ return (options?.client ?? this.client).get<ExperimentalWorkspaceListResponses, unknown, ThrowOnError>({
+ url: "/experimental/workspace",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
/**
- * List worktrees
+ * Create workspace
*
- * List all sandbox worktrees for the current project.
+ * Create a workspace for the current project.
*/
- public list<ThrowOnError extends boolean = false>(
+ public create<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
+ body?: {
+ branch?: string | null
+ } & {
+ type: "worktree"
+ name: string
+ }
},
options?: Options<never, ThrowOnError>,
) {
@@ -919,27 +918,37 @@ export class Worktree extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { key: "body", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).get<WorktreeListResponses, unknown, ThrowOnError>({
- url: "/experimental/worktree",
+ return (options?.client ?? this.client).post<
+ ExperimentalWorkspaceCreateResponses,
+ ExperimentalWorkspaceCreateErrors,
+ ThrowOnError
+ >({
+ url: "/experimental/workspace",
...options,
...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
})
}
/**
- * Create worktree
+ * Remove workspace
*
- * Create a new git worktree for the current project and run any configured startup scripts.
+ * Remove an existing workspace.
*/
- public create<ThrowOnError extends boolean = false>(
- parameters?: {
+ public remove<ThrowOnError extends boolean = false>(
+ parameters: {
+ id: string
directory?: string
workspace?: string
- worktreeCreateInput?: WorktreeCreateInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -948,35 +957,41 @@ export class Worktree extends HeyApiClient {
[
{
args: [
+ { in: "path", key: "id" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { key: "worktreeCreateInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).post<WorktreeCreateResponses, WorktreeCreateErrors, ThrowOnError>({
- url: "/experimental/worktree",
+ return (options?.client ?? this.client).delete<
+ ExperimentalWorkspaceRemoveResponses,
+ ExperimentalWorkspaceRemoveErrors,
+ ThrowOnError
+ >({
+ url: "/experimental/workspace/{id}",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
+}
+export class Session extends HeyApiClient {
/**
- * Reset worktree
+ * List sessions
*
- * Reset a worktree branch to the primary default branch.
+ * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.
*/
- public reset<ThrowOnError extends boolean = false>(
+ public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
- worktreeResetInput?: WorktreeResetInput
+ roots?: boolean
+ start?: number
+ cursor?: number
+ search?: string
+ limit?: number
+ archived?: boolean
},
options?: Options<never, ThrowOnError>,
) {
@@ -987,33 +1002,32 @@ export class Worktree extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { key: "worktreeResetInput", map: "body" },
+ { 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).post<WorktreeResetResponses, WorktreeResetErrors, ThrowOnError>({
- url: "/experimental/worktree/reset",
+ return (options?.client ?? this.client).get<ExperimentalSessionListResponses, unknown, ThrowOnError>({
+ url: "/experimental/session",
...options,
...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
})
}
}
-export class Workspace extends HeyApiClient {
+export class Resource extends HeyApiClient {
/**
- * Remove workspace
+ * Get MCP resources
*
- * Remove an existing workspace.
+ * Get all available MCP resources from connected servers. Optionally filter by name.
*/
- public remove<ThrowOnError extends boolean = false>(
- parameters: {
- id: string
+ public list<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
},
@@ -1024,39 +1038,48 @@ export class Workspace extends HeyApiClient {
[
{
args: [
- { in: "path", key: "id" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
- return (options?.client ?? this.client).delete<
- ExperimentalWorkspaceRemoveResponses,
- ExperimentalWorkspaceRemoveErrors,
- ThrowOnError
- >({
- url: "/experimental/workspace/{id}",
+ return (options?.client ?? this.client).get<ExperimentalResourceListResponses, unknown, ThrowOnError>({
+ url: "/experimental/resource",
...options,
...params,
})
}
+}
+
+export class Experimental extends HeyApiClient {
+ private _workspace?: Workspace
+ get workspace(): Workspace {
+ return (this._workspace ??= new Workspace({ 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 Worktree extends HeyApiClient {
/**
- * Create workspace
+ * Remove worktree
*
- * Create a workspace for the current project.
+ * Remove a git worktree and delete its branch.
*/
- public create<ThrowOnError extends boolean = false>(
- parameters: {
- id: string
+ public remove<ThrowOnError extends boolean = false>(
+ parameters?: {
directory?: string
workspace?: string
- branch?: string | null
- config?: {
- directory: string
- type: "worktree"
- }
+ worktreeRemoveInput?: WorktreeRemoveInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -1065,21 +1088,15 @@ export class Workspace extends HeyApiClient {
[
{
args: [
- { in: "path", key: "id" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
- { in: "body", key: "branch" },
- { in: "body", key: "config" },
+ { key: "worktreeRemoveInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).post<
- ExperimentalWorkspaceCreateResponses,
- ExperimentalWorkspaceCreateErrors,
- ThrowOnError
- >({
- url: "/experimental/workspace/{id}",
+ return (options?.client ?? this.client).delete<WorktreeRemoveResponses, WorktreeRemoveErrors, ThrowOnError>({
+ url: "/experimental/worktree",
...options,
...params,
headers: {
@@ -1091,9 +1108,9 @@ export class Workspace extends HeyApiClient {
}
/**
- * List workspaces
+ * List worktrees
*
- * List all workspaces.
+ * List all sandbox worktrees for the current project.
*/
public list<ThrowOnError extends boolean = false>(
parameters?: {
@@ -1113,30 +1130,23 @@ export class Workspace extends HeyApiClient {
},
],
)
- return (options?.client ?? this.client).get<ExperimentalWorkspaceListResponses, unknown, ThrowOnError>({
- url: "/experimental/workspace",
+ return (options?.client ?? this.client).get<WorktreeListResponses, unknown, ThrowOnError>({
+ url: "/experimental/worktree",
...options,
...params,
})
}
-}
-export class Session extends HeyApiClient {
/**
- * List sessions
+ * Create worktree
*
- * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.
+ * 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
- roots?: boolean
- start?: number
- cursor?: number
- search?: string
- limit?: number
- archived?: boolean
+ worktreeCreateInput?: WorktreeCreateInput
},
options?: Options<never, ThrowOnError>,
) {
@@ -1147,34 +1157,33 @@ 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" },
+ { key: "worktreeCreateInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).get<ExperimentalSessionListResponses, unknown, ThrowOnError>({
- url: "/experimental/session",
+ 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 Resource extends HeyApiClient {
/**
- * Get MCP resources
+ * Reset worktree
*
- * Get all available MCP resources from connected servers. Optionally filter by name.
+ * 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>,
) {
@@ -1185,35 +1194,24 @@ export class Resource extends HeyApiClient {
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
+ { key: "worktreeResetInput", map: "body" },
],
},
],
)
- return (options?.client ?? this.client).get<ExperimentalResourceListResponses, unknown, ThrowOnError>({
- url: "/experimental/resource",
+ 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 Experimental extends HeyApiClient {
- private _workspace?: Workspace
- get workspace(): Workspace {
- return (this._workspace ??= new Workspace({ 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 Session2 extends HeyApiClient {
/**
* List sessions
@@ -3898,16 +3896,16 @@ export class OpencodeClient extends HeyApiClient {
return (this._tool ??= new Tool({ client: this.client }))
}
- private _worktree?: Worktree
- get worktree(): Worktree {
- return (this._worktree ??= new Worktree({ client: this.client }))
- }
-
private _experimental?: Experimental
get experimental(): Experimental {
return (this._experimental ??= new Experimental({ client: this.client }))
}
+ private _worktree?: Worktree
+ get worktree(): Worktree {
+ return (this._worktree ??= new Worktree({ client: this.client }))
+ }
+
private _session?: Session2
get session(): Session2 {
return (this._session ??= new Session2({ 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 69d105610..e40eb13a3 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -1631,6 +1631,18 @@ export type ToolListItem = {
export type ToolList = Array<ToolListItem>
+export type Workspace = {
+ id: string
+ branch: string | null
+ projectID: string
+ config: {
+ type: "worktree"
+ directory: string
+ name: string
+ branch: string
+ }
+}
+
export type Worktree = {
name: string
branch: string
@@ -1645,16 +1657,6 @@ export type WorktreeCreateInput = {
startCommand?: string
}
-export type Workspace = {
- id: string
- branch: string | null
- projectID: string
- config: {
- directory: string
- type: "worktree"
- }
-}
-
export type WorktreeRemoveInput = {
directory: string
}
@@ -2444,80 +2446,60 @@ export type ToolListResponses = {
export type ToolListResponse = ToolListResponses[keyof ToolListResponses]
-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 WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses]
-
-export type WorktreeListData = {
+export type ExperimentalWorkspaceListData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/worktree"
+ url: "/experimental/workspace"
}
-export type WorktreeListResponses = {
+export type ExperimentalWorkspaceListResponses = {
/**
- * List of worktree directories
+ * Workspaces
*/
- 200: Array<string>
+ 200: Array<Workspace>
}
-export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses]
+export type ExperimentalWorkspaceListResponse =
+ ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses]
-export type WorktreeCreateData = {
- body?: WorktreeCreateInput
+export type ExperimentalWorkspaceCreateData = {
+ body?: {
+ branch?: string | null
+ } & {
+ type: "worktree"
+ name: string
+ }
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/worktree"
+ url: "/experimental/workspace"
}
-export type WorktreeCreateErrors = {
+export type ExperimentalWorkspaceCreateErrors = {
/**
* Bad request
*/
400: BadRequestError
}
-export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors]
+export type ExperimentalWorkspaceCreateError =
+ ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors]
-export type WorktreeCreateResponses = {
+export type ExperimentalWorkspaceCreateResponses = {
/**
- * Worktree created
+ * Workspace created
*/
- 200: Worktree
+ 200: Workspace
}
-export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses]
+export type ExperimentalWorkspaceCreateResponse =
+ ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses]
export type ExperimentalWorkspaceRemoveData = {
body?: never
@@ -2551,63 +2533,80 @@ export type ExperimentalWorkspaceRemoveResponses = {
export type ExperimentalWorkspaceRemoveResponse =
ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses]
-export type ExperimentalWorkspaceCreateData = {
- body?: {
- branch: string | null
- config: {
- directory: string
- type: "worktree"
- }
- }
- path: {
- id: string
- }
+export type WorktreeRemoveData = {
+ body?: WorktreeRemoveInput
+ path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/workspace/{id}"
+ url: "/experimental/worktree"
}
-export type ExperimentalWorkspaceCreateErrors = {
+export type WorktreeRemoveErrors = {
/**
* Bad request
*/
400: BadRequestError
}
-export type ExperimentalWorkspaceCreateError =
- ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors]
+export type WorktreeRemoveError = WorktreeRemoveErrors[keyof WorktreeRemoveErrors]
-export type ExperimentalWorkspaceCreateResponses = {
+export type WorktreeRemoveResponses = {
/**
- * Workspace created
+ * Worktree removed
*/
- 200: Workspace
+ 200: boolean
}
-export type ExperimentalWorkspaceCreateResponse =
- ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses]
+export type WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses]
-export type ExperimentalWorkspaceListData = {
+export type WorktreeListData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
- url: "/experimental/workspace"
+ url: "/experimental/worktree"
}
-export type ExperimentalWorkspaceListResponses = {
+export type WorktreeListResponses = {
/**
- * Workspaces
+ * List of worktree directories
*/
- 200: Array<Workspace>
+ 200: Array<string>
}
-export type ExperimentalWorkspaceListResponse =
- ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses]
+export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses]
+
+export type WorktreeCreateData = {
+ body?: WorktreeCreateInput
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/experimental/worktree"
+}
+
+export type WorktreeCreateErrors = {
+ /**
+ * Bad request
+ */
+ 400: BadRequestError
+}
+
+export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors]
+
+export type WorktreeCreateResponses = {
+ /**
+ * Worktree created
+ */
+ 200: Worktree
+}
+
+export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses]
export type WorktreeResetData = {
body?: WorktreeResetInput