Просмотр исходного кода

feat(core): expose workspace adaptors to plugins (#21927)

James Long 3 дней назад
Родитель
Сommit
bf50d1c028

+ 16 - 0
packages/opencode/migration/20260410174513_workspace-name/migration.sql

@@ -0,0 +1,16 @@
+PRAGMA foreign_keys=OFF;--> statement-breakpoint
+CREATE TABLE `__new_workspace` (
+	`id` text PRIMARY KEY,
+	`type` text NOT NULL,
+	`name` text DEFAULT '' NOT NULL,
+	`branch` text,
+	`directory` text,
+	`extra` text,
+	`project_id` text NOT NULL,
+	CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
+);
+--> statement-breakpoint
+INSERT INTO `__new_workspace`(`id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id`) SELECT `id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id` FROM `workspace`;--> statement-breakpoint
+DROP TABLE `workspace`;--> statement-breakpoint
+ALTER TABLE `__new_workspace` RENAME TO `workspace`;--> statement-breakpoint
+PRAGMA foreign_keys=ON;

+ 1337 - 0
packages/opencode/migration/20260410174513_workspace-name/snapshot.json

@@ -0,0 +1,1337 @@
+{
+  "version": "7",
+  "dialect": "sqlite",
+  "id": "b61476b8-3b92-49ae-9fa5-6eef586ed64b",
+  "prevIds": [
+    "f13dfa58-7fb4-47a2-8f6b-dc70258e14ed"
+  ],
+  "ddl": [
+    {
+      "name": "account_state",
+      "entityType": "tables"
+    },
+    {
+      "name": "account",
+      "entityType": "tables"
+    },
+    {
+      "name": "control_account",
+      "entityType": "tables"
+    },
+    {
+      "name": "workspace",
+      "entityType": "tables"
+    },
+    {
+      "name": "project",
+      "entityType": "tables"
+    },
+    {
+      "name": "message",
+      "entityType": "tables"
+    },
+    {
+      "name": "part",
+      "entityType": "tables"
+    },
+    {
+      "name": "permission",
+      "entityType": "tables"
+    },
+    {
+      "name": "session",
+      "entityType": "tables"
+    },
+    {
+      "name": "todo",
+      "entityType": "tables"
+    },
+    {
+      "name": "session_share",
+      "entityType": "tables"
+    },
+    {
+      "name": "event_sequence",
+      "entityType": "tables"
+    },
+    {
+      "name": "event",
+      "entityType": "tables"
+    },
+    {
+      "type": "integer",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "account_state"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "active_account_id",
+      "entityType": "columns",
+      "table": "account_state"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "active_org_id",
+      "entityType": "columns",
+      "table": "account_state"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "email",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "url",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "access_token",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "refresh_token",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "integer",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "token_expiry",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "email",
+      "entityType": "columns",
+      "table": "control_account"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "url",
+      "entityType": "columns",
+      "table": "control_account"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "access_token",
+      "entityType": "columns",
+      "table": "control_account"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "refresh_token",
+      "entityType": "columns",
+      "table": "control_account"
+    },
+    {
+      "type": "integer",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "token_expiry",
+      "entityType": "columns",
+      "table": "control_account"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "active",
+      "entityType": "columns",
+      "table": "control_account"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "control_account"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "control_account"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "type",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": "''",
+      "generated": null,
+      "name": "name",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "branch",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "directory",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "extra",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "project_id",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "worktree",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "vcs",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "name",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "icon_url",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "icon_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"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "aggregate_id",
+      "entityType": "columns",
+      "table": "event_sequence"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "seq",
+      "entityType": "columns",
+      "table": "event_sequence"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "event"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "aggregate_id",
+      "entityType": "columns",
+      "table": "event"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "seq",
+      "entityType": "columns",
+      "table": "event"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "type",
+      "entityType": "columns",
+      "table": "event"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "data",
+      "entityType": "columns",
+      "table": "event"
+    },
+    {
+      "columns": [
+        "active_account_id"
+      ],
+      "tableTo": "account",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "SET NULL",
+      "nameExplicit": false,
+      "name": "fk_account_state_active_account_id_account_id_fk",
+      "entityType": "fks",
+      "table": "account_state"
+    },
+    {
+      "columns": [
+        "project_id"
+      ],
+      "tableTo": "project",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_workspace_project_id_project_id_fk",
+      "entityType": "fks",
+      "table": "workspace"
+    },
+    {
+      "columns": [
+        "session_id"
+      ],
+      "tableTo": "session",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_message_session_id_session_id_fk",
+      "entityType": "fks",
+      "table": "message"
+    },
+    {
+      "columns": [
+        "message_id"
+      ],
+      "tableTo": "message",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_part_message_id_message_id_fk",
+      "entityType": "fks",
+      "table": "part"
+    },
+    {
+      "columns": [
+        "project_id"
+      ],
+      "tableTo": "project",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_permission_project_id_project_id_fk",
+      "entityType": "fks",
+      "table": "permission"
+    },
+    {
+      "columns": [
+        "project_id"
+      ],
+      "tableTo": "project",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_session_project_id_project_id_fk",
+      "entityType": "fks",
+      "table": "session"
+    },
+    {
+      "columns": [
+        "session_id"
+      ],
+      "tableTo": "session",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_todo_session_id_session_id_fk",
+      "entityType": "fks",
+      "table": "todo"
+    },
+    {
+      "columns": [
+        "session_id"
+      ],
+      "tableTo": "session",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_session_share_session_id_session_id_fk",
+      "entityType": "fks",
+      "table": "session_share"
+    },
+    {
+      "columns": [
+        "aggregate_id"
+      ],
+      "tableTo": "event_sequence",
+      "columnsTo": [
+        "aggregate_id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk",
+      "entityType": "fks",
+      "table": "event"
+    },
+    {
+      "columns": [
+        "email",
+        "url"
+      ],
+      "nameExplicit": false,
+      "name": "control_account_pk",
+      "entityType": "pks",
+      "table": "control_account"
+    },
+    {
+      "columns": [
+        "session_id",
+        "position"
+      ],
+      "nameExplicit": false,
+      "name": "todo_pk",
+      "entityType": "pks",
+      "table": "todo"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "account_state_pk",
+      "table": "account_state",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "account_pk",
+      "table": "account",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "workspace_pk",
+      "table": "workspace",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "project_pk",
+      "table": "project",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "message_pk",
+      "table": "message",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "part_pk",
+      "table": "part",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "project_id"
+      ],
+      "nameExplicit": false,
+      "name": "permission_pk",
+      "table": "permission",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "session_pk",
+      "table": "session",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "session_id"
+      ],
+      "nameExplicit": false,
+      "name": "session_share_pk",
+      "table": "session_share",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "aggregate_id"
+      ],
+      "nameExplicit": false,
+      "name": "event_sequence_pk",
+      "table": "event_sequence",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "event_pk",
+      "table": "event",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        {
+          "value": "session_id",
+          "isExpression": false
+        },
+        {
+          "value": "time_created",
+          "isExpression": false
+        },
+        {
+          "value": "id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "where": null,
+      "origin": "manual",
+      "name": "message_session_time_created_id_idx",
+      "entityType": "indexes",
+      "table": "message"
+    },
+    {
+      "columns": [
+        {
+          "value": "message_id",
+          "isExpression": false
+        },
+        {
+          "value": "id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "where": null,
+      "origin": "manual",
+      "name": "part_message_id_id_idx",
+      "entityType": "indexes",
+      "table": "part"
+    },
+    {
+      "columns": [
+        {
+          "value": "session_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "where": null,
+      "origin": "manual",
+      "name": "part_session_idx",
+      "entityType": "indexes",
+      "table": "part"
+    },
+    {
+      "columns": [
+        {
+          "value": "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": []
+}

+ 40 - 8
packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx

@@ -9,6 +9,12 @@ import { setTimeout as sleep } from "node:timers/promises"
 import { useSDK } from "../context/sdk"
 import { useToast } from "../ui/toast"
 
+type Adaptor = {
+  type: string
+  name: string
+  description: string
+}
+
 function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
   return createOpencodeClient({
     baseUrl: sdk.url,
@@ -63,9 +69,27 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
   const sdk = useSDK()
   const toast = useToast()
   const [creating, setCreating] = createSignal<string>()
+  const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
 
   onMount(() => {
     dialog.setSize("medium")
+    void (async () => {
+      const dir = sync.path.directory || sdk.directory
+      const url = new URL("/experimental/workspace/adaptor", sdk.url)
+      if (dir) url.searchParams.set("directory", dir)
+      const res = await sdk
+        .fetch(url)
+        .then((x) => x.json() as Promise<Adaptor[]>)
+        .catch(() => undefined)
+      if (!res) {
+        toast.show({
+          message: "Failed to load workspace adaptors",
+          variant: "error",
+        })
+        return
+      }
+      setAdaptors(res)
+    })()
   })
 
   const options = createMemo(() => {
@@ -79,13 +103,21 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
         },
       ]
     }
-    return [
-      {
-        title: "Worktree",
-        value: "worktree" as const,
-        description: "Create a local git worktree",
-      },
-    ]
+    const list = adaptors()
+    if (!list) {
+      return [
+        {
+          title: "Loading workspaces...",
+          value: "loading" as const,
+          description: "Fetching available workspace adaptors",
+        },
+      ]
+    }
+    return list.map((item) => ({
+      title: item.name,
+      value: item.type,
+      description: item.description,
+    }))
   })
 
   const create = async (type: string) => {
@@ -113,7 +145,7 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
       skipFilter={true}
       options={options()}
       onSelect={(option) => {
-        if (option.value === "creating") return
+        if (option.value === "creating" || option.value === "loading") return
         void create(option.value)
       }}
     />

+ 43 - 11
packages/opencode/src/control-plane/adaptors/index.ts

@@ -1,20 +1,52 @@
 import { lazy } from "@/util/lazy"
-import type { Adaptor } from "../types"
+import type { ProjectID } from "@/project/schema"
+import type { WorkspaceAdaptor } from "../types"
 
-const ADAPTORS: Record<string, () => Promise<Adaptor>> = {
+export type WorkspaceAdaptorEntry = {
+  type: string
+  name: string
+  description: string
+}
+
+const BUILTIN: Record<string, () => Promise<WorkspaceAdaptor>> = {
   worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor),
 }
 
-export function getAdaptor(type: string): Promise<Adaptor> {
-  return ADAPTORS[type]()
+const state = new Map<ProjectID, Map<string, WorkspaceAdaptor>>()
+
+export async function getAdaptor(projectID: ProjectID, type: string): Promise<WorkspaceAdaptor> {
+  const custom = state.get(projectID)?.get(type)
+  if (custom) return custom
+
+  const builtin = BUILTIN[type]
+  if (builtin) return builtin()
+
+  throw new Error(`Unknown workspace adaptor: ${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
+export async function listAdaptors(projectID: ProjectID): Promise<WorkspaceAdaptorEntry[]> {
+  const builtin = await Promise.all(
+    Object.entries(BUILTIN).map(async ([type, init]) => {
+      const adaptor = await init()
+      return {
+        type,
+        name: adaptor.name,
+        description: adaptor.description,
+      }
+    }),
+  )
+  const custom = [...(state.get(projectID)?.entries() ?? [])].map(([type, adaptor]) => ({
+    type,
+    name: adaptor.name,
+    description: adaptor.description,
+  }))
+  return [...builtin, ...custom]
+}
 
-  // @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
+// Plugins can be loaded per-project so we need to scope them. If you
+// want to install a global one pass `ProjectID.global`
+export function registerAdaptor(projectID: ProjectID, type: string, adaptor: WorkspaceAdaptor) {
+  const adaptors = state.get(projectID) ?? new Map<string, WorkspaceAdaptor>()
+  adaptors.set(type, adaptor)
+  state.set(projectID, adaptors)
 }

+ 10 - 10
packages/opencode/src/control-plane/adaptors/worktree.ts

@@ -1,18 +1,18 @@
 import z from "zod"
 import { Worktree } from "@/worktree"
-import { type Adaptor, WorkspaceInfo } from "../types"
+import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
 
-const Config = WorkspaceInfo.extend({
-  name: WorkspaceInfo.shape.name.unwrap(),
+const WorktreeConfig = z.object({
+  name: WorkspaceInfo.shape.name,
   branch: WorkspaceInfo.shape.branch.unwrap(),
   directory: WorkspaceInfo.shape.directory.unwrap(),
 })
 
-type Config = z.infer<typeof Config>
-
-export const WorktreeAdaptor: Adaptor = {
+export const WorktreeAdaptor: WorkspaceAdaptor = {
+  name: "Worktree",
+  description: "Create a git worktree",
   async configure(info) {
-    const worktree = await Worktree.makeWorktreeInfo(info.name ?? undefined)
+    const worktree = await Worktree.makeWorktreeInfo(undefined)
     return {
       ...info,
       name: worktree.name,
@@ -21,7 +21,7 @@ export const WorktreeAdaptor: Adaptor = {
     }
   },
   async create(info) {
-    const config = Config.parse(info)
+    const config = WorktreeConfig.parse(info)
     await Worktree.createFromInfo({
       name: config.name,
       directory: config.directory,
@@ -29,11 +29,11 @@ export const WorktreeAdaptor: Adaptor = {
     })
   },
   async remove(info) {
-    const config = Config.parse(info)
+    const config = WorktreeConfig.parse(info)
     await Worktree.remove({ directory: config.directory })
   },
   target(info) {
-    const config = Config.parse(info)
+    const config = WorktreeConfig.parse(info)
     return {
       type: "local",
       directory: config.directory,

+ 8 - 6
packages/opencode/src/control-plane/types.ts

@@ -5,8 +5,8 @@ import { WorkspaceID } from "./schema"
 export const WorkspaceInfo = z.object({
   id: WorkspaceID.zod,
   type: z.string(),
+  name: z.string(),
   branch: z.string().nullable(),
-  name: z.string().nullable(),
   directory: z.string().nullable(),
   extra: z.unknown().nullable(),
   projectID: ProjectID.zod,
@@ -24,9 +24,11 @@ export type Target =
       headers?: HeadersInit
     }
 
-export type Adaptor = {
-  configure(input: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
-  create(config: WorkspaceInfo, from?: WorkspaceInfo): Promise<void>
-  remove(config: WorkspaceInfo): Promise<void>
-  target(config: WorkspaceInfo): Target | Promise<Target>
+export type WorkspaceAdaptor = {
+  name: string
+  description: string
+  configure(info: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
+  create(info: WorkspaceInfo, from?: WorkspaceInfo): Promise<void>
+  remove(info: WorkspaceInfo): Promise<void>
+  target(info: WorkspaceInfo): Target | Promise<Target>
 }

+ 1 - 1
packages/opencode/src/control-plane/workspace.sql.ts

@@ -6,8 +6,8 @@ import type { WorkspaceID } from "./schema"
 export const WorkspaceTable = sqliteTable("workspace", {
   id: text().$type<WorkspaceID>().primaryKey(),
   type: text().notNull(),
+  name: text().notNull().default(""),
   branch: text(),
-  name: text(),
   directory: text(),
   extra: text({ mode: "json" }),
   project_id: text()

+ 5 - 4
packages/opencode/src/control-plane/workspace.ts

@@ -9,6 +9,7 @@ import { SyncEvent } from "@/sync"
 import { Log } from "@/util/log"
 import { Filesystem } from "@/util/filesystem"
 import { ProjectID } from "@/project/schema"
+import { Slug } from "@opencode-ai/util/slug"
 import { WorkspaceTable } from "./workspace.sql"
 import { getAdaptor } from "./adaptors"
 import { WorkspaceInfo } from "./types"
@@ -66,9 +67,9 @@ export namespace Workspace {
 
   export const create = fn(CreateInput, async (input) => {
     const id = WorkspaceID.ascending(input.id)
-    const adaptor = await getAdaptor(input.type)
+    const adaptor = await getAdaptor(input.projectID, input.type)
 
-    const config = await adaptor.configure({ ...input, id, name: null, directory: null })
+    const config = await adaptor.configure({ ...input, id, name: Slug.create(), directory: null })
 
     const info: Info = {
       id,
@@ -124,7 +125,7 @@ export namespace Workspace {
       stopSync(id)
 
       const info = fromRow(row)
-      const adaptor = await getAdaptor(row.type)
+      const adaptor = await getAdaptor(info.projectID, row.type)
       adaptor.remove(info)
       Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
       return info
@@ -162,7 +163,7 @@ export namespace Workspace {
       log.info("connecting to sync: " + space.id)
 
       setStatus(space.id, "connecting")
-      const adaptor = await getAdaptor(space.type)
+      const adaptor = await getAdaptor(space.projectID, space.type)
       const target = await adaptor.target(space)
 
       if (target.type === "local") return

+ 14 - 1
packages/opencode/src/plugin/index.ts

@@ -1,4 +1,10 @@
-import type { Hooks, PluginInput, Plugin as PluginInstance, PluginModule } from "@opencode-ai/plugin"
+import type {
+  Hooks,
+  PluginInput,
+  Plugin as PluginInstance,
+  PluginModule,
+  WorkspaceAdaptor as PluginWorkspaceAdaptor,
+} from "@opencode-ai/plugin"
 import { Config } from "../config/config"
 import { Bus } from "../bus"
 import { Log } from "../util/log"
@@ -18,6 +24,8 @@ import { makeRuntime } from "@/effect/run-service"
 import { errorMessage } from "@/util/error"
 import { PluginLoader } from "./loader"
 import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
+import { registerAdaptor } from "@/control-plane/adaptors"
+import type { WorkspaceAdaptor } from "@/control-plane/types"
 
 export namespace Plugin {
   const log = Log.create({ service: "plugin" })
@@ -132,6 +140,11 @@ export namespace Plugin {
             project: ctx.project,
             worktree: ctx.worktree,
             directory: ctx.directory,
+            experimental_workspace: {
+              register(type: string, adaptor: PluginWorkspaceAdaptor) {
+                registerAdaptor(ctx.project.id, type, adaptor as WorkspaceAdaptor)
+              },
+            },
             get serverUrl(): URL {
               return Server.url ?? new URL("http://localhost:4096")
             },

+ 1 - 1
packages/opencode/src/server/instance/middleware.ts

@@ -95,7 +95,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
       })
     }
 
-    const adaptor = await getAdaptor(workspace.type)
+    const adaptor = await getAdaptor(workspace.projectID, workspace.type)
     const target = await adaptor.target(workspace)
 
     if (target.type === "local") {

+ 28 - 0
packages/opencode/src/server/instance/workspace.ts

@@ -1,13 +1,41 @@
 import { Hono } from "hono"
 import { describeRoute, resolver, validator } from "hono-openapi"
 import z from "zod"
+import { listAdaptors } from "../../control-plane/adaptors"
 import { Workspace } from "../../control-plane/workspace"
 import { Instance } from "../../project/instance"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"
 
+const WorkspaceAdaptor = z.object({
+  type: z.string(),
+  name: z.string(),
+  description: z.string(),
+})
+
 export const WorkspaceRoutes = lazy(() =>
   new Hono()
+    .get(
+      "/adaptor",
+      describeRoute({
+        summary: "List workspace adaptors",
+        description: "List all available workspace adaptors for the current project.",
+        operationId: "experimental.workspace.adaptor.list",
+        responses: {
+          200: {
+            description: "Workspace adaptors",
+            content: {
+              "application/json": {
+                schema: resolver(z.array(WorkspaceAdaptor)),
+              },
+            },
+          },
+        },
+      }),
+      async (c) => {
+        return c.json(await listAdaptors(Instance.project.id))
+      },
+    )
     .post(
       "/",
       describeRoute({

+ 71 - 0
packages/opencode/test/control-plane/adaptors.test.ts

@@ -0,0 +1,71 @@
+import { describe, expect, test } from "bun:test"
+import { getAdaptor, registerAdaptor } from "../../src/control-plane/adaptors"
+import { ProjectID } from "../../src/project/schema"
+import type { WorkspaceInfo } from "../../src/control-plane/types"
+
+function info(projectID: WorkspaceInfo["projectID"], type: string): WorkspaceInfo {
+  return {
+    id: "workspace-test" as WorkspaceInfo["id"],
+    type,
+    name: "workspace-test",
+    branch: null,
+    directory: null,
+    extra: null,
+    projectID,
+  }
+}
+
+function adaptor(dir: string) {
+  return {
+    name: dir,
+    description: dir,
+    configure(input: WorkspaceInfo) {
+      return input
+    },
+    async create() {},
+    async remove() {},
+    target() {
+      return {
+        type: "local" as const,
+        directory: dir,
+      }
+    },
+  }
+}
+
+describe("control-plane/adaptors", () => {
+  test("isolates custom adaptors by project", async () => {
+    const type = `demo-${Math.random().toString(36).slice(2)}`
+    const one = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
+    const two = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
+    registerAdaptor(one, type, adaptor("/one"))
+    registerAdaptor(two, type, adaptor("/two"))
+
+    expect(await (await getAdaptor(one, type)).target(info(one, type))).toEqual({
+      type: "local",
+      directory: "/one",
+    })
+    expect(await (await getAdaptor(two, type)).target(info(two, type))).toEqual({
+      type: "local",
+      directory: "/two",
+    })
+  })
+
+  test("latest install wins within a project", async () => {
+    const type = `demo-${Math.random().toString(36).slice(2)}`
+    const id = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
+    registerAdaptor(id, type, adaptor("/one"))
+
+    expect(await (await getAdaptor(id, type)).target(info(id, type))).toEqual({
+      type: "local",
+      directory: "/one",
+    })
+
+    registerAdaptor(id, type, adaptor("/two"))
+
+    expect(await (await getAdaptor(id, type)).target(info(id, type))).toEqual({
+      type: "local",
+      directory: "/two",
+    })
+  })
+})

+ 3 - 0
packages/opencode/test/plugin/github-copilot-models.test.ts

@@ -125,6 +125,9 @@ test("remaps fallback oauth model urls to the enterprise host", async () => {
     project: {} as never,
     directory: "",
     worktree: "",
+    experimental_workspace: {
+      register() {},
+    },
     serverUrl: new URL("https://example.com"),
     $: {} as never,
   })

+ 99 - 0
packages/opencode/test/plugin/workspace-adaptor.test.ts

@@ -0,0 +1,99 @@
+import { afterAll, afterEach, describe, expect, test } from "bun:test"
+import path from "path"
+import { pathToFileURL } from "url"
+import { tmpdir } from "../fixture/fixture"
+
+const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
+process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1"
+
+const { Plugin } = await import("../../src/plugin/index")
+const { Workspace } = await import("../../src/control-plane/workspace")
+const { Instance } = await import("../../src/project/instance")
+
+afterEach(async () => {
+  await Instance.disposeAll()
+})
+
+afterAll(() => {
+  if (disableDefault === undefined) {
+    delete process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
+    return
+  }
+  process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = disableDefault
+})
+
+describe("plugin.workspace", () => {
+  test("plugin can install a workspace adaptor", async () => {
+    await using tmp = await tmpdir({
+      init: async (dir) => {
+        const type = `plug-${Math.random().toString(36).slice(2)}`
+        const file = path.join(dir, "plugin.ts")
+        const mark = path.join(dir, "created.json")
+        const space = path.join(dir, "space")
+        await Bun.write(
+          file,
+          [
+            "export default async ({ experimental_workspace }) => {",
+            `  experimental_workspace.register(${JSON.stringify(type)}, {`,
+            '    name: "plug",',
+            '    description: "plugin workspace adaptor",',
+            "    configure(input) {",
+            `      return { ...input, name: \"plug\", branch: \"plug/main\", directory: ${JSON.stringify(space)} }`,
+            "    },",
+            "    async create(input) {",
+            `      await Bun.write(${JSON.stringify(mark)}, JSON.stringify(input))`,
+            "    },",
+            "    async remove() {},",
+            "    target(input) {",
+            '      return { type: "local", directory: input.directory }',
+            "    },",
+            "  })",
+            "  return {}",
+            "}",
+            "",
+          ].join("\n"),
+        )
+
+        await Bun.write(
+          path.join(dir, "opencode.json"),
+          JSON.stringify(
+            {
+              $schema: "https://opencode.ai/config.json",
+              plugin: [pathToFileURL(file).href],
+            },
+            null,
+            2,
+          ),
+        )
+
+        return { mark, space, type }
+      },
+    })
+
+    const info = await Instance.provide({
+      directory: tmp.path,
+      fn: async () => {
+        await Plugin.init()
+        return Workspace.create({
+          type: tmp.extra.type,
+          branch: null,
+          extra: { key: "value" },
+          projectID: Instance.project.id,
+        })
+      },
+    })
+
+    expect(info.type).toBe(tmp.extra.type)
+    expect(info.name).toBe("plug")
+    expect(info.branch).toBe("plug/main")
+    expect(info.directory).toBe(tmp.extra.space)
+    expect(info.extra).toEqual({ key: "value" })
+    expect(JSON.parse(await Bun.file(tmp.extra.mark).text())).toMatchObject({
+      type: tmp.extra.type,
+      name: "plug",
+      branch: "plug/main",
+      directory: tmp.extra.space,
+      extra: { key: "value" },
+    })
+  })
+})

+ 34 - 0
packages/plugin/src/example-workspace.ts

@@ -0,0 +1,34 @@
+import type { Plugin } from "@opencode-ai/plugin"
+import { mkdir, rm } from "node:fs/promises"
+
+export const FolderWorkspacePlugin: Plugin = async ({ experimental_workspace }) => {
+  experimental_workspace.register("folder", {
+    name: "Folder",
+    description: "Create a blank folder",
+    configure(config) {
+      const rand = "" + Math.random()
+
+      return {
+        ...config,
+        directory: `/tmp/folder/folder-${rand}`,
+      }
+    },
+    async create(config) {
+      if (!config.directory) return
+      await mkdir(config.directory, { recursive: true })
+    },
+    async remove(config) {
+      await rm(config.directory!, { recursive: true, force: true })
+    },
+    target(config) {
+      return {
+        type: "local",
+        directory: config.directory!,
+      }
+    },
+  })
+
+  return {}
+}
+
+export default FolderWorkspacePlugin

+ 33 - 0
packages/plugin/src/index.ts

@@ -24,11 +24,44 @@ export type ProviderContext = {
   options: Record<string, any>
 }
 
+export type WorkspaceInfo = {
+  id: string
+  type: string
+  name: string
+  branch: string | null
+  directory: string | null
+  extra: unknown | null
+  projectID: string
+}
+
+export type WorkspaceTarget =
+  | {
+      type: "local"
+      directory: string
+    }
+  | {
+      type: "remote"
+      url: string | URL
+      headers?: HeadersInit
+    }
+
+export type WorkspaceAdaptor = {
+  name: string
+  description: string
+  configure(config: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
+  create(config: WorkspaceInfo, from?: WorkspaceInfo): Promise<void>
+  remove(config: WorkspaceInfo): Promise<void>
+  target(config: WorkspaceInfo): WorkspaceTarget | Promise<WorkspaceTarget>
+}
+
 export type PluginInput = {
   client: ReturnType<typeof createOpencodeClient>
   project: Project
   directory: string
   worktree: string
+  experimental_workspace: {
+    register(type: string, adaptor: WorkspaceAdaptor): void
+  }
   serverUrl: URL
   $: BunShell
 }

+ 1 - 0
packages/plugin/tsconfig.json

@@ -2,6 +2,7 @@
   "$schema": "https://json.schemastore.org/tsconfig.json",
   "extends": "@tsconfig/node22/tsconfig.json",
   "compilerOptions": {
+    "rootDir": "src",
     "outDir": "dist",
     "module": "nodenext",
     "declaration": true,