From 180ded6a27c49c0f95c8af5ff17ccacaa54eceab Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 10 Apr 2026 13:03:20 -0400 Subject: rector(core,tui): handle workspace state in project context, add workspace status, improve ui (#21896) --- packages/plugin/src/tui.ts | 4 ---- 1 file changed, 4 deletions(-) (limited to 'packages/plugin/src') diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 8f8439fab..e6f832f7e 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -272,10 +272,6 @@ export type TuiState = { directory: string } readonly vcs: { branch?: string } | undefined - readonly workspace: { - list: () => ReadonlyArray - get: (workspaceID: string) => Workspace | undefined - } session: { count: () => number diff: (sessionID: string) => ReadonlyArray -- cgit v1.2.3 From d84cc337428d3825caba14f97ec463c7781b5c77 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 10 Apr 2026 23:50:50 -0400 Subject: refactor(plugin): return Effect from ToolContext.ask (#21986) --- bun.lock | 1 + packages/opencode/src/tool/registry.ts | 3 +-- packages/plugin/package.json | 1 + packages/plugin/src/tool.ts | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) (limited to 'packages/plugin/src') diff --git a/bun.lock b/bun.lock index 3d6bf4f0f..cdffa8ea4 100644 --- a/bun.lock +++ b/bun.lock @@ -450,6 +450,7 @@ "version": "1.4.3", "dependencies": { "@opencode-ai/sdk": "workspace:*", + "effect": "catalog:", "zod": "catalog:", }, "devDependencies": { diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 0cd6d312f..afb19a468 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -30,7 +30,6 @@ import { Glob } from "../util/glob" import path from "path" import { pathToFileURL } from "url" import { Effect, Layer, Context } from "effect" -import { EffectLogger } from "@/effect/logger" import { FetchHttpClient, HttpClient } from "effect/unstable/http" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" @@ -137,7 +136,7 @@ export namespace ToolRegistry { Effect.gen(function* () { const pluginCtx: PluginToolContext = { ...toolCtx, - ask: (req) => Effect.runPromise(toolCtx.ask(req).pipe(Effect.provide(EffectLogger.layer))), + ask: (req) => toolCtx.ask(req), directory: ctx.directory, worktree: ctx.worktree, } diff --git a/packages/plugin/package.json b/packages/plugin/package.json index ced1523da..ba4735371 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -18,6 +18,7 @@ ], "dependencies": { "@opencode-ai/sdk": "workspace:*", + "effect": "catalog:", "zod": "catalog:" }, "peerDependencies": { diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/tool.ts index 23aa512d9..b568d0371 100644 --- a/packages/plugin/src/tool.ts +++ b/packages/plugin/src/tool.ts @@ -1,4 +1,5 @@ import { z } from "zod" +import { Effect } from "effect" export type ToolContext = { sessionID: string @@ -16,7 +17,7 @@ export type ToolContext = { worktree: string abort: AbortSignal metadata(input: { title?: string; metadata?: { [key: string]: any } }): void - ask(input: AskInput): Promise + ask(input: AskInput): Effect.Effect } type AskInput = { -- cgit v1.2.3 From bf50d1c028e973ccc0beffdf568fca417b62f020 Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 13 Apr 2026 13:33:13 -0400 Subject: feat(core): expose workspace adaptors to plugins (#21927) --- .../20260410174513_workspace-name/migration.sql | 16 + .../20260410174513_workspace-name/snapshot.json | 1337 ++++++++++++++++++++ .../cmd/tui/component/dialog-workspace-create.tsx | 48 +- .../opencode/src/control-plane/adaptors/index.ts | 54 +- .../src/control-plane/adaptors/worktree.ts | 20 +- packages/opencode/src/control-plane/types.ts | 14 +- .../opencode/src/control-plane/workspace.sql.ts | 2 +- packages/opencode/src/control-plane/workspace.ts | 9 +- packages/opencode/src/plugin/index.ts | 15 +- .../opencode/src/server/instance/middleware.ts | 2 +- packages/opencode/src/server/instance/workspace.ts | 28 + .../opencode/test/control-plane/adaptors.test.ts | 71 ++ .../test/plugin/github-copilot-models.test.ts | 3 + .../opencode/test/plugin/workspace-adaptor.test.ts | 99 ++ packages/plugin/src/example-workspace.ts | 34 + packages/plugin/src/index.ts | 33 + packages/plugin/tsconfig.json | 1 + 17 files changed, 1744 insertions(+), 42 deletions(-) create mode 100644 packages/opencode/migration/20260410174513_workspace-name/migration.sql create mode 100644 packages/opencode/migration/20260410174513_workspace-name/snapshot.json create mode 100644 packages/opencode/test/control-plane/adaptors.test.ts create mode 100644 packages/opencode/test/plugin/workspace-adaptor.test.ts create mode 100644 packages/plugin/src/example-workspace.ts (limited to 'packages/plugin/src') diff --git a/packages/opencode/migration/20260410174513_workspace-name/migration.sql b/packages/opencode/migration/20260410174513_workspace-name/migration.sql new file mode 100644 index 000000000..2a27248e4 --- /dev/null +++ b/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; \ No newline at end of file diff --git a/packages/opencode/migration/20260410174513_workspace-name/snapshot.json b/packages/opencode/migration/20260410174513_workspace-name/snapshot.json new file mode 100644 index 000000000..ab7028008 --- /dev/null +++ b/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": [] +} \ No newline at end of file diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index 40cc1013e..447a1c325 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/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, sync: ReturnType, 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() + const [adaptors, setAdaptors] = createSignal() 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) + .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) }} /> diff --git a/packages/opencode/src/control-plane/adaptors/index.ts b/packages/opencode/src/control-plane/adaptors/index.ts index a43fce248..291e392ea 100644 --- a/packages/opencode/src/control-plane/adaptors/index.ts +++ b/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 Promise> = { +export type WorkspaceAdaptorEntry = { + type: string + name: string + description: string +} + +const BUILTIN: Record Promise> = { worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor), } -export function getAdaptor(type: string): Promise { - return ADAPTORS[type]() +const state = new Map>() + +export async function getAdaptor(projectID: ProjectID, type: string): Promise { + 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 { + 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() + adaptors.set(type, adaptor) + state.set(projectID, adaptors) } diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts index 9fb6c7479..6cc4c20a4 100644 --- a/packages/opencode/src/control-plane/adaptors/worktree.ts +++ b/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 - -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, diff --git a/packages/opencode/src/control-plane/types.ts b/packages/opencode/src/control-plane/types.ts index dd17c56d9..4e499e45e 100644 --- a/packages/opencode/src/control-plane/types.ts +++ b/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 - create(config: WorkspaceInfo, from?: WorkspaceInfo): Promise - remove(config: WorkspaceInfo): Promise - target(config: WorkspaceInfo): Target | Promise +export type WorkspaceAdaptor = { + name: string + description: string + configure(info: WorkspaceInfo): WorkspaceInfo | Promise + create(info: WorkspaceInfo, from?: WorkspaceInfo): Promise + remove(info: WorkspaceInfo): Promise + target(info: WorkspaceInfo): Target | Promise } diff --git a/packages/opencode/src/control-plane/workspace.sql.ts b/packages/opencode/src/control-plane/workspace.sql.ts index 272907da1..a6a4ce2c8 100644 --- a/packages/opencode/src/control-plane/workspace.sql.ts +++ b/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().primaryKey(), type: text().notNull(), + name: text().notNull().default(""), branch: text(), - name: text(), directory: text(), extra: text({ mode: "json" }), project_id: text() diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index bbf79620c..f330e07b7 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/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 diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index e0478e0b3..60942915a 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/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") }, diff --git a/packages/opencode/src/server/instance/middleware.ts b/packages/opencode/src/server/instance/middleware.ts index 19bd26535..868131eb8 100644 --- a/packages/opencode/src/server/instance/middleware.ts +++ b/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") { diff --git a/packages/opencode/src/server/instance/workspace.ts b/packages/opencode/src/server/instance/workspace.ts index 419321654..7cee03197 100644 --- a/packages/opencode/src/server/instance/workspace.ts +++ b/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({ diff --git a/packages/opencode/test/control-plane/adaptors.test.ts b/packages/opencode/test/control-plane/adaptors.test.ts new file mode 100644 index 000000000..a8e490226 --- /dev/null +++ b/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", + }) + }) +}) diff --git a/packages/opencode/test/plugin/github-copilot-models.test.ts b/packages/opencode/test/plugin/github-copilot-models.test.ts index 0b67588a7..33ddef5dd 100644 --- a/packages/opencode/test/plugin/github-copilot-models.test.ts +++ b/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, }) diff --git a/packages/opencode/test/plugin/workspace-adaptor.test.ts b/packages/opencode/test/plugin/workspace-adaptor.test.ts new file mode 100644 index 000000000..cc098b124 --- /dev/null +++ b/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" }, + }) + }) +}) diff --git a/packages/plugin/src/example-workspace.ts b/packages/plugin/src/example-workspace.ts new file mode 100644 index 000000000..925328450 --- /dev/null +++ b/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 diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 1afb55daa..49d995c6f 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -24,11 +24,44 @@ export type ProviderContext = { options: Record } +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 + create(config: WorkspaceInfo, from?: WorkspaceInfo): Promise + remove(config: WorkspaceInfo): Promise + target(config: WorkspaceInfo): WorkspaceTarget | Promise +} + export type PluginInput = { client: ReturnType project: Project directory: string worktree: string + experimental_workspace: { + register(type: string, adaptor: WorkspaceAdaptor): void + } serverUrl: URL $: BunShell } diff --git a/packages/plugin/tsconfig.json b/packages/plugin/tsconfig.json index 117381878..f8e9370d8 100644 --- a/packages/plugin/tsconfig.json +++ b/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, -- cgit v1.2.3 From 34e2429c492495d059cbc63b86d02a58a1b3ca65 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:14:53 -0500 Subject: feat: add experimental.compaction.autocontinue hook to disable auto continuing after compaction (#22361) --- packages/opencode/src/session/compaction.ts | 70 +++++++++++++++-------- packages/opencode/test/session/compaction.test.ts | 57 ++++++++++++++++++ packages/plugin/src/index.ts | 18 ++++++ 3 files changed, 120 insertions(+), 25 deletions(-) (limited to 'packages/plugin/src') diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index b280971c7..c4934b625 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -310,31 +310,51 @@ When constructing the summary, try to stick to this template: } if (!replay) { - const continueMsg = yield* session.updateMessage({ - id: MessageID.ascending(), - role: "user", - sessionID: input.sessionID, - time: { created: Date.now() }, - agent: userMessage.agent, - model: userMessage.model, - }) - const text = - (input.overflow - ? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files.\n\n" - : "") + - "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed." - yield* session.updatePart({ - id: PartID.ascending(), - messageID: continueMsg.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text, - time: { - start: Date.now(), - end: Date.now(), - }, - }) + const info = yield* provider.getProvider(userMessage.model.providerID) + if ( + (yield* plugin.trigger( + "experimental.compaction.autocontinue", + { + sessionID: input.sessionID, + agent: userMessage.agent, + model: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID), + provider: { + source: info.source, + info, + options: info.options, + }, + message: userMessage, + overflow: input.overflow === true, + }, + { enabled: true }, + )).enabled + ) { + const continueMsg = yield* session.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID: input.sessionID, + time: { created: Date.now() }, + agent: userMessage.agent, + model: userMessage.model, + }) + const text = + (input.overflow + ? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files.\n\n" + : "") + + "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed." + yield* session.updatePart({ + id: PartID.ascending(), + messageID: continueMsg.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text, + time: { + start: Date.now(), + end: Date.now(), + }, + }) + } } } diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 2b0908ee9..206f417d1 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -244,6 +244,20 @@ function plugin(ready: ReturnType) { }) } +function autocontinue(enabled: boolean) { + return Layer.mock(Plugin.Service)({ + trigger: (name: Name, _input: Input, output: Output) => { + if (name !== "experimental.compaction.autocontinue") return Effect.succeed(output) + return Effect.sync(() => { + ;(output as { enabled: boolean }).enabled = enabled + return output + }) + }, + list: () => Effect.succeed([]), + init: () => Effect.void, + }) +} + describe("session.compaction.isOverflow", () => { test("returns true when token count exceeds usable context", async () => { await using tmp = await tmpdir() @@ -671,6 +685,49 @@ describe("session.compaction.process", () => { }) }) + test("allows plugins to disable synthetic continue prompt", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + const msg = await user(session.id, "hello") + const rt = runtime("continue", autocontinue(false), wide()) + try { + const msgs = await Session.messages({ sessionID: session.id }) + const result = await rt.runPromise( + SessionCompaction.Service.use((svc) => + svc.process({ + parentID: msg.id, + messages: msgs, + sessionID: session.id, + auto: true, + }), + ), + ) + + const all = await Session.messages({ sessionID: session.id }) + const last = all.at(-1) + + expect(result).toBe("continue") + expect(last?.info.role).toBe("assistant") + expect( + all.some( + (msg) => + msg.info.role === "user" && + msg.parts.some( + (part) => + part.type === "text" && part.synthetic && part.text.includes("Continue if you have next steps"), + ), + ), + ).toBe(false) + } finally { + await rt.dispose() + } + }, + }) + }) + test("replays the prior user turn on overflow when earlier context exists", async () => { await using tmp = await tmpdir() await Instance.provide({ diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 49d995c6f..d53c23a89 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -304,6 +304,24 @@ export interface Hooks { input: { sessionID: string }, output: { context: string[]; prompt?: string }, ) => Promise + /** + * Called after compaction succeeds and before a synthetic user + * auto-continue message is added. + * + * - `enabled`: Defaults to `true`. Set to `false` to skip the synthetic + * user "continue" turn. + */ + "experimental.compaction.autocontinue"?: ( + input: { + sessionID: string + agent: string + model: Model + provider: ProviderContext + message: UserMessage + overflow: boolean + }, + output: { enabled: boolean }, + ) => Promise "experimental.text.complete"?: ( input: { sessionID: string; messageID: string; partID: string }, output: { text: string }, -- cgit v1.2.3 From d6b14e24678db678163c281257322c5a9bf0e6fa Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 21:56:23 -0400 Subject: fix: prefix 32 unused parameters with underscore (#22694) --- packages/console/app/src/component/icon.tsx | 4 ++-- packages/console/app/src/routes/auth/status.ts | 2 +- packages/console/app/src/routes/debug/index.ts | 2 +- .../routes/zen/util/provider/openai-compatible.ts | 2 +- packages/console/app/src/routes/zen/v1/models.ts | 2 +- .../console/app/src/routes/zen/v1/models/[model].ts | 4 ++-- packages/function/src/api.ts | 4 ++-- packages/opencode/src/agent/agent.ts | 2 +- .../src/cli/cmd/tui/component/dialog-mcp.tsx | 2 +- .../opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/provider/schema.ts | 2 +- packages/opencode/src/provider/transform.ts | 2 +- packages/opencode/src/v2/session.ts | 4 ++-- packages/opencode/test/session/prompt-effect.test.ts | 20 ++++++++++---------- packages/opencode/test/session/retry.test.ts | 2 +- packages/plugin/src/example.ts | 2 +- 17 files changed, 30 insertions(+), 30 deletions(-) (limited to 'packages/plugin/src') diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx index 4627c0084..da2e87ef4 100644 --- a/packages/console/app/src/component/icon.tsx +++ b/packages/console/app/src/component/icon.tsx @@ -1,6 +1,6 @@ import { JSX } from "solid-js" -export function IconZen(props: JSX.SvgSVGAttributes) { +export function IconZen(_props: JSX.SvgSVGAttributes) { return ( @@ -13,7 +13,7 @@ export function IconZen(props: JSX.SvgSVGAttributes) { ) } -export function IconGo(props: JSX.SvgSVGAttributes) { +export function IconGo(_props: JSX.SvgSVGAttributes) { return ( diff --git a/packages/console/app/src/routes/auth/status.ts b/packages/console/app/src/routes/auth/status.ts index 215cae698..ed522d740 100644 --- a/packages/console/app/src/routes/auth/status.ts +++ b/packages/console/app/src/routes/auth/status.ts @@ -1,7 +1,7 @@ import { APIEvent } from "@solidjs/start" import { useAuthSession } from "~/context/auth" -export async function GET(input: APIEvent) { +export async function GET(_input: APIEvent) { const session = await useAuthSession() return Response.json(session.data) } diff --git a/packages/console/app/src/routes/debug/index.ts b/packages/console/app/src/routes/debug/index.ts index 2bdd269e7..4bfb63394 100644 --- a/packages/console/app/src/routes/debug/index.ts +++ b/packages/console/app/src/routes/debug/index.ts @@ -3,7 +3,7 @@ import { json } from "@solidjs/router" import { Database } from "@opencode-ai/console-core/drizzle/index.js" import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" -export async function GET(evt: APIEvent) { +export async function GET(_evt: APIEvent) { return json({ data: await Database.use(async (tx) => { const result = await tx.$count(UserTable) diff --git a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts index e05f0d6c0..97b0abc64 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts @@ -30,7 +30,7 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentif headers.set("authorization", `Bearer ${apiKey}`) headers.set("x-session-affinity", headers.get("x-opencode-session") ?? "") }, - modifyBody: (body: Record, workspaceID?: string) => { + modifyBody: (body: Record, _workspaceID?: string) => { return { ...body, ...(body.stream ? { stream_options: { include_usage: true } } : {}), diff --git a/packages/console/app/src/routes/zen/v1/models.ts b/packages/console/app/src/routes/zen/v1/models.ts index d2592d20b..6b4a878fc 100644 --- a/packages/console/app/src/routes/zen/v1/models.ts +++ b/packages/console/app/src/routes/zen/v1/models.ts @@ -5,7 +5,7 @@ import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.j import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js" import { ZenData } from "@opencode-ai/console-core/model.js" -export async function OPTIONS(input: APIEvent) { +export async function OPTIONS(_input: APIEvent) { return new Response(null, { status: 200, headers: { diff --git a/packages/console/app/src/routes/zen/v1/models/[model].ts b/packages/console/app/src/routes/zen/v1/models/[model].ts index a4edd5861..bc1168eb0 100644 --- a/packages/console/app/src/routes/zen/v1/models/[model].ts +++ b/packages/console/app/src/routes/zen/v1/models/[model].ts @@ -6,8 +6,8 @@ export function POST(input: APIEvent) { format: "google", modelList: "full", parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined, - parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "", - parseIsStream: (url: string, body: any) => + parseModel: (url: string, _body: any) => url.split("/").pop()?.split(":")?.[0] ?? "", + parseIsStream: (url: string, _body: any) => // ie. url: https://opencode.ai/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse' url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false, }) diff --git a/packages/function/src/api.ts b/packages/function/src/api.ts index 54b93ad71..d6565b287 100644 --- a/packages/function/src/api.ts +++ b/packages/function/src/api.ts @@ -49,9 +49,9 @@ export class SyncServer extends DurableObject { }) } - async webSocketMessage(ws, message) {} + async webSocketMessage(_ws, _message) {} - async webSocketClose(ws, code, reason, wasClean) { + async webSocketClose(ws, code, _reason, _wasClean) { ws.close(code, "Durable Object is closing WebSocket") } diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 8e6bfe5e9..8d11a93b3 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -80,7 +80,7 @@ export namespace Agent { const provider = yield* Provider.Service const state = yield* InstanceState.make( - Effect.fn("Agent.state")(function* (ctx) { + Effect.fn("Agent.state")(function* (_ctx) { const cfg = yield* config.get() const skillDirs = yield* skill.dirs() const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))] diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx index 9cfa30d4d..173c5ff60 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx @@ -78,7 +78,7 @@ export function DialogMcp() { title="MCPs" options={options()} keybind={keybinds()} - onSelect={(option) => { + onSelect={(_option) => { // Don't close on select, only on escape }} /> diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index ef75764a2..48adddaed 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -54,7 +54,7 @@ export function DialogConfirm(props: DialogConfirmProps) { paddingLeft={1} paddingRight={1} backgroundColor={key === store.active ? theme.primary : undefined} - onMouseUp={(evt) => { + onMouseUp={(_evt) => { if (key === "confirm") props.onConfirm?.() if (key === "cancel") props.onCancel?.() dialog.clear() diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f35e8c83d..63e41f445 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -510,7 +510,7 @@ export const Agent = z permission: Permission.optional(), }) .catchall(z.any()) - .transform((agent, ctx) => { + .transform((agent, _ctx) => { const knownKeys = new Set([ "name", "model", diff --git a/packages/opencode/src/provider/schema.ts b/packages/opencode/src/provider/schema.ts index 4490ca289..702616018 100644 --- a/packages/opencode/src/provider/schema.ts +++ b/packages/opencode/src/provider/schema.ts @@ -30,7 +30,7 @@ const modelIdSchema = Schema.String.pipe(Schema.brand("ModelID")) export type ModelID = typeof modelIdSchema.Type export const ModelID = modelIdSchema.pipe( - withStatics((schema: typeof modelIdSchema) => ({ + withStatics((_schema: typeof modelIdSchema) => ({ zod: z.string().pipe(z.custom()), })), ) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 3138f8e29..7b83c245f 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -49,7 +49,7 @@ export namespace ProviderTransform { function normalizeMessages( msgs: ModelMessage[], model: Provider.Model, - options: Record, + _options: Record, ): ModelMessage[] { // Anthropic rejects messages with empty content - filter out empty string messages // and remove empty text/reasoning parts from array content diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index 97df0a220..ce1b39031 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -40,11 +40,11 @@ export namespace SessionV2 { Effect.gen(function* () { const session = yield* Session.Service - const create: Interface["create"] = Effect.fn("Session.create")(function* (input) { + const create: Interface["create"] = Effect.fn("Session.create")(function* (_input) { throw new Error("Not implemented") }) - const prompt: Interface["prompt"] = Effect.fn("Session.prompt")(function* (input) { + const prompt: Interface["prompt"] = Effect.fn("Session.prompt")(function* (_input) { throw new Error("Not implemented") }) diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 91297aed1..7a118cb05 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -786,7 +786,7 @@ it.live( const { task } = yield* registry.named() const original = task.execute task.execute = (_args, ctx) => - Effect.callback((resume) => { + Effect.callback((_resume) => { ready.resolve() ctx.abort.addEventListener("abort", () => aborted.resolve(), { once: true }) return Effect.sync(() => aborted.resolve()) @@ -856,7 +856,7 @@ it.live( it.live("concurrent loop callers get same result", () => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, run, chat } = yield* boot() yield* seed(chat.id, { finish: "stop" }) @@ -997,7 +997,7 @@ it.live( it.live("assertNotBusy succeeds when idle", () => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const run = yield* SessionRunState.Service const sessions = yield* Session.Service @@ -1042,7 +1042,7 @@ it.live( unix("shell captures stdout and stderr in completed tool output", () => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, run, chat } = yield* boot() const result = yield* prompt.shell({ @@ -1117,7 +1117,7 @@ unix("shell lists files from the project directory", () => unix("shell captures stderr from a failing command", () => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, run, chat } = yield* boot() const result = yield* prompt.shell({ @@ -1143,7 +1143,7 @@ unix( () => withSh(() => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, chat } = yield* boot() @@ -1255,7 +1255,7 @@ unix( () => withSh(() => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, run, chat } = yield* boot() @@ -1292,7 +1292,7 @@ unix( () => withSh(() => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, chat } = yield* boot() @@ -1374,7 +1374,7 @@ unix( "cancel interrupts loop queued behind shell", () => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, chat } = yield* boot() @@ -1403,7 +1403,7 @@ unix( () => withSh(() => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, chat } = yield* boot() diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 2d01a8f35..ade264786 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -239,7 +239,7 @@ describe("session.message-v2.fromError", () => { using server = Bun.serve({ port: 0, idleTimeout: 8, - async fetch(req) { + async fetch(_req) { return new Response( new ReadableStream({ async pull(controller) { diff --git a/packages/plugin/src/example.ts b/packages/plugin/src/example.ts index 1cf042fe9..9d7e178a9 100644 --- a/packages/plugin/src/example.ts +++ b/packages/plugin/src/example.ts @@ -1,7 +1,7 @@ import { Plugin } from "./index.js" import { tool } from "./tool.js" -export const ExamplePlugin: Plugin = async (ctx) => { +export const ExamplePlugin: Plugin = async (_ctx) => { return { tool: { mytool: tool({ -- cgit v1.2.3 From cf423d27693ab5718eb836bdba7ba3ed357204b0 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:17:59 -0400 Subject: fix: remove 10 unused type-only imports and declarations (#22696) --- packages/app/src/components/file-tree.tsx | 1 - packages/app/src/context/global-sync/types.ts | 1 - packages/app/src/i18n/ko.ts | 2 -- packages/opencode/src/cli/cmd/tui/context/sdk.tsx | 2 +- packages/opencode/src/cli/cmd/tui/plugin/api.tsx | 1 - packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx | 2 +- packages/opencode/test/fixture/plugin-meta-worker.ts | 7 ------- packages/opencode/test/mcp/oauth-auto-connect.test.ts | 1 - packages/opencode/test/session/prompt-effect.test.ts | 1 - packages/plugin/src/tui.ts | 1 - 10 files changed, 2 insertions(+), 17 deletions(-) (limited to 'packages/plugin/src') diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 8fbecf671..211ce05ef 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -14,7 +14,6 @@ import { Switch, untrack, type ComponentProps, - type JSXElement, type ParentProps, } from "solid-js" import { Dynamic } from "solid-js/web" diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index b0f340a90..e3ec83c5e 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -8,7 +8,6 @@ import type { Part, Path, PermissionRequest, - Project, ProviderListResponse, QuestionRequest, Session, diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 0f2f7647a..1c1572009 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -1,7 +1,5 @@ import { dict as en } from "./en" -type Keys = keyof typeof en - export const dict = { "command.category.suggested": "추천", "command.category.view": "보기", diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index ad35aa45c..14d306288 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -1,5 +1,5 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2" -import type { GlobalEvent, Event } from "@opencode-ai/sdk/v2" +import type { GlobalEvent } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { createGlobalEmitter } from "@solid-primitives/event-bus" import { batch, onCleanup, onMount } from "solid-js" diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx index 42bf78adb..3af70d8c2 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx +++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx @@ -19,7 +19,6 @@ import { Prompt } from "../component/prompt" import { Slot as HostSlot } from "./slots" import type { useToast } from "../ui/toast" import { Installation } from "@/installation" -import { type OpencodeClient } from "@opencode-ai/sdk/v2" type RouteEntry = { key: symbol diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx index 4442eb9e6..513d34910 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx @@ -2,7 +2,7 @@ import { TextareaRenderable, TextAttributes } from "@opentui/core" import { useTheme } from "../context/theme" import { useDialog, type DialogContext } from "./dialog" import { createStore } from "solid-js/store" -import { onMount, Show, type JSX } from "solid-js" +import { onMount, Show } from "solid-js" import { useKeyboard } from "@opentui/solid" export type DialogExportOptionsProps = { diff --git a/packages/opencode/test/fixture/plugin-meta-worker.ts b/packages/opencode/test/fixture/plugin-meta-worker.ts index 86284b4c7..c02b448ae 100644 --- a/packages/opencode/test/fixture/plugin-meta-worker.ts +++ b/packages/opencode/test/fixture/plugin-meta-worker.ts @@ -1,10 +1,3 @@ -type Msg = { - file: string - spec: string - target: string - id: string -} - const raw = process.argv[2] if (!raw) throw new Error("Missing worker payload") diff --git a/packages/opencode/test/mcp/oauth-auto-connect.test.ts b/packages/opencode/test/mcp/oauth-auto-connect.test.ts index 13ae0bb34..89edd0908 100644 --- a/packages/opencode/test/mcp/oauth-auto-connect.test.ts +++ b/packages/opencode/test/mcp/oauth-auto-connect.test.ts @@ -1,6 +1,5 @@ import { test, expect, mock, beforeEach } from "bun:test" import { Effect } from "effect" -import type { MCP as MCPNS } from "../../src/mcp/index" // Mock UnauthorizedError to match the SDK's class class MockUnauthorizedError extends Error { diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 7a118cb05..5ff8bf342 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -14,7 +14,6 @@ import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" import { Provider as ProviderSvc } from "../../src/provider" import { Env } from "../../src/env" -import type { Provider } from "../../src/provider" import { ModelID, ProviderID } from "../../src/provider/schema" import { Question } from "../../src/question" import { Todo } from "../../src/session/todo" diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index e6f832f7e..099cf2758 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -13,7 +13,6 @@ import type { QuestionRequest, SessionStatus, TextPart, - Workspace, Config as SdkConfig, } from "@opencode-ai/sdk/v2" import type { CliRenderer, ParsedKey, RGBA, SlotMode } from "@opentui/core" -- cgit v1.2.3