Procházet zdrojové kódy

feat(core): rework workspace integration and adaptor interface (#15895)

James Long před 1 měsícem
rodič
revize
7f37acdaaa
23 změnil soubory, kde provedl 1614 přidání a 474 odebrání
  1. 5 0
      packages/opencode/migration/20260303231226_add_workspace_fields/migration.sql
  2. 1063 0
      packages/opencode/migration/20260303231226_add_workspace_fields/snapshot.json
  3. 0 7
      packages/opencode/src/cli/cmd/serve.ts
  4. 19 9
      packages/opencode/src/control-plane/adaptors/index.ts
  5. 0 7
      packages/opencode/src/control-plane/adaptors/types.ts
  6. 36 16
      packages/opencode/src/control-plane/adaptors/worktree.ts
  7. 0 10
      packages/opencode/src/control-plane/config.ts
  8. 0 46
      packages/opencode/src/control-plane/session-proxy-middleware.ts
  9. 20 0
      packages/opencode/src/control-plane/types.ts
  10. 50 0
      packages/opencode/src/control-plane/workspace-router-middleware.ts
  11. 43 3
      packages/opencode/src/control-plane/workspace-server/server.ts
  12. 4 2
      packages/opencode/src/control-plane/workspace.sql.ts
  13. 50 58
      packages/opencode/src/control-plane/workspace.ts
  14. 1 1
      packages/opencode/src/server/routes/experimental.ts
  15. 0 2
      packages/opencode/src/server/routes/session.ts
  16. 4 14
      packages/opencode/src/server/routes/workspace.ts
  17. 2 0
      packages/opencode/src/server/server.ts
  18. 18 6
      packages/opencode/src/worktree/index.ts
  19. 58 56
      packages/opencode/test/control-plane/session-proxy-middleware.test.ts
  20. 6 1
      packages/opencode/test/control-plane/workspace-server-sse.test.ts
  21. 31 29
      packages/opencode/test/control-plane/workspace-sync.test.ts
  22. 125 127
      packages/sdk/js/src/v2/gen/sdk.gen.ts
  23. 79 80
      packages/sdk/js/src/v2/gen/types.gen.ts

+ 5 - 0
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`;

+ 1063 - 0
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": []
+}

+ 0 - 7
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()))
   },
 })

+ 19 - 9
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
 }

+ 0 - 7
packages/opencode/src/control-plane/adaptors/types.ts

@@ -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>
-}

+ 36 - 16
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)
   },
 }

+ 0 - 10
packages/opencode/src/control-plane/config.ts

@@ -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>

+ 0 - 46
packages/opencode/src/control-plane/session-proxy-middleware.ts

@@ -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()
-}

+ 20 - 0
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>
+}

+ 50 - 0
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()
+}

+ 43 - 3
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 }) {

+ 4 - 2
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>(),
 })

+ 50 - 58
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) => {

+ 1 - 1
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({

+ 0 - 2
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({

+ 4 - 14
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.",
@@ -26,27 +26,17 @@ export const WorkspaceRoutes = lazy(() =>
           ...errors(400),
         },
       }),
-      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)
       },

+ 2 - 0
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, {

+ 18 - 6
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
   })
 

+ 58 - 56
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([])
+  // })
 })

+ 6 - 1
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)

+ 31 - 29
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")
   })
 })

+ 125 - 127
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 }))

+ 79 - 80
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