From 24c63914bf9ff46c3869f35e3cef026aa9f33b61 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Mon, 23 Feb 2026 16:51:29 -0500 Subject: [PATCH 01/94] fix: update workflows for better automation (#14809) --- .github/workflows/compliance-close.yml | 9 +++++++++ .github/workflows/pr-standards.yml | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/compliance-close.yml b/.github/workflows/compliance-close.yml index 5b424d0adfa7..c3bcf9f686f4 100644 --- a/.github/workflows/compliance-close.yml +++ b/.github/workflows/compliance-close.yml @@ -65,6 +65,15 @@ jobs: body: closeMessage, }); + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: item.number, + name: 'needs:compliance', + }); + } catch (e) {} + if (isPR) { await github.rest.pulls.update({ owner: context.repo.owner, diff --git a/.github/workflows/pr-standards.yml b/.github/workflows/pr-standards.yml index 27581d06b768..1edbd5d061dc 100644 --- a/.github/workflows/pr-standards.yml +++ b/.github/workflows/pr-standards.yml @@ -108,11 +108,11 @@ jobs: await removeLabel('needs:title'); - // Step 2: Check for linked issue (skip for docs/refactor PRs) - const skipIssueCheck = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); + // Step 2: Check for linked issue (skip for docs/refactor/feat PRs) + const skipIssueCheck = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); if (skipIssueCheck) { await removeLabel('needs:issue'); - console.log('Skipping issue check for docs/refactor PR'); + console.log('Skipping issue check for docs/refactor/feat PR'); return; } const query = ` @@ -189,7 +189,7 @@ jobs: const body = pr.body || ''; const title = pr.title; - const isDocsOrRefactor = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); + const isDocsRefactorOrFeat = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); const issues = []; @@ -225,8 +225,8 @@ jobs: } } - // Check: issue reference (skip for docs/refactor) - if (!isDocsOrRefactor && hasIssueSection) { + // Check: issue reference (skip for docs/refactor/feat) + if (!isDocsRefactorOrFeat && hasIssueSection) { const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/); const issueContent = issueMatch ? issueMatch[1].trim() : ''; const hasIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent); From ad5f0816a33d323f2a7e6a6228136fa6a6c4b056 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:13:31 +1000 Subject: [PATCH 02/94] fix(cicd): flakey typecheck (#14828) --- turbo.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/turbo.json b/turbo.json index f06ddb0e8b76..ba3d01d3603b 100644 --- a/turbo.json +++ b/turbo.json @@ -3,7 +3,9 @@ "globalEnv": ["CI", "OPENCODE_DISABLE_SHARE"], "globalPassThroughEnv": ["CI", "OPENCODE_DISABLE_SHARE"], "tasks": { - "typecheck": {}, + "typecheck": { + "dependsOn": ["^build"] + }, "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] From 34495a70d5069355bbad95c95625818afa677eb1 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:15:25 +1000 Subject: [PATCH 03/94] fix(win32): scripts/turbo commands would not run (#14829) --- packages/plugin/script/publish.ts | 3 ++- packages/sdk/js/package.json | 2 +- packages/sdk/js/script/build.ts | 3 ++- packages/sdk/js/script/publish.ts | 3 ++- script/publish.ts | 5 +++-- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/plugin/script/publish.ts b/packages/plugin/script/publish.ts index 647b56e5e2dd..d2fe49f23ce3 100755 --- a/packages/plugin/script/publish.ts +++ b/packages/plugin/script/publish.ts @@ -1,8 +1,9 @@ #!/usr/bin/env bun import { Script } from "@opencode-ai/script" import { $ } from "bun" +import { fileURLToPath } from "url" -const dir = new URL("..", import.meta.url).pathname +const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) await $`bun tsc` diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 4fe0794d0cea..bd3627e35b4c 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -6,7 +6,7 @@ "license": "MIT", "scripts": { "typecheck": "tsgo --noEmit", - "build": "./script/build.ts" + "build": "bun ./script/build.ts" }, "exports": { ".": "./src/index.ts", diff --git a/packages/sdk/js/script/build.ts b/packages/sdk/js/script/build.ts index 7568c54b0f2a..268233a012e9 100755 --- a/packages/sdk/js/script/build.ts +++ b/packages/sdk/js/script/build.ts @@ -1,6 +1,7 @@ #!/usr/bin/env bun +import { fileURLToPath } from "url" -const dir = new URL("..", import.meta.url).pathname +const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) import { $ } from "bun" diff --git a/packages/sdk/js/script/publish.ts b/packages/sdk/js/script/publish.ts index c21f06230d18..ea5c5d634b2b 100755 --- a/packages/sdk/js/script/publish.ts +++ b/packages/sdk/js/script/publish.ts @@ -2,8 +2,9 @@ import { Script } from "@opencode-ai/script" import { $ } from "bun" +import { fileURLToPath } from "url" -const dir = new URL("..", import.meta.url).pathname +const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) const pkg = (await import("../package.json").then((m) => m.default)) as { diff --git a/script/publish.ts b/script/publish.ts index 8aa921daa834..b7ed5c8221ca 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -2,6 +2,7 @@ import { $ } from "bun" import { Script } from "@opencode-ai/script" +import { fileURLToPath } from "url" const highlightsTemplate = ` statement-breakpoint +ALTER TABLE `billing` ADD `lite_subscription_id` varchar(28);--> statement-breakpoint +ALTER TABLE `billing` ADD `lite` json; \ No newline at end of file diff --git a/packages/console/core/migrations/20260224043338_nifty_starjammers/snapshot.json b/packages/console/core/migrations/20260224043338_nifty_starjammers/snapshot.json new file mode 100644 index 000000000000..703ee233f30b --- /dev/null +++ b/packages/console/core/migrations/20260224043338_nifty_starjammers/snapshot.json @@ -0,0 +1,2505 @@ +{ + "version": "6", + "dialect": "mysql", + "id": "5e506dec-61e7-4726-81d1-afa4ffbc61ed", + "prevIds": [ + "4bf45b3f-3edd-4db7-94d5-097aa55ca5f7" + ], + "ddl": [ + { + "name": "account", + "entityType": "tables" + }, + { + "name": "auth", + "entityType": "tables" + }, + { + "name": "benchmark", + "entityType": "tables" + }, + { + "name": "billing", + "entityType": "tables" + }, + { + "name": "lite", + "entityType": "tables" + }, + { + "name": "payment", + "entityType": "tables" + }, + { + "name": "subscription", + "entityType": "tables" + }, + { + "name": "usage", + "entityType": "tables" + }, + { + "name": "ip_rate_limit", + "entityType": "tables" + }, + { + "name": "ip", + "entityType": "tables" + }, + { + "name": "key", + "entityType": "tables" + }, + { + "name": "model", + "entityType": "tables" + }, + { + "name": "provider", + "entityType": "tables" + }, + { + "name": "user", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "account" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "auth" + }, + { + "type": "enum('email','github','google')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subject", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "mediumtext", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "result", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(32)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_type", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(4)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_last4", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "balance", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_trigger", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_amount", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_locked_till", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "enum('20','100','200')", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_plan", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_booked", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_selected", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite_subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "weekly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_weekly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "invoice_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "amount", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_refunded", + "entityType": "columns", + "table": "payment" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "fixed_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_fixed_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "input_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "output_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reasoning_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_read_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_5m_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_1h_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(10)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "ip" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "usage", + "entityType": "columns", + "table": "ip" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "provider" + }, + { + "type": "text", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "credentials", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_seen", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "color", + "entityType": "columns", + "table": "user" + }, + { + "type": "enum('admin','member')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "user" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "workspace" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "auth", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "benchmark", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "billing", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "lite", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "payment", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "subscription", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "usage", + "entityType": "pks" + }, + { + "columns": [ + "ip", + "interval" + ], + "name": "PRIMARY", + "table": "ip_rate_limit", + "entityType": "pks" + }, + { + "columns": [ + "ip" + ], + "name": "PRIMARY", + "table": "ip", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "key", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "model", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "provider", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "provider", + "isExpression": false + }, + { + "value": "subject", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "provider", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "account_id", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "time_created", + "entityType": "indexes", + "table": "benchmark" + }, + { + "columns": [ + { + "value": "customer_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_customer_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "subscription_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_subscription_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "lite" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "subscription" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "usage_time_created", + "entityType": "indexes", + "table": "usage" + }, + { + "columns": [ + { + "value": "key", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_key", + "entityType": "indexes", + "table": "key" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "model", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "model_workspace_model", + "entityType": "indexes", + "table": "model" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "provider", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_provider", + "entityType": "indexes", + "table": "provider" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "email", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "email", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "slug", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "slug", + "entityType": "indexes", + "table": "workspace" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/console/core/script/black-select-workspaces.ts b/packages/console/core/script/black-select-workspaces.ts index f22478e1b3d9..63bfab887503 100644 --- a/packages/console/core/script/black-select-workspaces.ts +++ b/packages/console/core/script/black-select-workspaces.ts @@ -1,10 +1,10 @@ import { Database, eq, and, sql, inArray, isNull, count } from "../src/drizzle/index.js" -import { BillingTable, SubscriptionPlan } from "../src/schema/billing.sql.js" +import { BillingTable, BlackPlans } from "../src/schema/billing.sql.js" import { UserTable } from "../src/schema/user.sql.js" import { AuthTable } from "../src/schema/auth.sql.js" -const plan = process.argv[2] as (typeof SubscriptionPlan)[number] -if (!SubscriptionPlan.includes(plan)) { +const plan = process.argv[2] as (typeof BlackPlans)[number] +if (!BlackPlans.includes(plan)) { console.error("Usage: bun foo.ts ") process.exit(1) } diff --git a/packages/console/core/script/lookup-user.ts b/packages/console/core/script/lookup-user.ts index 6367fd89a4ae..0dfda24116d0 100644 --- a/packages/console/core/script/lookup-user.ts +++ b/packages/console/core/script/lookup-user.ts @@ -1,13 +1,7 @@ import { Database, and, eq, sql } from "../src/drizzle/index.js" import { AuthTable } from "../src/schema/auth.sql.js" import { UserTable } from "../src/schema/user.sql.js" -import { - BillingTable, - PaymentTable, - SubscriptionTable, - SubscriptionPlan, - UsageTable, -} from "../src/schema/billing.sql.js" +import { BillingTable, PaymentTable, SubscriptionTable, BlackPlans, UsageTable } from "../src/schema/billing.sql.js" import { WorkspaceTable } from "../src/schema/workspace.sql.js" import { BlackData } from "../src/black.js" import { centsToMicroCents } from "../src/util/price.js" @@ -235,7 +229,7 @@ function formatRetryTime(seconds: number) { function getSubscriptionStatus(row: { subscription: { - plan: (typeof SubscriptionPlan)[number] + plan: (typeof BlackPlans)[number] } | null timeSubscriptionCreated: Date | null fixedUsage: number | null diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index 2c1cdb0687bb..fcf238a35385 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -1,6 +1,6 @@ import { Stripe } from "stripe" import { Database, eq, sql } from "./drizzle" -import { BillingTable, PaymentTable, SubscriptionTable, UsageTable } from "./schema/billing.sql" +import { BillingTable, LiteTable, PaymentTable, SubscriptionTable, UsageTable } from "./schema/billing.sql" import { Actor } from "./actor" import { fn } from "./util/fn" import { z } from "zod" @@ -9,6 +9,7 @@ import { Identifier } from "./identifier" import { centsToMicroCents } from "./util/price" import { User } from "./user" import { BlackData } from "./black" +import { LiteData } from "./lite" export namespace Billing { export const ITEM_CREDIT_NAME = "opencode credits" @@ -233,6 +234,56 @@ export namespace Billing { }, ) + export const generateLiteCheckoutUrl = fn( + z.object({ + successUrl: z.string(), + cancelUrl: z.string(), + }), + async (input) => { + const user = Actor.assert("user") + const { successUrl, cancelUrl } = input + + const email = await User.getAuthEmail(user.properties.userID) + const billing = await Billing.get() + + if (billing.subscriptionID) throw new Error("Already subscribed to Black") + if (billing.liteSubscriptionID) throw new Error("Already subscribed to Lite") + + const session = await Billing.stripe().checkout.sessions.create({ + mode: "subscription", + billing_address_collection: "required", + line_items: [{ price: LiteData.priceID(), quantity: 1 }], + ...(billing.customerID + ? { + customer: billing.customerID, + customer_update: { + name: "auto", + address: "auto", + }, + } + : { + customer_email: email!, + }), + currency: "usd", + payment_method_types: ["card"], + tax_id_collection: { + enabled: true, + }, + success_url: successUrl, + cancel_url: cancelUrl, + subscription_data: { + metadata: { + workspaceID: Actor.workspace(), + userID: user.properties.userID, + type: "lite", + }, + }, + }) + + return session.url + }, + ) + export const generateSessionUrl = fn( z.object({ returnUrl: z.string(), @@ -271,7 +322,7 @@ export namespace Billing { }, ) - export const subscribe = fn( + export const subscribeBlack = fn( z.object({ seats: z.number(), coupon: z.string().optional(), @@ -336,7 +387,7 @@ export namespace Billing { }, ) - export const unsubscribe = fn( + export const unsubscribeBlack = fn( z.object({ subscriptionID: z.string(), }), @@ -360,4 +411,29 @@ export namespace Billing { }) }, ) + + export const unsubscribeLite = fn( + z.object({ + subscriptionID: z.string(), + }), + async ({ subscriptionID }) => { + const workspaceID = await Database.use((tx) => + tx + .select({ workspaceID: BillingTable.workspaceID }) + .from(BillingTable) + .where(eq(BillingTable.liteSubscriptionID, subscriptionID)) + .then((rows) => rows[0]?.workspaceID), + ) + if (!workspaceID) throw new Error("Workspace ID not found for subscription") + + await Database.transaction(async (tx) => { + await tx + .update(BillingTable) + .set({ liteSubscriptionID: null, lite: null }) + .where(eq(BillingTable.workspaceID, workspaceID)) + + await tx.delete(LiteTable).where(eq(LiteTable.workspaceID, workspaceID)) + }) + }, + ) } diff --git a/packages/console/core/src/black.ts b/packages/console/core/src/black.ts index b4cc27064630..a18c5258d04a 100644 --- a/packages/console/core/src/black.ts +++ b/packages/console/core/src/black.ts @@ -1,7 +1,7 @@ import { z } from "zod" import { fn } from "./util/fn" import { Resource } from "@opencode-ai/console-resource" -import { SubscriptionPlan } from "./schema/billing.sql" +import { BlackPlans } from "./schema/billing.sql" export namespace BlackData { const Schema = z.object({ @@ -28,7 +28,7 @@ export namespace BlackData { export const getLimits = fn( z.object({ - plan: z.enum(SubscriptionPlan), + plan: z.enum(BlackPlans), }), ({ plan }) => { const json = JSON.parse(Resource.ZEN_BLACK_LIMITS.value) @@ -36,9 +36,11 @@ export namespace BlackData { }, ) + export const productID = fn(z.void(), () => Resource.ZEN_BLACK_PRICE.product) + export const planToPriceID = fn( z.object({ - plan: z.enum(SubscriptionPlan), + plan: z.enum(BlackPlans), }), ({ plan }) => { if (plan === "200") return Resource.ZEN_BLACK_PRICE.plan200 diff --git a/packages/console/core/src/identifier.ts b/packages/console/core/src/identifier.ts index b10bf32f6f1f..8aa324ba07f9 100644 --- a/packages/console/core/src/identifier.ts +++ b/packages/console/core/src/identifier.ts @@ -8,6 +8,7 @@ export namespace Identifier { benchmark: "ben", billing: "bil", key: "key", + lite: "lit", model: "mod", payment: "pay", provider: "prv", diff --git a/packages/console/core/src/lite.ts b/packages/console/core/src/lite.ts index d6679208d856..49d23e59ec09 100644 --- a/packages/console/core/src/lite.ts +++ b/packages/console/core/src/lite.ts @@ -4,9 +4,10 @@ import { Resource } from "@opencode-ai/console-resource" export namespace LiteData { const Schema = z.object({ - fixedLimit: z.number().int(), rollingLimit: z.number().int(), rollingWindow: z.number().int(), + weeklyLimit: z.number().int(), + monthlyLimit: z.number().int(), }) export const validate = fn(Schema, (input) => { @@ -18,11 +19,7 @@ export namespace LiteData { return Schema.parse(json) }) - export const planToPriceID = fn(z.void(), () => { - return Resource.ZEN_LITE_PRICE.price - }) - - export const priceIDToPlan = fn(z.void(), () => { - return "lite" - }) + export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product) + export const priceID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.price) + export const planName = fn(z.void(), () => "lite") } diff --git a/packages/console/core/src/schema/billing.sql.ts b/packages/console/core/src/schema/billing.sql.ts index 6d96fc7eb897..a5c70c211544 100644 --- a/packages/console/core/src/schema/billing.sql.ts +++ b/packages/console/core/src/schema/billing.sql.ts @@ -2,7 +2,7 @@ import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex, import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types" import { workspaceIndexes } from "./workspace.sql" -export const SubscriptionPlan = ["20", "100", "200"] as const +export const BlackPlans = ["20", "100", "200"] as const export const BillingTable = mysqlTable( "billing", { @@ -25,14 +25,18 @@ export const BillingTable = mysqlTable( subscription: json("subscription").$type<{ status: "subscribed" seats: number - plan: "20" | "100" | "200" + plan: (typeof BlackPlans)[number] useBalance?: boolean coupon?: string }>(), subscriptionID: varchar("subscription_id", { length: 28 }), - subscriptionPlan: mysqlEnum("subscription_plan", SubscriptionPlan), + subscriptionPlan: mysqlEnum("subscription_plan", BlackPlans), timeSubscriptionBooked: utc("time_subscription_booked"), timeSubscriptionSelected: utc("time_subscription_selected"), + liteSubscriptionID: varchar("lite_subscription_id", { length: 28 }), + lite: json("lite").$type<{ + useBalance?: boolean + }>(), }, (table) => [ ...workspaceIndexes(table), @@ -55,6 +59,22 @@ export const SubscriptionTable = mysqlTable( (table) => [...workspaceIndexes(table), uniqueIndex("workspace_user_id").on(table.workspaceID, table.userID)], ) +export const LiteTable = mysqlTable( + "lite", + { + ...workspaceColumns, + ...timestamps, + userID: ulid("user_id").notNull(), + rollingUsage: bigint("rolling_usage", { mode: "number" }), + weeklyUsage: bigint("weekly_usage", { mode: "number" }), + monthlyUsage: bigint("monthly_usage", { mode: "number" }), + timeRollingUpdated: utc("time_rolling_updated"), + timeWeeklyUpdated: utc("time_weekly_updated"), + timeMonthlyUpdated: utc("time_monthly_updated"), + }, + (table) => [...workspaceIndexes(table), uniqueIndex("workspace_user_id").on(table.workspaceID, table.userID)], +) + export const PaymentTable = mysqlTable( "payment", { diff --git a/packages/console/core/src/subscription.ts b/packages/console/core/src/subscription.ts index ca3b17042242..879f940e0ebc 100644 --- a/packages/console/core/src/subscription.ts +++ b/packages/console/core/src/subscription.ts @@ -1,7 +1,7 @@ import { z } from "zod" import { fn } from "./util/fn" import { centsToMicroCents } from "./util/price" -import { getWeekBounds } from "./util/date" +import { getWeekBounds, getMonthlyBounds } from "./util/date" export namespace Subscription { export const analyzeRollingUsage = fn( @@ -29,7 +29,7 @@ export namespace Subscription { return { status: "ok" as const, resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000), - usagePercent: Math.ceil(Math.min(100, (usage / rollingLimitInMicroCents) * 100)), + usagePercent: Math.floor(Math.min(100, (usage / rollingLimitInMicroCents) * 100)), } } return { @@ -61,7 +61,7 @@ export namespace Subscription { return { status: "ok" as const, resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000), - usagePercent: Math.ceil(Math.min(100, (usage / fixedLimitInMicroCents) * 100)), + usagePercent: Math.floor(Math.min(100, (usage / fixedLimitInMicroCents) * 100)), } } @@ -72,4 +72,38 @@ export namespace Subscription { } }, ) + + export const analyzeMonthlyUsage = fn( + z.object({ + limit: z.number().int(), + usage: z.number().int(), + timeUpdated: z.date(), + timeSubscribed: z.date(), + }), + ({ limit, usage, timeUpdated, timeSubscribed }) => { + const now = new Date() + const month = getMonthlyBounds(now, timeSubscribed) + const fixedLimitInMicroCents = centsToMicroCents(limit * 100) + if (timeUpdated < month.start) { + return { + status: "ok" as const, + resetInSec: Math.ceil((month.end.getTime() - now.getTime()) / 1000), + usagePercent: 0, + } + } + if (usage < fixedLimitInMicroCents) { + return { + status: "ok" as const, + resetInSec: Math.ceil((month.end.getTime() - now.getTime()) / 1000), + usagePercent: Math.floor(Math.min(100, (usage / fixedLimitInMicroCents) * 100)), + } + } + + return { + status: "rate-limited" as const, + resetInSec: Math.ceil((month.end.getTime() - now.getTime()) / 1000), + usagePercent: 100, + } + }, + ) } diff --git a/packages/console/core/src/util/date.test.ts b/packages/console/core/src/util/date.test.ts deleted file mode 100644 index 074df8a2fad5..000000000000 --- a/packages/console/core/src/util/date.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { getWeekBounds } from "./date" - -describe("util.date.getWeekBounds", () => { - test("returns a Monday-based week for Sunday dates", () => { - const date = new Date("2026-01-18T12:00:00Z") - const bounds = getWeekBounds(date) - - expect(bounds.start.toISOString()).toBe("2026-01-12T00:00:00.000Z") - expect(bounds.end.toISOString()).toBe("2026-01-19T00:00:00.000Z") - }) - - test("returns a seven day window", () => { - const date = new Date("2026-01-14T12:00:00Z") - const bounds = getWeekBounds(date) - - const span = bounds.end.getTime() - bounds.start.getTime() - expect(span).toBe(7 * 24 * 60 * 60 * 1000) - }) -}) diff --git a/packages/console/core/src/util/date.ts b/packages/console/core/src/util/date.ts index 9c1ab12d2c95..dea9c390e06a 100644 --- a/packages/console/core/src/util/date.ts +++ b/packages/console/core/src/util/date.ts @@ -7,3 +7,32 @@ export function getWeekBounds(date: Date) { end.setUTCDate(start.getUTCDate() + 7) return { start, end } } + +export function getMonthlyBounds(now: Date, subscribed: Date) { + const day = subscribed.getUTCDate() + const hh = subscribed.getUTCHours() + const mm = subscribed.getUTCMinutes() + const ss = subscribed.getUTCSeconds() + const ms = subscribed.getUTCMilliseconds() + + function anchor(year: number, month: number) { + const max = new Date(Date.UTC(year, month + 1, 0)).getUTCDate() + return new Date(Date.UTC(year, month, Math.min(day, max), hh, mm, ss, ms)) + } + + function shift(year: number, month: number, delta: number) { + const total = year * 12 + month + delta + return [Math.floor(total / 12), ((total % 12) + 12) % 12] as const + } + + let y = now.getUTCFullYear() + let m = now.getUTCMonth() + let start = anchor(y, m) + if (start > now) { + ;[y, m] = shift(y, m, -1) + start = anchor(y, m) + } + const [ny, nm] = shift(y, m, 1) + const end = anchor(ny, nm) + return { start, end } +} diff --git a/packages/console/core/test/date.test.ts b/packages/console/core/test/date.test.ts new file mode 100644 index 000000000000..e5a0a90e551b --- /dev/null +++ b/packages/console/core/test/date.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, test } from "bun:test" +import { getWeekBounds, getMonthlyBounds } from "../src/util/date" + +describe("util.date.getWeekBounds", () => { + test("returns a Monday-based week for Sunday dates", () => { + const date = new Date("2026-01-18T12:00:00Z") + const bounds = getWeekBounds(date) + + expect(bounds.start.toISOString()).toBe("2026-01-12T00:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-01-19T00:00:00.000Z") + }) + + test("returns a seven day window", () => { + const date = new Date("2026-01-14T12:00:00Z") + const bounds = getWeekBounds(date) + + const span = bounds.end.getTime() - bounds.start.getTime() + expect(span).toBe(7 * 24 * 60 * 60 * 1000) + }) +}) + +describe("util.date.getMonthlyBounds", () => { + test("resets on subscription day mid-month", () => { + const now = new Date("2026-03-20T10:00:00Z") + const subscribed = new Date("2026-01-15T08:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-03-15T08:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-04-15T08:00:00.000Z") + }) + + test("before subscription day in current month uses previous month anchor", () => { + const now = new Date("2026-03-10T10:00:00Z") + const subscribed = new Date("2026-01-15T08:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-02-15T08:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-03-15T08:00:00.000Z") + }) + + test("clamps day for short months", () => { + const now = new Date("2026-03-01T10:00:00Z") + const subscribed = new Date("2026-01-31T12:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-02-28T12:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-03-31T12:00:00.000Z") + }) + + test("handles subscription on the 1st", () => { + const now = new Date("2026-04-15T00:00:00Z") + const subscribed = new Date("2026-01-01T00:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-04-01T00:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-05-01T00:00:00.000Z") + }) + + test("exactly on the reset boundary uses current period", () => { + const now = new Date("2026-03-15T08:00:00Z") + const subscribed = new Date("2026-01-15T08:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-03-15T08:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-04-15T08:00:00.000Z") + }) + + test("february to march with day 30 subscription", () => { + const now = new Date("2026-02-15T06:00:00Z") + const subscribed = new Date("2025-12-30T06:00:00Z") + const bounds = getMonthlyBounds(now, subscribed) + + expect(bounds.start.toISOString()).toBe("2026-01-30T06:00:00.000Z") + expect(bounds.end.toISOString()).toBe("2026-02-28T06:00:00.000Z") + }) +}) diff --git a/packages/console/core/test/subscription.test.ts b/packages/console/core/test/subscription.test.ts new file mode 100644 index 000000000000..57e63f94c41b --- /dev/null +++ b/packages/console/core/test/subscription.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, test, setSystemTime, afterEach } from "bun:test" +import { Subscription } from "../src/subscription" +import { centsToMicroCents } from "../src/util/price" + +afterEach(() => { + setSystemTime() +}) + +describe("Subscription.analyzeMonthlyUsage", () => { + const subscribed = new Date("2026-01-15T08:00:00Z") + + test("returns ok with 0% when usage was last updated before current period", () => { + setSystemTime(new Date("2026-03-20T10:00:00Z")) + const result = Subscription.analyzeMonthlyUsage({ + limit: 10, + usage: centsToMicroCents(500), + timeUpdated: new Date("2026-02-10T00:00:00Z"), + timeSubscribed: subscribed, + }) + + expect(result.status).toBe("ok") + expect(result.usagePercent).toBe(0) + // reset should be seconds until 2026-04-15T08:00:00Z + const expected = Math.ceil( + (new Date("2026-04-15T08:00:00Z").getTime() - new Date("2026-03-20T10:00:00Z").getTime()) / 1000, + ) + expect(result.resetInSec).toBe(expected) + }) + + test("returns ok with usage percent when under limit", () => { + setSystemTime(new Date("2026-03-20T10:00:00Z")) + const limit = 10 // $10 + const half = centsToMicroCents(10 * 100) / 2 + const result = Subscription.analyzeMonthlyUsage({ + limit, + usage: half, + timeUpdated: new Date("2026-03-18T00:00:00Z"), + timeSubscribed: subscribed, + }) + + expect(result.status).toBe("ok") + expect(result.usagePercent).toBe(50) + }) + + test("returns rate-limited when at or over limit", () => { + setSystemTime(new Date("2026-03-20T10:00:00Z")) + const limit = 10 + const result = Subscription.analyzeMonthlyUsage({ + limit, + usage: centsToMicroCents(limit * 100), + timeUpdated: new Date("2026-03-18T00:00:00Z"), + timeSubscribed: subscribed, + }) + + expect(result.status).toBe("rate-limited") + expect(result.usagePercent).toBe(100) + }) + + test("resets usage when crossing monthly boundary", () => { + // subscribed on 15th, now is April 16th — period is Apr 15 to May 15 + // timeUpdated is March 20 (previous period) + setSystemTime(new Date("2026-04-16T10:00:00Z")) + const result = Subscription.analyzeMonthlyUsage({ + limit: 10, + usage: centsToMicroCents(10 * 100), + timeUpdated: new Date("2026-03-20T00:00:00Z"), + timeSubscribed: subscribed, + }) + + expect(result.status).toBe("ok") + expect(result.usagePercent).toBe(0) + }) + + test("caps usage percent at 100", () => { + setSystemTime(new Date("2026-03-20T10:00:00Z")) + const limit = 10 + const result = Subscription.analyzeMonthlyUsage({ + limit, + usage: centsToMicroCents(limit * 100) - 1, + timeUpdated: new Date("2026-03-18T00:00:00Z"), + timeSubscribed: subscribed, + }) + + expect(result.status).toBe("ok") + expect(result.usagePercent).toBeLessThanOrEqual(100) + }) + + test("handles subscription day 31 in short month", () => { + const sub31 = new Date("2026-01-31T12:00:00Z") + // now is March 1 — period should be Feb 28 to Mar 31 + setSystemTime(new Date("2026-03-01T10:00:00Z")) + const result = Subscription.analyzeMonthlyUsage({ + limit: 10, + usage: 0, + timeUpdated: new Date("2026-03-01T09:00:00Z"), + timeSubscribed: sub31, + }) + + expect(result.status).toBe("ok") + expect(result.usagePercent).toBe(0) + const expected = Math.ceil( + (new Date("2026-03-31T12:00:00Z").getTime() - new Date("2026-03-01T10:00:00Z").getTime()) / 1000, + ) + expect(result.resetInSec).toBe(expected) + }) +}) From 744059a00f06ab20369cf4ce71072b41302e2f35 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 24 Feb 2026 09:47:20 +0000 Subject: [PATCH 12/94] chore: generate --- .../snapshot.json | 76 +++++-------------- 1 file changed, 17 insertions(+), 59 deletions(-) diff --git a/packages/console/core/migrations/20260224043338_nifty_starjammers/snapshot.json b/packages/console/core/migrations/20260224043338_nifty_starjammers/snapshot.json index 703ee233f30b..bc20ee2b964f 100644 --- a/packages/console/core/migrations/20260224043338_nifty_starjammers/snapshot.json +++ b/packages/console/core/migrations/20260224043338_nifty_starjammers/snapshot.json @@ -2,9 +2,7 @@ "version": "6", "dialect": "mysql", "id": "5e506dec-61e7-4726-81d1-afa4ffbc61ed", - "prevIds": [ - "4bf45b3f-3edd-4db7-94d5-097aa55ca5f7" - ], + "prevIds": ["4bf45b3f-3edd-4db7-94d5-097aa55ca5f7"], "ddl": [ { "name": "account", @@ -2083,131 +2081,91 @@ "table": "workspace" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "account", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "auth", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "benchmark", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "billing", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "lite", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "payment", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "subscription", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "usage", "entityType": "pks" }, { - "columns": [ - "ip", - "interval" - ], + "columns": ["ip", "interval"], "name": "PRIMARY", "table": "ip_rate_limit", "entityType": "pks" }, { - "columns": [ - "ip" - ], + "columns": ["ip"], "name": "PRIMARY", "table": "ip", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "key", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "model", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "provider", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "user", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "workspace", "entityType": "pks" @@ -2502,4 +2460,4 @@ } ], "renames": [] -} \ No newline at end of file +} From a592bd968454f0b8c55733f7a8df85e38a293de5 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:56:41 +1000 Subject: [PATCH 13/94] fix: update createOpenReviewFile test to match new call order (#14881) --- packages/app/src/pages/session/helpers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/session/helpers.test.ts b/packages/app/src/pages/session/helpers.test.ts index 7d357e6572c5..aaa5b932fe96 100644 --- a/packages/app/src/pages/session/helpers.test.ts +++ b/packages/app/src/pages/session/helpers.test.ts @@ -16,7 +16,7 @@ describe("createOpenReviewFile", () => { openReviewFile("src/a.ts") - expect(calls).toEqual(["show", "tab:src/a.ts", "open:file://src/a.ts", "load:src/a.ts"]) + expect(calls).toEqual(["show", "load:src/a.ts", "tab:src/a.ts", "open:file://src/a.ts"]) }) }) From de796d9a00544001fe196d9a3068ea241165293a Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:07:56 +1000 Subject: [PATCH 14/94] fix(test): use path.join for cross-platform glob test assertions (#14837) --- packages/opencode/test/util/glob.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/test/util/glob.test.ts b/packages/opencode/test/util/glob.test.ts index ae1bcdcf82e0..e58d92c85c64 100644 --- a/packages/opencode/test/util/glob.test.ts +++ b/packages/opencode/test/util/glob.test.ts @@ -63,7 +63,7 @@ describe("Glob", () => { const results = await Glob.scan("**/*.txt", { cwd: tmp.path }) - expect(results).toEqual(["nested/deep.txt"]) + expect(results).toEqual([path.join("nested", "deep.txt")]) }) test("returns empty array for no matches", async () => { @@ -82,7 +82,7 @@ describe("Glob", () => { const results = await Glob.scan("**/*.txt", { cwd: tmp.path }) - expect(results).toEqual(["realdir/file.txt"]) + expect(results).toEqual([path.join("realdir", "file.txt")]) }) test("follows symlinks when symlink option is true", async () => { @@ -93,7 +93,7 @@ describe("Glob", () => { const results = await Glob.scan("**/*.txt", { cwd: tmp.path, symlink: true }) - expect(results.sort()).toEqual(["linkdir/file.txt", "realdir/file.txt"]) + expect(results.sort()).toEqual([path.join("linkdir", "file.txt"), path.join("realdir", "file.txt")]) }) test("includes dotfiles when dot option is true", async () => { From 3201a7d34b03210f108e6caf49f20260d531a1a6 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:25:15 +1000 Subject: [PATCH 15/94] fix(win32): add bun prefix to console app build scripts (#14884) --- packages/console/app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 904aeadd8e06..395feeb4af5c 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,7 +7,7 @@ "typecheck": "tsgo --noEmit", "dev": "vite dev --host 0.0.0.0", "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev", - "build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json", + "build": "bun ./script/generate-sitemap.ts && vite build && bun ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vite start" }, "dependencies": { From 659068942eda0e48f8453d96b03724cfb1f9698d Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:33:22 +1000 Subject: [PATCH 16/94] fix(win32): handle CRLF line endings in markdown frontmatter parsing (#14886) --- packages/opencode/src/config/markdown.ts | 2 +- packages/opencode/test/config/markdown.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index 5b4ccf047710..3c9709b5b3bf 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/packages/opencode/src/config/markdown.ts @@ -22,7 +22,7 @@ export namespace ConfigMarkdown { if (!match) return content const frontmatter = match[1] - const lines = frontmatter.split("\n") + const lines = frontmatter.split(/\r?\n/) const result: string[] = [] for (const line of lines) { diff --git a/packages/opencode/test/config/markdown.test.ts b/packages/opencode/test/config/markdown.test.ts index c6133317e2c0..865af2107738 100644 --- a/packages/opencode/test/config/markdown.test.ts +++ b/packages/opencode/test/config/markdown.test.ts @@ -197,7 +197,7 @@ describe("ConfigMarkdown: frontmatter parsing w/ Markdown header", async () => { test("should parse and match", () => { expect(result).toBeDefined() expect(result.data).toEqual({}) - expect(result.content.trim()).toBe(`# Response Formatting Requirements + expect(result.content.trim().replace(/\r\n/g, "\n")).toBe(`# Response Formatting Requirements Always structure your responses using clear markdown formatting: From 13cabae29f7ed2bd658037c0c676f7807d63d8b3 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:14:16 +1000 Subject: [PATCH 17/94] fix(win32): add git flags for snapshot operations and fix tests for cross-platform (#14890) --- packages/opencode/src/snapshot/index.ts | 33 ++++--- packages/opencode/test/preload.ts | 2 +- .../opencode/test/snapshot/snapshot.test.ts | 86 ++++++++++--------- 3 files changed, 67 insertions(+), 54 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 833999e7615e..cf254b4cef74 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -64,6 +64,9 @@ export namespace Snapshot { .nothrow() // Configure git to not convert line endings on Windows await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow() + await $`git --git-dir ${git} config core.longpaths true`.quiet().nothrow() + await $`git --git-dir ${git} config core.symlinks true`.quiet().nothrow() + await $`git --git-dir ${git} config core.fsmonitor false`.quiet().nothrow() log.info("initialized") } await add(git) @@ -86,7 +89,7 @@ export namespace Snapshot { const git = gitdir() await add(git) const result = - await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .` + await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .` .quiet() .cwd(Instance.directory) .nothrow() @@ -113,7 +116,7 @@ export namespace Snapshot { log.info("restore", { commit: snapshot }) const git = gitdir() const result = - await $`git --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f` + await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f` .quiet() .cwd(Instance.worktree) .nothrow() @@ -135,14 +138,15 @@ export namespace Snapshot { for (const file of item.files) { if (files.has(file)) continue log.info("reverting", { file, hash: item.hash }) - const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}` - .quiet() - .cwd(Instance.worktree) - .nothrow() + const result = + await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}` + .quiet() + .cwd(Instance.worktree) + .nothrow() if (result.exitCode !== 0) { const relativePath = path.relative(Instance.worktree, file) const checkTree = - await $`git --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}` + await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}` .quiet() .cwd(Instance.worktree) .nothrow() @@ -164,7 +168,7 @@ export namespace Snapshot { const git = gitdir() await add(git) const result = - await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .` + await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .` .quiet() .cwd(Instance.worktree) .nothrow() @@ -201,7 +205,7 @@ export namespace Snapshot { const status = new Map() const statuses = - await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-status --no-renames ${from} ${to} -- .` + await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-status --no-renames ${from} ${to} -- .` .quiet() .cwd(Instance.directory) .nothrow() @@ -215,7 +219,7 @@ export namespace Snapshot { status.set(file, kind) } - for await (const line of $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .` + for await (const line of $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .` .quiet() .cwd(Instance.directory) .nothrow() @@ -225,13 +229,13 @@ export namespace Snapshot { const isBinaryFile = additions === "-" && deletions === "-" const before = isBinaryFile ? "" - : await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}` + : await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}` .quiet() .nothrow() .text() const after = isBinaryFile ? "" - : await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}` + : await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}` .quiet() .nothrow() .text() @@ -256,7 +260,10 @@ export namespace Snapshot { async function add(git: string) { await syncExclude(git) - await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() + await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} add .` + .quiet() + .cwd(Instance.directory) + .nothrow() } async function syncExclude(git: string) { diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index dee7045707ea..a6d96cf17bd8 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -10,7 +10,7 @@ import { afterAll } from "bun:test" const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid) await fs.mkdir(dir, { recursive: true }) afterAll(() => { - fsSync.rmSync(dir, { recursive: true, force: true }) + fsSync.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 500 }) }) process.env["XDG_DATA_HOME"] = path.join(dir, "share") diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index 9a0622c4a5a1..79b1a83cd3a1 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -1,11 +1,17 @@ import { test, expect } from "bun:test" import { $ } from "bun" import fs from "fs/promises" +import path from "path" import { Snapshot } from "../../src/snapshot" import { Instance } from "../../src/project/instance" import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" +// Git always outputs /-separated paths internally. Snapshot.patch() joins them +// with path.join (which produces \ on Windows) then normalizes back to /. +// This helper does the same for expected values so assertions match cross-platform. +const fwd = (...parts: string[]) => path.join(...parts).replaceAll("\\", "/") + async function bootstrap() { return tmpdir({ git: true, @@ -35,7 +41,7 @@ test("tracks deleted files correctly", async () => { await $`rm ${tmp.path}/a.txt`.quiet() - expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/a.txt`) + expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "a.txt")) }, }) }) @@ -143,7 +149,7 @@ test("binary file handling", async () => { await Filesystem.write(`${tmp.path}/image.png`, new Uint8Array([0x89, 0x50, 0x4e, 0x47])) const patch = await Snapshot.patch(before!) - expect(patch.files).toContain(`${tmp.path}/image.png`) + expect(patch.files).toContain(fwd(tmp.path, "image.png")) await Snapshot.revert([patch]) expect( @@ -164,9 +170,9 @@ test("symlink handling", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await $`ln -s ${tmp.path}/a.txt ${tmp.path}/link.txt`.quiet() + await fs.symlink(`${tmp.path}/a.txt`, `${tmp.path}/link.txt`, "file") - expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/link.txt`) + expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "link.txt")) }, }) }) @@ -181,7 +187,7 @@ test("large file handling", async () => { await Filesystem.write(`${tmp.path}/large.txt`, "x".repeat(1024 * 1024)) - expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/large.txt`) + expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "large.txt")) }, }) }) @@ -222,9 +228,9 @@ test("special characters in filenames", async () => { await Filesystem.write(`${tmp.path}/file_with_underscores.txt`, "UNDERSCORES") const files = (await Snapshot.patch(before!)).files - expect(files).toContain(`${tmp.path}/file with spaces.txt`) - expect(files).toContain(`${tmp.path}/file-with-dashes.txt`) - expect(files).toContain(`${tmp.path}/file_with_underscores.txt`) + expect(files).toContain(fwd(tmp.path, "file with spaces.txt")) + expect(files).toContain(fwd(tmp.path, "file-with-dashes.txt")) + expect(files).toContain(fwd(tmp.path, "file_with_underscores.txt")) }, }) }) @@ -293,10 +299,10 @@ test("unicode filenames", async () => { expect(before).toBeTruthy() const unicodeFiles = [ - { path: `${tmp.path}/文件.txt`, content: "chinese content" }, - { path: `${tmp.path}/🚀rocket.txt`, content: "emoji content" }, - { path: `${tmp.path}/café.txt`, content: "accented content" }, - { path: `${tmp.path}/файл.txt`, content: "cyrillic content" }, + { path: fwd(tmp.path, "文件.txt"), content: "chinese content" }, + { path: fwd(tmp.path, "🚀rocket.txt"), content: "emoji content" }, + { path: fwd(tmp.path, "café.txt"), content: "accented content" }, + { path: fwd(tmp.path, "файл.txt"), content: "cyrillic content" }, ] for (const file of unicodeFiles) { @@ -329,8 +335,8 @@ test.skip("unicode filenames modification and restore", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const chineseFile = `${tmp.path}/文件.txt` - const cyrillicFile = `${tmp.path}/файл.txt` + const chineseFile = fwd(tmp.path, "文件.txt") + const cyrillicFile = fwd(tmp.path, "файл.txt") await Filesystem.write(chineseFile, "original chinese") await Filesystem.write(cyrillicFile, "original cyrillic") @@ -362,7 +368,7 @@ test("unicode filenames in subdirectories", async () => { expect(before).toBeTruthy() await $`mkdir -p "${tmp.path}/目录/подкаталог"`.quiet() - const deepFile = `${tmp.path}/目录/подкаталог/文件.txt` + const deepFile = fwd(tmp.path, "目录", "подкаталог", "文件.txt") await Filesystem.write(deepFile, "deep unicode content") const patch = await Snapshot.patch(before!) @@ -388,7 +394,7 @@ test("very long filenames", async () => { expect(before).toBeTruthy() const longName = "a".repeat(200) + ".txt" - const longFile = `${tmp.path}/${longName}` + const longFile = fwd(tmp.path, longName) await Filesystem.write(longFile, "long filename content") @@ -419,9 +425,9 @@ test("hidden files", async () => { await Filesystem.write(`${tmp.path}/.config`, "config content") const patch = await Snapshot.patch(before!) - expect(patch.files).toContain(`${tmp.path}/.hidden`) - expect(patch.files).toContain(`${tmp.path}/.gitignore`) - expect(patch.files).toContain(`${tmp.path}/.config`) + expect(patch.files).toContain(fwd(tmp.path, ".hidden")) + expect(patch.files).toContain(fwd(tmp.path, ".gitignore")) + expect(patch.files).toContain(fwd(tmp.path, ".config")) }, }) }) @@ -436,12 +442,12 @@ test("nested symlinks", async () => { await $`mkdir -p ${tmp.path}/sub/dir`.quiet() await Filesystem.write(`${tmp.path}/sub/dir/target.txt`, "target content") - await $`ln -s ${tmp.path}/sub/dir/target.txt ${tmp.path}/sub/dir/link.txt`.quiet() - await $`ln -s ${tmp.path}/sub ${tmp.path}/sub-link`.quiet() + await fs.symlink(`${tmp.path}/sub/dir/target.txt`, `${tmp.path}/sub/dir/link.txt`, "file") + await fs.symlink(`${tmp.path}/sub`, `${tmp.path}/sub-link`, "dir") const patch = await Snapshot.patch(before!) - expect(patch.files).toContain(`${tmp.path}/sub/dir/link.txt`) - expect(patch.files).toContain(`${tmp.path}/sub-link`) + expect(patch.files).toContain(fwd(tmp.path, "sub", "dir", "link.txt")) + expect(patch.files).toContain(fwd(tmp.path, "sub-link")) }, }) }) @@ -476,7 +482,7 @@ test("circular symlinks", async () => { expect(before).toBeTruthy() // Create circular symlink - await $`ln -s ${tmp.path}/circular ${tmp.path}/circular`.quiet().nothrow() + await fs.symlink(`${tmp.path}/circular`, `${tmp.path}/circular`, "dir").catch(() => {}) const patch = await Snapshot.patch(before!) expect(patch.files.length).toBeGreaterThanOrEqual(0) // Should not crash @@ -499,11 +505,11 @@ test("gitignore changes", async () => { const patch = await Snapshot.patch(before!) // Should track gitignore itself - expect(patch.files).toContain(`${tmp.path}/.gitignore`) + expect(patch.files).toContain(fwd(tmp.path, ".gitignore")) // Should track normal files - expect(patch.files).toContain(`${tmp.path}/normal.txt`) + expect(patch.files).toContain(fwd(tmp.path, "normal.txt")) // Should not track ignored files (git won't see them) - expect(patch.files).not.toContain(`${tmp.path}/test.ignored`) + expect(patch.files).not.toContain(fwd(tmp.path, "test.ignored")) }, }) }) @@ -523,8 +529,8 @@ test("git info exclude changes", async () => { await Bun.write(`${tmp.path}/normal.txt`, "normal content") const patch = await Snapshot.patch(before!) - expect(patch.files).toContain(`${tmp.path}/normal.txt`) - expect(patch.files).not.toContain(`${tmp.path}/ignored.txt`) + expect(patch.files).toContain(fwd(tmp.path, "normal.txt")) + expect(patch.files).not.toContain(fwd(tmp.path, "ignored.txt")) const after = await Snapshot.track() const diffs = await Snapshot.diffFull(before!, after!) @@ -559,9 +565,9 @@ test("git info exclude keeps global excludes", async () => { await Bun.write(`${tmp.path}/normal.txt`, "normal content") const patch = await Snapshot.patch(before!) - expect(patch.files).toContain(`${tmp.path}/normal.txt`) - expect(patch.files).not.toContain(`${tmp.path}/global.tmp`) - expect(patch.files).not.toContain(`${tmp.path}/info.tmp`) + expect(patch.files).toContain(fwd(tmp.path, "normal.txt")) + expect(patch.files).not.toContain(fwd(tmp.path, "global.tmp")) + expect(patch.files).not.toContain(fwd(tmp.path, "info.tmp")) } finally { if (prev) process.env.GIT_CONFIG_GLOBAL = prev else delete process.env.GIT_CONFIG_GLOBAL @@ -610,7 +616,7 @@ test("snapshot state isolation between projects", async () => { const before1 = await Snapshot.track() await Filesystem.write(`${tmp1.path}/project1.txt`, "project1 content") const patch1 = await Snapshot.patch(before1!) - expect(patch1.files).toContain(`${tmp1.path}/project1.txt`) + expect(patch1.files).toContain(fwd(tmp1.path, "project1.txt")) }, }) @@ -620,10 +626,10 @@ test("snapshot state isolation between projects", async () => { const before2 = await Snapshot.track() await Filesystem.write(`${tmp2.path}/project2.txt`, "project2 content") const patch2 = await Snapshot.patch(before2!) - expect(patch2.files).toContain(`${tmp2.path}/project2.txt`) + expect(patch2.files).toContain(fwd(tmp2.path, "project2.txt")) // Ensure project1 files don't appear in project2 - expect(patch2.files).not.toContain(`${tmp1?.path}/project1.txt`) + expect(patch2.files).not.toContain(fwd(tmp1?.path ?? "", "project1.txt")) }, }) }) @@ -647,7 +653,7 @@ test("patch detects changes in secondary worktree", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - const worktreeFile = `${worktreePath}/worktree.txt` + const worktreeFile = fwd(worktreePath, "worktree.txt") await Filesystem.write(worktreeFile, "worktree content") const patch = await Snapshot.patch(before!) @@ -681,7 +687,7 @@ test("revert only removes files in invoking worktree", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - const worktreeFile = `${worktreePath}/worktree.txt` + const worktreeFile = fwd(worktreePath, "worktree.txt") await Filesystem.write(worktreeFile, "worktree content") const patch = await Snapshot.patch(before!) @@ -832,7 +838,7 @@ test("revert should not delete files that existed but were deleted in snapshot", await Filesystem.write(`${tmp.path}/a.txt`, "recreated content") const patch = await Snapshot.patch(snapshot2!) - expect(patch.files).toContain(`${tmp.path}/a.txt`) + expect(patch.files).toContain(fwd(tmp.path, "a.txt")) await Snapshot.revert([patch]) @@ -861,8 +867,8 @@ test("revert preserves file that existed in snapshot when deleted then recreated await Filesystem.write(`${tmp.path}/newfile.txt`, "new") const patch = await Snapshot.patch(snapshot!) - expect(patch.files).toContain(`${tmp.path}/existing.txt`) - expect(patch.files).toContain(`${tmp.path}/newfile.txt`) + expect(patch.files).toContain(fwd(tmp.path, "existing.txt")) + expect(patch.files).toContain(fwd(tmp.path, "newfile.txt")) await Snapshot.revert([patch]) From 888b123387718aa1fc802fbcae7341c7aeef6f73 Mon Sep 17 00:00:00 2001 From: Noam Bressler Date: Tue, 24 Feb 2026 13:14:47 +0200 Subject: [PATCH 18/94] feat: ACP - stream bash output and synthetic pending events (#14079) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/acp/agent.ts | 123 ++++++++++---- .../test/acp/event-subscription.test.ts | 158 +++++++++++++++++- 2 files changed, 244 insertions(+), 37 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 765c741c0d62..5db98bc7044f 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -41,7 +41,7 @@ import { Config } from "@/config/config" import { Todo } from "@/session/todo" import { z } from "zod" import { LoadAPIKeyError } from "ai" -import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" +import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2" import { applyPatch } from "diff" type ModeOption = { id: string; name: string; description?: string } @@ -135,6 +135,8 @@ export namespace ACP { private sessionManager: ACPSessionManager private eventAbort = new AbortController() private eventStarted = false + private bashSnapshots = new Map() + private toolStarts = new Set() private permissionQueues = new Map>() private permissionOptions: PermissionOption[] = [ { optionId: "once", kind: "allow_once", name: "Allow once" }, @@ -266,47 +268,68 @@ export namespace ACP { const session = this.sessionManager.tryGet(part.sessionID) if (!session) return const sessionId = session.id - const directory = session.cwd - - const message = await this.sdk.session - .message( - { - sessionID: part.sessionID, - messageID: part.messageID, - directory, - }, - { throwOnError: true }, - ) - .then((x) => x.data) - .catch((error) => { - log.error("unexpected error when fetching message", { error }) - return undefined - }) - - if (!message || message.info.role !== "assistant") return if (part.type === "tool") { + if (!this.toolStarts.has(part.callID)) { + this.toolStarts.add(part.callID) + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call", + toolCallId: part.callID, + title: part.tool, + kind: toToolKind(part.tool), + status: "pending", + locations: [], + rawInput: {}, + }, + }) + .catch((error) => { + log.error("failed to send tool pending to ACP", { error }) + }) + } + switch (part.state.status) { case "pending": - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call", - toolCallId: part.callID, - title: part.tool, - kind: toToolKind(part.tool), - status: "pending", - locations: [], - rawInput: {}, - }, - }) - .catch((error) => { - log.error("failed to send tool pending to ACP", { error }) - }) + this.bashSnapshots.delete(part.callID) return case "running": + const output = this.bashOutput(part) + const content: ToolCallContent[] = [] + if (output) { + const hash = String(Bun.hash(output)) + if (part.tool === "bash") { + if (this.bashSnapshots.get(part.callID) === hash) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "in_progress", + kind: toToolKind(part.tool), + title: part.tool, + locations: toLocations(part.tool, part.state.input), + rawInput: part.state.input, + }, + }) + .catch((error) => { + log.error("failed to send tool in_progress to ACP", { error }) + }) + return + } + this.bashSnapshots.set(part.callID, hash) + } + content.push({ + type: "content", + content: { + type: "text", + text: output, + }, + }) + } await this.connection .sessionUpdate({ sessionId, @@ -318,6 +341,7 @@ export namespace ACP { title: part.tool, locations: toLocations(part.tool, part.state.input), rawInput: part.state.input, + ...(content.length > 0 && { content }), }, }) .catch((error) => { @@ -326,6 +350,8 @@ export namespace ACP { return case "completed": { + this.toolStarts.delete(part.callID) + this.bashSnapshots.delete(part.callID) const kind = toToolKind(part.tool) const content: ToolCallContent[] = [ { @@ -405,6 +431,8 @@ export namespace ACP { return } case "error": + this.toolStarts.delete(part.callID) + this.bashSnapshots.delete(part.callID) await this.connection .sessionUpdate({ sessionId, @@ -426,6 +454,7 @@ export namespace ACP { ], rawOutput: { error: part.state.error, + metadata: part.state.metadata, }, }, }) @@ -802,6 +831,7 @@ export namespace ACP { if (part.type === "tool") { switch (part.state.status) { case "pending": + this.bashSnapshots.delete(part.callID) await this.connection .sessionUpdate({ sessionId, @@ -820,6 +850,17 @@ export namespace ACP { }) break case "running": + const output = this.bashOutput(part) + const runningContent: ToolCallContent[] = [] + if (output) { + runningContent.push({ + type: "content", + content: { + type: "text", + text: output, + }, + }) + } await this.connection .sessionUpdate({ sessionId, @@ -831,6 +872,7 @@ export namespace ACP { title: part.tool, locations: toLocations(part.tool, part.state.input), rawInput: part.state.input, + ...(runningContent.length > 0 && { content: runningContent }), }, }) .catch((err) => { @@ -838,6 +880,7 @@ export namespace ACP { }) break case "completed": + this.bashSnapshots.delete(part.callID) const kind = toToolKind(part.tool) const content: ToolCallContent[] = [ { @@ -916,6 +959,7 @@ export namespace ACP { }) break case "error": + this.bashSnapshots.delete(part.callID) await this.connection .sessionUpdate({ sessionId, @@ -937,6 +981,7 @@ export namespace ACP { ], rawOutput: { error: part.state.error, + metadata: part.state.metadata, }, }, }) @@ -1063,6 +1108,14 @@ export namespace ACP { } } + private bashOutput(part: ToolPart) { + if (part.tool !== "bash") return + if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return + const output = part.state.metadata["output"] + if (typeof output !== "string") return + return output + } + private async loadAvailableModes(directory: string): Promise { const agents = await this.config.sdk.app .agents( diff --git a/packages/opencode/test/acp/event-subscription.test.ts b/packages/opencode/test/acp/event-subscription.test.ts index 1145a1357d2d..7372c55bac99 100644 --- a/packages/opencode/test/acp/event-subscription.test.ts +++ b/packages/opencode/test/acp/event-subscription.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import { ACP } from "../../src/acp/agent" import type { AgentSideConnection } from "@agentclientprotocol/sdk" -import type { Event } from "@opencode-ai/sdk/v2" +import type { Event, EventMessagePartUpdated, ToolStatePending, ToolStateRunning } from "@opencode-ai/sdk/v2" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" @@ -19,6 +19,61 @@ type EventController = { close: () => void } +function inProgressText(update: SessionUpdateParams["update"]) { + if (update.sessionUpdate !== "tool_call_update") return undefined + if (update.status !== "in_progress") return undefined + if (!update.content || !Array.isArray(update.content)) return undefined + const first = update.content[0] + if (!first || first.type !== "content") return undefined + if (first.content.type !== "text") return undefined + return first.content.text +} + +function isToolCallUpdate( + update: SessionUpdateParams["update"], +): update is Extract { + return update.sessionUpdate === "tool_call_update" +} + +function toolEvent( + sessionId: string, + cwd: string, + opts: { + callID: string + tool: string + input: Record + } & ({ status: "running"; metadata?: Record } | { status: "pending"; raw: string }), +): GlobalEventEnvelope { + const state: ToolStatePending | ToolStateRunning = + opts.status === "running" + ? { + status: "running", + input: opts.input, + ...(opts.metadata && { metadata: opts.metadata }), + time: { start: Date.now() }, + } + : { + status: "pending", + input: opts.input, + raw: opts.raw, + } + const payload: EventMessagePartUpdated = { + type: "message.part.updated", + properties: { + part: { + id: `part_${opts.callID}`, + sessionID: sessionId, + messageID: `msg_${opts.callID}`, + type: "tool", + callID: opts.callID, + tool: opts.tool, + state, + }, + }, + } + return { directory: cwd, payload } +} + function createEventStream() { const queue: GlobalEventEnvelope[] = [] const waiters: Array<(value: GlobalEventEnvelope | undefined) => void> = [] @@ -65,6 +120,7 @@ function createEventStream() { function createFakeAgent() { const updates = new Map() const chunks = new Map() + const sessionUpdates: SessionUpdateParams[] = [] const record = (sessionId: string, type: string) => { const list = updates.get(sessionId) ?? [] list.push(type) @@ -73,6 +129,7 @@ function createFakeAgent() { const connection = { async sessionUpdate(params: SessionUpdateParams) { + sessionUpdates.push(params) const update = params.update const type = update?.sessionUpdate ?? "unknown" record(params.sessionId, type) @@ -197,7 +254,7 @@ function createFakeAgent() { ;(agent as any).eventAbort.abort() } - return { agent, controller, calls, updates, chunks, stop, sdk, connection } + return { agent, controller, calls, updates, chunks, sessionUpdates, stop, sdk, connection } } describe("acp.agent event subscription", () => { @@ -435,4 +492,101 @@ describe("acp.agent event subscription", () => { }, }) }) + + test("streams running bash output snapshots and de-dupes identical snapshots", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const { agent, controller, sessionUpdates, stop } = createFakeAgent() + const cwd = "/tmp/opencode-acp-test" + const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + const input = { command: "echo hello", description: "run command" } + + for (const output of ["a", "a", "ab"]) { + controller.push( + toolEvent(sessionId, cwd, { callID: "call_1", tool: "bash", status: "running", input, metadata: { output } }), + ) + } + await new Promise((r) => setTimeout(r, 20)) + + const snapshots = sessionUpdates + .filter((u) => u.sessionId === sessionId) + .filter((u) => isToolCallUpdate(u.update)) + .map((u) => inProgressText(u.update)) + + expect(snapshots).toEqual(["a", undefined, "ab"]) + stop() + }, + }) + }) + + test("emits synthetic pending before first running update for any tool", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const { agent, controller, sessionUpdates, stop } = createFakeAgent() + const cwd = "/tmp/opencode-acp-test" + const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + + controller.push( + toolEvent(sessionId, cwd, { + callID: "call_bash", + tool: "bash", + status: "running", + input: { command: "echo hi", description: "run command" }, + metadata: { output: "hi\n" }, + }), + ) + controller.push( + toolEvent(sessionId, cwd, { + callID: "call_read", + tool: "read", + status: "running", + input: { filePath: "/tmp/example.txt" }, + }), + ) + await new Promise((r) => setTimeout(r, 20)) + + const types = sessionUpdates + .filter((u) => u.sessionId === sessionId) + .map((u) => u.update.sessionUpdate) + .filter((u) => u === "tool_call" || u === "tool_call_update") + expect(types).toEqual(["tool_call", "tool_call_update", "tool_call", "tool_call_update"]) + + const pendings = sessionUpdates.filter( + (u) => u.sessionId === sessionId && u.update.sessionUpdate === "tool_call", + ) + expect(pendings.every((p) => p.update.sessionUpdate === "tool_call" && p.update.status === "pending")).toBe(true) + stop() + }, + }) + }) + + test("clears bash snapshot marker on pending state", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const { agent, controller, sessionUpdates, stop } = createFakeAgent() + const cwd = "/tmp/opencode-acp-test" + const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + const input = { command: "echo hello", description: "run command" } + + controller.push(toolEvent(sessionId, cwd, { callID: "call_1", tool: "bash", status: "running", input, metadata: { output: "a" } })) + controller.push(toolEvent(sessionId, cwd, { callID: "call_1", tool: "bash", status: "pending", input, raw: '{"command":"echo hello"}' })) + controller.push(toolEvent(sessionId, cwd, { callID: "call_1", tool: "bash", status: "running", input, metadata: { output: "a" } })) + await new Promise((r) => setTimeout(r, 20)) + + const snapshots = sessionUpdates + .filter((u) => u.sessionId === sessionId) + .filter((u) => isToolCallUpdate(u.update)) + .map((u) => inProgressText(u.update)) + + expect(snapshots).toEqual(["a", "a"]) + stop() + }, + }) + }) }) From ef7f222d80d1b5d2f3c18e86efba99a1f308c1f9 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 24 Feb 2026 11:15:39 +0000 Subject: [PATCH 19/94] chore: generate --- .../test/acp/event-subscription.test.ts | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/opencode/test/acp/event-subscription.test.ts b/packages/opencode/test/acp/event-subscription.test.ts index 7372c55bac99..d61240d715bc 100644 --- a/packages/opencode/test/acp/event-subscription.test.ts +++ b/packages/opencode/test/acp/event-subscription.test.ts @@ -505,7 +505,13 @@ describe("acp.agent event subscription", () => { for (const output of ["a", "a", "ab"]) { controller.push( - toolEvent(sessionId, cwd, { callID: "call_1", tool: "bash", status: "running", input, metadata: { output } }), + toolEvent(sessionId, cwd, { + callID: "call_1", + tool: "bash", + status: "running", + input, + metadata: { output }, + }), ) } await new Promise((r) => setTimeout(r, 20)) @@ -558,7 +564,9 @@ describe("acp.agent event subscription", () => { const pendings = sessionUpdates.filter( (u) => u.sessionId === sessionId && u.update.sessionUpdate === "tool_call", ) - expect(pendings.every((p) => p.update.sessionUpdate === "tool_call" && p.update.status === "pending")).toBe(true) + expect(pendings.every((p) => p.update.sessionUpdate === "tool_call" && p.update.status === "pending")).toBe( + true, + ) stop() }, }) @@ -574,9 +582,33 @@ describe("acp.agent event subscription", () => { const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) const input = { command: "echo hello", description: "run command" } - controller.push(toolEvent(sessionId, cwd, { callID: "call_1", tool: "bash", status: "running", input, metadata: { output: "a" } })) - controller.push(toolEvent(sessionId, cwd, { callID: "call_1", tool: "bash", status: "pending", input, raw: '{"command":"echo hello"}' })) - controller.push(toolEvent(sessionId, cwd, { callID: "call_1", tool: "bash", status: "running", input, metadata: { output: "a" } })) + controller.push( + toolEvent(sessionId, cwd, { + callID: "call_1", + tool: "bash", + status: "running", + input, + metadata: { output: "a" }, + }), + ) + controller.push( + toolEvent(sessionId, cwd, { + callID: "call_1", + tool: "bash", + status: "pending", + input, + raw: '{"command":"echo hello"}', + }), + ) + controller.push( + toolEvent(sessionId, cwd, { + callID: "call_1", + tool: "bash", + status: "running", + input, + metadata: { output: "a" }, + }), + ) await new Promise((r) => setTimeout(r, 20)) const snapshots = sessionUpdates From 79254c10201a3978ac72ef2a047bb4070efdc41d Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:40:38 +1000 Subject: [PATCH 20/94] fix(test): normalize git excludesFile path for Windows (#14893) --- packages/opencode/test/snapshot/snapshot.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index 79b1a83cd3a1..1804ab5c2a2b 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -548,7 +548,7 @@ test("git info exclude keeps global excludes", async () => { const global = `${tmp.path}/global.ignore` const config = `${tmp.path}/global.gitconfig` await Bun.write(global, "global.tmp\n") - await Bun.write(config, `[core]\n\texcludesFile = ${global}\n`) + await Bun.write(config, `[core]\n\texcludesFile = ${global.replaceAll("\\", "/")}\n`) const prev = process.env.GIT_CONFIG_GLOBAL process.env.GIT_CONFIG_GLOBAL = config From a292eddeb516ebf1774e68640b4c62ad284472b2 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:59:14 +1000 Subject: [PATCH 21/94] fix(test): harden preload cleanup against Windows EBUSY (#14895) --- packages/opencode/src/storage/db.ts | 13 +++++++++++++ packages/opencode/test/preload.ts | 21 ++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 6d7bfd728102..f29aac18d163 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -33,6 +33,10 @@ export namespace Database { type Journal = { sql: string; timestamp: number }[] + const state = { + sqlite: undefined as BunDatabase | undefined, + } + function time(tag: string) { const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag) if (!match) return 0 @@ -69,6 +73,7 @@ export namespace Database { log.info("opening database", { path: path.join(Global.Path.data, "opencode.db") }) const sqlite = new BunDatabase(path.join(Global.Path.data, "opencode.db"), { create: true }) + state.sqlite = sqlite sqlite.run("PRAGMA journal_mode = WAL") sqlite.run("PRAGMA synchronous = NORMAL") @@ -95,6 +100,14 @@ export namespace Database { return db }) + export function close() { + const sqlite = state.sqlite + if (!sqlite) return + sqlite.close() + state.sqlite = undefined + Client.reset() + } + export type TxOrDb = Transaction | Client const ctx = Context.create<{ diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index a6d96cf17bd8..41028633e83e 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -3,14 +3,29 @@ import os from "os" import path from "path" import fs from "fs/promises" -import fsSync from "fs" import { afterAll } from "bun:test" // Set XDG env vars FIRST, before any src/ imports const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid) await fs.mkdir(dir, { recursive: true }) -afterAll(() => { - fsSync.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 500 }) +afterAll(async () => { + const { Database } = await import("../src/storage/db") + Database.close() + const busy = (error: unknown) => + typeof error === "object" && error !== null && "code" in error && error.code === "EBUSY" + const rm = async (left: number): Promise => { + Bun.gc(true) + await Bun.sleep(100) + return fs.rm(dir, { recursive: true, force: true }).catch((error) => { + if (!busy(error)) throw error + if (left <= 1) throw error + return rm(left - 1) + }) + } + + // Windows can keep SQLite WAL handles alive until GC finalizers run, so we + // force GC and retry teardown to avoid flaky EBUSY in test cleanup. + await rm(30) }) process.env["XDG_DATA_HOME"] = path.join(dir, "share") From 1af3e9e557a6df4f933a01d0dad2e52e418ebd52 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:20:57 +1000 Subject: [PATCH 22/94] fix(win32): fix plugin resolution with createRequire fallback (#14898) --- packages/opencode/src/config/config.ts | 15 ++++++++++++--- packages/opencode/test/config/config.test.ts | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index aad0fd76c4be..71cf43d6da43 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1,6 +1,7 @@ import { Log } from "../util/log" import path from "path" -import { pathToFileURL } from "url" +import { pathToFileURL, fileURLToPath } from "url" +import { createRequire } from "module" import os from "os" import z from "zod" import { Filesystem } from "../util/filesystem" @@ -276,7 +277,6 @@ export namespace Config { "@opencode-ai/plugin": targetVersion, } await Filesystem.writeJson(pkg, json) - await new Promise((resolve) => setTimeout(resolve, 3000)) const gitignore = path.join(dir, ".gitignore") const hasGitIgnore = await Filesystem.exists(gitignore) @@ -1332,7 +1332,16 @@ export namespace Config { const plugin = data.plugin[i] try { data.plugin[i] = import.meta.resolve!(plugin, options.path) - } catch (err) {} + } catch (e) { + try { + // import.meta.resolve sometimes fails with newly created node_modules + const require = createRequire(options.path) + const resolvedPath = require.resolve(plugin) + data.plugin[i] = pathToFileURL(resolvedPath).href + } catch { + // Ignore, plugin might be a generic string identifier like "mcp-server" + } + } } } return data diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 56773570af57..2b1ba816ea3b 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -689,7 +689,7 @@ test("resolves scoped npm plugins in config", async () => { const pluginEntries = config.plugin ?? [] const baseUrl = pathToFileURL(path.join(tmp.path, "opencode.json")).href - const expected = import.meta.resolve("@scope/plugin", baseUrl) + const expected = pathToFileURL(path.join(tmp.path, "node_modules", "@scope", "plugin", "index.js")).href expect(pluginEntries.includes(expected)).toBe(true) From 1a0639e5b89265ac89afd7bcfae835a64744768d Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:42:48 +1000 Subject: [PATCH 23/94] fix(win32): normalize backslash paths in config rel() and file ignore (#14903) --- packages/opencode/src/config/config.ts | 5 +++-- packages/opencode/src/file/ignore.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 71cf43d6da43..b1e00fccb850 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -342,10 +342,11 @@ export namespace Config { } function rel(item: string, patterns: string[]) { + const normalizedItem = item.replaceAll("\\", "/") for (const pattern of patterns) { - const index = item.indexOf(pattern) + const index = normalizedItem.indexOf(pattern) if (index === -1) continue - return item.slice(index + pattern.length) + return normalizedItem.slice(index + pattern.length) } } diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index 94ffaf5ce049..b9731040c7d7 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -67,7 +67,7 @@ export namespace FileIgnore { if (Glob.match(pattern, filepath)) return false } - const parts = filepath.split(sep) + const parts = filepath.split(/[/\\]/) for (let i = 0; i < parts.length; i++) { if (FOLDERS.has(parts[i])) return true } From 06f25c78f655257819d681b39598bf151837caf6 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:51:56 +1000 Subject: [PATCH 24/94] fix(test): use path.sep in discovery test for cross-platform path matching (#14905) --- packages/opencode/test/skill/discovery.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts index d1963f697b98..5664fa32b8ad 100644 --- a/packages/opencode/test/skill/discovery.test.ts +++ b/packages/opencode/test/skill/discovery.test.ts @@ -77,7 +77,7 @@ describe("Discovery.pull", () => { test("downloads reference files alongside SKILL.md", async () => { const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL) // find a skill dir that should have reference files (e.g. agents-sdk) - const agentsSdk = dirs.find((d) => d.endsWith("/agents-sdk")) + const agentsSdk = dirs.find((d) => d.endsWith(path.sep + "agents-sdk")) expect(agentsSdk).toBeDefined() if (agentsSdk) { const refs = path.join(agentsSdk, "references") From 3d379c20c4973ef2b1c0305dbd1064ba0f1d8e3f Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:03:18 +1000 Subject: [PATCH 25/94] fix(test): replace Unix-only assumptions with cross-platform alternatives (#14906) --- packages/opencode/test/tool/bash.test.ts | 10 ++++++---- .../opencode/test/tool/external-directory.test.ts | 4 ++-- packages/opencode/test/tool/write.test.ts | 15 +++++++++++---- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index db05f8f623f6..ac93016927ac 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test" +import os from "os" import path from "path" import { BashTool } from "../../src/tool/bash" import { Instance } from "../../src/project/instance" @@ -138,14 +139,14 @@ describe("tool.bash permissions", () => { await bash.execute( { command: "ls", - workdir: "/tmp", - description: "List /tmp", + workdir: os.tmpdir(), + description: "List temp dir", }, testCtx, ) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeDefined() - expect(extDirReq!.patterns).toContain("/tmp/*") + expect(extDirReq!.patterns).toContain(path.join(os.tmpdir(), "*")) }, }) }) @@ -366,7 +367,8 @@ describe("tool.bash truncation", () => { ctx, ) expect((result.metadata as any).truncated).toBe(false) - expect(result.output).toBe("hello\n") + const eol = process.platform === "win32" ? "\r\n" : "\n" + expect(result.output).toBe(`hello${eol}`) }, }) }) diff --git a/packages/opencode/test/tool/external-directory.test.ts b/packages/opencode/test/tool/external-directory.test.ts index 33c5e2c7397f..a75f767b3b6c 100644 --- a/packages/opencode/test/tool/external-directory.test.ts +++ b/packages/opencode/test/tool/external-directory.test.ts @@ -65,7 +65,7 @@ describe("tool.assertExternalDirectory", () => { const directory = "/tmp/project" const target = "/tmp/outside/file.txt" - const expected = path.join(path.dirname(target), "*") + const expected = path.join(path.dirname(target), "*").replaceAll("\\", "/") await Instance.provide({ directory, @@ -91,7 +91,7 @@ describe("tool.assertExternalDirectory", () => { const directory = "/tmp/project" const target = "/tmp/outside" - const expected = path.join(target, "*") + const expected = path.join(target, "*").replaceAll("\\", "/") await Instance.provide({ directory, diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index 4f1a7d28e8cf..695d48ccbbc7 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -293,19 +293,26 @@ describe("tool.write", () => { }) describe("error handling", () => { - test("throws error for paths outside project", async () => { + test("throws error when OS denies write access", async () => { await using tmp = await tmpdir() - const outsidePath = "/etc/passwd" + const readonlyPath = path.join(tmp.path, "readonly.txt") + + // Create a read-only file + await fs.writeFile(readonlyPath, "test", "utf-8") + await fs.chmod(readonlyPath, 0o444) await Instance.provide({ directory: tmp.path, fn: async () => { + const { FileTime } = await import("../../src/file/time") + FileTime.read(ctx.sessionID, readonlyPath) + const write = await WriteTool.init() await expect( write.execute( { - filePath: outsidePath, - content: "test", + filePath: readonlyPath, + content: "new content", }, ctx, ), From 36197f5ff8d98e582b2ea9da3e851937102d2888 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:10:10 +1000 Subject: [PATCH 26/94] fix(win32): add 50ms tolerance for NTFS mtime fuzziness in FileTime assert (#14907) --- packages/opencode/src/file/time.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index c85781eb4116..efb1c437647f 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -61,7 +61,8 @@ export namespace FileTime { const time = get(sessionID, filepath) if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`) const mtime = Filesystem.stat(filepath)?.mtime - if (mtime && mtime.getTime() > time.getTime()) { + // Allow a 50ms tolerance for Windows NTFS timestamp fuzziness / async flushing + if (mtime && mtime.getTime() > time.getTime() + 50) { throw new Error( `File ${filepath} has been modified since it was last read.\nLast modification: ${mtime.toISOString()}\nLast read: ${time.toISOString()}\n\nPlease read the file again before modifying it.`, ) From 32417774c4baccbcb23820162f0b9c196bbe06de Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:16:24 +1000 Subject: [PATCH 27/94] fix(test): replace structuredClone with spread for process.env (#14908) --- packages/opencode/test/ide/ide.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/ide/ide.test.ts b/packages/opencode/test/ide/ide.test.ts index 4d70140197fe..e10700e80ffe 100644 --- a/packages/opencode/test/ide/ide.test.ts +++ b/packages/opencode/test/ide/ide.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test, afterEach } from "bun:test" import { Ide } from "../../src/ide" describe("ide", () => { - const original = structuredClone(process.env) + const original = { ...process.env } afterEach(() => { Object.keys(process.env).forEach((key) => { From e27d3d5d4017b33b73d4278fac561513454b1cae Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Tue, 24 Feb 2026 07:32:07 -0600 Subject: [PATCH 28/94] fix(app): remove filetree tooltips --- packages/app/src/components/file-tree.tsx | 163 +++++++--------------- 1 file changed, 51 insertions(+), 112 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index cec094354254..3840f18ed871 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -3,7 +3,6 @@ import { encodeFilePath } from "@/context/file/path" import { Collapsible } from "@opencode-ai/ui/collapsible" import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" -import { Tooltip } from "@opencode-ai/ui/tooltip" import { createEffect, createMemo, @@ -192,59 +191,6 @@ const FileTreeNode = ( ) } -const FileTreeNodeTooltip = (props: { enabled: boolean; node: FileNode; kind?: Kind; children: JSXElement }) => { - if (!props.enabled) return props.children - - const parts = props.node.path.split("/") - const leaf = parts[parts.length - 1] ?? props.node.path - const head = parts.slice(0, -1).join("/") - const prefix = head ? `${head}/` : "" - const label = - props.kind === "add" - ? "Additions" - : props.kind === "del" - ? "Deletions" - : props.kind === "mix" - ? "Modifications" - : undefined - - return ( - - - {prefix} - - {leaf} - - {(text) => ( - <> - - {text()} - - )} - - - <> - - Ignored - - - - } - > - {props.children} - - ) -} - export default function FileTree(props: { path: string class?: string @@ -255,7 +201,6 @@ export default function FileTree(props: { modified?: readonly string[] kinds?: ReadonlyMap draggable?: boolean - tooltip?: boolean onFileClick?: (file: FileNode) => void _filter?: Filter @@ -267,7 +212,6 @@ export default function FileTree(props: { const file = useFile() const level = props.level ?? 0 const draggable = () => props.draggable ?? true - const tooltip = () => props.tooltip ?? true const key = (p: string) => file @@ -467,21 +411,19 @@ export default function FileTree(props: { onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))} > - - -
- -
-
-
+ +
+ +
+
- - props.onFileClick?.(node)} - > -
- - + props.onFileClick?.(node)} + > +
+ + + + + + + + + - - - - - - - - - - - - + + + + ) From 2cee947671fa373098db308b173c859cada0b108 Mon Sep 17 00:00:00 2001 From: Noam Bressler Date: Tue, 24 Feb 2026 15:54:10 +0200 Subject: [PATCH 29/94] =?UTF-8?q?fix:=20ACP=20both=20live=20and=20load=20s?= =?UTF-8?q?hare=20synthetic=20pending=20status=20preceeding=E2=80=A6=20(#1?= =?UTF-8?q?4916)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/opencode/src/acp/agent.ts | 60 ++++++++----------- .../test/acp/event-subscription.test.ts | 59 ++++++++++++++++++ 2 files changed, 84 insertions(+), 35 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 5db98bc7044f..8b338f1b5716 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -270,25 +270,7 @@ export namespace ACP { const sessionId = session.id if (part.type === "tool") { - if (!this.toolStarts.has(part.callID)) { - this.toolStarts.add(part.callID) - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call", - toolCallId: part.callID, - title: part.tool, - kind: toToolKind(part.tool), - status: "pending", - locations: [], - rawInput: {}, - }, - }) - .catch((error) => { - log.error("failed to send tool pending to ACP", { error }) - }) - } + await this.toolStart(sessionId, part) switch (part.state.status) { case "pending": @@ -829,25 +811,10 @@ export namespace ACP { for (const part of message.parts) { if (part.type === "tool") { + await this.toolStart(sessionId, part) switch (part.state.status) { case "pending": this.bashSnapshots.delete(part.callID) - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call", - toolCallId: part.callID, - title: part.tool, - kind: toToolKind(part.tool), - status: "pending", - locations: [], - rawInput: {}, - }, - }) - .catch((err) => { - log.error("failed to send tool pending to ACP", { error: err }) - }) break case "running": const output = this.bashOutput(part) @@ -880,6 +847,7 @@ export namespace ACP { }) break case "completed": + this.toolStarts.delete(part.callID) this.bashSnapshots.delete(part.callID) const kind = toToolKind(part.tool) const content: ToolCallContent[] = [ @@ -959,6 +927,7 @@ export namespace ACP { }) break case "error": + this.toolStarts.delete(part.callID) this.bashSnapshots.delete(part.callID) await this.connection .sessionUpdate({ @@ -1116,6 +1085,27 @@ export namespace ACP { return output } + private async toolStart(sessionId: string, part: ToolPart) { + if (this.toolStarts.has(part.callID)) return + this.toolStarts.add(part.callID) + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call", + toolCallId: part.callID, + title: part.tool, + kind: toToolKind(part.tool), + status: "pending", + locations: [], + rawInput: {}, + }, + }) + .catch((error) => { + log.error("failed to send tool pending to ACP", { error }) + }) + } + private async loadAvailableModes(directory: string): Promise { const agents = await this.config.sdk.app .agents( diff --git a/packages/opencode/test/acp/event-subscription.test.ts b/packages/opencode/test/acp/event-subscription.test.ts index d61240d715bc..1abf578281df 100644 --- a/packages/opencode/test/acp/event-subscription.test.ts +++ b/packages/opencode/test/acp/event-subscription.test.ts @@ -572,6 +572,65 @@ describe("acp.agent event subscription", () => { }) }) + test("does not emit duplicate synthetic pending after replayed running tool", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const { agent, controller, sessionUpdates, stop, sdk } = createFakeAgent() + const cwd = "/tmp/opencode-acp-test" + const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + const input = { command: "echo hi", description: "run command" } + + sdk.session.messages = async () => ({ + data: [ + { + info: { + role: "assistant", + sessionID: sessionId, + }, + parts: [ + { + type: "tool", + callID: "call_1", + tool: "bash", + state: { + status: "running", + input, + metadata: { output: "hi\n" }, + time: { start: Date.now() }, + }, + }, + ], + }, + ], + }) + + await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any) + controller.push( + toolEvent(sessionId, cwd, { + callID: "call_1", + tool: "bash", + status: "running", + input, + metadata: { output: "hi\nthere\n" }, + }), + ) + await new Promise((r) => setTimeout(r, 20)) + + const types = sessionUpdates + .filter((u) => u.sessionId === sessionId) + .map((u) => u.update) + .filter((u) => "toolCallId" in u && u.toolCallId === "call_1") + .map((u) => u.sessionUpdate) + .filter((u) => u === "tool_call" || u === "tool_call_update") + + expect(types).toEqual(["tool_call", "tool_call_update", "tool_call_update"]) + stop() + }, + }) + }) + test("clears bash snapshot marker on pending state", async () => { await using tmp = await tmpdir() await Instance.provide({ From 082f0cc12734ccc961797ab9a63dd88a2ce3eed5 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:03:15 +1000 Subject: [PATCH 30/94] fix(app): preserve native path separators in file path helpers (#14912) --- packages/app/src/context/file/path.test.ts | 4 ++-- packages/app/src/context/file/path.ts | 24 ++++++++++------------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/app/src/context/file/path.test.ts b/packages/app/src/context/file/path.test.ts index 7eb5e8b2a358..feef6d466ef4 100644 --- a/packages/app/src/context/file/path.test.ts +++ b/packages/app/src/context/file/path.test.ts @@ -15,10 +15,10 @@ describe("file path helpers", () => { test("normalizes Windows absolute paths with mixed separators", () => { const path = createPathHelpers(() => "C:\\repo") - expect(path.normalize("C:\\repo\\src\\app.ts")).toBe("src/app.ts") + expect(path.normalize("C:\\repo\\src\\app.ts")).toBe("src\\app.ts") expect(path.normalize("C:/repo/src/app.ts")).toBe("src/app.ts") expect(path.normalize("file://C:/repo/src/app.ts")).toBe("src/app.ts") - expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src/app.ts") + expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src\\app.ts") }) test("keeps query/hash stripping behavior stable", () => { diff --git a/packages/app/src/context/file/path.ts b/packages/app/src/context/file/path.ts index 72c058aec6b7..53f072b6cb26 100644 --- a/packages/app/src/context/file/path.ts +++ b/packages/app/src/context/file/path.ts @@ -103,32 +103,30 @@ export function encodeFilePath(filepath: string): string { export function createPathHelpers(scope: () => string) { const normalize = (input: string) => { - const root = scope().replace(/\\/g, "/") + const root = scope() - let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input)))).replace(/\\/g, "/") + let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input)))) - // Remove initial root prefix, if it's a complete match or followed by / - // (don't want /foo/bar to root of /f). - // For Windows paths, also check for case-insensitive match. - const windows = /^[A-Za-z]:/.test(root) - const canonRoot = windows ? root.toLowerCase() : root - const canonPath = windows ? path.toLowerCase() : path + // Separator-agnostic prefix stripping for Cygwin/native Windows compatibility + // Only case-insensitive on Windows (drive letter or UNC paths) + const windows = /^[A-Za-z]:/.test(root) || root.startsWith("\\\\") + const canonRoot = windows ? root.replace(/\\/g, "/").toLowerCase() : root.replace(/\\/g, "/") + const canonPath = windows ? path.replace(/\\/g, "/").toLowerCase() : path.replace(/\\/g, "/") if ( canonPath.startsWith(canonRoot) && - (canonRoot.endsWith("/") || canonPath === canonRoot || canonPath.startsWith(canonRoot + "/")) + (canonRoot.endsWith("/") || canonPath === canonRoot || canonPath[canonRoot.length] === "/") ) { - // If we match canonRoot + "/", the slash will be removed below. + // Slice from original path to preserve native separators path = path.slice(root.length) } - if (path.startsWith("./")) { + if (path.startsWith("./") || path.startsWith(".\\")) { path = path.slice(2) } - if (path.startsWith("/")) { + if (path.startsWith("/") || path.startsWith("\\")) { path = path.slice(1) } - return path } From c92913e9627ae29b0df64e86bd158302c0578c63 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:21:02 -0600 Subject: [PATCH 31/94] chore: cleanup --- packages/ui/src/components/session-review.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index b9a2180cb8db..ec048d009bb4 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -10,6 +10,11 @@ display: none; } + .scroll-view__viewport { + display: flex; + flex-direction: column; + } + [data-slot="session-review-container"] { flex: 1 1 auto; padding-right: 4px; From 5190589632c97b570bb6f9035aa5c80c0fe833e7 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 24 Feb 2026 09:43:16 -0500 Subject: [PATCH 32/94] zen: remove alpha models from models endpoint --- packages/console/app/src/routes/zen/v1/models.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/console/app/src/routes/zen/v1/models.ts b/packages/console/app/src/routes/zen/v1/models.ts index f9c14ededdc0..d2592d20b070 100644 --- a/packages/console/app/src/routes/zen/v1/models.ts +++ b/packages/console/app/src/routes/zen/v1/models.ts @@ -25,6 +25,7 @@ export async function GET(input: APIEvent) { object: "list", data: Object.entries(zenData.models) .filter(([id]) => !disabledModels.includes(id)) + .filter(([id]) => !id.startsWith("alpha-")) .map(([id, _model]) => ({ id, object: "model", From cc02476ea5e02d3c827006dcd0c830f7673556e5 Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:48:59 +0100 Subject: [PATCH 33/94] refactor: replace error handling with serverErrorMessage utility and checks for if error is ConfigInvalidError (#14685) --- packages/app/src/context/global-sync.tsx | 9 +-- .../app/src/context/global-sync/bootstrap.ts | 8 ++- packages/app/src/utils/server-errors.test.ts | 69 +++++++++++++++++++ packages/app/src/utils/server-errors.ts | 32 +++++++++ 4 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 packages/app/src/utils/server-errors.test.ts create mode 100644 packages/app/src/utils/server-errors.ts diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 7e242130f157..9fbc93bde63e 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -36,6 +36,7 @@ import type { ProjectMeta } from "./global-sync/types" import { SESSION_RECENT_LIMIT } from "./global-sync/types" import { sanitizeProject } from "./global-sync/utils" import { usePlatform } from "./platform" +import { formatServerError } from "@/utils/server-errors" type GlobalStore = { ready: boolean @@ -51,11 +52,6 @@ type GlobalStore = { reload: undefined | "pending" | "complete" } -function errorMessage(error: unknown) { - if (error instanceof Error && error.message) return error.message - if (typeof error === "string" && error) return error - return "Unknown error" -} function createGlobalSync() { const globalSDK = useGlobalSDK() @@ -207,8 +203,9 @@ function createGlobalSync() { console.error("Failed to load sessions", err) const project = getFilename(directory) showToast({ + variant: "error", title: language.t("toast.session.listFailed.title", { project }), - description: errorMessage(err), + description: formatServerError(err), }) }) diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 6e7714828900..b35f1cd80145 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -16,6 +16,7 @@ import { batch } from "solid-js" import { reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { State, VcsCache } from "./types" import { cmp, normalizeProviderList } from "./utils" +import { formatServerError } from "@/utils/server-errors" type GlobalStore = { ready: boolean @@ -133,8 +134,11 @@ export async function bootstrapDirectory(input: { } catch (err) { console.error("Failed to bootstrap instance", err) const project = getFilename(input.directory) - const message = err instanceof Error ? err.message : String(err) - showToast({ title: `Failed to reload ${project}`, description: message }) + showToast({ + variant: "error", + title: `Failed to reload ${project}`, + description: formatServerError(err) + }) input.setStore("status", "partial") return } diff --git a/packages/app/src/utils/server-errors.test.ts b/packages/app/src/utils/server-errors.test.ts new file mode 100644 index 000000000000..1969d1afc271 --- /dev/null +++ b/packages/app/src/utils/server-errors.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, test } from "bun:test" +import type { ConfigInvalidError } from "./server-errors" +import { formatServerError, parseReabaleConfigInvalidError } from "./server-errors" + +describe("parseReabaleConfigInvalidError", () => { + test("formats issues with file path", () => { + const error = { + name: "ConfigInvalidError", + data: { + path: "opencode.config.ts", + issues: [ + { path: ["settings", "host"], message: "Required" }, + { path: ["mode"], message: "Invalid" }, + ], + }, + } satisfies ConfigInvalidError + + const result = parseReabaleConfigInvalidError(error) + + expect(result).toBe( + ["Invalid configuration", "opencode.config.ts", "settings.host: Required", "mode: Invalid"].join("\n"), + ) + }) + + test("uses trimmed message when issues are missing", () => { + const error = { + name: "ConfigInvalidError", + data: { + path: "config", + message: " Bad value ", + }, + } satisfies ConfigInvalidError + + const result = parseReabaleConfigInvalidError(error) + + expect(result).toBe(["Invalid configuration", "Bad value"].join("\n")) + }) +}) + +describe("formatServerError", () => { + test("formats config invalid errors", () => { + const error = { + name: "ConfigInvalidError", + data: { + message: "Missing host", + }, + } satisfies ConfigInvalidError + + const result = formatServerError(error) + + expect(result).toBe(["Invalid configuration", "Missing host"].join("\n")) + }) + + test("returns error messages", () => { + expect(formatServerError(new Error("Request failed with status 503"))).toBe("Request failed with status 503") + }) + + test("returns provided string errors", () => { + expect(formatServerError("Failed to connect to server")).toBe("Failed to connect to server") + }) + + test("falls back to unknown", () => { + expect(formatServerError(0)).toBe("Unknown error") + }) + + test("falls back for unknown error objects and names", () => { + expect(formatServerError({ name: "ServerTimeoutError", data: { seconds: 30 } })).toBe("Unknown error") + }) +}) diff --git a/packages/app/src/utils/server-errors.ts b/packages/app/src/utils/server-errors.ts new file mode 100644 index 000000000000..4b9727e61d8f --- /dev/null +++ b/packages/app/src/utils/server-errors.ts @@ -0,0 +1,32 @@ +export type ConfigInvalidError = { + name: "ConfigInvalidError" + data: { + path?: string + message?: string + issues?: Array<{ message: string; path: string[] }> + } +} + +export function formatServerError(error: unknown) { + if (isConfigInvalidErrorLike(error)) return parseReabaleConfigInvalidError(error) + if (error instanceof Error && error.message) return error.message + if (typeof error === "string" && error) return error + return "Unknown error" +} + +function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError { + if (typeof error !== "object" || error === null) return false + const o = error as Record + return o.name === "ConfigInvalidError" && typeof o.data === "object" && o.data !== null +} + +export function parseReabaleConfigInvalidError(errorInput: ConfigInvalidError) { + const head = "Invalid configuration" + const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : "" + const detail = errorInput.data.message?.trim() ?? "" + const issues = (errorInput.data.issues ?? []).map((issue) => { + return `${issue.path.join(".")}: ${issue.message}` + }) + if (issues.length) return [head, file, "", ...issues].filter(Boolean).join("\n") + return [head, file, detail].filter(Boolean).join("\n") +} From 0d0d0578ebcf1b097d97d57e817664322a0740cc Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 24 Feb 2026 14:49:52 +0000 Subject: [PATCH 34/94] chore: generate --- packages/app/src/context/global-sync.tsx | 1 - packages/app/src/context/global-sync/bootstrap.ts | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 9fbc93bde63e..f87c3fb394ed 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -52,7 +52,6 @@ type GlobalStore = { reload: undefined | "pending" | "complete" } - function createGlobalSync() { const globalSDK = useGlobalSDK() const platform = usePlatform() diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index b35f1cd80145..b2610656103d 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -134,10 +134,10 @@ export async function bootstrapDirectory(input: { } catch (err) { console.error("Failed to bootstrap instance", err) const project = getFilename(input.directory) - showToast({ - variant: "error", - title: `Failed to reload ${project}`, - description: formatServerError(err) + showToast({ + variant: "error", + title: `Failed to reload ${project}`, + description: formatServerError(err), }) input.setStore("status", "partial") return From c6d8e7624deb7470538c1156b73c6f33d2b9935c Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:55:17 +0100 Subject: [PATCH 35/94] fix(app): on cancel comment unhighlight lines (#14103) --- packages/app/src/pages/session/file-tabs.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index 032756cabd8d..4b30915d865c 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -371,6 +371,12 @@ export function FileTabContent(props: { tab: string }) { }) } + const cancelCommenting = () => { + const p = path() + if (p) file.setSelectedLines(p, null) + setNote("commenting", null) + } + createEffect( on( () => state()?.loaded, @@ -484,7 +490,7 @@ export function FileTabContent(props: { tab: string }) { value={note.draft} selection={formatCommentLabel(range())} onInput={(value) => setNote("draft", value)} - onCancel={() => setCommenting(null)} + onCancel={cancelCommenting} onSubmit={(value) => { const p = path() if (!p) return @@ -498,7 +504,7 @@ export function FileTabContent(props: { tab: string }) { setTimeout(() => { if (!document.activeElement || !current.contains(document.activeElement)) { - setCommenting(null) + cancelCommenting() } }, 0) }} From f8cfb697bd10a328afab4e6a074148c2e651fcb2 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 24 Feb 2026 09:56:05 -0500 Subject: [PATCH 36/94] zen: restrict alpha models to admin workspaces --- packages/console/app/src/routes/zen/util/handler.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 80a4b3ab77b4..1719625839ce 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -34,6 +34,7 @@ import { createDataDumper } from "./dataDumper" import { createTrialLimiter } from "./trialLimiter" import { createStickyTracker } from "./stickyProviderTracker" import { LiteData } from "@opencode-ai/console-core/lite.js" +import { Resource } from "@opencode-ai/console-resource" type ZenData = Awaited> type RetryOptions = { @@ -59,7 +60,7 @@ export async function handler( const MAX_FAILOVER_RETRIES = 3 const MAX_429_RETRIES = 3 - const FREE_WORKSPACES = [ + const ADMIN_WORKSPACES = [ "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank "wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench ] @@ -520,6 +521,13 @@ export async function handler( ) if (!data) throw new AuthError("Invalid API key.") + if ( + modelInfo.id.startsWith("alpha-") && + Resource.App.stage === "production" && + !ADMIN_WORKSPACES.includes(data.workspaceID) + ) + throw new AuthError(`Model ${modelInfo.id} not supported`) + logger.metric({ api_key: data.apiKey, workspace: data.workspaceID, @@ -546,7 +554,7 @@ export async function handler( black: data.black, lite: data.lite, provider: data.provider, - isFree: FREE_WORKSPACES.includes(data.workspaceID), + isFree: ADMIN_WORKSPACES.includes(data.workspaceID), isDisabled: !!data.timeDisabled, } } From 68cf011fd3432ffe5f38848c6ec747702077dfbe Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:48:22 -0600 Subject: [PATCH 37/94] fix(app): ignore stale part deltas --- packages/app/src/context/global-sdk.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index 8c0035d555b7..c1a87b95b890 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -49,9 +49,12 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo let queue: Queued[] = [] let buffer: Queued[] = [] const coalesced = new Map() + const staleDeltas = new Set() let timer: ReturnType | undefined let last = 0 + const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}` + const key = (directory: string, payload: Event) => { if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}` if (payload.type === "lsp.updated") return `lsp.updated:${directory}` @@ -68,14 +71,20 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo if (queue.length === 0) return const events = queue + const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined queue = buffer buffer = events queue.length = 0 coalesced.clear() + staleDeltas.clear() last = Date.now() batch(() => { for (const event of events) { + if (skip && event.payload.type === "message.part.delta") { + const props = event.payload.properties + if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue + } emitter.emit(event.directory, event.payload) } }) @@ -144,6 +153,10 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo const i = coalesced.get(k) if (i !== undefined) { queue[i] = { directory, payload } + if (payload.type === "message.part.updated") { + const part = payload.properties.part + staleDeltas.add(deltaKey(directory, part.messageID, part.id)) + } continue } coalesced.set(k, queue.length) From 2a87860c06b6aaa0e3d017b5cc464d83983efd1d Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 24 Feb 2026 14:49:05 -0500 Subject: [PATCH 38/94] zen: gpt 5.3 codex --- packages/web/src/content/docs/zen.mdx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 453093206b98..48c040cf2dff 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -64,6 +64,7 @@ You can also access our models through the following API endpoints. | Model | Model ID | Endpoint | AI SDK Package | | ------------------ | ------------------ | -------------------------------------------------- | --------------------------- | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -88,11 +89,9 @@ You can also access our models through the following API endpoints. | MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.1 | minimax-m2.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 Free | glm-5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 4.7 | glm-4.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 Free | kimi-k2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2 Thinking | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -124,11 +123,9 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | MiniMax M2.5 Free | Free | Free | Free | - | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - | -| GLM 5 Free | Free | Free | Free | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | GLM 4.7 | $0.60 | $2.20 | $0.10 | - | | GLM 4.6 | $0.60 | $2.20 | $0.10 | - | -| Kimi K2.5 Free | Free | Free | Free | - | | Kimi K2.5 | $0.60 | $3.00 | $0.08 | - | | Kimi K2 Thinking | $0.40 | $2.50 | - | - | | Kimi K2 | $0.40 | $2.50 | - | - | @@ -150,6 +147,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | +| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - | | GPT 5.2 | $1.75 | $14.00 | $0.175 | - | | GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - | | GPT 5.1 | $1.07 | $8.50 | $0.107 | - | @@ -168,8 +166,6 @@ Credit card fees are passed along at cost (4.4% + $0.30 per transaction); we don The free models: -- GLM 5 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. -- Kimi K2.5 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. - MiniMax M2.5 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. - Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. @@ -201,8 +197,6 @@ charging you more than $20 if your balance goes below $5. All our models are hosted in the US. Our providers follow a zero-retention policy and do not use your data for model training, with the following exceptions: - Big Pickle: During its free period, collected data may be used to improve the model. -- GLM 5 Free: During its free period, collected data may be used to improve the model. -- Kimi K2.5 Free: During its free period, collected data may be used to improve the model. - MiniMax M2.5 Free: During its free period, collected data may be used to improve the model. - OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). - Anthropic APIs: Requests are retained for 30 days in accordance with [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage). From 2c00eb60bdc6e6ff0362e792e731eaa39204bf72 Mon Sep 17 00:00:00 2001 From: James Long Date: Tue, 24 Feb 2026 17:34:34 -0500 Subject: [PATCH 39/94] feat(core): add workspace-serve command (experimental) (#14960) --- .../opencode/src/cli/cmd/workspace-serve.ts | 59 +++++++++++++++++++ packages/opencode/src/index.ts | 9 ++- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/src/cli/cmd/workspace-serve.ts diff --git a/packages/opencode/src/cli/cmd/workspace-serve.ts b/packages/opencode/src/cli/cmd/workspace-serve.ts new file mode 100644 index 000000000000..9b47defd392f --- /dev/null +++ b/packages/opencode/src/cli/cmd/workspace-serve.ts @@ -0,0 +1,59 @@ +import { cmd } from "./cmd" +import { withNetworkOptions, resolveNetworkOptions } from "../network" +import { Installation } from "../../installation" + +export const WorkspaceServeCommand = cmd({ + command: "workspace-serve", + builder: (yargs) => withNetworkOptions(yargs), + describe: "starts a remote workspace websocket server", + handler: async (args) => { + const opts = await resolveNetworkOptions(args) + const server = Bun.serve<{ id: string }>({ + hostname: opts.hostname, + port: opts.port, + fetch(req, server) { + const url = new URL(req.url) + if (url.pathname === "/ws") { + const id = Bun.randomUUIDv7() + if (server.upgrade(req, { data: { id } })) return + return new Response("Upgrade failed", { status: 400 }) + } + + if (url.pathname === "/health") { + return new Response("ok", { + status: 200, + headers: { + "content-type": "text/plain; charset=utf-8", + }, + }) + } + + return new Response( + JSON.stringify({ + service: "workspace-server", + ws: `ws://${server.hostname}:${server.port}/ws`, + }), + { + status: 200, + headers: { + "content-type": "application/json; charset=utf-8", + }, + }, + ) + }, + websocket: { + open(ws) { + ws.send(JSON.stringify({ type: "ready", id: ws.data.id })) + }, + message(ws, msg) { + const text = typeof msg === "string" ? msg : msg.toString() + ws.send(JSON.stringify({ type: "message", id: ws.data.id, text })) + }, + close() {}, + }, + }) + + console.log(`workspace websocket server listening on ws://${server.hostname}:${server.port}/ws`) + await new Promise(() => {}) + }, +}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 65515658862b..9af79278c061 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -13,6 +13,7 @@ import { Installation } from "./installation" import { NamedError } from "@opencode-ai/util/error" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" +import { WorkspaceServeCommand } from "./cli/cmd/workspace-serve" import { Filesystem } from "./util/filesystem" import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" @@ -45,7 +46,7 @@ process.on("uncaughtException", (e) => { }) }) -const cli = yargs(hideBin(process.argv)) +let cli = yargs(hideBin(process.argv)) .parserConfiguration({ "populate--": true }) .scriptName("opencode") .wrap(100) @@ -141,6 +142,12 @@ const cli = yargs(hideBin(process.argv)) .command(PrCommand) .command(SessionCommand) .command(DbCommand) + +if (Installation.isLocal()) { + cli = cli.command(WorkspaceServeCommand) +} + +cli = cli .fail((msg, err) => { if ( msg?.startsWith("Unknown argument") || From 29ddd55088af6f31c24f392c9f5dbf472918114f Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 24 Feb 2026 23:29:02 +0000 Subject: [PATCH 40/94] release: v1.2.11 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index d68a9228fe77..d81245ff7d4f 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -75,7 +75,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -109,7 +109,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -136,7 +136,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -160,7 +160,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -184,7 +184,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -217,7 +217,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -246,7 +246,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -262,7 +262,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.10", + "version": "1.2.11", "bin": { "opencode": "./bin/opencode", }, @@ -376,7 +376,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -396,7 +396,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.10", + "version": "1.2.11", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -407,7 +407,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -420,7 +420,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -462,7 +462,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "zod": "catalog:", }, @@ -473,7 +473,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index b9397b0f40de..360cbbcc01bb 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.10", + "version": "1.2.11", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 395feeb4af5c..a866785ce081 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.10", + "version": "1.2.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index aac79d669009..c06964f7d851 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.10", + "version": "1.2.11", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 386ee19df23f..351f78bddcb8 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.10", + "version": "1.2.11", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 7a08244bb629..f61e7f9ab41d 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.10", + "version": "1.2.11", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index dc25cb020373..fba0730b05e4 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.10", + "version": "1.2.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index fae66ab31a87..229f6b2552a0 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.10", + "version": "1.2.11", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index a112d793fd76..5e13ecdb6951 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.10" +version = "1.2.11" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index c67be670961b..26fc3c0410d0 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.10", + "version": "1.2.11", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index d19376adf38d..857e912c30e2 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.10", + "version": "1.2.11", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 623a117929f7..97da559e7557 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.10", + "version": "1.2.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index bd3627e35b4c..d2b3c6115901 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.10", + "version": "1.2.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index d000cb479943..678380474769 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.10", + "version": "1.2.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 3519996085d7..505d8bb8c533 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.10", + "version": "1.2.11", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 4bcbb0305d4e..fbb123591b31 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.10", + "version": "1.2.11", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 110c6ca2354f..0b71c07142a7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.10", + "version": "1.2.11", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 2e2807923eab..fffd9e149dd9 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.10", + "version": "1.2.11", "publisher": "sst-dev", "repository": { "type": "git", From 3af12c53c433d1f49abde0874dc02c2e6c018930 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:24:47 +1000 Subject: [PATCH 41/94] fix(opencode): import custom tools via file URL (#14971) --- packages/opencode/src/tool/registry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index ef0e78ffa863..cf3c2cad838e 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -28,6 +28,7 @@ import { Truncate } from "./truncation" import { PlanExitTool, PlanEnterTool } from "./plan" import { ApplyPatchTool } from "./apply_patch" import { Glob } from "../util/glob" +import { pathToFileURL } from "url" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) @@ -43,7 +44,7 @@ export namespace ToolRegistry { if (matches.length) await Config.waitForDependencies() for (const match of matches) { const namespace = path.basename(match, path.extname(match)) - const mod = await import(match) + const mod = await import(pathToFileURL(match).href) for (const [id, def] of Object.entries(mod)) { custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def)) } From e7182637784b7d558657da5b6aede92f0db1c11f Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:46:12 +1000 Subject: [PATCH 42/94] fix(project): await git id cache write (#14977) --- packages/opencode/src/project/project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index adbe2b9fb159..a75a0a02e78f 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -138,7 +138,7 @@ export namespace Project { id = roots[0] if (id) { - void Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined) + await Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined) } } From da40ab7b3d242208b5c759e55e548c13c658372a Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:38:23 +1000 Subject: [PATCH 43/94] fix(opencode): disable config bun cache in CI (#14985) --- packages/opencode/src/bun/index.ts | 2 +- packages/opencode/src/config/config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index 79aaae2bcc4a..35ad74ec4c34 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -93,7 +93,7 @@ export namespace BunProc { "--force", "--exact", // TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936) - ...(proxied() ? ["--no-cache"] : []), + ...(proxied() || process.env.CI ? ["--no-cache"] : []), "--cwd", Global.Path.cache, pkg + "@" + version, diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index b1e00fccb850..761ce23f3d6c 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -289,7 +289,7 @@ export namespace Config { [ "install", // TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936) - ...(proxied() ? ["--no-cache"] : []), + ...(proxied() || process.env.CI ? ["--no-cache"] : []), ], { cwd: dir }, ).catch((err) => { From 814c1d398cc4d8c3a6e321e8f96699d6f1dc10ae Mon Sep 17 00:00:00 2001 From: Dax Date: Tue, 24 Feb 2026 23:04:15 -0500 Subject: [PATCH 44/94] refactor: migrate Bun.spawn to Process utility with timeout and cleanup (#14448) --- packages/opencode/src/bun/index.ts | 21 ++---- packages/opencode/src/bun/registry.ts | 10 +-- packages/opencode/src/cli/cmd/auth.ts | 12 +++- packages/opencode/src/cli/cmd/session.ts | 9 ++- .../src/cli/cmd/tui/util/clipboard.ts | 13 ++-- .../opencode/src/cli/cmd/tui/util/editor.ts | 4 +- packages/opencode/src/file/ripgrep.ts | 57 +++++++-------- packages/opencode/src/format/formatter.ts | 10 +-- packages/opencode/src/format/index.ts | 17 +++-- packages/opencode/src/lsp/server.ts | 48 ++++++------- packages/opencode/src/tool/grep.ts | 14 ++-- packages/opencode/src/util/git.ts | 23 +++--- packages/opencode/src/util/process.ts | 71 +++++++++++++++++++ 13 files changed, 199 insertions(+), 110 deletions(-) create mode 100644 packages/opencode/src/util/process.ts diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index 35ad74ec4c34..e3bddcc22639 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -4,20 +4,21 @@ import { Log } from "../util/log" import path from "path" import { Filesystem } from "../util/filesystem" import { NamedError } from "@opencode-ai/util/error" -import { readableStreamToText } from "bun" +import { text } from "node:stream/consumers" import { Lock } from "../util/lock" import { PackageRegistry } from "./registry" import { proxied } from "@/util/proxied" +import { Process } from "../util/process" export namespace BunProc { const log = Log.create({ service: "bun" }) - export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject) { + export async function run(cmd: string[], options?: Process.Options) { log.info("running", { cmd: [which(), ...cmd], ...options, }) - const result = Bun.spawn([which(), ...cmd], { + const result = Process.spawn([which(), ...cmd], { ...options, stdout: "pipe", stderr: "pipe", @@ -28,23 +29,15 @@ export namespace BunProc { }, }) const code = await result.exited - const stdout = result.stdout - ? typeof result.stdout === "number" - ? result.stdout - : await readableStreamToText(result.stdout) - : undefined - const stderr = result.stderr - ? typeof result.stderr === "number" - ? result.stderr - : await readableStreamToText(result.stderr) - : undefined + const stdout = result.stdout ? await text(result.stdout) : undefined + const stderr = result.stderr ? await text(result.stderr) : undefined log.info("done", { code, stdout, stderr, }) if (code !== 0) { - throw new Error(`Command failed with exit code ${result.exitCode}`) + throw new Error(`Command failed with exit code ${code}`) } return result } diff --git a/packages/opencode/src/bun/registry.ts b/packages/opencode/src/bun/registry.ts index c567668acd71..a85a6c989c82 100644 --- a/packages/opencode/src/bun/registry.ts +++ b/packages/opencode/src/bun/registry.ts @@ -1,5 +1,7 @@ -import { readableStreamToText, semver } from "bun" +import { semver } from "bun" +import { text } from "node:stream/consumers" import { Log } from "../util/log" +import { Process } from "../util/process" export namespace PackageRegistry { const log = Log.create({ service: "bun" }) @@ -9,7 +11,7 @@ export namespace PackageRegistry { } export async function info(pkg: string, field: string, cwd?: string): Promise { - const result = Bun.spawn([which(), "info", pkg, field], { + const result = Process.spawn([which(), "info", pkg, field], { cwd, stdout: "pipe", stderr: "pipe", @@ -20,8 +22,8 @@ export namespace PackageRegistry { }) const code = await result.exited - const stdout = result.stdout ? await readableStreamToText(result.stdout) : "" - const stderr = result.stderr ? await readableStreamToText(result.stderr) : "" + const stdout = result.stdout ? await text(result.stdout) : "" + const stderr = result.stderr ? await text(result.stderr) : "" if (code !== 0) { log.warn("bun info failed", { pkg, field, code, stderr }) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index e050a0abf803..4a97a5e0b83c 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -11,6 +11,8 @@ import { Global } from "../../global" import { Plugin } from "../../plugin" import { Instance } from "../../project/instance" import type { Hooks } from "@opencode-ai/plugin" +import { Process } from "../../util/process" +import { text } from "node:stream/consumers" type PluginAuth = NonNullable @@ -263,8 +265,7 @@ export const AuthLoginCommand = cmd({ if (args.url) { const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any) prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``) - const proc = Bun.spawn({ - cmd: wellknown.auth.command, + const proc = Process.spawn(wellknown.auth.command, { stdout: "pipe", }) const exit = await proc.exited @@ -273,7 +274,12 @@ export const AuthLoginCommand = cmd({ prompts.outro("Done") return } - const token = await new Response(proc.stdout).text() + if (!proc.stdout) { + prompts.log.error("Failed") + prompts.outro("Done") + return + } + const token = await text(proc.stdout) await Auth.set(args.url, { type: "wellknown", key: wellknown.auth.env, diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 4aa702359d17..7fb5fda97b9b 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -6,6 +6,7 @@ import { UI } from "../ui" import { Locale } from "../../util/locale" import { Flag } from "../../flag/flag" import { Filesystem } from "../../util/filesystem" +import { Process } from "../../util/process" import { EOL } from "os" import path from "path" @@ -102,13 +103,17 @@ export const SessionListCommand = cmd({ const shouldPaginate = process.stdout.isTTY && !args.maxCount && args.format === "table" if (shouldPaginate) { - const proc = Bun.spawn({ - cmd: pagerCmd(), + const proc = Process.spawn(pagerCmd(), { stdin: "pipe", stdout: "inherit", stderr: "inherit", }) + if (!proc.stdin) { + console.log(output) + return + } + proc.stdin.write(output) proc.stdin.end() await proc.exited diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 7d1aad3a86e8..1a8197bf4e81 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -5,6 +5,7 @@ import { lazy } from "../../../../util/lazy.js" import { tmpdir } from "os" import path from "path" import { Filesystem } from "../../../../util/filesystem" +import { Process } from "../../../../util/process" /** * Writes text to clipboard via OSC 52 escape sequence. @@ -87,7 +88,8 @@ export namespace Clipboard { if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) { console.log("clipboard: using wl-copy") return async (text: string) => { - const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" }) + const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" }) + if (!proc.stdin) return proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) @@ -96,11 +98,12 @@ export namespace Clipboard { if (Bun.which("xclip")) { console.log("clipboard: using xclip") return async (text: string) => { - const proc = Bun.spawn(["xclip", "-selection", "clipboard"], { + const proc = Process.spawn(["xclip", "-selection", "clipboard"], { stdin: "pipe", stdout: "ignore", stderr: "ignore", }) + if (!proc.stdin) return proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) @@ -109,11 +112,12 @@ export namespace Clipboard { if (Bun.which("xsel")) { console.log("clipboard: using xsel") return async (text: string) => { - const proc = Bun.spawn(["xsel", "--clipboard", "--input"], { + const proc = Process.spawn(["xsel", "--clipboard", "--input"], { stdin: "pipe", stdout: "ignore", stderr: "ignore", }) + if (!proc.stdin) return proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) @@ -125,7 +129,7 @@ export namespace Clipboard { console.log("clipboard: using powershell") return async (text: string) => { // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.) - const proc = Bun.spawn( + const proc = Process.spawn( [ "powershell.exe", "-NonInteractive", @@ -140,6 +144,7 @@ export namespace Clipboard { }, ) + if (!proc.stdin) return proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) diff --git a/packages/opencode/src/cli/cmd/tui/util/editor.ts b/packages/opencode/src/cli/cmd/tui/util/editor.ts index cb7c691bbde9..6d32c63c0010 100644 --- a/packages/opencode/src/cli/cmd/tui/util/editor.ts +++ b/packages/opencode/src/cli/cmd/tui/util/editor.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os" import { join } from "node:path" import { CliRenderer } from "@opentui/core" import { Filesystem } from "@/util/filesystem" +import { Process } from "@/util/process" export namespace Editor { export async function open(opts: { value: string; renderer: CliRenderer }): Promise { @@ -17,8 +18,7 @@ export namespace Editor { opts.renderer.suspend() opts.renderer.currentRenderBuffer.clear() const parts = editor.split(" ") - const proc = Bun.spawn({ - cmd: [...parts, filepath], + const proc = Process.spawn([...parts, filepath], { stdin: "inherit", stdout: "inherit", stderr: "inherit", diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index ca1eadae8e00..9c4e9cf02846 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -7,6 +7,8 @@ import { NamedError } from "@opencode-ai/util/error" import { lazy } from "../util/lazy" import { $ } from "bun" import { Filesystem } from "../util/filesystem" +import { Process } from "../util/process" +import { text } from "node:stream/consumers" import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js" import { Log } from "@/util/log" @@ -153,17 +155,19 @@ export namespace Ripgrep { if (platformKey.endsWith("-darwin")) args.push("--include=*/rg") if (platformKey.endsWith("-linux")) args.push("--wildcards", "*/rg") - const proc = Bun.spawn(args, { + const proc = Process.spawn(args, { cwd: Global.Path.bin, stderr: "pipe", stdout: "pipe", }) - await proc.exited - if (proc.exitCode !== 0) + const exit = await proc.exited + if (exit !== 0) { + const stderr = proc.stderr ? await text(proc.stderr) : "" throw new ExtractionFailedError({ filepath, - stderr: await Bun.readableStreamToText(proc.stderr), + stderr, }) + } } if (config.extension === "zip") { const zipFileReader = new ZipReader(new BlobReader(new Blob([arrayBuffer]))) @@ -227,8 +231,7 @@ export namespace Ripgrep { } } - // Bun.spawn should throw this, but it incorrectly reports that the executable does not exist. - // See https://github.com/oven-sh/bun/issues/24012 + // Guard against invalid cwd to provide a consistent ENOENT error. if (!(await fs.stat(input.cwd).catch(() => undefined))?.isDirectory()) { throw Object.assign(new Error(`No such file or directory: '${input.cwd}'`), { code: "ENOENT", @@ -237,41 +240,35 @@ export namespace Ripgrep { }) } - const proc = Bun.spawn(args, { + const proc = Process.spawn(args, { cwd: input.cwd, stdout: "pipe", stderr: "ignore", - maxBuffer: 1024 * 1024 * 20, - signal: input.signal, + abort: input.signal, }) - const reader = proc.stdout.getReader() - const decoder = new TextDecoder() - let buffer = "" - - try { - while (true) { - input.signal?.throwIfAborted() + if (!proc.stdout) { + throw new Error("Process output not available") + } - const { done, value } = await reader.read() - if (done) break + let buffer = "" + const stream = proc.stdout as AsyncIterable + for await (const chunk of stream) { + input.signal?.throwIfAborted() - buffer += decoder.decode(value, { stream: true }) - // Handle both Unix (\n) and Windows (\r\n) line endings - const lines = buffer.split(/\r?\n/) - buffer = lines.pop() || "" + buffer += typeof chunk === "string" ? chunk : chunk.toString() + // Handle both Unix (\n) and Windows (\r\n) line endings + const lines = buffer.split(/\r?\n/) + buffer = lines.pop() || "" - for (const line of lines) { - if (line) yield line - } + for (const line of lines) { + if (line) yield line } - - if (buffer) yield buffer - } finally { - reader.releaseLock() - await proc.exited } + if (buffer) yield buffer + await proc.exited + input.signal?.throwIfAborted() } diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 47b2d6a12d21..19b9e2cbe971 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -1,7 +1,8 @@ -import { readableStreamToText } from "bun" +import { text } from "node:stream/consumers" import { BunProc } from "../bun" import { Instance } from "../project/instance" import { Filesystem } from "../util/filesystem" +import { Process } from "../util/process" import { Flag } from "@/flag/flag" export interface Info { @@ -213,12 +214,13 @@ export const rlang: Info = { if (airPath == null) return false try { - const proc = Bun.spawn(["air", "--help"], { + const proc = Process.spawn(["air", "--help"], { stdout: "pipe", stderr: "pipe", }) await proc.exited - const output = await readableStreamToText(proc.stdout) + if (!proc.stdout) return false + const output = await text(proc.stdout) // Check for "Air: An R language server and formatter" const firstLine = output.split("\n")[0] @@ -238,7 +240,7 @@ export const uvformat: Info = { async enabled() { if (await ruff.enabled()) return false if (Bun.which("uv") !== null) { - const proc = Bun.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" }) + const proc = Process.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" }) const code = await proc.exited return code === 0 } diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index bab758030b9f..b849f778ecef 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -8,6 +8,7 @@ import * as Formatter from "./formatter" import { Config } from "../config/config" import { mergeDeep } from "remeda" import { Instance } from "../project/instance" +import { Process } from "../util/process" export namespace Format { const log = Log.create({ service: "format" }) @@ -110,13 +111,15 @@ export namespace Format { for (const item of await getFormatter(ext)) { log.info("running", { command: item.command }) try { - const proc = Bun.spawn({ - cmd: item.command.map((x) => x.replace("$FILE", file)), - cwd: Instance.directory, - env: { ...process.env, ...item.environment }, - stdout: "ignore", - stderr: "ignore", - }) + const proc = Process.spawn( + item.command.map((x) => x.replace("$FILE", file)), + { + cwd: Instance.directory, + env: { ...process.env, ...item.environment }, + stdout: "ignore", + stderr: "ignore", + }, + ) const exit = await proc.exited if (exit !== 0) log.error("failed", { diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index a4ebeb5a2567..afd297a5ed60 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -4,12 +4,14 @@ import os from "os" import { Global } from "../global" import { Log } from "../util/log" import { BunProc } from "../bun" -import { $, readableStreamToText } from "bun" +import { $ } from "bun" +import { text } from "node:stream/consumers" import fs from "fs/promises" import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" import { Flag } from "../flag/flag" import { Archive } from "../util/archive" +import { Process } from "../util/process" export namespace LSPServer { const log = Log.create({ service: "lsp.server" }) @@ -133,7 +135,7 @@ export namespace LSPServer { ) if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], { + await Process.spawn([BunProc.which(), "install", "@vue/language-server"], { cwd: Global.Path.bin, env: { ...process.env, @@ -263,14 +265,16 @@ export namespace LSPServer { } if (lintBin) { - const proc = Bun.spawn([lintBin, "--help"], { stdout: "pipe" }) + const proc = Process.spawn([lintBin, "--help"], { stdout: "pipe" }) await proc.exited - const help = await readableStreamToText(proc.stdout) - if (help.includes("--lsp")) { - return { - process: spawn(lintBin, ["--lsp"], { - cwd: root, - }), + if (proc.stdout) { + const help = await text(proc.stdout) + if (help.includes("--lsp")) { + return { + process: spawn(lintBin, ["--lsp"], { + cwd: root, + }), + } } } } @@ -372,8 +376,7 @@ export namespace LSPServer { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return log.info("installing gopls") - const proc = Bun.spawn({ - cmd: ["go", "install", "golang.org/x/tools/gopls@latest"], + const proc = Process.spawn(["go", "install", "golang.org/x/tools/gopls@latest"], { env: { ...process.env, GOBIN: Global.Path.bin }, stdout: "pipe", stderr: "pipe", @@ -414,8 +417,7 @@ export namespace LSPServer { } if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return log.info("installing rubocop") - const proc = Bun.spawn({ - cmd: ["gem", "install", "rubocop", "--bindir", Global.Path.bin], + const proc = Process.spawn(["gem", "install", "rubocop", "--bindir", Global.Path.bin], { stdout: "pipe", stderr: "pipe", stdin: "pipe", @@ -513,7 +515,7 @@ export namespace LSPServer { const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js") if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - await Bun.spawn([BunProc.which(), "install", "pyright"], { + await Process.spawn([BunProc.which(), "install", "pyright"], { cwd: Global.Path.bin, env: { ...process.env, @@ -746,8 +748,7 @@ export namespace LSPServer { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return log.info("installing csharp-ls via dotnet tool") - const proc = Bun.spawn({ - cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin], + const proc = Process.spawn(["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin], { stdout: "pipe", stderr: "pipe", stdin: "pipe", @@ -786,8 +787,7 @@ export namespace LSPServer { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return log.info("installing fsautocomplete via dotnet tool") - const proc = Bun.spawn({ - cmd: ["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin], + const proc = Process.spawn(["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin], { stdout: "pipe", stderr: "pipe", stdin: "pipe", @@ -1047,7 +1047,7 @@ export namespace LSPServer { const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js") if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], { + await Process.spawn([BunProc.which(), "install", "svelte-language-server"], { cwd: Global.Path.bin, env: { ...process.env, @@ -1094,7 +1094,7 @@ export namespace LSPServer { const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js") if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], { + await Process.spawn([BunProc.which(), "install", "@astrojs/language-server"], { cwd: Global.Path.bin, env: { ...process.env, @@ -1339,7 +1339,7 @@ export namespace LSPServer { const exists = await Filesystem.exists(js) if (!exists) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], { + await Process.spawn([BunProc.which(), "install", "yaml-language-server"], { cwd: Global.Path.bin, env: { ...process.env, @@ -1518,7 +1518,7 @@ export namespace LSPServer { const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js") if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - await Bun.spawn([BunProc.which(), "install", "intelephense"], { + await Process.spawn([BunProc.which(), "install", "intelephense"], { cwd: Global.Path.bin, env: { ...process.env, @@ -1615,7 +1615,7 @@ export namespace LSPServer { const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js") if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - await Bun.spawn([BunProc.which(), "install", "bash-language-server"], { + await Process.spawn([BunProc.which(), "install", "bash-language-server"], { cwd: Global.Path.bin, env: { ...process.env, @@ -1827,7 +1827,7 @@ export namespace LSPServer { const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js") if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], { + await Process.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], { cwd: Global.Path.bin, env: { ...process.env, diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 00497d4e3fd8..82e7ac1667e1 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -1,7 +1,9 @@ import z from "zod" +import { text } from "node:stream/consumers" import { Tool } from "./tool" import { Filesystem } from "../util/filesystem" import { Ripgrep } from "../file/ripgrep" +import { Process } from "../util/process" import DESCRIPTION from "./grep.txt" import { Instance } from "../project/instance" @@ -44,14 +46,18 @@ export const GrepTool = Tool.define("grep", { } args.push(searchPath) - const proc = Bun.spawn([rgPath, ...args], { + const proc = Process.spawn([rgPath, ...args], { stdout: "pipe", stderr: "pipe", - signal: ctx.abort, + abort: ctx.abort, }) - const output = await new Response(proc.stdout).text() - const errorOutput = await new Response(proc.stderr).text() + if (!proc.stdout || !proc.stderr) { + throw new Error("Process output not available") + } + + const output = await text(proc.stdout) + const errorOutput = await text(proc.stderr) const exitCode = await proc.exited // Exit codes: 0 = matches found, 1 = no matches, 2 = errors (but may still have matches) diff --git a/packages/opencode/src/util/git.ts b/packages/opencode/src/util/git.ts index 201def36a8c6..8e1427c99d54 100644 --- a/packages/opencode/src/util/git.ts +++ b/packages/opencode/src/util/git.ts @@ -1,5 +1,7 @@ import { $ } from "bun" +import { buffer } from "node:stream/consumers" import { Flag } from "../flag/flag" +import { Process } from "./process" export interface GitResult { exitCode: number @@ -14,12 +16,12 @@ export interface GitResult { * Uses Bun's lightweight `$` shell by default. When the process is running * as an ACP client, child processes inherit the parent's stdin pipe which * carries protocol data – on Windows this causes git to deadlock. In that - * case we fall back to `Bun.spawn` with `stdin: "ignore"`. + * case we fall back to `Process.spawn` with `stdin: "ignore"`. */ export async function git(args: string[], opts: { cwd: string; env?: Record }): Promise { if (Flag.OPENCODE_CLIENT === "acp") { try { - const proc = Bun.spawn(["git", ...args], { + const proc = Process.spawn(["git", ...args], { stdin: "ignore", stdout: "pipe", stderr: "pipe", @@ -27,18 +29,15 @@ export async function git(args: string[], opts: { cwd: string; env?: Record stdoutBuf.toString(), - stdout: stdoutBuf, - stderr: stderrBuf, + text: () => out.toString(), + stdout: out, + stderr: err, } } catch (error) { const stderr = Buffer.from(error instanceof Error ? error.message : String(error)) diff --git a/packages/opencode/src/util/process.ts b/packages/opencode/src/util/process.ts new file mode 100644 index 000000000000..09c55661fddd --- /dev/null +++ b/packages/opencode/src/util/process.ts @@ -0,0 +1,71 @@ +import { spawn as launch, type ChildProcess } from "child_process" + +export namespace Process { + export type Stdio = "inherit" | "pipe" | "ignore" + + export interface Options { + cwd?: string + env?: NodeJS.ProcessEnv | null + stdin?: Stdio + stdout?: Stdio + stderr?: Stdio + abort?: AbortSignal + kill?: NodeJS.Signals | number + timeout?: number + } + + export type Child = ChildProcess & { exited: Promise } + + export function spawn(cmd: string[], options: Options = {}): Child { + if (cmd.length === 0) throw new Error("Command is required") + options.abort?.throwIfAborted() + + const proc = launch(cmd[0], cmd.slice(1), { + cwd: options.cwd, + env: options.env === null ? {} : options.env ? { ...process.env, ...options.env } : undefined, + stdio: [options.stdin ?? "ignore", options.stdout ?? "ignore", options.stderr ?? "ignore"], + }) + + let aborted = false + let timer: ReturnType | undefined + + const abort = () => { + if (aborted) return + if (proc.exitCode !== null || proc.signalCode !== null) return + aborted = true + + proc.kill(options.kill ?? "SIGTERM") + + const timeout = options.timeout ?? 5_000 + if (timeout <= 0) return + + timer = setTimeout(() => { + proc.kill("SIGKILL") + }, timeout) + } + + const exited = new Promise((resolve, reject) => { + const done = () => { + options.abort?.removeEventListener("abort", abort) + if (timer) clearTimeout(timer) + } + proc.once("exit", (exitCode, signal) => { + done() + resolve(exitCode ?? (signal ? 1 : 0)) + }) + proc.once("error", (error) => { + done() + reject(error) + }) + }) + + if (options.abort) { + options.abort.addEventListener("abort", abort, { once: true }) + if (options.abort.aborted) abort() + } + + const child = proc as Child + child.exited = exited + return child + } +} From fa559b0385374222b933ba8f86dc3cd92f53c0de Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 24 Feb 2026 19:16:17 -0500 Subject: [PATCH 45/94] core: temporarily disable plan enter tool to prevent unintended mode switches during task execution --- packages/opencode/src/tool/plan.ts | 3 ++- packages/opencode/src/tool/registry.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts index 6cb7a691c88b..ff84dccec447 100644 --- a/packages/opencode/src/tool/plan.ts +++ b/packages/opencode/src/tool/plan.ts @@ -8,7 +8,6 @@ import { Identifier } from "../id/id" import { Provider } from "../provider/provider" import { Instance } from "../project/instance" import EXIT_DESCRIPTION from "./plan-exit.txt" -import ENTER_DESCRIPTION from "./plan-enter.txt" async function getLastModel(sessionID: string) { for await (const item of MessageV2.stream(sessionID)) { @@ -72,6 +71,7 @@ export const PlanExitTool = Tool.define("plan_exit", { }, }) +/* export const PlanEnterTool = Tool.define("plan_enter", { description: ENTER_DESCRIPTION, parameters: z.object({}), @@ -128,3 +128,4 @@ export const PlanEnterTool = Tool.define("plan_enter", { } }, }) +*/ diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index cf3c2cad838e..c6d7fbc1e4b2 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -1,3 +1,4 @@ +import { PlanExitTool } from "./plan" import { QuestionTool } from "./question" import { BashTool } from "./bash" import { EditTool } from "./edit" @@ -25,7 +26,7 @@ import { Flag } from "@/flag/flag" import { Log } from "@/util/log" import { LspTool } from "./lsp" import { Truncate } from "./truncation" -import { PlanExitTool, PlanEnterTool } from "./plan" + import { ApplyPatchTool } from "./apply_patch" import { Glob } from "../util/glob" import { pathToFileURL } from "url" @@ -118,7 +119,7 @@ export namespace ToolRegistry { ApplyPatchTool, ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), ...(config.experimental?.batch_tool === true ? [BatchTool] : []), - ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []), + ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool] : []), ...custom, ] } From 637059a515a6afd983a8a615f90650d997a821ce Mon Sep 17 00:00:00 2001 From: Dax Date: Tue, 24 Feb 2026 23:15:11 -0500 Subject: [PATCH 46/94] feat: show LSP errors for apply_patch tool (#14715) --- .../src/cli/cmd/tui/routes/session/index.tsx | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index f5a7f6f6ca49..365eb3314726 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1762,11 +1762,6 @@ function Write(props: ToolProps) { return props.input.content }) - const diagnostics = createMemo(() => { - const filePath = Filesystem.normalizePath(props.input.filePath ?? "") - return props.metadata.diagnostics?.[filePath] ?? [] - }) - return ( @@ -1780,15 +1775,7 @@ function Write(props: ToolProps) { content={code()} /> - - - {(diagnostic) => ( - - Error [{diagnostic.range.start.line}:{diagnostic.range.start.character}]: {diagnostic.message} - - )} - - + @@ -1972,12 +1959,6 @@ function Edit(props: ToolProps) { const diffContent = createMemo(() => props.metadata.diff) - const diagnostics = createMemo(() => { - const filePath = Filesystem.normalizePath(props.input.filePath ?? "") - const arr = props.metadata.diagnostics?.[filePath] ?? [] - return arr.filter((x) => x.severity === 1).slice(0, 3) - }) - return ( @@ -2003,18 +1984,7 @@ function Edit(props: ToolProps) { removedLineNumberBg={theme.diffRemovedLineNumberBg} /> - - - - {(diagnostic) => ( - - Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]{" "} - {diagnostic.message} - - )} - - - + @@ -2086,6 +2056,7 @@ function ApplyPatch(props: ToolProps) { } > + )} @@ -2163,6 +2134,29 @@ function Skill(props: ToolProps) { ) } +function Diagnostics(props: { diagnostics?: Record[]>; filePath: string }) { + const { theme } = useTheme() + const errors = createMemo(() => { + const normalized = Filesystem.normalizePath(props.filePath) + const arr = props.diagnostics?.[normalized] ?? [] + return arr.filter((x) => x.severity === 1).slice(0, 3) + }) + + return ( + + + + {(diagnostic) => ( + + Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}] {diagnostic.message} + + )} + + + + ) +} + function normalizePath(input?: string) { if (!input) return "" if (path.isAbsolute(input)) { From a487f11a30981f44896ac771f7ade87fba9d6092 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 24 Feb 2026 23:17:31 -0500 Subject: [PATCH 47/94] ci: auto-resolve merge conflicts in beta sync using opencode When merging PRs into the beta branch, the sync script now attempts to automatically resolve merge conflicts using opencode before failing. This reduces manual intervention needed for beta releases when multiple PRs have overlapping changes. --- .github/workflows/beta.yml | 4 ++ script/beta.ts | 75 +++++++++++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 20d2bc18d825..a7106667b116 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -27,7 +27,11 @@ jobs: opencode-app-id: ${{ vars.OPENCODE_APP_ID }} opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + - name: Install OpenCode + run: bun i -g opencode-ai + - name: Sync beta branch env: GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} run: bun script/beta.ts diff --git a/script/beta.ts b/script/beta.ts index a5fb027e6330..fbb1214093d6 100755 --- a/script/beta.ts +++ b/script/beta.ts @@ -30,6 +30,52 @@ Please resolve this issue to include this PR in the next beta release.` } } +async function conflicts() { + const out = await $`git diff --name-only --diff-filter=U`.text().catch(() => "") + return out + .split("\n") + .map((x) => x.trim()) + .filter(Boolean) +} + +async function cleanup() { + try { + await $`git merge --abort` + } catch {} + try { + await $`git checkout -- .` + } catch {} + try { + await $`git clean -fd` + } catch {} +} + +async function fix(pr: PR, files: string[]) { + console.log(` Trying to auto-resolve ${files.length} conflict(s) with opencode...`) + const prompt = [ + `Resolve the current git merge conflicts while merging PR #${pr.number} into the beta branch.`, + `Only touch these files: ${files.join(", ")}.`, + "Keep the merge in progress, do not abort the merge, and do not create a commit.", + "When done, leave the working tree with no unmerged files.", + ].join("\n") + + try { + await $`opencode run ${prompt}` + } catch (err) { + console.log(` opencode failed: ${err}`) + return false + } + + const left = await conflicts() + if (left.length > 0) { + console.log(` Conflicts remain: ${left.join(", ")}`) + return false + } + + console.log(" Conflicts resolved with opencode") + return true +} + async function main() { console.log("Fetching open PRs with beta label...") @@ -69,19 +115,22 @@ async function main() { try { await $`git merge --no-commit --no-ff pr/${pr.number}` } catch { - console.log(" Failed to merge (conflicts)") - try { - await $`git merge --abort` - } catch {} - try { - await $`git checkout -- .` - } catch {} - try { - await $`git clean -fd` - } catch {} - failed.push({ number: pr.number, title: pr.title, reason: "Merge conflicts" }) - await commentOnPR(pr.number, "Merge conflicts with dev branch") - continue + const files = await conflicts() + if (files.length > 0) { + console.log(" Failed to merge (conflicts)") + if (!(await fix(pr, files))) { + await cleanup() + failed.push({ number: pr.number, title: pr.title, reason: "Merge conflicts" }) + await commentOnPR(pr.number, "Merge conflicts with dev branch") + continue + } + } else { + console.log(" Failed to merge") + await cleanup() + failed.push({ number: pr.number, title: pr.title, reason: "Merge failed" }) + await commentOnPR(pr.number, "Merge failed") + continue + } } try { From 0b3fb5d46002745b37eb448115014cb814d49921 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 24 Feb 2026 23:22:56 -0500 Subject: [PATCH 48/94] ci: specify opencode/kimi-k2.5 model in beta script to ensure consistent PR processing --- script/beta.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/beta.ts b/script/beta.ts index fbb1214093d6..e931c9c0df41 100755 --- a/script/beta.ts +++ b/script/beta.ts @@ -60,7 +60,7 @@ async function fix(pr: PR, files: string[]) { ].join("\n") try { - await $`opencode run ${prompt}` + await $`opencode run -m opencode/kimi-k2.5 ${prompt}` } catch (err) { console.log(` opencode failed: ${err}`) return false From 6af7ddf03bd16b1e1d1cd7250e6b60aca87d437b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 24 Feb 2026 23:26:03 -0500 Subject: [PATCH 49/94] ci: switch beta script to gpt-5.3-codex for improved code generation quality --- script/beta.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/beta.ts b/script/beta.ts index e931c9c0df41..b0e6c2dcc15f 100755 --- a/script/beta.ts +++ b/script/beta.ts @@ -60,7 +60,7 @@ async function fix(pr: PR, files: string[]) { ].join("\n") try { - await $`opencode run -m opencode/kimi-k2.5 ${prompt}` + await $`opencode run -m opencode/gpt-5.3-codex ${prompt}` } catch (err) { console.log(` opencode failed: ${err}`) return false From 76b60f3779b2e4d54fa4036759b7064c4649c9ca Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 25 Feb 2026 12:28:48 +0800 Subject: [PATCH 50/94] desktop: make readme more accurate --- packages/desktop/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/desktop/README.md b/packages/desktop/README.md index ebaf48822313..358b7d24d511 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -2,6 +2,10 @@ Native OpenCode desktop app, built with Tauri v2. +## Prerequisites + +Building the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions. + ## Development From the repo root: @@ -11,22 +15,18 @@ bun install bun run --cwd packages/desktop tauri dev ``` -This starts the Vite dev server on http://localhost:1420 and opens the native window. - -If you only want the web dev server (no native shell): +## Build ```bash -bun run --cwd packages/desktop dev +bun run --cwd packages/desktop tauri build ``` -## Build +## Troubleshooting + +### Rust compiler not found -To create a production `dist/` and build the native app bundle: +If you see errors about Rust not being found, install it via [rustup](https://rustup.rs/): ```bash -bun run --cwd packages/desktop tauri build +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` - -## Prerequisites - -Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions. From 6fc5506293eba8f7fc8c2c751fa5d9309d6eaea8 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 25 Feb 2026 00:31:46 -0500 Subject: [PATCH 51/94] zen: go --- packages/console/app/src/i18n/ar.ts | 17 ++++++++++------- packages/console/app/src/i18n/br.ts | 17 ++++++++++------- packages/console/app/src/i18n/da.ts | 17 ++++++++++------- packages/console/app/src/i18n/de.ts | 17 ++++++++++------- packages/console/app/src/i18n/en.ts | 17 ++++++++++------- packages/console/app/src/i18n/es.ts | 17 ++++++++++------- packages/console/app/src/i18n/fr.ts | 17 ++++++++++------- packages/console/app/src/i18n/it.ts | 17 ++++++++++------- packages/console/app/src/i18n/ja.ts | 17 ++++++++++------- packages/console/app/src/i18n/ko.ts | 17 ++++++++++------- packages/console/app/src/i18n/no.ts | 17 ++++++++++------- packages/console/app/src/i18n/pl.ts | 17 ++++++++++------- packages/console/app/src/i18n/ru.ts | 17 ++++++++++------- packages/console/app/src/i18n/th.ts | 17 ++++++++++------- packages/console/app/src/i18n/tr.ts | 17 ++++++++++------- packages/console/app/src/i18n/zh.ts | 17 ++++++++++------- packages/console/app/src/i18n/zht.ts | 17 ++++++++++------- .../src/routes/workspace/[id]/billing/index.tsx | 2 +- .../[id]/billing/lite-section.module.css | 16 +++++++++++++++- .../workspace/[id]/billing/lite-section.tsx | 7 +++++++ 20 files changed, 193 insertions(+), 121 deletions(-) diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 36c86ef101e1..79f1d34971a5 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -491,21 +491,24 @@ export const dict = { "workspace.lite.time.minute": "دقيقة", "workspace.lite.time.minutes": "دقائق", "workspace.lite.time.fewSeconds": "بضع ثوان", - "workspace.lite.subscription.title": "اشتراك Lite", - "workspace.lite.subscription.message": "أنت مشترك في OpenCode Lite.", + "workspace.lite.subscription.title": "اشتراك Go", + "workspace.lite.subscription.message": "أنت مشترك في OpenCode Go.", "workspace.lite.subscription.manage": "إدارة الاشتراك", "workspace.lite.subscription.rollingUsage": "الاستخدام المتجدد", "workspace.lite.subscription.weeklyUsage": "الاستخدام الأسبوعي", "workspace.lite.subscription.monthlyUsage": "الاستخدام الشهري", "workspace.lite.subscription.resetsIn": "إعادة تعيين في", "workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام", - "workspace.lite.other.title": "اشتراك Lite", + "workspace.lite.other.title": "اشتراك Go", "workspace.lite.other.message": - "عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Lite. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.", - "workspace.lite.promo.title": "OpenCode Lite", + "عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Go. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "احصل على وصول إلى أفضل النماذج المفتوحة — Kimi K2.5، و GLM-5، و MiniMax M2.5 — مع حدود استخدام سخية مقابل $10 شهريًا.", - "workspace.lite.promo.subscribe": "الاشتراك في Lite", + "OpenCode Go هو اشتراك بسعر $10 شهريًا يوفر وصولاً موثوقًا إلى نماذج البرمجة المفتوحة الشائعة مع حدود استخدام سخية.", + "workspace.lite.promo.modelsTitle": "ما يتضمنه", + "workspace.lite.promo.footer": + "تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. قد تتغير الأسعار وحدود الاستخدام بناءً على تعلمنا من الاستخدام المبكر والملاحظات.", + "workspace.lite.promo.subscribe": "الاشتراك في Go", "workspace.lite.promo.subscribing": "جارٍ إعادة التوجيه...", "download.title": "OpenCode | تنزيل", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index 5367a748bfeb..ff4e6ab7aa1d 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -497,21 +497,24 @@ export const dict = { "workspace.lite.time.minute": "minuto", "workspace.lite.time.minutes": "minutos", "workspace.lite.time.fewSeconds": "alguns segundos", - "workspace.lite.subscription.title": "Assinatura Lite", - "workspace.lite.subscription.message": "Você assina o OpenCode Lite.", + "workspace.lite.subscription.title": "Assinatura Go", + "workspace.lite.subscription.message": "Você assina o OpenCode Go.", "workspace.lite.subscription.manage": "Gerenciar Assinatura", "workspace.lite.subscription.rollingUsage": "Uso Contínuo", "workspace.lite.subscription.weeklyUsage": "Uso Semanal", "workspace.lite.subscription.monthlyUsage": "Uso Mensal", "workspace.lite.subscription.resetsIn": "Reinicia em", "workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso", - "workspace.lite.other.title": "Assinatura Lite", + "workspace.lite.other.title": "Assinatura Go", "workspace.lite.other.message": - "Outro membro neste workspace já assina o OpenCode Lite. Apenas um membro por workspace pode assinar.", - "workspace.lite.promo.title": "OpenCode Lite", + "Outro membro neste workspace já assina o OpenCode Go. Apenas um membro por workspace pode assinar.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Tenha acesso aos melhores modelos abertos — Kimi K2.5, GLM-5 e MiniMax M2.5 — com limites de uso generosos por $10 por mês.", - "workspace.lite.promo.subscribe": "Assinar Lite", + "O OpenCode Go é uma assinatura de $10 por mês que fornece acesso confiável a modelos abertos de codificação populares com limites de uso generosos.", + "workspace.lite.promo.modelsTitle": "O que está incluído", + "workspace.lite.promo.footer": + "O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável. Preços e limites de uso podem mudar conforme aprendemos com o uso inicial e feedback.", + "workspace.lite.promo.subscribe": "Assinar Go", "workspace.lite.promo.subscribing": "Redirecionando...", "download.title": "OpenCode | Baixar", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 2f1be69cadd9..e3a7b789213e 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -495,21 +495,24 @@ export const dict = { "workspace.lite.time.minute": "minut", "workspace.lite.time.minutes": "minutter", "workspace.lite.time.fewSeconds": "et par sekunder", - "workspace.lite.subscription.title": "Lite-abonnement", - "workspace.lite.subscription.message": "Du abonnerer på OpenCode Lite.", + "workspace.lite.subscription.title": "Go-abonnement", + "workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.", "workspace.lite.subscription.manage": "Administrer abonnement", "workspace.lite.subscription.rollingUsage": "Løbende forbrug", "workspace.lite.subscription.weeklyUsage": "Ugentligt forbrug", "workspace.lite.subscription.monthlyUsage": "Månedligt forbrug", "workspace.lite.subscription.resetsIn": "Nulstiller i", "workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne", - "workspace.lite.other.title": "Lite-abonnement", + "workspace.lite.other.title": "Go-abonnement", "workspace.lite.other.message": - "Et andet medlem i dette workspace abonnerer allerede på OpenCode Lite. Kun ét medlem pr. workspace kan abonnere.", - "workspace.lite.promo.title": "OpenCode Lite", + "Et andet medlem i dette workspace abonnerer allerede på OpenCode Go. Kun ét medlem pr. workspace kan abonnere.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Få adgang til de bedste åbne modeller — Kimi K2.5, GLM-5 og MiniMax M2.5 — med generøse forbrugsgrænser for $10 om måneden.", - "workspace.lite.promo.subscribe": "Abonner på Lite", + "OpenCode Go er et abonnement til $10 om måneden, der giver pålidelig adgang til populære åbne kodningsmodeller med generøse forbrugsgrænser.", + "workspace.lite.promo.modelsTitle": "Hvad er inkluderet", + "workspace.lite.promo.footer": + "Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af tidlig brug og feedback.", + "workspace.lite.promo.subscribe": "Abonner på Go", "workspace.lite.promo.subscribing": "Omdirigerer...", "download.title": "OpenCode | Download", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 49df65f8dc0b..888069b71246 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -497,21 +497,24 @@ export const dict = { "workspace.lite.time.minute": "Minute", "workspace.lite.time.minutes": "Minuten", "workspace.lite.time.fewSeconds": "einige Sekunden", - "workspace.lite.subscription.title": "Lite-Abonnement", - "workspace.lite.subscription.message": "Du hast OpenCode Lite abonniert.", + "workspace.lite.subscription.title": "Go-Abonnement", + "workspace.lite.subscription.message": "Du hast OpenCode Go abonniert.", "workspace.lite.subscription.manage": "Abo verwalten", "workspace.lite.subscription.rollingUsage": "Fortlaufende Nutzung", "workspace.lite.subscription.weeklyUsage": "Wöchentliche Nutzung", "workspace.lite.subscription.monthlyUsage": "Monatliche Nutzung", "workspace.lite.subscription.resetsIn": "Setzt zurück in", "workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind", - "workspace.lite.other.title": "Lite-Abonnement", + "workspace.lite.other.title": "Go-Abonnement", "workspace.lite.other.message": - "Ein anderes Mitglied in diesem Workspace hat OpenCode Lite bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.", - "workspace.lite.promo.title": "OpenCode Lite", + "Ein anderes Mitglied in diesem Workspace hat OpenCode Go bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Erhalte Zugriff auf die besten offenen Modelle — Kimi K2.5, GLM-5 und MiniMax M2.5 — mit großzügigen Nutzungslimits für $10 pro Monat.", - "workspace.lite.promo.subscribe": "Lite abonnieren", + "OpenCode Go ist ein Abonnement für $10 pro Monat, das zuverlässigen Zugriff auf beliebte offene Coding-Modelle mit großzügigen Nutzungslimits bietet.", + "workspace.lite.promo.modelsTitle": "Was enthalten ist", + "workspace.lite.promo.footer": + "Der Plan wurde hauptsächlich für internationale Nutzer entwickelt, wobei die Modelle in den USA, der EU und Singapur gehostet werden, um einen stabilen weltweiten Zugriff zu gewährleisten. Preise und Nutzungslimits können sich ändern, während wir aus der frühen Nutzung und dem Feedback lernen.", + "workspace.lite.promo.subscribe": "Go abonnieren", "workspace.lite.promo.subscribing": "Leite weiter...", "download.title": "OpenCode | Download", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 42b88dd16e5d..6080e2848173 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -489,21 +489,24 @@ export const dict = { "workspace.lite.time.minute": "minute", "workspace.lite.time.minutes": "minutes", "workspace.lite.time.fewSeconds": "a few seconds", - "workspace.lite.subscription.title": "Lite Subscription", - "workspace.lite.subscription.message": "You are subscribed to OpenCode Lite.", + "workspace.lite.subscription.title": "Go Subscription", + "workspace.lite.subscription.message": "You are subscribed to OpenCode Go.", "workspace.lite.subscription.manage": "Manage Subscription", "workspace.lite.subscription.rollingUsage": "Rolling Usage", "workspace.lite.subscription.weeklyUsage": "Weekly Usage", "workspace.lite.subscription.monthlyUsage": "Monthly Usage", "workspace.lite.subscription.resetsIn": "Resets in", "workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits", - "workspace.lite.other.title": "Lite Subscription", + "workspace.lite.other.title": "Go Subscription", "workspace.lite.other.message": - "Another member in this workspace is already subscribed to OpenCode Lite. Only one member per workspace can subscribe.", - "workspace.lite.promo.title": "OpenCode Lite", + "Another member in this workspace is already subscribed to OpenCode Go. Only one member per workspace can subscribe.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Get access to the best open models — Kimi K2.5, GLM-5, and MiniMax M2.5 — with generous usage limits for $10 per month.", - "workspace.lite.promo.subscribe": "Subscribe to Lite", + "OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models with generous usage limits.", + "workspace.lite.promo.modelsTitle": "What's Included", + "workspace.lite.promo.footer": + "The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Pricing and usage limits may change as we learn from early usage and feedback.", + "workspace.lite.promo.subscribe": "Subscribe to Go", "workspace.lite.promo.subscribing": "Redirecting...", "download.title": "OpenCode | Download", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index f4ac1cc63739..0cb5f0bd5440 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -498,21 +498,24 @@ export const dict = { "workspace.lite.time.minute": "minuto", "workspace.lite.time.minutes": "minutos", "workspace.lite.time.fewSeconds": "unos pocos segundos", - "workspace.lite.subscription.title": "Suscripción Lite", - "workspace.lite.subscription.message": "Estás suscrito a OpenCode Lite.", + "workspace.lite.subscription.title": "Suscripción Go", + "workspace.lite.subscription.message": "Estás suscrito a OpenCode Go.", "workspace.lite.subscription.manage": "Gestionar Suscripción", "workspace.lite.subscription.rollingUsage": "Uso Continuo", "workspace.lite.subscription.weeklyUsage": "Uso Semanal", "workspace.lite.subscription.monthlyUsage": "Uso Mensual", "workspace.lite.subscription.resetsIn": "Se reinicia en", "workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso", - "workspace.lite.other.title": "Suscripción Lite", + "workspace.lite.other.title": "Suscripción Go", "workspace.lite.other.message": - "Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Lite. Solo un miembro por espacio de trabajo puede suscribirse.", - "workspace.lite.promo.title": "OpenCode Lite", + "Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Go. Solo un miembro por espacio de trabajo puede suscribirse.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Obtén acceso a los mejores modelos abiertos — Kimi K2.5, GLM-5 y MiniMax M2.5 — con generosos límites de uso por $10 al mes.", - "workspace.lite.promo.subscribe": "Suscribirse a Lite", + "OpenCode Go es una suscripción de $10 al mes que proporciona acceso confiable a modelos de codificación abiertos populares con generosos límites de uso.", + "workspace.lite.promo.modelsTitle": "Qué incluye", + "workspace.lite.promo.footer": + "El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., la UE y Singapur para un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y los comentarios.", + "workspace.lite.promo.subscribe": "Suscribirse a Go", "workspace.lite.promo.subscribing": "Redirigiendo...", "download.title": "OpenCode | Descargar", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 05ee4e843528..9f4f42eaf7d7 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -506,8 +506,8 @@ export const dict = { "workspace.lite.time.minute": "minute", "workspace.lite.time.minutes": "minutes", "workspace.lite.time.fewSeconds": "quelques secondes", - "workspace.lite.subscription.title": "Abonnement Lite", - "workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Lite.", + "workspace.lite.subscription.title": "Abonnement Go", + "workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Go.", "workspace.lite.subscription.manage": "Gérer l'abonnement", "workspace.lite.subscription.rollingUsage": "Utilisation glissante", "workspace.lite.subscription.weeklyUsage": "Utilisation hebdomadaire", @@ -515,13 +515,16 @@ export const dict = { "workspace.lite.subscription.resetsIn": "Réinitialisation dans", "workspace.lite.subscription.useBalance": "Utilisez votre solde disponible après avoir atteint les limites d'utilisation", - "workspace.lite.other.title": "Abonnement Lite", + "workspace.lite.other.title": "Abonnement Go", "workspace.lite.other.message": - "Un autre membre de cet espace de travail est déjà abonné à OpenCode Lite. Un seul membre par espace de travail peut s'abonner.", - "workspace.lite.promo.title": "OpenCode Lite", + "Un autre membre de cet espace de travail est déjà abonné à OpenCode Go. Un seul membre par espace de travail peut s'abonner.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Accédez aux meilleurs modèles ouverts — Kimi K2.5, GLM-5 et MiniMax M2.5 — avec des limites d'utilisation généreuses pour 10 $ par mois.", - "workspace.lite.promo.subscribe": "S'abonner à Lite", + "OpenCode Go est un abonnement à 10 $ par mois qui offre un accès fiable aux modèles de codage ouverts populaires avec des limites d'utilisation généreuses.", + "workspace.lite.promo.modelsTitle": "Ce qui est inclus", + "workspace.lite.promo.footer": + "Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. Les tarifs et les limites d'utilisation peuvent changer à mesure que nous apprenons des premières utilisations et des commentaires.", + "workspace.lite.promo.subscribe": "S'abonner à Go", "workspace.lite.promo.subscribing": "Redirection...", "download.title": "OpenCode | Téléchargement", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 08b7955047d1..3cfa8bc29ea5 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -497,21 +497,24 @@ export const dict = { "workspace.lite.time.minute": "minuto", "workspace.lite.time.minutes": "minuti", "workspace.lite.time.fewSeconds": "pochi secondi", - "workspace.lite.subscription.title": "Abbonamento Lite", - "workspace.lite.subscription.message": "Sei abbonato a OpenCode Lite.", + "workspace.lite.subscription.title": "Abbonamento Go", + "workspace.lite.subscription.message": "Sei abbonato a OpenCode Go.", "workspace.lite.subscription.manage": "Gestisci Abbonamento", "workspace.lite.subscription.rollingUsage": "Utilizzo Continuativo", "workspace.lite.subscription.weeklyUsage": "Utilizzo Settimanale", "workspace.lite.subscription.monthlyUsage": "Utilizzo Mensile", "workspace.lite.subscription.resetsIn": "Si resetta tra", "workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo", - "workspace.lite.other.title": "Abbonamento Lite", + "workspace.lite.other.title": "Abbonamento Go", "workspace.lite.other.message": - "Un altro membro in questo workspace è già abbonato a OpenCode Lite. Solo un membro per workspace può abbonarsi.", - "workspace.lite.promo.title": "OpenCode Lite", + "Un altro membro in questo workspace è già abbonato a OpenCode Go. Solo un membro per workspace può abbonarsi.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Ottieni l'accesso ai migliori modelli aperti — Kimi K2.5, GLM-5 e MiniMax M2.5 — con limiti di utilizzo generosi per $10 al mese.", - "workspace.lite.promo.subscribe": "Abbonati a Lite", + "OpenCode Go è un abbonamento a $10 al mese che fornisce un accesso affidabile a popolari modelli di coding aperti con generosi limiti di utilizzo.", + "workspace.lite.promo.modelsTitle": "Cosa è incluso", + "workspace.lite.promo.footer": + "Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati in US, EU e Singapore per un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare man mano che impariamo dall'utilizzo iniziale e dal feedback.", + "workspace.lite.promo.subscribe": "Abbonati a Go", "workspace.lite.promo.subscribing": "Reindirizzamento...", "download.title": "OpenCode | Download", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 2c8e9d6b4d00..25393b5b22c1 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -495,21 +495,24 @@ export const dict = { "workspace.lite.time.minute": "分", "workspace.lite.time.minutes": "分", "workspace.lite.time.fewSeconds": "数秒", - "workspace.lite.subscription.title": "Liteサブスクリプション", - "workspace.lite.subscription.message": "あなたは OpenCode Lite を購読しています。", + "workspace.lite.subscription.title": "Goサブスクリプション", + "workspace.lite.subscription.message": "あなたは OpenCode Go を購読しています。", "workspace.lite.subscription.manage": "サブスクリプションの管理", "workspace.lite.subscription.rollingUsage": "ローリング利用量", "workspace.lite.subscription.weeklyUsage": "週間利用量", "workspace.lite.subscription.monthlyUsage": "月間利用量", "workspace.lite.subscription.resetsIn": "リセットまで", "workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する", - "workspace.lite.other.title": "Liteサブスクリプション", + "workspace.lite.other.title": "Goサブスクリプション", "workspace.lite.other.message": - "このワークスペースの別のメンバーが既に OpenCode Lite を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。", - "workspace.lite.promo.title": "OpenCode Lite", + "このワークスペースの別のメンバーが既に OpenCode Go を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "月額$10で、十分な利用枠が設けられた最高のオープンモデル — Kimi K2.5、GLM-5、および MiniMax M2.5 — にアクセスできます。", - "workspace.lite.promo.subscribe": "Liteを購読する", + "OpenCode Goは月額$10のサブスクリプションプランで、人気のオープンコーディングモデルへの安定したアクセスを十分な利用枠で提供します。", + "workspace.lite.promo.modelsTitle": "含まれるもの", + "workspace.lite.promo.footer": + "このプランは主にグローバルユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。料金と利用制限は、初期の利用状況やフィードバックに基づいて変更される可能性があります。", + "workspace.lite.promo.subscribe": "Goを購読する", "workspace.lite.promo.subscribing": "リダイレクト中...", "download.title": "OpenCode | ダウンロード", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 8f4e58e7de42..88acbf2742a3 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -490,21 +490,24 @@ export const dict = { "workspace.lite.time.minute": "분", "workspace.lite.time.minutes": "분", "workspace.lite.time.fewSeconds": "몇 초", - "workspace.lite.subscription.title": "Lite 구독", - "workspace.lite.subscription.message": "현재 OpenCode Lite를 구독 중입니다.", + "workspace.lite.subscription.title": "Go 구독", + "workspace.lite.subscription.message": "현재 OpenCode Go를 구독 중입니다.", "workspace.lite.subscription.manage": "구독 관리", "workspace.lite.subscription.rollingUsage": "롤링 사용량", "workspace.lite.subscription.weeklyUsage": "주간 사용량", "workspace.lite.subscription.monthlyUsage": "월간 사용량", "workspace.lite.subscription.resetsIn": "초기화까지 남은 시간:", "workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용", - "workspace.lite.other.title": "Lite 구독", + "workspace.lite.other.title": "Go 구독", "workspace.lite.other.message": - "이 워크스페이스의 다른 멤버가 이미 OpenCode Lite를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.", - "workspace.lite.promo.title": "OpenCode Lite", + "이 워크스페이스의 다른 멤버가 이미 OpenCode Go를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "월 $10의 넉넉한 사용 한도로 최고의 오픈 모델인 Kimi K2.5, GLM-5, MiniMax M2.5에 액세스하세요.", - "workspace.lite.promo.subscribe": "Lite 구독하기", + "OpenCode Go는 넉넉한 사용 한도와 함께 인기 있는 오픈 코딩 모델에 대한 안정적인 액세스를 제공하는 월 $10의 구독입니다.", + "workspace.lite.promo.modelsTitle": "포함 내역", + "workspace.lite.promo.footer": + "이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU 및 싱가포르에 모델이 호스팅되어 있습니다. 가격 및 사용 한도는 초기 사용을 통해 학습하고 피드백을 수집함에 따라 변경될 수 있습니다.", + "workspace.lite.promo.subscribe": "Go 구독하기", "workspace.lite.promo.subscribing": "리디렉션 중...", "download.title": "OpenCode | 다운로드", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index e5bfef989dd1..565becc014ec 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -495,21 +495,24 @@ export const dict = { "workspace.lite.time.minute": "minutt", "workspace.lite.time.minutes": "minutter", "workspace.lite.time.fewSeconds": "noen få sekunder", - "workspace.lite.subscription.title": "Lite-abonnement", - "workspace.lite.subscription.message": "Du abonnerer på OpenCode Lite.", + "workspace.lite.subscription.title": "Go-abonnement", + "workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.", "workspace.lite.subscription.manage": "Administrer abonnement", "workspace.lite.subscription.rollingUsage": "Løpende bruk", "workspace.lite.subscription.weeklyUsage": "Ukentlig bruk", "workspace.lite.subscription.monthlyUsage": "Månedlig bruk", "workspace.lite.subscription.resetsIn": "Nullstilles om", "workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene", - "workspace.lite.other.title": "Lite-abonnement", + "workspace.lite.other.title": "Go-abonnement", "workspace.lite.other.message": - "Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Lite. Kun ett medlem per arbeidsområde kan abonnere.", - "workspace.lite.promo.title": "OpenCode Lite", + "Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Go. Kun ett medlem per arbeidsområde kan abonnere.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Få tilgang til de beste åpne modellene — Kimi K2.5, GLM-5 og MiniMax M2.5 — med generøse bruksgrenser for $10 per måned.", - "workspace.lite.promo.subscribe": "Abonner på Lite", + "OpenCode Go er et abonnement til $10 per måned som gir pålitelig tilgang til populære åpne kodemodeller med rause bruksgrenser.", + "workspace.lite.promo.modelsTitle": "Hva som er inkludert", + "workspace.lite.promo.footer": + "Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer fra tidlig bruk og tilbakemeldinger.", + "workspace.lite.promo.subscribe": "Abonner på Go", "workspace.lite.promo.subscribing": "Omdirigerer...", "download.title": "OpenCode | Last ned", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index c2f9b3712ef9..666da5668e1d 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -496,21 +496,24 @@ export const dict = { "workspace.lite.time.minute": "minuta", "workspace.lite.time.minutes": "minut(y)", "workspace.lite.time.fewSeconds": "kilka sekund", - "workspace.lite.subscription.title": "Subskrypcja Lite", - "workspace.lite.subscription.message": "Subskrybujesz OpenCode Lite.", + "workspace.lite.subscription.title": "Subskrypcja Go", + "workspace.lite.subscription.message": "Subskrybujesz OpenCode Go.", "workspace.lite.subscription.manage": "Zarządzaj subskrypcją", "workspace.lite.subscription.rollingUsage": "Użycie kroczące", "workspace.lite.subscription.weeklyUsage": "Użycie tygodniowe", "workspace.lite.subscription.monthlyUsage": "Użycie miesięczne", "workspace.lite.subscription.resetsIn": "Resetuje się za", "workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia", - "workspace.lite.other.title": "Subskrypcja Lite", + "workspace.lite.other.title": "Subskrypcja Go", "workspace.lite.other.message": - "Inny członek tego obszaru roboczego już subskrybuje OpenCode Lite. Tylko jeden członek na obszar roboczy może subskrybować.", - "workspace.lite.promo.title": "OpenCode Lite", + "Inny członek tego obszaru roboczego już subskrybuje OpenCode Go. Tylko jeden członek na obszar roboczy może subskrybować.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Uzyskaj dostęp do najlepszych otwartych modeli — Kimi K2.5, GLM-5 i MiniMax M2.5 — z hojnymi limitami użycia za $10 miesięcznie.", - "workspace.lite.promo.subscribe": "Subskrybuj Lite", + "OpenCode Go to subskrypcja za $10 miesięcznie, która zapewnia niezawodny dostęp do popularnych otwartych modeli do kodowania z hojnymi limitami użycia.", + "workspace.lite.promo.modelsTitle": "Co zawiera", + "workspace.lite.promo.footer": + "Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę analizy wczesnego użycia i zbierania opinii.", + "workspace.lite.promo.subscribe": "Subskrybuj Go", "workspace.lite.promo.subscribing": "Przekierowywanie...", "download.title": "OpenCode | Pobierz", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 3bedf80b5ba4..36205b048065 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -501,21 +501,24 @@ export const dict = { "workspace.lite.time.minute": "минута", "workspace.lite.time.minutes": "минут", "workspace.lite.time.fewSeconds": "несколько секунд", - "workspace.lite.subscription.title": "Подписка Lite", - "workspace.lite.subscription.message": "Вы подписаны на OpenCode Lite.", + "workspace.lite.subscription.title": "Подписка Go", + "workspace.lite.subscription.message": "Вы подписаны на OpenCode Go.", "workspace.lite.subscription.manage": "Управление подпиской", "workspace.lite.subscription.rollingUsage": "Скользящее использование", "workspace.lite.subscription.weeklyUsage": "Недельное использование", "workspace.lite.subscription.monthlyUsage": "Ежемесячное использование", "workspace.lite.subscription.resetsIn": "Сброс через", "workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов", - "workspace.lite.other.title": "Подписка Lite", + "workspace.lite.other.title": "Подписка Go", "workspace.lite.other.message": - "Другой участник в этом рабочем пространстве уже подписан на OpenCode Lite. Только один участник в рабочем пространстве может оформить подписку.", - "workspace.lite.promo.title": "OpenCode Lite", + "Другой участник в этом рабочем пространстве уже подписан на OpenCode Go. Только один участник в рабочем пространстве может оформить подписку.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Получите доступ к лучшим открытым моделям — Kimi K2.5, GLM-5 и MiniMax M2.5 — с щедрыми лимитами использования за $10 в месяц.", - "workspace.lite.promo.subscribe": "Подписаться на Lite", + "OpenCode Go — это подписка за $10 в месяц, которая предоставляет надежный доступ к популярным открытым моделям для кодинга с щедрыми лимитами использования.", + "workspace.lite.promo.modelsTitle": "Что включено", + "workspace.lite.promo.footer": + "План предназначен в первую очередь для международных пользователей. Модели размещены в США, ЕС и Сингапуре для стабильного глобального доступа. Цены и лимиты использования могут меняться по мере того, как мы изучаем раннее использование и собираем отзывы.", + "workspace.lite.promo.subscribe": "Подписаться на Go", "workspace.lite.promo.subscribing": "Перенаправление...", "download.title": "OpenCode | Скачать", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 3d36dcbb2272..6fd026fd710c 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -494,21 +494,24 @@ export const dict = { "workspace.lite.time.minute": "นาที", "workspace.lite.time.minutes": "นาที", "workspace.lite.time.fewSeconds": "ไม่กี่วินาที", - "workspace.lite.subscription.title": "การสมัครสมาชิก Lite", - "workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Lite แล้ว", + "workspace.lite.subscription.title": "การสมัครสมาชิก Go", + "workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Go แล้ว", "workspace.lite.subscription.manage": "จัดการการสมัครสมาชิก", "workspace.lite.subscription.rollingUsage": "การใช้งานแบบหมุนเวียน", "workspace.lite.subscription.weeklyUsage": "การใช้งานรายสัปดาห์", "workspace.lite.subscription.monthlyUsage": "การใช้งานรายเดือน", "workspace.lite.subscription.resetsIn": "รีเซ็ตใน", "workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน", - "workspace.lite.other.title": "การสมัครสมาชิก Lite", + "workspace.lite.other.title": "การสมัครสมาชิก Go", "workspace.lite.other.message": - "สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Lite แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น", - "workspace.lite.promo.title": "OpenCode Lite", + "สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Go แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "เข้าถึงโมเดลเปิดที่ดีที่สุด — Kimi K2.5, GLM-5 และ MiniMax M2.5 — พร้อมขีดจำกัดการใช้งานมากมายในราคา $10 ต่อเดือน", - "workspace.lite.promo.subscribe": "สมัครสมาชิก Lite", + "OpenCode Go เป็นการสมัครสมาชิกราคา 10 ดอลลาร์ต่อเดือน ที่ให้การเข้าถึงโมเดลโอเพนโค้ดดิงยอดนิยมได้อย่างเสถียร ด้วยขีดจำกัดการใช้งานที่ครอบคลุม", + "workspace.lite.promo.modelsTitle": "สิ่งที่รวมอยู่ด้วย", + "workspace.lite.promo.footer": + "แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์อยู่ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจมีการเปลี่ยนแปลงตามที่เราได้เรียนรู้จากการใช้งานในช่วงแรกและข้อเสนอแนะ", + "workspace.lite.promo.subscribe": "สมัครสมาชิก Go", "workspace.lite.promo.subscribing": "กำลังเปลี่ยนเส้นทาง...", "download.title": "OpenCode | ดาวน์โหลด", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index bfa7d09aef3f..24828d3bc290 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -497,21 +497,24 @@ export const dict = { "workspace.lite.time.minute": "dakika", "workspace.lite.time.minutes": "dakika", "workspace.lite.time.fewSeconds": "birkaç saniye", - "workspace.lite.subscription.title": "Lite Aboneliği", - "workspace.lite.subscription.message": "OpenCode Lite abonesisiniz.", + "workspace.lite.subscription.title": "Go Aboneliği", + "workspace.lite.subscription.message": "OpenCode Go abonesisiniz.", "workspace.lite.subscription.manage": "Aboneliği Yönet", "workspace.lite.subscription.rollingUsage": "Devam Eden Kullanım", "workspace.lite.subscription.weeklyUsage": "Haftalık Kullanım", "workspace.lite.subscription.monthlyUsage": "Aylık Kullanım", "workspace.lite.subscription.resetsIn": "Sıfırlama süresi", "workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın", - "workspace.lite.other.title": "Lite Aboneliği", + "workspace.lite.other.title": "Go Aboneliği", "workspace.lite.other.message": - "Bu çalışma alanındaki başka bir üye zaten OpenCode Lite abonesi. Çalışma alanı başına yalnızca bir üye abone olabilir.", - "workspace.lite.promo.title": "OpenCode Lite", + "Bu çalışma alanındaki başka bir üye zaten OpenCode Go abonesi. Çalışma alanı başına yalnızca bir üye abone olabilir.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Ayda $10 karşılığında cömert kullanım limitleriyle en iyi açık modellere — Kimi K2.5, GLM-5 ve MiniMax M2.5 — erişin.", - "workspace.lite.promo.subscribe": "Lite'a Abone Ol", + "OpenCode Go, cömert kullanım limitleriyle popüler açık kodlama modellerine güvenilir erişim sağlayan aylık 10$'lık bir aboneliktir.", + "workspace.lite.promo.modelsTitle": "Neler Dahil", + "workspace.lite.promo.footer": + "Plan öncelikle uluslararası kullanıcılar için tasarlanmıştır; modeller istikrarlı küresel erişim için ABD, AB ve Singapur'da barındırılmaktadır. Erken kullanımdan öğrendikçe ve geri bildirim topladıkça fiyatlandırma ve kullanım limitleri değişebilir.", + "workspace.lite.promo.subscribe": "Go'ya Abone Ol", "workspace.lite.promo.subscribing": "Yönlendiriliyor...", "download.title": "OpenCode | İndir", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index 2c41be7cf7c9..e2777c8cfcec 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -481,20 +481,23 @@ export const dict = { "workspace.lite.time.minute": "分钟", "workspace.lite.time.minutes": "分钟", "workspace.lite.time.fewSeconds": "几秒钟", - "workspace.lite.subscription.title": "Lite 订阅", - "workspace.lite.subscription.message": "您已订阅 OpenCode Lite。", + "workspace.lite.subscription.title": "Go 订阅", + "workspace.lite.subscription.message": "您已订阅 OpenCode Go。", "workspace.lite.subscription.manage": "管理订阅", "workspace.lite.subscription.rollingUsage": "滚动用量", "workspace.lite.subscription.weeklyUsage": "每周用量", "workspace.lite.subscription.monthlyUsage": "每月用量", "workspace.lite.subscription.resetsIn": "重置于", "workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额", - "workspace.lite.other.title": "Lite 订阅", - "workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Lite。每个工作区只有一名成员可以订阅。", - "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.other.title": "Go 订阅", + "workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Go。每个工作区只有一名成员可以订阅。", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "每月仅需 $10 即可访问最优秀的开源模型 — Kimi K2.5, GLM-5, 和 MiniMax M2.5 — 并享受充裕的使用限额。", - "workspace.lite.promo.subscribe": "订阅 Lite", + "OpenCode Go 是一个每月 $10 的订阅计划,提供对主流开源编码模型的稳定访问,并配备充足的使用额度。", + "workspace.lite.promo.modelsTitle": "包含模型", + "workspace.lite.promo.footer": + "该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保全球范围内的稳定访问体验。定价和使用额度可能会根据早期用户的使用情况和反馈持续调整与优化。", + "workspace.lite.promo.subscribe": "订阅 Go", "workspace.lite.promo.subscribing": "正在重定向...", "download.title": "OpenCode | 下载", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 87fcaa8e89e6..dd440ed7e0b8 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -481,20 +481,23 @@ export const dict = { "workspace.lite.time.minute": "分鐘", "workspace.lite.time.minutes": "分鐘", "workspace.lite.time.fewSeconds": "幾秒", - "workspace.lite.subscription.title": "Lite 訂閱", - "workspace.lite.subscription.message": "您已訂閱 OpenCode Lite。", + "workspace.lite.subscription.title": "Go 訂閱", + "workspace.lite.subscription.message": "您已訂閱 OpenCode Go。", "workspace.lite.subscription.manage": "管理訂閱", "workspace.lite.subscription.rollingUsage": "滾動使用量", "workspace.lite.subscription.weeklyUsage": "每週使用量", "workspace.lite.subscription.monthlyUsage": "每月使用量", "workspace.lite.subscription.resetsIn": "重置時間:", "workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額", - "workspace.lite.other.title": "Lite 訂閱", - "workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Lite。每個工作區只能有一位成員訂閱。", - "workspace.lite.promo.title": "OpenCode Lite", + "workspace.lite.other.title": "Go 訂閱", + "workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Go。每個工作區只能有一位成員訂閱。", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "每月只需 $10 即可使用最佳的開放模型 — Kimi K2.5、GLM-5 和 MiniMax M2.5 — 並享有慷慨的使用限制。", - "workspace.lite.promo.subscribe": "訂閱 Lite", + "OpenCode Go 是一個每月 $10 的訂閱方案,提供對主流開放原始碼編碼模型的穩定存取,並配備充足的使用額度。", + "workspace.lite.promo.modelsTitle": "包含模型", + "workspace.lite.promo.footer": + "該計畫主要面向國際用戶設計,模型部署在美國、歐盟和新加坡,以確保全球範圍內的穩定存取體驗。定價和使用額度可能會根據早期用戶的使用情況和回饋持續調整與優化。", + "workspace.lite.promo.subscribe": "訂閱 Go", "workspace.lite.promo.subscribing": "重新導向中...", "download.title": "OpenCode | 下載", diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx index 9fbdad2ef742..e039a09ef8b3 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx @@ -21,7 +21,7 @@ export default function () { - + diff --git a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css index 20662ab61863..077ac40e0d90 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css @@ -147,12 +147,26 @@ } [data-slot="promo-description"] { - font-size: var(--font-size-sm); + font-size: var(--font-size-md); color: var(--color-text-secondary); line-height: 1.5; margin-top: var(--space-2); } + [data-slot="promo-models-title"] { + font-size: var(--font-size-md); + font-weight: 600; + margin-top: var(--space-4); + } + + [data-slot="promo-models"] { + margin: var(--space-2) 0 0 var(--space-4); + padding: 0; + font-size: var(--font-size-md); + color: var(--color-text-secondary); + line-height: 1.4; + } + [data-slot="subscribe-button"] { align-self: flex-start; margin-top: var(--space-4); diff --git a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx index c9192fdcf695..568a8710f6bd 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx @@ -252,6 +252,13 @@ export function LiteSection() {

{i18n.t("workspace.lite.promo.title")}

{i18n.t("workspace.lite.promo.description")}

+

{i18n.t("workspace.lite.promo.modelsTitle")}

+
    +
  • Kimi K2.5
  • +
  • GLM-5
  • +
  • MiniMax M2.5
  • +
+

{i18n.t("workspace.lite.promo.footer")}

+
+ {i18n.t("workspace.lite.subscription.selectProvider")}{" "} + + {i18n.t("common.learnMore")} + + . +
diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index db3bfeaeebeb..34e3626499cb 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -57,7 +57,39 @@ tested and verified to work well with OpenCode. [Learn more](/docs/zen). If you are new, we recommend starting with OpenCode Zen. ::: -1. Run the `/connect` command in the TUI, select opencode, and head to [opencode.ai/auth](https://opencode.ai/auth). +1. Run the `/connect` command in the TUI, select `OpenCode Zen`, and head to [opencode.ai/auth](https://opencode.ai/zen). + + ```txt + /connect + ``` + +2. Sign in, add your billing details, and copy your API key. + +3. Paste your API key. + + ```txt + ┌ API key + │ + │ + └ enter + ``` + +4. Run `/models` in the TUI to see the list of models we recommend. + + ```txt + /models + ``` + +It works like any other provider in OpenCode and is completely optional to use. + +--- + +## OpenCode Go + +OpenCode Go is a low cost subscription plan that provides reliable access to popular open coding models provided by the OpenCode team that have been +tested and verified to work well with OpenCode. + +1. Run the `/connect` command in the TUI, select `OpenCode Go`, and head to [opencode.ai/auth](https://opencode.ai/zen). ```txt /connect From fc6e7934bd365ad1665dea68556dbfc80ac3b611 Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Wed, 25 Feb 2026 07:39:58 +0100 Subject: [PATCH 56/94] feat(desktop): enhance Windows app resolution and UI loading states (#13320) Co-authored-by: Brendan Allan Co-authored-by: Brendan Allan --- .../src/components/session/session-header.tsx | 127 +++-- packages/desktop/src-tauri/Cargo.lock | 5 +- packages/desktop/src-tauri/Cargo.toml | 3 +- packages/desktop/src-tauri/src/cli.rs | 4 +- packages/desktop/src-tauri/src/lib.rs | 158 +------ packages/desktop/src-tauri/src/os/mod.rs | 2 + packages/desktop/src-tauri/src/os/windows.rs | 439 ++++++++++++++++++ 7 files changed, 556 insertions(+), 182 deletions(-) create mode 100644 packages/desktop/src-tauri/src/os/mod.rs create mode 100644 packages/desktop/src-tauri/src/os/windows.rs diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 825d1dab6cff..d531fa50ab60 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -1,28 +1,28 @@ +import { AppIcon } from "@opencode-ai/ui/app-icon" +import { Button } from "@opencode-ai/ui/button" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Keybind } from "@opencode-ai/ui/keybind" +import { Popover } from "@opencode-ai/ui/popover" +import { Spinner } from "@opencode-ai/ui/spinner" +import { TextField } from "@opencode-ai/ui/text-field" +import { showToast } from "@opencode-ai/ui/toast" +import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { getFilename } from "@opencode-ai/util/path" +import { useParams } from "@solidjs/router" import { createEffect, createMemo, For, onCleanup, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" -import { useParams } from "@solidjs/router" -import { useLayout } from "@/context/layout" import { useCommand } from "@/context/command" +import { useGlobalSDK } from "@/context/global-sdk" import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" import { usePlatform } from "@/context/platform" import { useServer } from "@/context/server" import { useSync } from "@/context/sync" -import { useGlobalSDK } from "@/context/global-sdk" -import { getFilename } from "@opencode-ai/util/path" import { decode64 } from "@/utils/base64" import { Persist, persisted } from "@/utils/persist" - -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Button } from "@opencode-ai/ui/button" -import { AppIcon } from "@opencode-ai/ui/app-icon" -import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { Popover } from "@opencode-ai/ui/popover" -import { TextField } from "@opencode-ai/ui/text-field" -import { Keybind } from "@opencode-ai/ui/keybind" -import { showToast } from "@opencode-ai/ui/toast" import { StatusPopover } from "../status-popover" const OPEN_APPS = [ @@ -45,32 +45,67 @@ type OpenApp = (typeof OPEN_APPS)[number] type OS = "macos" | "windows" | "linux" | "unknown" const MAC_APPS = [ - { id: "vscode", label: "VS Code", icon: "vscode", openWith: "Visual Studio Code" }, + { + id: "vscode", + label: "VS Code", + icon: "vscode", + openWith: "Visual Studio Code", + }, { id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" }, { id: "zed", label: "Zed", icon: "zed", openWith: "Zed" }, { id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" }, - { id: "antigravity", label: "Antigravity", icon: "antigravity", openWith: "Antigravity" }, + { + id: "antigravity", + label: "Antigravity", + icon: "antigravity", + openWith: "Antigravity", + }, { id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" }, { id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" }, { id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" }, { id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" }, - { id: "android-studio", label: "Android Studio", icon: "android-studio", openWith: "Android Studio" }, - { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" }, + { + id: "android-studio", + label: "Android Studio", + icon: "android-studio", + openWith: "Android Studio", + }, + { + id: "sublime-text", + label: "Sublime Text", + icon: "sublime-text", + openWith: "Sublime Text", + }, ] as const const WINDOWS_APPS = [ { id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" }, { id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" }, { id: "zed", label: "Zed", icon: "zed", openWith: "zed" }, - { id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell" }, - { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" }, + { + id: "powershell", + label: "PowerShell", + icon: "powershell", + openWith: "powershell", + }, + { + id: "sublime-text", + label: "Sublime Text", + icon: "sublime-text", + openWith: "Sublime Text", + }, ] as const const LINUX_APPS = [ { id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" }, { id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" }, { id: "zed", label: "Zed", icon: "zed", openWith: "zed" }, - { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" }, + { + id: "sublime-text", + label: "Sublime Text", + icon: "sublime-text", + openWith: "Sublime Text", + }, ] as const type OpenOption = (typeof MAC_APPS)[number] | (typeof WINDOWS_APPS)[number] | (typeof LINUX_APPS)[number] @@ -213,7 +248,9 @@ export function SessionHeader() { const view = createMemo(() => layout.view(sessionKey)) const os = createMemo(() => detectOS(platform)) - const [exists, setExists] = createStore>>({ finder: true }) + const [exists, setExists] = createStore>>({ + finder: true, + }) const apps = createMemo(() => { if (os() === "macos") return MAC_APPS @@ -259,18 +296,34 @@ export function SessionHeader() { const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp })) const [menu, setMenu] = createStore({ open: false }) + const [openRequest, setOpenRequest] = createStore({ + app: undefined as OpenApp | undefined, + }) const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal()) const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0]) + const opening = createMemo(() => openRequest.app !== undefined) + + createEffect(() => { + const value = prefs.app + if (options().some((o) => o.id === value)) return + setPrefs("app", options()[0]?.id ?? "finder") + }) const openDir = (app: OpenApp) => { + if (opening() || !canOpen() || !platform.openPath) return const directory = projectDirectory() if (!directory) return - if (!canOpen()) return const item = options().find((o) => o.id === app) const openWith = item && "openWith" in item ? item.openWith : undefined - Promise.resolve(platform.openPath?.(directory, openWith)).catch((err: unknown) => showRequestError(language, err)) + setOpenRequest("app", app) + platform + .openPath(directory, openWith) + .catch((err: unknown) => showRequestError(language, err)) + .finally(() => { + setOpenRequest("app", undefined) + }) } const copyPath = () => { @@ -315,7 +368,9 @@ export function SessionHeader() {
- {language.t("session.header.search.placeholder", { project: name() })} + {language.t("session.header.search.placeholder", { + project: name(), + })}
@@ -357,12 +412,21 @@ export function SessionHeader() {
@@ -377,7 +441,11 @@ export function SessionHeader() { as={IconButton} icon="chevron-down" variant="ghost" - class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-hover" + disabled={opening()} + class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default" + classList={{ + "bg-surface-raised-base-active": opening(), + }} aria-label={language.t("session.header.open.menu")} /> @@ -395,6 +463,7 @@ export function SessionHeader() { {(o) => ( { setMenu("open", false) openDir(o.id) diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index f9516350e13a..55f0d5f36033 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -1988,7 +1988,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -3136,7 +3136,8 @@ dependencies = [ "tracing-subscriber", "uuid", "webkit2gtk", - "windows 0.62.2", + "windows-core 0.62.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index e98b8965c163..b228c7b6162c 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -55,7 +55,8 @@ tokio-stream = { version = "0.1.18", features = ["sync"] } process-wrap = { version = "9.0.3", features = ["tokio1"] } [target.'cfg(windows)'.dependencies] -windows = { version = "0.62", features = ["Win32_System_Threading"] } +windows-sys = { version = "0.61", features = ["Win32_System_Threading", "Win32_System_Registry"] } +windows-core = "0.62" [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18.2" diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index acab0fa7034c..0c5dfebaf5e5 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -19,7 +19,7 @@ use tokio::{ use tokio_stream::wrappers::ReceiverStream; use tracing::Instrument; #[cfg(windows)] -use windows::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED}; +use windows_sys::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED}; use crate::server::get_wsl_config; @@ -32,7 +32,7 @@ struct WinCreationFlags; #[cfg(windows)] impl CommandWrapper for WinCreationFlags { fn pre_spawn(&mut self, command: &mut Command, _core: &CommandWrap) -> std::io::Result<()> { - command.creation_flags((CREATE_NO_WINDOW | CREATE_SUSPENDED).0); + command.creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED); Ok(()) } } diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 7ea3aaa8a76b..71fe8407f029 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -6,6 +6,7 @@ pub mod linux_display; pub mod linux_windowing; mod logging; mod markdown; +mod os; mod server; mod window_customizer; mod windows; @@ -42,7 +43,7 @@ struct ServerReadyData { url: String, username: Option, password: Option, - is_sidecar: bool + is_sidecar: bool, } #[derive(Clone, Copy, serde::Serialize, specta::Type, Debug)] @@ -148,7 +149,7 @@ async fn await_initialization( fn check_app_exists(app_name: &str) -> bool { #[cfg(target_os = "windows")] { - check_windows_app(app_name) + os::windows::check_windows_app(app_name) } #[cfg(target_os = "macos")] @@ -162,156 +163,12 @@ fn check_app_exists(app_name: &str) -> bool { } } -#[cfg(target_os = "windows")] -fn check_windows_app(_app_name: &str) -> bool { - // Check if command exists in PATH, including .exe - return true; -} - -#[cfg(target_os = "windows")] -fn resolve_windows_app_path(app_name: &str) -> Option { - use std::path::{Path, PathBuf}; - - // Try to find the command using 'where' - let output = Command::new("where").arg(app_name).output().ok()?; - - if !output.status.success() { - return None; - } - - let paths = String::from_utf8_lossy(&output.stdout) - .lines() - .map(str::trim) - .filter(|line| !line.is_empty()) - .map(PathBuf::from) - .collect::>(); - - let has_ext = |path: &Path, ext: &str| { - path.extension() - .and_then(|v| v.to_str()) - .map(|v| v.eq_ignore_ascii_case(ext)) - .unwrap_or(false) - }; - - if let Some(path) = paths.iter().find(|path| has_ext(path, "exe")) { - return Some(path.to_string_lossy().to_string()); - } - - let resolve_cmd = |path: &Path| -> Option { - let content = std::fs::read_to_string(path).ok()?; - - for token in content.split('"') { - let lower = token.to_ascii_lowercase(); - if !lower.contains(".exe") { - continue; - } - - if let Some(index) = lower.find("%~dp0") { - let base = path.parent()?; - let suffix = &token[index + 5..]; - let mut resolved = PathBuf::from(base); - - for part in suffix.replace('/', "\\").split('\\') { - if part.is_empty() || part == "." { - continue; - } - if part == ".." { - let _ = resolved.pop(); - continue; - } - resolved.push(part); - } - - if resolved.exists() { - return Some(resolved.to_string_lossy().to_string()); - } - } - - let resolved = PathBuf::from(token); - if resolved.exists() { - return Some(resolved.to_string_lossy().to_string()); - } - } - - None - }; - - for path in &paths { - if has_ext(path, "cmd") || has_ext(path, "bat") { - if let Some(resolved) = resolve_cmd(path) { - return Some(resolved); - } - } - - if path.extension().is_none() { - let cmd = path.with_extension("cmd"); - if cmd.exists() { - if let Some(resolved) = resolve_cmd(&cmd) { - return Some(resolved); - } - } - - let bat = path.with_extension("bat"); - if bat.exists() { - if let Some(resolved) = resolve_cmd(&bat) { - return Some(resolved); - } - } - } - } - - let key = app_name - .chars() - .filter(|v| v.is_ascii_alphanumeric()) - .flat_map(|v| v.to_lowercase()) - .collect::(); - - if !key.is_empty() { - for path in &paths { - let dirs = [ - path.parent(), - path.parent().and_then(|dir| dir.parent()), - path.parent() - .and_then(|dir| dir.parent()) - .and_then(|dir| dir.parent()), - ]; - - for dir in dirs.into_iter().flatten() { - if let Ok(entries) = std::fs::read_dir(dir) { - for entry in entries.flatten() { - let candidate = entry.path(); - if !has_ext(&candidate, "exe") { - continue; - } - - let Some(stem) = candidate.file_stem().and_then(|v| v.to_str()) else { - continue; - }; - - let name = stem - .chars() - .filter(|v| v.is_ascii_alphanumeric()) - .flat_map(|v| v.to_lowercase()) - .collect::(); - - if name.contains(&key) || key.contains(&name) { - return Some(candidate.to_string_lossy().to_string()); - } - } - } - } - } - } - - paths.first().map(|path| path.to_string_lossy().to_string()) -} - #[tauri::command] #[specta::specta] fn resolve_app_path(app_name: &str) -> Option { #[cfg(target_os = "windows")] { - resolve_windows_app_path(app_name) + os::windows::resolve_windows_app_path(app_name) } #[cfg(not(target_os = "windows"))] @@ -634,7 +491,12 @@ async fn initialize(app: AppHandle) { app.state::().set_child(Some(child)); - Ok(ServerReadyData { url, username,password, is_sidecar: true }) + Ok(ServerReadyData { + url, + username, + password, + is_sidecar: true, + }) } .map(move |res| { let _ = server_ready_tx.send(res); diff --git a/packages/desktop/src-tauri/src/os/mod.rs b/packages/desktop/src-tauri/src/os/mod.rs new file mode 100644 index 000000000000..8c36e53f779f --- /dev/null +++ b/packages/desktop/src-tauri/src/os/mod.rs @@ -0,0 +1,2 @@ +#[cfg(windows)] +pub mod windows; diff --git a/packages/desktop/src-tauri/src/os/windows.rs b/packages/desktop/src-tauri/src/os/windows.rs new file mode 100644 index 000000000000..cab265b626bd --- /dev/null +++ b/packages/desktop/src-tauri/src/os/windows.rs @@ -0,0 +1,439 @@ +use std::{ + ffi::c_void, + os::windows::process::CommandExt, + path::{Path, PathBuf}, + process::Command, +}; +use windows_sys::Win32::{ + Foundation::ERROR_SUCCESS, + System::Registry::{ + HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, REG_EXPAND_SZ, REG_SZ, RRF_RT_REG_EXPAND_SZ, + RRF_RT_REG_SZ, RegGetValueW, + }, +}; + +pub fn check_windows_app(app_name: &str) -> bool { + resolve_windows_app_path(app_name).is_some() +} + +pub fn resolve_windows_app_path(app_name: &str) -> Option { + fn expand_env(value: &str) -> String { + let mut out = String::with_capacity(value.len()); + let mut index = 0; + + while let Some(start) = value[index..].find('%') { + let start = index + start; + out.push_str(&value[index..start]); + + let Some(end_rel) = value[start + 1..].find('%') else { + out.push_str(&value[start..]); + return out; + }; + + let end = start + 1 + end_rel; + let key = &value[start + 1..end]; + if key.is_empty() { + out.push('%'); + index = end + 1; + continue; + } + + if let Ok(v) = std::env::var(key) { + out.push_str(&v); + index = end + 1; + continue; + } + + out.push_str(&value[start..=end]); + index = end + 1; + } + + out.push_str(&value[index..]); + out + } + + fn extract_exe(value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + return None; + } + + if let Some(rest) = value.strip_prefix('"') { + if let Some(end) = rest.find('"') { + let inner = rest[..end].trim(); + if inner.to_ascii_lowercase().contains(".exe") { + return Some(inner.to_string()); + } + } + } + + let lower = value.to_ascii_lowercase(); + let end = lower.find(".exe")?; + Some(value[..end + 4].trim().trim_matches('"').to_string()) + } + + fn candidates(app_name: &str) -> Vec { + let app_name = app_name.trim().trim_matches('"'); + if app_name.is_empty() { + return vec![]; + } + + let mut out = Vec::::new(); + let mut push = |value: String| { + let value = value.trim().trim_matches('"').to_string(); + if value.is_empty() { + return; + } + if out.iter().any(|v| v.eq_ignore_ascii_case(&value)) { + return; + } + out.push(value); + }; + + push(app_name.to_string()); + + let lower = app_name.to_ascii_lowercase(); + if !lower.ends_with(".exe") { + push(format!("{app_name}.exe")); + } + + let snake = { + let mut s = String::new(); + let mut underscore = false; + for c in lower.chars() { + if c.is_ascii_alphanumeric() { + s.push(c); + underscore = false; + continue; + } + if underscore { + continue; + } + s.push('_'); + underscore = true; + } + s.trim_matches('_').to_string() + }; + + if !snake.is_empty() { + push(snake.clone()); + if !snake.ends_with(".exe") { + push(format!("{snake}.exe")); + } + } + + let alnum = lower + .chars() + .filter(|c| c.is_ascii_alphanumeric()) + .collect::(); + + if !alnum.is_empty() { + push(alnum.clone()); + push(format!("{alnum}.exe")); + } + + match lower.as_str() { + "sublime text" | "sublime-text" | "sublime_text" | "sublime text.exe" => { + push("subl".to_string()); + push("subl.exe".to_string()); + push("sublime_text".to_string()); + push("sublime_text.exe".to_string()); + } + _ => {} + } + + out + } + + fn reg_app_path(exe: &str) -> Option { + let exe = exe.trim().trim_matches('"'); + if exe.is_empty() { + return None; + } + + let query = |root: *mut c_void, subkey: &str| -> Option { + let flags = RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ; + let mut kind: u32 = 0; + let mut size = 0u32; + + let mut key = subkey.encode_utf16().collect::>(); + key.push(0); + + let status = unsafe { + RegGetValueW( + root, + key.as_ptr(), + std::ptr::null(), + flags, + &mut kind, + std::ptr::null_mut(), + &mut size, + ) + }; + + if status != ERROR_SUCCESS || size == 0 { + return None; + } + + if kind != REG_SZ && kind != REG_EXPAND_SZ { + return None; + } + + let mut data = vec![0u8; size as usize]; + let status = unsafe { + RegGetValueW( + root, + key.as_ptr(), + std::ptr::null(), + flags, + &mut kind, + data.as_mut_ptr() as *mut c_void, + &mut size, + ) + }; + + if status != ERROR_SUCCESS || size < 2 { + return None; + } + + let words = unsafe { + std::slice::from_raw_parts(data.as_ptr().cast::(), (size as usize) / 2) + }; + let len = words.iter().position(|v| *v == 0).unwrap_or(words.len()); + let value = String::from_utf16_lossy(&words[..len]).trim().to_string(); + + if value.is_empty() { + return None; + } + + Some(value) + }; + + let keys = [ + ( + HKEY_CURRENT_USER, + format!(r"Software\Microsoft\Windows\CurrentVersion\App Paths\{exe}"), + ), + ( + HKEY_LOCAL_MACHINE, + format!(r"Software\Microsoft\Windows\CurrentVersion\App Paths\{exe}"), + ), + ( + HKEY_LOCAL_MACHINE, + format!(r"Software\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\{exe}"), + ), + ]; + + for (root, key) in keys { + let Some(value) = query(root, &key) else { + continue; + }; + + let Some(exe) = extract_exe(&value) else { + continue; + }; + + let exe = expand_env(&exe); + let path = Path::new(exe.trim().trim_matches('"')); + if path.exists() { + return Some(path.to_string_lossy().to_string()); + } + } + + None + } + + let app_name = app_name.trim().trim_matches('"'); + if app_name.is_empty() { + return None; + } + + let direct = Path::new(app_name); + if direct.is_absolute() && direct.exists() { + return Some(direct.to_string_lossy().to_string()); + } + + let key = app_name + .chars() + .filter(|v| v.is_ascii_alphanumeric()) + .flat_map(|v| v.to_lowercase()) + .collect::(); + + let has_ext = |path: &Path, ext: &str| { + path.extension() + .and_then(|v| v.to_str()) + .map(|v| v.eq_ignore_ascii_case(ext)) + .unwrap_or(false) + }; + + let resolve_cmd = |path: &Path| -> Option { + let bytes = std::fs::read(path).ok()?; + let content = String::from_utf8_lossy(&bytes); + + for token in content.split('"') { + let Some(exe) = extract_exe(token) else { + continue; + }; + + let lower = exe.to_ascii_lowercase(); + if let Some(index) = lower.find("%~dp0") { + let base = path.parent()?; + let suffix = &exe[index + 5..]; + let mut resolved = PathBuf::from(base); + + for part in suffix.replace('/', "\\").split('\\') { + if part.is_empty() || part == "." { + continue; + } + if part == ".." { + let _ = resolved.pop(); + continue; + } + resolved.push(part); + } + + if resolved.exists() { + return Some(resolved.to_string_lossy().to_string()); + } + + continue; + } + + let resolved = PathBuf::from(expand_env(&exe)); + if resolved.exists() { + return Some(resolved.to_string_lossy().to_string()); + } + } + + None + }; + + let resolve_where = |query: &str| -> Option { + let output = Command::new("where") + .creation_flags(0x08000000) + .arg(query) + .output() + .ok()?; + if !output.status.success() { + return None; + } + + let paths = String::from_utf8_lossy(&output.stdout) + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(PathBuf::from) + .collect::>(); + + if paths.is_empty() { + return None; + } + + if let Some(path) = paths.iter().find(|path| has_ext(path, "exe")) { + return Some(path.to_string_lossy().to_string()); + } + + for path in &paths { + if has_ext(path, "cmd") || has_ext(path, "bat") { + if let Some(resolved) = resolve_cmd(path) { + return Some(resolved); + } + } + + if path.extension().is_none() { + let cmd = path.with_extension("cmd"); + if cmd.exists() { + if let Some(resolved) = resolve_cmd(&cmd) { + return Some(resolved); + } + } + + let bat = path.with_extension("bat"); + if bat.exists() { + if let Some(resolved) = resolve_cmd(&bat) { + return Some(resolved); + } + } + } + } + + if !key.is_empty() { + for path in &paths { + let dirs = [ + path.parent(), + path.parent().and_then(|dir| dir.parent()), + path.parent() + .and_then(|dir| dir.parent()) + .and_then(|dir| dir.parent()), + ]; + + for dir in dirs.into_iter().flatten() { + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let candidate = entry.path(); + if !has_ext(&candidate, "exe") { + continue; + } + + let Some(stem) = candidate.file_stem().and_then(|v| v.to_str()) else { + continue; + }; + + let name = stem + .chars() + .filter(|v| v.is_ascii_alphanumeric()) + .flat_map(|v| v.to_lowercase()) + .collect::(); + + if name.contains(&key) || key.contains(&name) { + return Some(candidate.to_string_lossy().to_string()); + } + } + } + } + } + } + + paths.first().map(|path| path.to_string_lossy().to_string()) + }; + + let list = candidates(app_name); + for query in &list { + if let Some(path) = resolve_where(query) { + return Some(path); + } + } + + let mut exes = Vec::::new(); + for query in &list { + let query = query.trim().trim_matches('"'); + if query.is_empty() { + continue; + } + + let name = Path::new(query) + .file_name() + .and_then(|v| v.to_str()) + .unwrap_or(query); + + let exe = if name.to_ascii_lowercase().ends_with(".exe") { + name.to_string() + } else { + format!("{name}.exe") + }; + + if exes.iter().any(|v| v.eq_ignore_ascii_case(&exe)) { + continue; + } + + exes.push(exe); + } + + for exe in exes { + if let Some(path) = reg_app_path(&exe) { + return Some(path); + } + } + + None +} From 3c6c74457d53a01a3f42a758ad1317cd6ed1b963 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 25 Feb 2026 01:38:58 -0500 Subject: [PATCH 57/94] sync --- packages/opencode/BUN_SHELL_MIGRATION_PLAN.md | 136 ++++++++++++++++++ packages/opencode/src/util/git.ts | 74 +++------- packages/opencode/src/util/process.ts | 97 ++++++++++--- packages/opencode/test/util/process.test.ts | 59 ++++++++ 4 files changed, 294 insertions(+), 72 deletions(-) create mode 100644 packages/opencode/BUN_SHELL_MIGRATION_PLAN.md create mode 100644 packages/opencode/test/util/process.test.ts diff --git a/packages/opencode/BUN_SHELL_MIGRATION_PLAN.md b/packages/opencode/BUN_SHELL_MIGRATION_PLAN.md new file mode 100644 index 000000000000..6cb21ac8f61f --- /dev/null +++ b/packages/opencode/BUN_SHELL_MIGRATION_PLAN.md @@ -0,0 +1,136 @@ +# Bun shell migration plan + +Practical phased replacement of Bun `$` calls. + +## Goal + +Replace runtime Bun shell template-tag usage in `packages/opencode/src` with a unified `Process` API in `util/process.ts`. + +Keep behavior stable while improving safety, testability, and observability. + +Current baseline from audit: + +- 143 runtime command invocations across 17 files +- 84 are git commands +- Largest hotspots: + - `src/cli/cmd/github.ts` (33) + - `src/worktree/index.ts` (22) + - `src/lsp/server.ts` (21) + - `src/installation/index.ts` (20) + - `src/snapshot/index.ts` (18) + +## Decisions + +- Extend `src/util/process.ts` (do not create a separate exec module). +- Proceed with phased migration for both git and non-git paths. +- Keep plugin `$` compatibility in 1.x and remove in 2.0. + +## Non-goals + +- Do not remove plugin `$` compatibility in this effort. +- Do not redesign command semantics beyond what is needed to preserve behavior. + +## Constraints + +- Keep migration phased, not big-bang. +- Minimize behavioral drift. +- Keep these explicit shell-only exceptions: + - `src/session/prompt.ts` raw command execution + - worktree start scripts in `src/worktree/index.ts` + +## Process API proposal (`src/util/process.ts`) + +Add higher-level wrappers on top of current spawn support. + +Core methods: + +- `Process.run(cmd, opts)` +- `Process.text(cmd, opts)` +- `Process.lines(cmd, opts)` +- `Process.status(cmd, opts)` +- `Process.shell(command, opts)` for intentional shell execution + +Git helpers: + +- `Process.git(args, opts)` +- `Process.gitText(args, opts)` + +Shared options: + +- `cwd`, `env`, `stdin`, `stdout`, `stderr`, `abort`, `timeout`, `kill` +- `allowFailure` / non-throw mode +- optional redaction + trace metadata + +Standard result shape: + +- `code`, `stdout`, `stderr`, `duration_ms`, `cmd` +- helpers like `text()` and `arrayBuffer()` where useful + +## Phased rollout + +### Phase 0: Foundation + +- Implement Process wrappers in `src/util/process.ts`. +- Refactor `src/util/git.ts` to use Process only. +- Add tests for exit handling, timeout, abort, and output capture. + +### Phase 1: High-impact hotspots + +Migrate these first: + +- `src/cli/cmd/github.ts` +- `src/worktree/index.ts` +- `src/lsp/server.ts` +- `src/installation/index.ts` +- `src/snapshot/index.ts` + +Within each file, migrate git paths first where applicable. + +### Phase 2: Remaining git-heavy files + +Migrate git-centric call sites to `Process.git*` helpers: + +- `src/file/index.ts` +- `src/project/vcs.ts` +- `src/file/watcher.ts` +- `src/storage/storage.ts` +- `src/cli/cmd/pr.ts` + +### Phase 3: Remaining non-git files + +Migrate residual non-git usages: + +- `src/cli/cmd/tui/util/clipboard.ts` +- `src/util/archive.ts` +- `src/file/ripgrep.ts` +- `src/tool/bash.ts` +- `src/cli/cmd/uninstall.ts` + +### Phase 4: Stabilize + +- Remove dead wrappers and one-off patterns. +- Keep plugin `$` compatibility isolated and documented as temporary. +- Create linked 2.0 task for plugin `$` removal. + +## Validation strategy + +- Unit tests for new `Process` methods and options. +- Integration tests on hotspot modules. +- Smoke tests for install, snapshot, worktree, and GitHub flows. +- Regression checks for output parsing behavior. + +## Risk mitigation + +- File-by-file PRs with small diffs. +- Preserve behavior first, simplify second. +- Keep shell-only exceptions explicit and documented. +- Add consistent error shaping and logging at Process layer. + +## Definition of done + +- Runtime Bun `$` usage in `packages/opencode/src` is removed except: + - approved shell-only exceptions + - temporary plugin compatibility path (1.x) +- Git paths use `Process.git*` consistently. +- CI and targeted smoke tests pass. +- 2.0 issue exists for plugin `$` removal. diff --git a/packages/opencode/src/util/git.ts b/packages/opencode/src/util/git.ts index 8e1427c99d54..731131357f21 100644 --- a/packages/opencode/src/util/git.ts +++ b/packages/opencode/src/util/git.ts @@ -1,63 +1,35 @@ -import { $ } from "bun" -import { buffer } from "node:stream/consumers" -import { Flag } from "../flag/flag" import { Process } from "./process" export interface GitResult { exitCode: number - text(): string | Promise - stdout: Buffer | ReadableStream - stderr: Buffer | ReadableStream + text(): string + stdout: Buffer + stderr: Buffer } /** * Run a git command. * - * Uses Bun's lightweight `$` shell by default. When the process is running - * as an ACP client, child processes inherit the parent's stdin pipe which - * carries protocol data – on Windows this causes git to deadlock. In that - * case we fall back to `Process.spawn` with `stdin: "ignore"`. + * Uses Process helpers with stdin ignored to avoid protocol pipe inheritance + * issues in embedded/client environments. */ export async function git(args: string[], opts: { cwd: string; env?: Record }): Promise { - if (Flag.OPENCODE_CLIENT === "acp") { - try { - const proc = Process.spawn(["git", ...args], { - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - cwd: opts.cwd, - env: opts.env ? { ...process.env, ...opts.env } : process.env, - }) - // Read output concurrently with exit to avoid pipe buffer deadlock - if (!proc.stdout || !proc.stderr) { - throw new Error("Process output not available") - } - const [exitCode, out, err] = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)]) - return { - exitCode, - text: () => out.toString(), - stdout: out, - stderr: err, - } - } catch (error) { - const stderr = Buffer.from(error instanceof Error ? error.message : String(error)) - return { - exitCode: 1, - text: () => "", - stdout: Buffer.alloc(0), - stderr, - } - } - } - - const env = opts.env ? { ...process.env, ...opts.env } : undefined - let cmd = $`git ${args}`.quiet().nothrow().cwd(opts.cwd) - if (env) cmd = cmd.env(env) - const result = await cmd - return { - exitCode: result.exitCode, - text: () => result.text(), - stdout: result.stdout, - stderr: result.stderr, - } + return Process.run(["git", ...args], { + cwd: opts.cwd, + env: opts.env, + stdin: "ignore", + nothrow: true, + }) + .then((result) => ({ + exitCode: result.code, + text: () => result.stdout.toString(), + stdout: result.stdout, + stderr: result.stderr, + })) + .catch((error) => ({ + exitCode: 1, + text: () => "", + stdout: Buffer.alloc(0), + stderr: Buffer.from(error instanceof Error ? error.message : String(error)), + })) } diff --git a/packages/opencode/src/util/process.ts b/packages/opencode/src/util/process.ts index 09c55661fddd..71f001a86a13 100644 --- a/packages/opencode/src/util/process.ts +++ b/packages/opencode/src/util/process.ts @@ -1,4 +1,5 @@ import { spawn as launch, type ChildProcess } from "child_process" +import { buffer } from "node:stream/consumers" export namespace Process { export type Stdio = "inherit" | "pipe" | "ignore" @@ -14,58 +15,112 @@ export namespace Process { timeout?: number } + export interface RunOptions extends Omit { + nothrow?: boolean + } + + export interface Result { + code: number + stdout: Buffer + stderr: Buffer + } + + export class RunFailedError extends Error { + readonly cmd: string[] + readonly code: number + readonly stdout: Buffer + readonly stderr: Buffer + + constructor(cmd: string[], code: number, stdout: Buffer, stderr: Buffer) { + const text = stderr.toString().trim() + super( + text + ? `Command failed with code ${code}: ${cmd.join(" ")}\n${text}` + : `Command failed with code ${code}: ${cmd.join(" ")}`, + ) + this.name = "ProcessRunFailedError" + this.cmd = [...cmd] + this.code = code + this.stdout = stdout + this.stderr = stderr + } + } + export type Child = ChildProcess & { exited: Promise } - export function spawn(cmd: string[], options: Options = {}): Child { + export function spawn(cmd: string[], opts: Options = {}): Child { if (cmd.length === 0) throw new Error("Command is required") - options.abort?.throwIfAborted() + opts.abort?.throwIfAborted() const proc = launch(cmd[0], cmd.slice(1), { - cwd: options.cwd, - env: options.env === null ? {} : options.env ? { ...process.env, ...options.env } : undefined, - stdio: [options.stdin ?? "ignore", options.stdout ?? "ignore", options.stderr ?? "ignore"], + cwd: opts.cwd, + env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined, + stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"], }) - let aborted = false + let closed = false let timer: ReturnType | undefined const abort = () => { - if (aborted) return + if (closed) return if (proc.exitCode !== null || proc.signalCode !== null) return - aborted = true - - proc.kill(options.kill ?? "SIGTERM") + closed = true - const timeout = options.timeout ?? 5_000 - if (timeout <= 0) return + proc.kill(opts.kill ?? "SIGTERM") - timer = setTimeout(() => { - proc.kill("SIGKILL") - }, timeout) + const ms = opts.timeout ?? 5_000 + if (ms <= 0) return + timer = setTimeout(() => proc.kill("SIGKILL"), ms) } const exited = new Promise((resolve, reject) => { const done = () => { - options.abort?.removeEventListener("abort", abort) + opts.abort?.removeEventListener("abort", abort) if (timer) clearTimeout(timer) } - proc.once("exit", (exitCode, signal) => { + + proc.once("exit", (code, signal) => { done() - resolve(exitCode ?? (signal ? 1 : 0)) + resolve(code ?? (signal ? 1 : 0)) }) + proc.once("error", (error) => { done() reject(error) }) }) - if (options.abort) { - options.abort.addEventListener("abort", abort, { once: true }) - if (options.abort.aborted) abort() + if (opts.abort) { + opts.abort.addEventListener("abort", abort, { once: true }) + if (opts.abort.aborted) abort() } const child = proc as Child child.exited = exited return child } + + export async function run(cmd: string[], opts: RunOptions = {}): Promise { + const proc = spawn(cmd, { + cwd: opts.cwd, + env: opts.env, + stdin: opts.stdin, + abort: opts.abort, + kill: opts.kill, + timeout: opts.timeout, + stdout: "pipe", + stderr: "pipe", + }) + + if (!proc.stdout || !proc.stderr) throw new Error("Process output not available") + + const [code, stdout, stderr] = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)]) + const out = { + code, + stdout, + stderr, + } + if (out.code === 0 || opts.nothrow) return out + throw new RunFailedError(cmd, out.code, out.stdout, out.stderr) + } } diff --git a/packages/opencode/test/util/process.test.ts b/packages/opencode/test/util/process.test.ts new file mode 100644 index 000000000000..ce599d6d8f06 --- /dev/null +++ b/packages/opencode/test/util/process.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test" +import { Process } from "../../src/util/process" + +function node(script: string) { + return [process.execPath, "-e", script] +} + +describe("util.process", () => { + test("captures stdout and stderr", async () => { + const out = await Process.run(node('process.stdout.write("out");process.stderr.write("err")')) + expect(out.code).toBe(0) + expect(out.stdout.toString()).toBe("out") + expect(out.stderr.toString()).toBe("err") + }) + + test("returns code when nothrow is enabled", async () => { + const out = await Process.run(node("process.exit(7)"), { nothrow: true }) + expect(out.code).toBe(7) + }) + + test("throws RunFailedError on non-zero exit", async () => { + const err = await Process.run(node('process.stderr.write("bad");process.exit(3)')).catch((error) => error) + expect(err).toBeInstanceOf(Process.RunFailedError) + if (!(err instanceof Process.RunFailedError)) throw err + expect(err.code).toBe(3) + expect(err.stderr.toString()).toBe("bad") + }) + + test("aborts a running process", async () => { + const abort = new AbortController() + const started = Date.now() + setTimeout(() => abort.abort(), 25) + + const out = await Process.run(node("setInterval(() => {}, 1000)"), { + abort: abort.signal, + nothrow: true, + }) + + expect(out.code).not.toBe(0) + expect(Date.now() - started).toBeLessThan(1000) + }, 3000) + + test("kills after timeout when process ignores terminate signal", async () => { + if (process.platform === "win32") return + + const abort = new AbortController() + const started = Date.now() + setTimeout(() => abort.abort(), 25) + + const out = await Process.run(node('process.on("SIGTERM", () => {}); setInterval(() => {}, 1000)'), { + abort: abort.signal, + nothrow: true, + timeout: 25, + }) + + expect(out.code).not.toBe(0) + expect(Date.now() - started).toBeLessThan(1000) + }, 3000) +}) From 561f9f5f059ccefdad89ca01e79cc1234b913820 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 25 Feb 2026 01:54:28 -0500 Subject: [PATCH 58/94] opencode go copy --- .../cli/cmd/tui/component/dialog-provider.tsx | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 9682bee4ead2..d88dfdd86f17 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -16,10 +16,11 @@ import { useToast } from "../ui/toast" const PROVIDER_PRIORITY: Record = { opencode: 0, - anthropic: 1, + openai: 1, "github-copilot": 2, - openai: 3, - google: 4, + "opencode-go": 3, + anthropic: 4, + google: 5, } export function createDialogProviderOptions() { @@ -37,6 +38,7 @@ export function createDialogProviderOptions() { opencode: "(Recommended)", anthropic: "(Claude Max or API key)", openai: "(ChatGPT Plus/Pro or API key)", + "opencode-go": "(Low cost)", }[provider.id], category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", async onSelect() { @@ -214,16 +216,30 @@ function ApiMethod(props: ApiMethodProps) { title={props.title} placeholder="API key" description={ - props.providerID === "opencode" ? ( - - - OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key. - - - Go to https://opencode.ai/zen to get a key - - - ) : undefined + { + opencode: ( + + + OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API + key. + + + Go to https://opencode.ai/zen to get a key + + + ), + "opencode-go": ( + + + OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models + with generous usage limits. + + + Go to https://opencode.ai/zen and enable OpenCode Go + + + ), + }[props.providerID] ?? undefined } onConfirm={async (value) => { if (!value) return From d848c9b6a32f408e8b9bf6448b83af05629454d0 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 25 Feb 2026 07:27:19 +0000 Subject: [PATCH 59/94] release: v1.2.13 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index d81245ff7d4f..52bc415a7f31 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -75,7 +75,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -109,7 +109,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -136,7 +136,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -160,7 +160,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -184,7 +184,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -217,7 +217,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -246,7 +246,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -262,7 +262,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.11", + "version": "1.2.13", "bin": { "opencode": "./bin/opencode", }, @@ -376,7 +376,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -396,7 +396,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.11", + "version": "1.2.13", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -407,7 +407,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -420,7 +420,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -462,7 +462,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "zod": "catalog:", }, @@ -473,7 +473,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 360cbbcc01bb..beaeb2a316b6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.11", + "version": "1.2.13", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index a866785ce081..bfed09c3ebb6 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.11", + "version": "1.2.13", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index c06964f7d851..5427c906a76d 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.11", + "version": "1.2.13", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 351f78bddcb8..6cd13bf0fcaa 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.11", + "version": "1.2.13", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index f61e7f9ab41d..2dd381581b67 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.11", + "version": "1.2.13", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index fba0730b05e4..6db85fcbddb2 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.11", + "version": "1.2.13", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 229f6b2552a0..9d39fcf9ee5b 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.11", + "version": "1.2.13", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 5e13ecdb6951..6353f7653843 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.11" +version = "1.2.13" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.13/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.13/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.13/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.13/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.13/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 26fc3c0410d0..04969d9827d5 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.11", + "version": "1.2.13", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 857e912c30e2..0e3594dc7d79 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.11", + "version": "1.2.13", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 97da559e7557..339b87647807 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.11", + "version": "1.2.13", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index d2b3c6115901..158683ba4933 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.11", + "version": "1.2.13", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 678380474769..e241919b2ca7 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.11", + "version": "1.2.13", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 505d8bb8c533..7a198d9bae14 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.11", + "version": "1.2.13", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index fbb123591b31..fa89670d9799 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.11", + "version": "1.2.13", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 0b71c07142a7..a8f7decb4576 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.11", + "version": "1.2.13", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index fffd9e149dd9..cfe9d6cedb4c 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.11", + "version": "1.2.13", "publisher": "sst-dev", "repository": { "type": "git", From 088a81c116f3fda865851292c92754385292b92d Mon Sep 17 00:00:00 2001 From: Ayush Thakur <51413362+Ayushlm10@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:52:52 +0530 Subject: [PATCH 60/94] fix: consume stdout concurrently with process exit in auth login (#15058) --- packages/opencode/src/cli/cmd/auth.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index 4a97a5e0b83c..95635916413a 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -268,18 +268,17 @@ export const AuthLoginCommand = cmd({ const proc = Process.spawn(wellknown.auth.command, { stdout: "pipe", }) - const exit = await proc.exited - if (exit !== 0) { + if (!proc.stdout) { prompts.log.error("Failed") prompts.outro("Done") return } - if (!proc.stdout) { + const [exit, token] = await Promise.all([proc.exited, text(proc.stdout)]) + if (exit !== 0) { prompts.log.error("Failed") prompts.outro("Done") return } - const token = await text(proc.stdout) await Auth.set(args.url, { type: "wellknown", key: wellknown.auth.env, From 79b5ce58e9d3ad940330c2fd82784a4d8b7e004d Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Wed, 25 Feb 2026 14:25:26 +0000 Subject: [PATCH 61/94] feat(core): add message delete endpoint (#14417) --- .../opencode/src/server/routes/session.ts | 36 +++++++++++++++++ packages/opencode/src/session/index.ts | 8 +++- packages/sdk/js/src/v2/gen/sdk.gen.ts | 38 ++++++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 40 +++++++++++++++++++ 4 files changed, 120 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index 1195529e06a8..12938aeaba04 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -618,6 +618,42 @@ export const SessionRoutes = lazy(() => return c.json(message) }, ) + .delete( + "/:sessionID/message/:messageID", + describeRoute({ + summary: "Delete message", + description: + "Permanently delete a specific message (and all of its parts) from a session. This does not revert any file changes that may have been made while processing the message.", + operationId: "session.deleteMessage", + responses: { + 200: { + description: "Successfully deleted message", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400, 404), + }, + }), + validator( + "param", + z.object({ + sessionID: z.string().meta({ description: "Session ID" }), + messageID: z.string().meta({ description: "Message ID" }), + }), + ), + async (c) => { + const params = c.req.valid("param") + SessionPrompt.assertNotBusy(params.sessionID) + await Session.removeMessage({ + sessionID: params.sessionID, + messageID: params.messageID, + }) + return c.json(true) + }, + ) .delete( "/:sessionID/message/:messageID/part/:partID", describeRoute({ diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 8454a9c3e975..22de477f8d18 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -697,7 +697,9 @@ export namespace Session { async (input) => { // CASCADE delete handles parts automatically Database.use((db) => { - db.delete(MessageTable).where(eq(MessageTable.id, input.messageID)).run() + db.delete(MessageTable) + .where(and(eq(MessageTable.id, input.messageID), eq(MessageTable.session_id, input.sessionID))) + .run() Database.effect(() => Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, @@ -717,7 +719,9 @@ export namespace Session { }), async (input) => { Database.use((db) => { - db.delete(PartTable).where(eq(PartTable.id, input.partID)).run() + db.delete(PartTable) + .where(and(eq(PartTable.id, input.partID), eq(PartTable.session_id, input.sessionID))) + .run() Database.effect(() => Bus.publish(MessageV2.Event.PartRemoved, { sessionID: input.sessionID, diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index b4848e605404..6165c0f7b096 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -107,6 +107,8 @@ import type { SessionCreateErrors, SessionCreateResponses, SessionDeleteErrors, + SessionDeleteMessageErrors, + SessionDeleteMessageResponses, SessionDeleteResponses, SessionDiffResponses, SessionForkResponses, @@ -1561,6 +1563,42 @@ export class Session2 extends HeyApiClient { }) } + /** + * Delete message + * + * Permanently delete a specific message (and all of its parts) from a session. This does not revert any file changes that may have been made while processing the message. + */ + public deleteMessage( + parameters: { + sessionID: string + messageID: string + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "query", key: "directory" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete< + SessionDeleteMessageResponses, + SessionDeleteMessageErrors, + ThrowOnError + >({ + url: "/session/{sessionID}/message/{messageID}", + ...options, + ...params, + }) + } + /** * Get message * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 4050ef15738c..28d5caa02bba 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3564,6 +3564,46 @@ export type SessionPromptResponses = { export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses] +export type SessionDeleteMessageData = { + body?: never + path: { + /** + * Session ID + */ + sessionID: string + /** + * Message ID + */ + messageID: string + } + query?: { + directory?: string + } + url: "/session/{sessionID}/message/{messageID}" +} + +export type SessionDeleteMessageErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionDeleteMessageError = SessionDeleteMessageErrors[keyof SessionDeleteMessageErrors] + +export type SessionDeleteMessageResponses = { + /** + * Successfully deleted message + */ + 200: boolean +} + +export type SessionDeleteMessageResponse = SessionDeleteMessageResponses[keyof SessionDeleteMessageResponses] + export type SessionMessageData = { body?: never path: { From de2bc25677b419d2af0da8b6a24a05d3f22b67a8 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 25 Feb 2026 14:55:56 +0000 Subject: [PATCH 62/94] release: v1.2.14 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index 52bc415a7f31..2bdab5cb6afb 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -75,7 +75,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -109,7 +109,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -136,7 +136,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -160,7 +160,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -184,7 +184,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -217,7 +217,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -246,7 +246,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -262,7 +262,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.13", + "version": "1.2.14", "bin": { "opencode": "./bin/opencode", }, @@ -376,7 +376,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -396,7 +396,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.13", + "version": "1.2.14", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -407,7 +407,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -420,7 +420,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -462,7 +462,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "zod": "catalog:", }, @@ -473,7 +473,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index beaeb2a316b6..37d2801baf78 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.13", + "version": "1.2.14", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index bfed09c3ebb6..adf2d2d28dad 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.13", + "version": "1.2.14", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 5427c906a76d..078d662072da 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.13", + "version": "1.2.14", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 6cd13bf0fcaa..ac1c6bfd89a3 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.13", + "version": "1.2.14", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 2dd381581b67..1b91c7cbe01f 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.13", + "version": "1.2.14", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 6db85fcbddb2..2bd9cce9a35e 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.13", + "version": "1.2.14", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 9d39fcf9ee5b..0cd3ec690831 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.13", + "version": "1.2.14", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 6353f7653843..436b2e9e191f 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.13" +version = "1.2.14" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.13/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.13/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.13/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.13/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.13/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 04969d9827d5..7a68ef5b9d29 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.13", + "version": "1.2.14", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 0e3594dc7d79..e23d2e41ad3e 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.13", + "version": "1.2.14", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 339b87647807..c4ed60455abe 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.13", + "version": "1.2.14", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 158683ba4933..3faee471736b 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.13", + "version": "1.2.14", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index e241919b2ca7..c61ff7521b34 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.13", + "version": "1.2.14", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 7a198d9bae14..08f46d633bc7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.13", + "version": "1.2.14", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index fa89670d9799..d389d3ade1b2 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.13", + "version": "1.2.14", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index a8f7decb4576..12bffe86d6b3 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.13", + "version": "1.2.14", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index cfe9d6cedb4c..a661b25d80f0 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.13", + "version": "1.2.14", "publisher": "sst-dev", "repository": { "type": "git", From 5e5823ed85ff83e1e3461b861fb582f27cc38969 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 25 Feb 2026 14:26:38 +0000 Subject: [PATCH 63/94] chore: generate --- packages/sdk/openapi.json | 70 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 2741c2362ec4..80a4a8d72ae0 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -2630,6 +2630,76 @@ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.message({\n ...\n})" } ] + }, + "delete": { + "operationId": "session.deleteMessage", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + }, + { + "in": "path", + "name": "messageID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Message ID" + } + ], + "summary": "Delete message", + "description": "Permanently delete a specific message (and all of its parts) from a session. This does not revert any file changes that may have been made while processing the message.", + "responses": { + "200": { + "description": "Successfully deleted message", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.deleteMessage({\n ...\n})" + } + ] } }, "/session/{sessionID}/message/{messageID}/part/{partID}": { From e48c1ccf0714a2a78a8bbf27e8ade0b8bcbdcf3b Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Wed, 25 Feb 2026 09:42:52 -0500 Subject: [PATCH 64/94] chore(workflows): label vouched users and restrict vouch managers (#15075) --- .github/workflows/vouch-check-issue.yml | 58 ++++++++++++++------- .github/workflows/vouch-check-pr.yml | 55 +++++++++++++------ .github/workflows/vouch-manage-by-issue.yml | 1 + 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/.github/workflows/vouch-check-issue.yml b/.github/workflows/vouch-check-issue.yml index 94569f47312a..4c2aa960b2a8 100644 --- a/.github/workflows/vouch-check-issue.yml +++ b/.github/workflows/vouch-check-issue.yml @@ -42,15 +42,17 @@ jobs: throw error; } - // Parse the .td file for denounced users + // Parse the .td file for vouched and denounced users + const vouched = new Set(); const denounced = new Map(); for (const line of content.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; - if (!trimmed.startsWith('-')) continue; - const rest = trimmed.slice(1).trim(); + const isDenounced = trimmed.startsWith('-'); + const rest = isDenounced ? trimmed.slice(1).trim() : trimmed; if (!rest) continue; + const spaceIdx = rest.indexOf(' '); const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx); const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim(); @@ -65,32 +67,50 @@ jobs: const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1); if (!username) continue; - denounced.set(username.toLowerCase(), reason); + if (isDenounced) { + denounced.set(username.toLowerCase(), reason); + continue; + } + + vouched.add(username.toLowerCase()); } // Check if the author is denounced const reason = denounced.get(author.toLowerCase()); - if (reason === undefined) { - core.info(`User ${author} is not denounced. Allowing issue.`); + if (reason !== undefined) { + // Author is denounced — close the issue + const body = 'This issue has been automatically closed.'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body, + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + state: 'closed', + state_reason: 'not_planned', + }); + + core.info(`Closed issue #${issueNumber} from denounced user ${author}`); return; } - // Author is denounced — close the issue - const body = 'This issue has been automatically closed.'; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body, - }); + // Author is positively vouched — add label + if (!vouched.has(author.toLowerCase())) { + core.info(`User ${author} is not denounced or vouched. Allowing issue.`); + return; + } - await github.rest.issues.update({ + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, - state: 'closed', - state_reason: 'not_planned', + labels: ['Vouched'], }); - core.info(`Closed issue #${issueNumber} from denounced user ${author}`); + core.info(`Added vouched label to issue #${issueNumber} from ${author}`); diff --git a/.github/workflows/vouch-check-pr.yml b/.github/workflows/vouch-check-pr.yml index 470b8e0a5ad7..51816dfb7590 100644 --- a/.github/workflows/vouch-check-pr.yml +++ b/.github/workflows/vouch-check-pr.yml @@ -6,6 +6,7 @@ on: permissions: contents: read + issues: write pull-requests: write jobs: @@ -42,15 +43,17 @@ jobs: throw error; } - // Parse the .td file for denounced users + // Parse the .td file for vouched and denounced users + const vouched = new Set(); const denounced = new Map(); for (const line of content.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; - if (!trimmed.startsWith('-')) continue; - const rest = trimmed.slice(1).trim(); + const isDenounced = trimmed.startsWith('-'); + const rest = isDenounced ? trimmed.slice(1).trim() : trimmed; if (!rest) continue; + const spaceIdx = rest.indexOf(' '); const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx); const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim(); @@ -65,29 +68,47 @@ jobs: const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1); if (!username) continue; - denounced.set(username.toLowerCase(), reason); + if (isDenounced) { + denounced.set(username.toLowerCase(), reason); + continue; + } + + vouched.add(username.toLowerCase()); } // Check if the author is denounced const reason = denounced.get(author.toLowerCase()); - if (reason === undefined) { - core.info(`User ${author} is not denounced. Allowing PR.`); + if (reason !== undefined) { + // Author is denounced — close the PR + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: 'This pull request has been automatically closed.', + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + state: 'closed', + }); + + core.info(`Closed PR #${prNumber} from denounced user ${author}`); return; } - // Author is denounced — close the PR - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: 'This pull request has been automatically closed.', - }); + // Author is positively vouched — add label + if (!vouched.has(author.toLowerCase())) { + core.info(`User ${author} is not denounced or vouched. Allowing PR.`); + return; + } - await github.rest.pulls.update({ + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, - pull_number: prNumber, - state: 'closed', + issue_number: prNumber, + labels: ['Vouched'], }); - core.info(`Closed PR #${prNumber} from denounced user ${author}`); + core.info(`Added vouched label to PR #${prNumber} from ${author}`); diff --git a/.github/workflows/vouch-manage-by-issue.yml b/.github/workflows/vouch-manage-by-issue.yml index cf0524c21a8e..9604bf87f375 100644 --- a/.github/workflows/vouch-manage-by-issue.yml +++ b/.github/workflows/vouch-manage-by-issue.yml @@ -33,5 +33,6 @@ jobs: with: issue-id: ${{ github.event.issue.number }} comment-id: ${{ github.event.comment.id }} + roles: admin,maintain env: GITHUB_TOKEN: ${{ steps.committer.outputs.token }} From 286992269623bcb410f0de89e128ff14361d5e97 Mon Sep 17 00:00:00 2001 From: Oleksii Pavliuk <71220725+Oleksii-Pavliuk@users.noreply.github.com> Date: Thu, 26 Feb 2026 01:54:15 +1100 Subject: [PATCH 65/94] fix(app): correct Copilot provider description in i18n files (#15071) --- packages/app/src/i18n/bs.ts | 2 +- packages/app/src/i18n/da.ts | 2 +- packages/app/src/i18n/en.ts | 2 +- packages/app/src/i18n/es.ts | 2 +- packages/app/src/i18n/no.ts | 2 +- packages/app/src/i18n/pl.ts | 2 +- packages/app/src/i18n/ru.ts | 2 +- packages/app/src/i18n/th.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index d658926268e2..cb0274042ed0 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -100,7 +100,7 @@ export const dict = { "dialog.provider.tag.recommended": "Preporučeno", "dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge", "dialog.provider.anthropic.note": "Direktan pristup Claude modelima, uključujući Pro i Max", - "dialog.provider.copilot.note": "Claude modeli za pomoć pri kodiranju", + "dialog.provider.copilot.note": "AI modeli za pomoć pri kodiranju putem GitHub Copilot", "dialog.provider.openai.note": "GPT modeli za brze, sposobne opšte AI zadatke", "dialog.provider.google.note": "Gemini modeli za brze, strukturirane odgovore", "dialog.provider.openrouter.note": "Pristup svim podržanim modelima preko jednog provajdera", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index fabefcab7562..30cc555eb1a0 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -100,7 +100,7 @@ export const dict = { "dialog.provider.tag.recommended": "Anbefalet", "dialog.provider.opencode.note": "Udvalgte modeller inklusive Claude, GPT, Gemini og flere", "dialog.provider.anthropic.note": "Direkte adgang til Claude-modeller, inklusive Pro og Max", - "dialog.provider.copilot.note": "Claude-modeller til kodningsassistance", + "dialog.provider.copilot.note": "AI-modeller til kodningsassistance via GitHub Copilot", "dialog.provider.openai.note": "GPT-modeller til hurtige, kompetente generelle AI-opgaver", "dialog.provider.google.note": "Gemini-modeller til hurtige, strukturerede svar", "dialog.provider.openrouter.note": "Få adgang til alle understøttede modeller fra én udbyder", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 992509fcfa4e..0b4388ceb19a 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -100,7 +100,7 @@ export const dict = { "dialog.provider.tag.recommended": "Recommended", "dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more", "dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max", - "dialog.provider.copilot.note": "Claude models for coding assistance", + "dialog.provider.copilot.note": "AI models for coding assistance via GitHub Copilot", "dialog.provider.openai.note": "GPT models for fast, capable general AI tasks", "dialog.provider.google.note": "Gemini models for fast, structured responses", "dialog.provider.openrouter.note": "Access all supported models from one provider", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index b55d54c0ca55..3566226d7bfd 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -100,7 +100,7 @@ export const dict = { "dialog.provider.tag.recommended": "Recomendado", "dialog.provider.opencode.note": "Modelos seleccionados incluyendo Claude, GPT, Gemini y más", "dialog.provider.anthropic.note": "Acceso directo a modelos Claude, incluyendo Pro y Max", - "dialog.provider.copilot.note": "Modelos Claude para asistencia de codificación", + "dialog.provider.copilot.note": "Modelos de IA para asistencia de codificación a través de GitHub Copilot", "dialog.provider.openai.note": "Modelos GPT para tareas de IA generales rápidas y capaces", "dialog.provider.google.note": "Modelos Gemini para respuestas rápidas y estructuradas", "dialog.provider.openrouter.note": "Accede a todos los modelos soportados desde un solo proveedor", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 8e1b1ce629dc..3fbe75716d0c 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -103,7 +103,7 @@ export const dict = { "dialog.provider.tag.recommended": "Anbefalt", "dialog.provider.opencode.note": "Utvalgte modeller inkludert Claude, GPT, Gemini og mer", "dialog.provider.anthropic.note": "Direkte tilgang til Claude-modeller, inkludert Pro og Max", - "dialog.provider.copilot.note": "Claude-modeller for kodeassistanse", + "dialog.provider.copilot.note": "AI-modeller for kodeassistanse via GitHub Copilot", "dialog.provider.openai.note": "GPT-modeller for raske, dyktige generelle AI-oppgaver", "dialog.provider.google.note": "Gemini-modeller for raske, strukturerte svar", "dialog.provider.openrouter.note": "Tilgang til alle støttede modeller fra én leverandør", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 9b924fd642ea..d8ae150d7cb5 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -92,7 +92,7 @@ export const dict = { "dialog.provider.tag.recommended": "Zalecane", "dialog.provider.opencode.note": "Wyselekcjonowane modele, w tym Claude, GPT, Gemini i inne", "dialog.provider.anthropic.note": "Bezpośredni dostęp do modeli Claude, w tym Pro i Max", - "dialog.provider.copilot.note": "Modele Claude do pomocy w kodowaniu", + "dialog.provider.copilot.note": "Modele AI do pomocy w kodowaniu przez GitHub Copilot", "dialog.provider.openai.note": "Modele GPT do szybkich i wszechstronnych zadań AI", "dialog.provider.google.note": "Modele Gemini do szybkich i ustrukturyzowanych odpowiedzi", "dialog.provider.openrouter.note": "Dostęp do wszystkich obsługiwanych modeli od jednego dostawcy", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index cf02285821e4..a7d328924a41 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -100,7 +100,7 @@ export const dict = { "dialog.provider.tag.recommended": "Рекомендуемые", "dialog.provider.opencode.note": "Отобранные модели, включая Claude, GPT, Gemini и другие", "dialog.provider.anthropic.note": "Прямой доступ к моделям Claude, включая Pro и Max", - "dialog.provider.copilot.note": "Модели Claude для помощи в кодировании", + "dialog.provider.copilot.note": "ИИ-модели для помощи в кодировании через GitHub Copilot", "dialog.provider.openai.note": "Модели GPT для быстрых и мощных задач общего ИИ", "dialog.provider.google.note": "Модели Gemini для быстрых и структурированных ответов", "dialog.provider.openrouter.note": "Доступ ко всем поддерживаемым моделям через одного провайдера", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 1b8abe953b77..1e9773bf021c 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -100,7 +100,7 @@ export const dict = { "dialog.provider.tag.recommended": "แนะนำ", "dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ", "dialog.provider.anthropic.note": "เข้าถึงโมเดล Claude โดยตรง รวมถึง Pro และ Max", - "dialog.provider.copilot.note": "โมเดล Claude สำหรับการช่วยเหลือในการเขียนโค้ด", + "dialog.provider.copilot.note": "โมเดล AI สำหรับการช่วยเหลือในการเขียนโค้ดผ่าน GitHub Copilot", "dialog.provider.openai.note": "โมเดล GPT สำหรับงาน AI ทั่วไปที่รวดเร็วและมีความสามารถ", "dialog.provider.google.note": "โมเดล Gemini สำหรับการตอบสนองที่รวดเร็วและมีโครงสร้าง", "dialog.provider.openrouter.note": "เข้าถึงโมเดลที่รองรับทั้งหมดจากผู้ให้บริการเดียว", From 45191ad144f6546c051fb3a94f9f3cb1e2c00ed3 Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:57:13 +0100 Subject: [PATCH 66/94] fix(app): keyboard navigation previous/next message (#15047) --- packages/app/src/pages/session.tsx | 11 ++++++----- packages/app/src/pages/session/message-timeline.tsx | 1 + .../app/src/pages/session/use-session-hash-scroll.ts | 4 +++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index e0ef92682d94..2e440a6b036c 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -254,12 +254,13 @@ export default function Page() { const msgs = visibleUserMessages() if (msgs.length === 0) return - const current = activeMessage() - const currentIndex = current ? msgs.findIndex((m) => m.id === current.id) : -1 - const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset - if (targetIndex < 0 || targetIndex >= msgs.length) return + const current = store.messageId + const base = current ? msgs.findIndex((m) => m.id === current) : msgs.length + const currentIndex = base === -1 ? msgs.length : base + const targetIndex = currentIndex + offset + if (targetIndex < 0 || targetIndex > msgs.length) return - if (targetIndex === msgs.length - 1) { + if (targetIndex === msgs.length) { resumeScroll() return } diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 615d1a0bea4d..b84109035507 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -376,6 +376,7 @@ export function MessageTimeline(props: { >
Date: Fri, 20 Feb 2026 01:46:21 +0000 Subject: [PATCH 67/94] tweak(ui): keep reasoning inline code subdued in dark mode --- packages/ui/src/components/message-part.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index ce76d8e18877..f063076e079b 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -252,6 +252,12 @@ } } +@media (prefers-color-scheme: dark) { + [data-component="reasoning-part"] [data-component="markdown"] :not(pre) > code { + opacity: 0.6; + } +} + [data-component="tool-error"] { display: flex; align-items: start; From b368181ac90b0365af535b3d0bd8284c2032240c Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:29:02 -0600 Subject: [PATCH 68/94] chore: move glossary --- .github/workflows/docs-locale-sync.yml | 8 ++++---- .opencode/agent/translator.md | 2 +- .opencode/{agent => }/glossary/README.md | 0 .opencode/{agent => }/glossary/ar.md | 0 .opencode/{agent => }/glossary/br.md | 0 .opencode/{agent => }/glossary/bs.md | 0 .opencode/{agent => }/glossary/da.md | 0 .opencode/{agent => }/glossary/de.md | 0 .opencode/{agent => }/glossary/es.md | 0 .opencode/{agent => }/glossary/fr.md | 0 .opencode/{agent => }/glossary/ja.md | 0 .opencode/{agent => }/glossary/ko.md | 0 .opencode/{agent => }/glossary/no.md | 0 .opencode/{agent => }/glossary/pl.md | 0 .opencode/{agent => }/glossary/ru.md | 0 .opencode/{agent => }/glossary/th.md | 0 .opencode/{agent => }/glossary/zh-cn.md | 0 .opencode/{agent => }/glossary/zh-tw.md | 0 18 files changed, 5 insertions(+), 5 deletions(-) rename .opencode/{agent => }/glossary/README.md (100%) rename .opencode/{agent => }/glossary/ar.md (100%) rename .opencode/{agent => }/glossary/br.md (100%) rename .opencode/{agent => }/glossary/bs.md (100%) rename .opencode/{agent => }/glossary/da.md (100%) rename .opencode/{agent => }/glossary/de.md (100%) rename .opencode/{agent => }/glossary/es.md (100%) rename .opencode/{agent => }/glossary/fr.md (100%) rename .opencode/{agent => }/glossary/ja.md (100%) rename .opencode/{agent => }/glossary/ko.md (100%) rename .opencode/{agent => }/glossary/no.md (100%) rename .opencode/{agent => }/glossary/pl.md (100%) rename .opencode/{agent => }/glossary/ru.md (100%) rename .opencode/{agent => }/glossary/th.md (100%) rename .opencode/{agent => }/glossary/zh-cn.md (100%) rename .opencode/{agent => }/glossary/zh-tw.md (100%) diff --git a/.github/workflows/docs-locale-sync.yml b/.github/workflows/docs-locale-sync.yml index 1aafc5d1e3b1..f62afae4b9f1 100644 --- a/.github/workflows/docs-locale-sync.yml +++ b/.github/workflows/docs-locale-sync.yml @@ -65,9 +65,9 @@ jobs: "packages/web/src/content/docs/*/*.mdx": "allow", ".opencode": "allow", ".opencode/agent": "allow", - ".opencode/agent/glossary": "allow", + ".opencode/glossary": "allow", ".opencode/agent/translator.md": "allow", - ".opencode/agent/glossary/*.md": "allow" + ".opencode/glossary/*.md": "allow" }, "edit": { "*": "deny", @@ -76,7 +76,7 @@ jobs: "glob": { "*": "deny", "packages/web/src/content/docs*": "allow", - ".opencode/agent/glossary*": "allow" + ".opencode/glossary*": "allow" }, "task": { "*": "deny", @@ -90,7 +90,7 @@ jobs: "read": { "*": "deny", ".opencode/agent/translator.md": "allow", - ".opencode/agent/glossary/*.md": "allow" + ".opencode/glossary/*.md": "allow" } } } diff --git a/.opencode/agent/translator.md b/.opencode/agent/translator.md index f0b3f8e9270b..6ef6d0847a37 100644 --- a/.opencode/agent/translator.md +++ b/.opencode/agent/translator.md @@ -13,7 +13,7 @@ Requirements: - Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure). - Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks. - Also preserve every term listed in the Do-Not-Translate glossary below. -- Also apply locale-specific guidance from `.opencode/agent/glossary/.md` when available (for example, `zh-cn.md`). +- Also apply locale-specific guidance from `.opencode/glossary/.md` when available (for example, `zh-cn.md`). - Do not modify fenced code blocks. - Output ONLY the translation (no commentary). diff --git a/.opencode/agent/glossary/README.md b/.opencode/glossary/README.md similarity index 100% rename from .opencode/agent/glossary/README.md rename to .opencode/glossary/README.md diff --git a/.opencode/agent/glossary/ar.md b/.opencode/glossary/ar.md similarity index 100% rename from .opencode/agent/glossary/ar.md rename to .opencode/glossary/ar.md diff --git a/.opencode/agent/glossary/br.md b/.opencode/glossary/br.md similarity index 100% rename from .opencode/agent/glossary/br.md rename to .opencode/glossary/br.md diff --git a/.opencode/agent/glossary/bs.md b/.opencode/glossary/bs.md similarity index 100% rename from .opencode/agent/glossary/bs.md rename to .opencode/glossary/bs.md diff --git a/.opencode/agent/glossary/da.md b/.opencode/glossary/da.md similarity index 100% rename from .opencode/agent/glossary/da.md rename to .opencode/glossary/da.md diff --git a/.opencode/agent/glossary/de.md b/.opencode/glossary/de.md similarity index 100% rename from .opencode/agent/glossary/de.md rename to .opencode/glossary/de.md diff --git a/.opencode/agent/glossary/es.md b/.opencode/glossary/es.md similarity index 100% rename from .opencode/agent/glossary/es.md rename to .opencode/glossary/es.md diff --git a/.opencode/agent/glossary/fr.md b/.opencode/glossary/fr.md similarity index 100% rename from .opencode/agent/glossary/fr.md rename to .opencode/glossary/fr.md diff --git a/.opencode/agent/glossary/ja.md b/.opencode/glossary/ja.md similarity index 100% rename from .opencode/agent/glossary/ja.md rename to .opencode/glossary/ja.md diff --git a/.opencode/agent/glossary/ko.md b/.opencode/glossary/ko.md similarity index 100% rename from .opencode/agent/glossary/ko.md rename to .opencode/glossary/ko.md diff --git a/.opencode/agent/glossary/no.md b/.opencode/glossary/no.md similarity index 100% rename from .opencode/agent/glossary/no.md rename to .opencode/glossary/no.md diff --git a/.opencode/agent/glossary/pl.md b/.opencode/glossary/pl.md similarity index 100% rename from .opencode/agent/glossary/pl.md rename to .opencode/glossary/pl.md diff --git a/.opencode/agent/glossary/ru.md b/.opencode/glossary/ru.md similarity index 100% rename from .opencode/agent/glossary/ru.md rename to .opencode/glossary/ru.md diff --git a/.opencode/agent/glossary/th.md b/.opencode/glossary/th.md similarity index 100% rename from .opencode/agent/glossary/th.md rename to .opencode/glossary/th.md diff --git a/.opencode/agent/glossary/zh-cn.md b/.opencode/glossary/zh-cn.md similarity index 100% rename from .opencode/agent/glossary/zh-cn.md rename to .opencode/glossary/zh-cn.md diff --git a/.opencode/agent/glossary/zh-tw.md b/.opencode/glossary/zh-tw.md similarity index 100% rename from .opencode/agent/glossary/zh-tw.md rename to .opencode/glossary/zh-tw.md From 1172fa418e9aa5e0fcfccea326c6c9d35e1d57fd Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 25 Feb 2026 12:39:48 -0500 Subject: [PATCH 69/94] wip: zen go --- infra/console.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/console.ts b/infra/console.ts index 283fe2c37cad..de72cb072eed 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -101,7 +101,7 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint", }) const zenLiteProduct = new stripe.Product("ZenLite", { - name: "OpenCode Lite", + name: "OpenCode Go", }) const zenLitePrice = new stripe.Price("ZenLitePrice", { product: zenLiteProduct.id, From 9d29d692c6d93322f5894cca4232d80106e7c81a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 25 Feb 2026 23:53:09 +0100 Subject: [PATCH 70/94] split tui/server config (#13968) --- packages/console/app/package.json | 2 +- packages/opencode/script/schema.ts | 90 ++-- packages/opencode/src/cli/cmd/tui/app.tsx | 63 ++- packages/opencode/src/cli/cmd/tui/attach.ts | 8 + .../cli/cmd/tui/component/dialog-command.tsx | 5 +- .../src/cli/cmd/tui/component/tips.tsx | 8 +- .../src/cli/cmd/tui/context/keybind.tsx | 16 +- .../src/cli/cmd/tui/context/theme.tsx | 8 +- .../src/cli/cmd/tui/context/tui-config.tsx | 9 + .../src/cli/cmd/tui/routes/session/index.tsx | 10 +- .../cli/cmd/tui/routes/session/permission.tsx | 5 +- packages/opencode/src/cli/cmd/tui/thread.ts | 8 + packages/opencode/src/config/config.ts | 182 ++----- .../opencode/src/config/migrate-tui-config.ts | 155 ++++++ packages/opencode/src/config/paths.ts | 174 ++++++ packages/opencode/src/config/tui-schema.ts | 34 ++ packages/opencode/src/config/tui.ts | 118 ++++ packages/opencode/src/flag/flag.ts | 12 + packages/opencode/test/config/config.test.ts | 54 +- packages/opencode/test/config/tui.test.ts | 510 ++++++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 409 -------------- packages/web/astro.config.mjs | 2 +- packages/web/src/content/docs/cli.mdx | 1 + packages/web/src/content/docs/config.mdx | 57 +- packages/web/src/content/docs/keybinds.mdx | 12 +- packages/web/src/content/docs/themes.mdx | 6 +- packages/web/src/content/docs/tui.mdx | 32 +- 27 files changed, 1284 insertions(+), 706 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/context/tui-config.tsx create mode 100644 packages/opencode/src/config/migrate-tui-config.ts create mode 100644 packages/opencode/src/config/paths.ts create mode 100644 packages/opencode/src/config/tui-schema.ts create mode 100644 packages/opencode/src/config/tui.ts create mode 100644 packages/opencode/test/config/tui.test.ts diff --git a/packages/console/app/package.json b/packages/console/app/package.json index adf2d2d28dad..05d2309a423d 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,7 +7,7 @@ "typecheck": "tsgo --noEmit", "dev": "vite dev --host 0.0.0.0", "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev", - "build": "bun ./script/generate-sitemap.ts && vite build && bun ../../opencode/script/schema.ts ./.output/public/config.json", + "build": "bun ./script/generate-sitemap.ts && vite build && bun ../../opencode/script/schema.ts ./.output/public/config.json ./.output/public/tui.json", "start": "vite start" }, "dependencies": { diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts index 585701c95184..61d11ea7c93c 100755 --- a/packages/opencode/script/schema.ts +++ b/packages/opencode/script/schema.ts @@ -2,46 +2,62 @@ import { z } from "zod" import { Config } from "../src/config/config" +import { TuiConfig } from "../src/config/tui" + +function generate(schema: z.ZodType) { + const result = z.toJSONSchema(schema, { + io: "input", // Generate input shape (treats optional().default() as not required) + /** + * We'll use the `default` values of the field as the only value in `examples`. + * This will ensure no docs are needed to be read, as the configuration is + * self-documenting. + * + * See https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.9.5 + */ + override(ctx) { + const schema = ctx.jsonSchema + + // Preserve strictness: set additionalProperties: false for objects + if ( + schema && + typeof schema === "object" && + schema.type === "object" && + schema.additionalProperties === undefined + ) { + schema.additionalProperties = false + } + + // Add examples and default descriptions for string fields with defaults + if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) { + if (!schema.examples) { + schema.examples = [schema.default] + } -const file = process.argv[2] -console.log(file) - -const result = z.toJSONSchema(Config.Info, { - io: "input", // Generate input shape (treats optional().default() as not required) - /** - * We'll use the `default` values of the field as the only value in `examples`. - * This will ensure no docs are needed to be read, as the configuration is - * self-documenting. - * - * See https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.9.5 - */ - override(ctx) { - const schema = ctx.jsonSchema - - // Preserve strictness: set additionalProperties: false for objects - if (schema && typeof schema === "object" && schema.type === "object" && schema.additionalProperties === undefined) { - schema.additionalProperties = false - } - - // Add examples and default descriptions for string fields with defaults - if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) { - if (!schema.examples) { - schema.examples = [schema.default] + schema.description = [schema.description || "", `default: \`${schema.default}\``] + .filter(Boolean) + .join("\n\n") + .trim() } + }, + }) as Record & { + allowComments?: boolean + allowTrailingCommas?: boolean + } + + // used for json lsps since config supports jsonc + result.allowComments = true + result.allowTrailingCommas = true - schema.description = [schema.description || "", `default: \`${schema.default}\``] - .filter(Boolean) - .join("\n\n") - .trim() - } - }, -}) as Record & { - allowComments?: boolean - allowTrailingCommas?: boolean + return result } -// used for json lsps since config supports jsonc -result.allowComments = true -result.allowTrailingCommas = true +const configFile = process.argv[2] +const tuiFile = process.argv[3] -await Bun.write(file, JSON.stringify(result, null, 2)) +console.log(configFile) +await Bun.write(configFile, JSON.stringify(generate(Config.Info), null, 2)) + +if (tuiFile) { + console.log(tuiFile) + await Bun.write(tuiFile, JSON.stringify(generate(TuiConfig.Info), null, 2)) +} diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index ab3d09689252..97c910a47d4b 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -38,6 +38,8 @@ import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" import { writeHeapSnapshot } from "v8" import { PromptRefProvider, usePromptRef } from "./context/prompt" +import { TuiConfigProvider } from "./context/tui-config" +import { TuiConfig } from "@/config/tui" async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { // can't set raw mode if not a TTY @@ -104,6 +106,7 @@ import type { EventSource } from "./context/sdk" export function tui(input: { url: string args: Args + config: TuiConfig.Info directory?: string fetch?: typeof fetch headers?: RequestInit["headers"] @@ -138,35 +141,37 @@ export function tui(input: { - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index a2559cfce679..e892f9922d1b 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -2,6 +2,9 @@ import { cmd } from "../cmd" import { UI } from "@/cli/ui" import { tui } from "./app" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" +import { TuiConfig } from "@/config/tui" +import { Instance } from "@/project/instance" +import { existsSync } from "fs" export const AttachCommand = cmd({ command: "attach ", @@ -63,8 +66,13 @@ export const AttachCommand = cmd({ const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` return { Authorization: auth } })() + const config = await Instance.provide({ + directory: directory && existsSync(directory) ? directory : process.cwd(), + fn: () => TuiConfig.get(), + }) await tui({ url: args.url, + config, args: { continue: args.continue, sessionID: args.session, diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx index 38dc402758b2..be031296e905 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx @@ -10,8 +10,7 @@ import { type ParentProps, } from "solid-js" import { useKeyboard } from "@opentui/solid" -import { useKeybind } from "@tui/context/keybind" -import type { KeybindsConfig } from "@opencode-ai/sdk/v2" +import { type KeybindKey, useKeybind } from "@tui/context/keybind" type Context = ReturnType const ctx = createContext() @@ -22,7 +21,7 @@ export type Slash = { } export type CommandOption = DialogSelectOption & { - keybind?: keyof KeybindsConfig + keybind?: KeybindKey suggested?: boolean slash?: Slash hidden?: boolean diff --git a/packages/opencode/src/cli/cmd/tui/component/tips.tsx b/packages/opencode/src/cli/cmd/tui/component/tips.tsx index d0a7e5b44eca..73d82248adb4 100644 --- a/packages/opencode/src/cli/cmd/tui/component/tips.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/tips.tsx @@ -80,11 +80,11 @@ const TIPS = [ "Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes", "Use {highlight}@agent-name{/highlight} in prompts to invoke specialized subagents", "Press {highlight}Ctrl+X Right/Left{/highlight} to cycle through parent and child sessions", - "Create {highlight}opencode.json{/highlight} in project root for project-specific settings", - "Place settings in {highlight}~/.config/opencode/opencode.json{/highlight} for global config", + "Create {highlight}opencode.json{/highlight} for server settings and {highlight}tui.json{/highlight} for TUI settings", + "Place TUI settings in {highlight}~/.config/opencode/tui.json{/highlight} for global config", "Add {highlight}$schema{/highlight} to your config for autocomplete in your editor", "Configure {highlight}model{/highlight} in config to set your default model", - "Override any keybind in config via the {highlight}keybinds{/highlight} section", + "Override any keybind in {highlight}tui.json{/highlight} via the {highlight}keybinds{/highlight} section", "Set any keybind to {highlight}none{/highlight} to disable it completely", "Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section", "OpenCode auto-handles OAuth for remote MCP servers requiring auth", @@ -140,7 +140,7 @@ const TIPS = [ "Press {highlight}Ctrl+X G{/highlight} or {highlight}/timeline{/highlight} to jump to specific messages", "Press {highlight}Ctrl+X H{/highlight} to toggle code block visibility in messages", "Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info", - "Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling", + "Enable {highlight}scroll_acceleration{/highlight} in {highlight}tui.json{/highlight} for smooth macOS-style scrolling", "Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight})", "Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} for containerized use", "Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models", diff --git a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx index 0dbbbc6f9ee1..566d66ade508 100644 --- a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx @@ -1,20 +1,22 @@ import { createMemo } from "solid-js" -import { useSync } from "@tui/context/sync" import { Keybind } from "@/util/keybind" import { pipe, mapValues } from "remeda" -import type { KeybindsConfig } from "@opencode-ai/sdk/v2" +import type { TuiConfig } from "@/config/tui" import type { ParsedKey, Renderable } from "@opentui/core" import { createStore } from "solid-js/store" import { useKeyboard, useRenderer } from "@opentui/solid" import { createSimpleContext } from "./helper" +import { useTuiConfig } from "./tui-config" + +export type KeybindKey = keyof NonNullable & string export const { use: useKeybind, provider: KeybindProvider } = createSimpleContext({ name: "Keybind", init: () => { - const sync = useSync() - const keybinds = createMemo(() => { + const config = useTuiConfig() + const keybinds = createMemo>(() => { return pipe( - sync.data.config.keybinds ?? {}, + (config.keybinds ?? {}) as Record, mapValues((value) => Keybind.parse(value)), ) }) @@ -78,7 +80,7 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex } return Keybind.fromParsedKey(evt, store.leader) }, - match(key: keyof KeybindsConfig, evt: ParsedKey) { + match(key: KeybindKey, evt: ParsedKey) { const keybind = keybinds()[key] if (!keybind) return false const parsed: Keybind.Info = result.parse(evt) @@ -88,7 +90,7 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex } } }, - print(key: keyof KeybindsConfig) { + print(key: KeybindKey) { const first = keybinds()[key]?.at(0) if (!first) return "" const result = Keybind.toString(first) diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 465ed805ea17..2320c08ccc6e 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -1,7 +1,6 @@ import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core" import path from "path" import { createEffect, createMemo, onMount } from "solid-js" -import { useSync } from "@tui/context/sync" import { createSimpleContext } from "./helper" import { Glob } from "../../../../util/glob" import aura from "./theme/aura.json" with { type: "json" } @@ -42,6 +41,7 @@ import { useRenderer } from "@opentui/solid" import { createStore, produce } from "solid-js/store" import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" +import { useTuiConfig } from "./tui-config" type ThemeColors = { primary: RGBA @@ -280,17 +280,17 @@ function ansiToRgba(code: number): RGBA { export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ name: "Theme", init: (props: { mode: "dark" | "light" }) => { - const sync = useSync() + const config = useTuiConfig() const kv = useKV() const [store, setStore] = createStore({ themes: DEFAULT_THEMES, mode: kv.get("theme_mode", props.mode), - active: (sync.data.config.theme ?? kv.get("theme", "opencode")) as string, + active: (config.theme ?? kv.get("theme", "opencode")) as string, ready: false, }) createEffect(() => { - const theme = sync.data.config.theme + const theme = config.theme if (theme) setStore("active", theme) }) diff --git a/packages/opencode/src/cli/cmd/tui/context/tui-config.tsx b/packages/opencode/src/cli/cmd/tui/context/tui-config.tsx new file mode 100644 index 000000000000..62dbf1ebd1b9 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/context/tui-config.tsx @@ -0,0 +1,9 @@ +import { TuiConfig } from "@/config/tui" +import { createSimpleContext } from "./helper" + +export const { use: useTuiConfig, provider: TuiConfigProvider } = createSimpleContext({ + name: "TuiConfig", + init: (props: { config: TuiConfig.Info }) => { + return props.config + }, +}) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 365eb3314726..f20267e0820e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -78,6 +78,7 @@ import { QuestionPrompt } from "./question" import { DialogExportOptions } from "../../ui/dialog-export-options" import { formatTranscript } from "../../util/transcript" import { UI } from "@/cli/ui.ts" +import { useTuiConfig } from "../../context/tui-config" addDefaultParsers(parsers.parsers) @@ -101,6 +102,7 @@ const context = createContext<{ showGenericToolOutput: () => boolean diffWrapMode: () => "word" | "none" sync: ReturnType + tui: ReturnType }>() function use() { @@ -113,6 +115,7 @@ export function Session() { const route = useRouteData("session") const { navigate } = useRoute() const sync = useSync() + const tuiConfig = useTuiConfig() const kv = useKV() const { theme } = useTheme() const promptRef = usePromptRef() @@ -166,7 +169,7 @@ export function Session() { const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4) const scrollAcceleration = createMemo(() => { - const tui = sync.data.config.tui + const tui = tuiConfig if (tui?.scroll_acceleration?.enabled) { return new MacOSScrollAccel() } @@ -988,6 +991,7 @@ export function Session() { showGenericToolOutput, diffWrapMode, sync, + tui: tuiConfig, }} > @@ -1949,7 +1953,7 @@ function Edit(props: ToolProps) { const { theme, syntax } = useTheme() const view = createMemo(() => { - const diffStyle = ctx.sync.data.config.tui?.diff_style + const diffStyle = ctx.tui.diff_style if (diffStyle === "stacked") return "unified" // Default to "auto" behavior return ctx.width > 120 ? "split" : "unified" @@ -2003,7 +2007,7 @@ function ApplyPatch(props: ToolProps) { const files = createMemo(() => props.metadata.files ?? []) const view = createMemo(() => { - const diffStyle = ctx.sync.data.config.tui?.diff_style + const diffStyle = ctx.tui.diff_style if (diffStyle === "stacked") return "unified" return ctx.width > 120 ? "split" : "unified" }) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index 389fc2418cc6..a50cd96fc843 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -15,6 +15,7 @@ import { Keybind } from "@/util/keybind" import { Locale } from "@/util/locale" import { Global } from "@/global" import { useDialog } from "../../ui/dialog" +import { useTuiConfig } from "../../context/tui-config" type PermissionStage = "permission" | "always" | "reject" @@ -48,14 +49,14 @@ function EditBody(props: { request: PermissionRequest }) { const themeState = useTheme() const theme = themeState.theme const syntax = themeState.syntax - const sync = useSync() + const config = useTuiConfig() const dimensions = useTerminalDimensions() const filepath = createMemo(() => (props.request.metadata?.filepath as string) ?? "") const diff = createMemo(() => (props.request.metadata?.diff as string) ?? "") const view = createMemo(() => { - const diffStyle = sync.data.config.tui?.diff_style + const diffStyle = config.diff_style if (diffStyle === "stacked") return "unified" return dimensions().width > 120 ? "split" : "unified" }) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 50f63c3dfbd1..750347d9d636 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -12,6 +12,8 @@ import { Filesystem } from "@/util/filesystem" import type { Event } from "@opencode-ai/sdk/v2" import type { EventSource } from "./context/sdk" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" +import { TuiConfig } from "@/config/tui" +import { Instance } from "@/project/instance" declare global { const OPENCODE_WORKER_PATH: string @@ -135,6 +137,10 @@ export const TuiThreadCommand = cmd({ if (!args.prompt) return piped return piped ? piped + "\n" + args.prompt : args.prompt }) + const config = await Instance.provide({ + directory: cwd, + fn: () => TuiConfig.get(), + }) // Check if server should be started (port or hostname explicitly set in CLI or config) const networkOpts = await resolveNetworkOptions(args) @@ -163,6 +169,8 @@ export const TuiThreadCommand = cmd({ const tuiPromise = tui({ url, + config, + directory: cwd, fetch: customFetch, events, args: { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 761ce23f3d6c..28aea4d67772 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -4,7 +4,6 @@ import { pathToFileURL, fileURLToPath } from "url" import { createRequire } from "module" import os from "os" import z from "zod" -import { Filesystem } from "../util/filesystem" import { ModelsDev } from "../provider/models" import { mergeDeep, pipe, unique } from "remeda" import { Global } from "../global" @@ -34,6 +33,8 @@ import { PackageRegistry } from "@/bun/registry" import { proxied } from "@/util/proxied" import { iife } from "@/util/iife" import { Control } from "@/control" +import { ConfigPaths } from "./paths" +import { Filesystem } from "@/util/filesystem" export namespace Config { const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" }) @@ -42,7 +43,7 @@ export namespace Config { // Managed settings directory for enterprise deployments (highest priority, admin-controlled) // These settings override all user and project settings - function getManagedConfigDir(): string { + function systemManagedConfigDir(): string { switch (process.platform) { case "darwin": return "/Library/Application Support/opencode" @@ -53,10 +54,14 @@ export namespace Config { } } - const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || getManagedConfigDir() + export function managedConfigDir() { + return process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || systemManagedConfigDir() + } + + const managedDir = managedConfigDir() // Custom merge function that concatenates array fields instead of replacing them - function merge(target: Info, source: Info): Info { + function mergeConfigConcatArrays(target: Info, source: Info): Info { const merged = mergeDeep(target, source) if (target.plugin && source.plugin) { merged.plugin = Array.from(new Set([...target.plugin, ...source.plugin])) @@ -91,7 +96,7 @@ export namespace Config { const remoteConfig = wellknown.config ?? {} // Add $schema to prevent load() from trying to write back to a non-existent file if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json" - result = merge( + result = mergeConfigConcatArrays( result, await load(JSON.stringify(remoteConfig), { dir: path.dirname(`${key}/.well-known/opencode`), @@ -107,21 +112,18 @@ export namespace Config { } // Global user config overrides remote config. - result = merge(result, await global()) + result = mergeConfigConcatArrays(result, await global()) // Custom config path overrides global config. if (Flag.OPENCODE_CONFIG) { - result = merge(result, await loadFile(Flag.OPENCODE_CONFIG)) + result = mergeConfigConcatArrays(result, await loadFile(Flag.OPENCODE_CONFIG)) log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG }) } // Project config overrides global and remote config. if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { - for (const file of ["opencode.jsonc", "opencode.json"]) { - const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree) - for (const resolved of found.toReversed()) { - result = merge(result, await loadFile(resolved)) - } + for (const file of await ConfigPaths.projectFiles("opencode", Instance.directory, Instance.worktree)) { + result = mergeConfigConcatArrays(result, await loadFile(file)) } } @@ -129,31 +131,10 @@ export namespace Config { result.mode = result.mode || {} result.plugin = result.plugin || [] - const directories = [ - Global.Path.config, - // Only scan project .opencode/ directories when project discovery is enabled - ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG - ? await Array.fromAsync( - Filesystem.up({ - targets: [".opencode"], - start: Instance.directory, - stop: Instance.worktree, - }), - ) - : []), - // Always scan ~/.opencode/ (user home directory) - ...(await Array.fromAsync( - Filesystem.up({ - targets: [".opencode"], - start: Global.Path.home, - stop: Global.Path.home, - }), - )), - ] + const directories = await ConfigPaths.directories(Instance.directory, Instance.worktree) // .opencode directory config overrides (project and global) config sources. if (Flag.OPENCODE_CONFIG_DIR) { - directories.push(Flag.OPENCODE_CONFIG_DIR) log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR }) } @@ -163,7 +144,7 @@ export namespace Config { if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) { for (const file of ["opencode.jsonc", "opencode.json"]) { log.debug(`loading config from ${path.join(dir, file)}`) - result = merge(result, await loadFile(path.join(dir, file))) + result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file))) // to satisfy the type checker result.agent ??= {} result.mode ??= {} @@ -186,7 +167,7 @@ export namespace Config { // Inline config content overrides all non-managed config sources. if (process.env.OPENCODE_CONFIG_CONTENT) { - result = merge( + result = mergeConfigConcatArrays( result, await load(process.env.OPENCODE_CONFIG_CONTENT, { dir: Instance.directory, @@ -200,9 +181,9 @@ export namespace Config { // Kept separate from directories array to avoid write operations when installing plugins // which would fail on system directories requiring elevated permissions // This way it only loads config file and not skills/plugins/commands - if (existsSync(managedConfigDir)) { + if (existsSync(managedDir)) { for (const file of ["opencode.jsonc", "opencode.json"]) { - result = merge(result, await loadFile(path.join(managedConfigDir, file))) + result = mergeConfigConcatArrays(result, await loadFile(path.join(managedDir, file))) } } @@ -241,8 +222,6 @@ export namespace Config { result.share = "auto" } - if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({}) - // Apply flag overrides for compaction settings if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) { result.compaction = { ...result.compaction, auto: false } @@ -306,7 +285,7 @@ export namespace Config { } } - async function needsInstall(dir: string) { + export async function needsInstall(dir: string) { // Some config dirs may be read-only. // Installing deps there will fail; skip installation in that case. const writable = await isWritable(dir) @@ -930,20 +909,6 @@ export namespace Config { ref: "KeybindsConfig", }) - export const TUI = z.object({ - scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"), - scroll_acceleration: z - .object({ - enabled: z.boolean().describe("Enable scroll acceleration"), - }) - .optional() - .describe("Scroll acceleration settings"), - diff_style: z - .enum(["auto", "stacked"]) - .optional() - .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), - }) - export const Server = z .object({ port: z.number().int().positive().optional().describe("Port to listen on"), @@ -1018,10 +983,7 @@ export namespace Config { export const Info = z .object({ $schema: z.string().optional().describe("JSON schema reference for configuration validation"), - theme: z.string().optional().describe("Theme name to use for the interface"), - keybinds: Keybinds.optional().describe("Custom keybind configurations"), logLevel: Log.Level.optional().describe("Log level"), - tui: TUI.optional().describe("TUI specific settings"), server: Server.optional().describe("Server configuration for opencode serve and web commands"), command: z .record(z.string(), Command) @@ -1241,86 +1203,37 @@ export namespace Config { return result }) + export const { readFile } = ConfigPaths + async function loadFile(filepath: string): Promise { log.info("loading", { path: filepath }) - let text = await Filesystem.readText(filepath).catch((err: any) => { - if (err.code === "ENOENT") return - throw new JsonError({ path: filepath }, { cause: err }) - }) + const text = await readFile(filepath) if (!text) return {} return load(text, { path: filepath }) } async function load(text: string, options: { path: string } | { dir: string; source: string }) { const original = text - const configDir = "path" in options ? path.dirname(options.path) : options.dir const source = "path" in options ? options.path : options.source const isFile = "path" in options + const data = await ConfigPaths.parseText( + text, + "path" in options ? options.path : { source: options.source, dir: options.dir }, + ) - text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => { - return process.env[varName] || "" - }) - - const fileMatches = text.match(/\{file:[^}]+\}/g) - if (fileMatches) { - const lines = text.split("\n") - - for (const match of fileMatches) { - const lineIndex = lines.findIndex((line) => line.includes(match)) - if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) { - continue - } - let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "") - if (filePath.startsWith("~/")) { - filePath = path.join(os.homedir(), filePath.slice(2)) - } - const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath) - const fileContent = ( - await Bun.file(resolvedPath) - .text() - .catch((error) => { - const errMsg = `bad file reference: "${match}"` - if (error.code === "ENOENT") { - throw new InvalidError( - { - path: source, - message: errMsg + ` ${resolvedPath} does not exist`, - }, - { cause: error }, - ) - } - throw new InvalidError({ path: source, message: errMsg }, { cause: error }) - }) - ).trim() - text = text.replace(match, () => JSON.stringify(fileContent).slice(1, -1)) - } - } - - const errors: JsoncParseError[] = [] - const data = parseJsonc(text, errors, { allowTrailingComma: true }) - if (errors.length) { - const lines = text.split("\n") - const errorDetails = errors - .map((e) => { - const beforeOffset = text.substring(0, e.offset).split("\n") - const line = beforeOffset.length - const column = beforeOffset[beforeOffset.length - 1].length + 1 - const problemLine = lines[line - 1] - - const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}` - if (!problemLine) return error - - return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^` - }) - .join("\n") - - throw new JsonError({ - path: source, - message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`, - }) - } + const normalized = (() => { + if (!data || typeof data !== "object" || Array.isArray(data)) return data + const copy = { ...(data as Record) } + const hadLegacy = "theme" in copy || "keybinds" in copy || "tui" in copy + if (!hadLegacy) return copy + delete copy.theme + delete copy.keybinds + delete copy.tui + log.warn("tui keys in opencode config are deprecated; move them to tui.json", { path: source }) + return copy + })() - const parsed = Info.safeParse(data) + const parsed = Info.safeParse(normalized) if (parsed.success) { if (!parsed.data.$schema && isFile) { parsed.data.$schema = "https://opencode.ai/config.json" @@ -1353,13 +1266,7 @@ export namespace Config { issues: parsed.error.issues, }) } - export const JsonError = NamedError.create( - "ConfigJsonError", - z.object({ - path: z.string(), - message: z.string().optional(), - }), - ) + export const { JsonError, InvalidError } = ConfigPaths export const ConfigDirectoryTypoError = NamedError.create( "ConfigDirectoryTypoError", @@ -1370,15 +1277,6 @@ export namespace Config { }), ) - export const InvalidError = NamedError.create( - "ConfigInvalidError", - z.object({ - path: z.string(), - issues: z.custom().optional(), - message: z.string().optional(), - }), - ) - export async function get() { return state().then((x) => x.config) } diff --git a/packages/opencode/src/config/migrate-tui-config.ts b/packages/opencode/src/config/migrate-tui-config.ts new file mode 100644 index 000000000000..b426e4fbd106 --- /dev/null +++ b/packages/opencode/src/config/migrate-tui-config.ts @@ -0,0 +1,155 @@ +import path from "path" +import { type ParseError as JsoncParseError, applyEdits, modify, parse as parseJsonc } from "jsonc-parser" +import { unique } from "remeda" +import z from "zod" +import { ConfigPaths } from "./paths" +import { TuiInfo, TuiOptions } from "./tui-schema" +import { Instance } from "@/project/instance" +import { Flag } from "@/flag/flag" +import { Log } from "@/util/log" +import { Filesystem } from "@/util/filesystem" +import { Global } from "@/global" + +const log = Log.create({ service: "tui.migrate" }) + +const TUI_SCHEMA_URL = "https://opencode.ai/tui.json" + +const LegacyTheme = TuiInfo.shape.theme.optional() +const LegacyRecord = z.record(z.string(), z.unknown()).optional() + +const TuiLegacy = z + .object({ + scroll_speed: TuiOptions.shape.scroll_speed.catch(undefined), + scroll_acceleration: TuiOptions.shape.scroll_acceleration.catch(undefined), + diff_style: TuiOptions.shape.diff_style.catch(undefined), + }) + .strip() + +interface MigrateInput { + directories: string[] + custom?: string + managed: string +} + +/** + * Migrates tui-specific keys (theme, keybinds, tui) from opencode.json files + * into dedicated tui.json files. Migration is performed per-directory and + * skips only locations where a tui.json already exists. + */ +export async function migrateTuiConfig(input: MigrateInput) { + const opencode = await opencodeFiles(input) + for (const file of opencode) { + const source = await Filesystem.readText(file).catch((error) => { + log.warn("failed to read config for tui migration", { path: file, error }) + return undefined + }) + if (!source) continue + const errors: JsoncParseError[] = [] + const data = parseJsonc(source, errors, { allowTrailingComma: true }) + if (errors.length || !data || typeof data !== "object" || Array.isArray(data)) continue + + const theme = LegacyTheme.safeParse("theme" in data ? data.theme : undefined) + const keybinds = LegacyRecord.safeParse("keybinds" in data ? data.keybinds : undefined) + const legacyTui = LegacyRecord.safeParse("tui" in data ? data.tui : undefined) + const extracted = { + theme: theme.success ? theme.data : undefined, + keybinds: keybinds.success ? keybinds.data : undefined, + tui: legacyTui.success ? legacyTui.data : undefined, + } + const tui = extracted.tui ? normalizeTui(extracted.tui) : undefined + if (extracted.theme === undefined && extracted.keybinds === undefined && !tui) continue + + const target = path.join(path.dirname(file), "tui.json") + const targetExists = await Filesystem.exists(target) + if (targetExists) continue + + const payload: Record = { + $schema: TUI_SCHEMA_URL, + } + if (extracted.theme !== undefined) payload.theme = extracted.theme + if (extracted.keybinds !== undefined) payload.keybinds = extracted.keybinds + if (tui) Object.assign(payload, tui) + + const wrote = await Bun.write(target, JSON.stringify(payload, null, 2)) + .then(() => true) + .catch((error) => { + log.warn("failed to write tui migration target", { from: file, to: target, error }) + return false + }) + if (!wrote) continue + + const stripped = await backupAndStripLegacy(file, source) + if (!stripped) { + log.warn("tui config migrated but source file was not stripped", { from: file, to: target }) + continue + } + log.info("migrated tui config", { from: file, to: target }) + } +} + +function normalizeTui(data: Record) { + const parsed = TuiLegacy.parse(data) + if ( + parsed.scroll_speed === undefined && + parsed.diff_style === undefined && + parsed.scroll_acceleration === undefined + ) { + return + } + return parsed +} + +async function backupAndStripLegacy(file: string, source: string) { + const backup = file + ".tui-migration.bak" + const hasBackup = await Filesystem.exists(backup) + const backed = hasBackup + ? true + : await Bun.write(backup, source) + .then(() => true) + .catch((error) => { + log.warn("failed to backup source config during tui migration", { path: file, backup, error }) + return false + }) + if (!backed) return false + + const text = ["theme", "keybinds", "tui"].reduce((acc, key) => { + const edits = modify(acc, [key], undefined, { + formattingOptions: { + insertSpaces: true, + tabSize: 2, + }, + }) + if (!edits.length) return acc + return applyEdits(acc, edits) + }, source) + + return Bun.write(file, text) + .then(() => { + log.info("stripped tui keys from server config", { path: file, backup }) + return true + }) + .catch((error) => { + log.warn("failed to strip legacy tui keys from server config", { path: file, backup, error }) + return false + }) +} + +async function opencodeFiles(input: { directories: string[]; managed: string }) { + const project = Flag.OPENCODE_DISABLE_PROJECT_CONFIG + ? [] + : await ConfigPaths.projectFiles("opencode", Instance.directory, Instance.worktree) + const files = [...project, ...ConfigPaths.fileInDirectory(Global.Path.config, "opencode")] + for (const dir of unique(input.directories)) { + files.push(...ConfigPaths.fileInDirectory(dir, "opencode")) + } + if (Flag.OPENCODE_CONFIG) files.push(Flag.OPENCODE_CONFIG) + files.push(...ConfigPaths.fileInDirectory(input.managed, "opencode")) + + const existing = await Promise.all( + unique(files).map(async (file) => { + const ok = await Filesystem.exists(file) + return ok ? file : undefined + }), + ) + return existing.filter((file): file is string => !!file) +} diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts new file mode 100644 index 000000000000..396417e9a5bf --- /dev/null +++ b/packages/opencode/src/config/paths.ts @@ -0,0 +1,174 @@ +import path from "path" +import os from "os" +import z from "zod" +import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser" +import { NamedError } from "@opencode-ai/util/error" +import { Filesystem } from "@/util/filesystem" +import { Flag } from "@/flag/flag" +import { Global } from "@/global" + +export namespace ConfigPaths { + export async function projectFiles(name: string, directory: string, worktree: string) { + const files: string[] = [] + for (const file of [`${name}.jsonc`, `${name}.json`]) { + const found = await Filesystem.findUp(file, directory, worktree) + for (const resolved of found.toReversed()) { + files.push(resolved) + } + } + return files + } + + export async function directories(directory: string, worktree: string) { + return [ + Global.Path.config, + ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG + ? await Array.fromAsync( + Filesystem.up({ + targets: [".opencode"], + start: directory, + stop: worktree, + }), + ) + : []), + ...(await Array.fromAsync( + Filesystem.up({ + targets: [".opencode"], + start: Global.Path.home, + stop: Global.Path.home, + }), + )), + ...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []), + ] + } + + export function fileInDirectory(dir: string, name: string) { + return [path.join(dir, `${name}.jsonc`), path.join(dir, `${name}.json`)] + } + + export const JsonError = NamedError.create( + "ConfigJsonError", + z.object({ + path: z.string(), + message: z.string().optional(), + }), + ) + + export const InvalidError = NamedError.create( + "ConfigInvalidError", + z.object({ + path: z.string(), + issues: z.custom().optional(), + message: z.string().optional(), + }), + ) + + /** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */ + export async function readFile(filepath: string) { + return Filesystem.readText(filepath).catch((err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") return + throw new JsonError({ path: filepath }, { cause: err }) + }) + } + + type ParseSource = string | { source: string; dir: string } + + function source(input: ParseSource) { + return typeof input === "string" ? input : input.source + } + + function dir(input: ParseSource) { + return typeof input === "string" ? path.dirname(input) : input.dir + } + + /** Apply {env:VAR} and {file:path} substitutions to config text. */ + async function substitute(text: string, input: ParseSource, missing: "error" | "empty" = "error") { + text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => { + return process.env[varName] || "" + }) + + const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g)) + if (!fileMatches.length) return text + + const configDir = dir(input) + const configSource = source(input) + let out = "" + let cursor = 0 + + for (const match of fileMatches) { + const token = match[0] + const index = match.index! + out += text.slice(cursor, index) + + const lineStart = text.lastIndexOf("\n", index - 1) + 1 + const prefix = text.slice(lineStart, index).trimStart() + if (prefix.startsWith("//")) { + out += token + cursor = index + token.length + continue + } + + let filePath = token.replace(/^\{file:/, "").replace(/\}$/, "") + if (filePath.startsWith("~/")) { + filePath = path.join(os.homedir(), filePath.slice(2)) + } + + const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath) + const fileContent = ( + await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => { + if (missing === "empty") return "" + + const errMsg = `bad file reference: "${token}"` + if (error.code === "ENOENT") { + throw new InvalidError( + { + path: configSource, + message: errMsg + ` ${resolvedPath} does not exist`, + }, + { cause: error }, + ) + } + throw new InvalidError({ path: configSource, message: errMsg }, { cause: error }) + }) + ).trim() + + out += JSON.stringify(fileContent).slice(1, -1) + cursor = index + token.length + } + + out += text.slice(cursor) + return out + } + + /** Substitute and parse JSONC text, throwing JsonError on syntax errors. */ + export async function parseText(text: string, input: ParseSource, missing: "error" | "empty" = "error") { + const configSource = source(input) + text = await substitute(text, input, missing) + + const errors: JsoncParseError[] = [] + const data = parseJsonc(text, errors, { allowTrailingComma: true }) + if (errors.length) { + const lines = text.split("\n") + const errorDetails = errors + .map((e) => { + const beforeOffset = text.substring(0, e.offset).split("\n") + const line = beforeOffset.length + const column = beforeOffset[beforeOffset.length - 1].length + 1 + const problemLine = lines[line - 1] + + const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}` + if (!problemLine) return error + + return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^` + }) + .join("\n") + + throw new JsonError({ + path: configSource, + message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`, + }) + } + + return data + } +} diff --git a/packages/opencode/src/config/tui-schema.ts b/packages/opencode/src/config/tui-schema.ts new file mode 100644 index 000000000000..f9068e3f01d3 --- /dev/null +++ b/packages/opencode/src/config/tui-schema.ts @@ -0,0 +1,34 @@ +import z from "zod" +import { Config } from "./config" + +const KeybindOverride = z + .object( + Object.fromEntries(Object.keys(Config.Keybinds.shape).map((key) => [key, z.string().optional()])) as Record< + string, + z.ZodOptional + >, + ) + .strict() + +export const TuiOptions = z.object({ + scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"), + scroll_acceleration: z + .object({ + enabled: z.boolean().describe("Enable scroll acceleration"), + }) + .optional() + .describe("Scroll acceleration settings"), + diff_style: z + .enum(["auto", "stacked"]) + .optional() + .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), +}) + +export const TuiInfo = z + .object({ + $schema: z.string().optional(), + theme: z.string().optional(), + keybinds: KeybindOverride.optional(), + }) + .extend(TuiOptions.shape) + .strict() diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts new file mode 100644 index 000000000000..f0964f63b35f --- /dev/null +++ b/packages/opencode/src/config/tui.ts @@ -0,0 +1,118 @@ +import { existsSync } from "fs" +import z from "zod" +import { mergeDeep, unique } from "remeda" +import { Config } from "./config" +import { ConfigPaths } from "./paths" +import { migrateTuiConfig } from "./migrate-tui-config" +import { TuiInfo } from "./tui-schema" +import { Instance } from "@/project/instance" +import { Flag } from "@/flag/flag" +import { Log } from "@/util/log" +import { Global } from "@/global" + +export namespace TuiConfig { + const log = Log.create({ service: "tui.config" }) + + export const Info = TuiInfo + + export type Info = z.output + + function mergeInfo(target: Info, source: Info): Info { + return mergeDeep(target, source) + } + + function customPath() { + return Flag.OPENCODE_TUI_CONFIG + } + + const state = Instance.state(async () => { + let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG + ? [] + : await ConfigPaths.projectFiles("tui", Instance.directory, Instance.worktree) + const directories = await ConfigPaths.directories(Instance.directory, Instance.worktree) + const custom = customPath() + const managed = Config.managedConfigDir() + await migrateTuiConfig({ directories, custom, managed }) + // Re-compute after migration since migrateTuiConfig may have created new tui.json files + projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG + ? [] + : await ConfigPaths.projectFiles("tui", Instance.directory, Instance.worktree) + + let result: Info = {} + + for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) { + result = mergeInfo(result, await loadFile(file)) + } + + if (custom) { + result = mergeInfo(result, await loadFile(custom)) + log.debug("loaded custom tui config", { path: custom }) + } + + for (const file of projectFiles) { + result = mergeInfo(result, await loadFile(file)) + } + + for (const dir of unique(directories)) { + if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue + for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { + result = mergeInfo(result, await loadFile(file)) + } + } + + if (existsSync(managed)) { + for (const file of ConfigPaths.fileInDirectory(managed, "tui")) { + result = mergeInfo(result, await loadFile(file)) + } + } + + result.keybinds = Config.Keybinds.parse(result.keybinds ?? {}) + + return { + config: result, + } + }) + + export async function get() { + return state().then((x) => x.config) + } + + async function loadFile(filepath: string): Promise { + const text = await ConfigPaths.readFile(filepath) + if (!text) return {} + return load(text, filepath).catch((error) => { + log.warn("failed to load tui config", { path: filepath, error }) + return {} + }) + } + + async function load(text: string, configFilepath: string): Promise { + const data = await ConfigPaths.parseText(text, configFilepath, "empty") + if (!data || typeof data !== "object" || Array.isArray(data)) return {} + + // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json + // (mirroring the old opencode.json shape) still get their settings applied. + const normalized = (() => { + const copy = { ...(data as Record) } + if (!("tui" in copy)) return copy + if (!copy.tui || typeof copy.tui !== "object" || Array.isArray(copy.tui)) { + delete copy.tui + return copy + } + const tui = copy.tui as Record + delete copy.tui + return { + ...tui, + ...copy, + } + })() + + const parsed = Info.safeParse(normalized) + if (!parsed.success) { + log.warn("invalid tui config", { path: configFilepath, issues: parsed.error.issues }) + return {} + } + + return parsed.data + } +} diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 0049d716d095..e02f191c709b 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -7,6 +7,7 @@ export namespace Flag { export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE") export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"] export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"] + export declare const OPENCODE_TUI_CONFIG: string | undefined export declare const OPENCODE_CONFIG_DIR: string | undefined export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"] export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE") @@ -74,6 +75,17 @@ Object.defineProperty(Flag, "OPENCODE_DISABLE_PROJECT_CONFIG", { configurable: false, }) +// Dynamic getter for OPENCODE_TUI_CONFIG +// This must be evaluated at access time, not module load time, +// because tests and external tooling may set this env var at runtime +Object.defineProperty(Flag, "OPENCODE_TUI_CONFIG", { + get() { + return process.env["OPENCODE_TUI_CONFIG"] + }, + enumerable: true, + configurable: false, +}) + // Dynamic getter for OPENCODE_CONFIG_DIR // This must be evaluated at access time, not module load time, // because external tooling may set this env var at runtime diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 2b1ba816ea3b..f245dc3493d2 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -56,6 +56,28 @@ test("loads JSON config file", async () => { }) }) +test("ignores legacy tui keys in opencode config", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await writeConfig(dir, { + $schema: "https://opencode.ai/config.json", + model: "test/model", + theme: "legacy", + tui: { scroll_speed: 4 }, + }) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.model).toBe("test/model") + expect((config as Record).theme).toBeUndefined() + expect((config as Record).tui).toBeUndefined() + }, + }) +}) + test("loads JSONC config file", async () => { await using tmp = await tmpdir({ init: async (dir) => { @@ -110,14 +132,14 @@ test("merges multiple config files with correct precedence", async () => { test("handles environment variable substitution", async () => { const originalEnv = process.env["TEST_VAR"] - process.env["TEST_VAR"] = "test_theme" + process.env["TEST_VAR"] = "test-user" try { await using tmp = await tmpdir({ init: async (dir) => { await writeConfig(dir, { $schema: "https://opencode.ai/config.json", - theme: "{env:TEST_VAR}", + username: "{env:TEST_VAR}", }) }, }) @@ -125,7 +147,7 @@ test("handles environment variable substitution", async () => { directory: tmp.path, fn: async () => { const config = await Config.get() - expect(config.theme).toBe("test_theme") + expect(config.username).toBe("test-user") }, }) } finally { @@ -148,7 +170,7 @@ test("preserves env variables when adding $schema to config", async () => { await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ - theme: "{env:PRESERVE_VAR}", + username: "{env:PRESERVE_VAR}", }), ) }, @@ -157,7 +179,7 @@ test("preserves env variables when adding $schema to config", async () => { directory: tmp.path, fn: async () => { const config = await Config.get() - expect(config.theme).toBe("secret_value") + expect(config.username).toBe("secret_value") // Read the file to verify the env variable was preserved const content = await Filesystem.readText(path.join(tmp.path, "opencode.json")) @@ -178,10 +200,10 @@ test("preserves env variables when adding $schema to config", async () => { test("handles file inclusion substitution", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Filesystem.write(path.join(dir, "included.txt"), "test_theme") + await Filesystem.write(path.join(dir, "included.txt"), "test-user") await writeConfig(dir, { $schema: "https://opencode.ai/config.json", - theme: "{file:included.txt}", + username: "{file:included.txt}", }) }, }) @@ -189,7 +211,7 @@ test("handles file inclusion substitution", async () => { directory: tmp.path, fn: async () => { const config = await Config.get() - expect(config.theme).toBe("test_theme") + expect(config.username).toBe("test-user") }, }) }) @@ -200,7 +222,7 @@ test("handles file inclusion with replacement tokens", async () => { await Filesystem.write(path.join(dir, "included.md"), "const out = await Bun.$`echo hi`") await writeConfig(dir, { $schema: "https://opencode.ai/config.json", - theme: "{file:included.md}", + username: "{file:included.md}", }) }, }) @@ -208,7 +230,7 @@ test("handles file inclusion with replacement tokens", async () => { directory: tmp.path, fn: async () => { const config = await Config.get() - expect(config.theme).toBe("const out = await Bun.$`echo hi`") + expect(config.username).toBe("const out = await Bun.$`echo hi`") }, }) }) @@ -1043,7 +1065,6 @@ test("managed settings override project settings", async () => { $schema: "https://opencode.ai/config.json", autoupdate: true, disabled_providers: [], - theme: "dark", }) }, }) @@ -1060,7 +1081,6 @@ test("managed settings override project settings", async () => { const config = await Config.get() expect(config.autoupdate).toBe(false) expect(config.disabled_providers).toEqual(["openai"]) - expect(config.theme).toBe("dark") }, }) }) @@ -1809,7 +1829,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => { process.env["TEST_CONFIG_VAR"] = "test_api_key_12345" process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ $schema: "https://opencode.ai/config.json", - theme: "{env:TEST_CONFIG_VAR}", + username: "{env:TEST_CONFIG_VAR}", }) try { @@ -1818,7 +1838,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => { directory: tmp.path, fn: async () => { const config = await Config.get() - expect(config.theme).toBe("test_api_key_12345") + expect(config.username).toBe("test_api_key_12345") }, }) } finally { @@ -1841,10 +1861,10 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => { try { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write(path.join(dir, "api_key.txt"), "secret_key_from_file") + await Filesystem.write(path.join(dir, "api_key.txt"), "secret_key_from_file") process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ $schema: "https://opencode.ai/config.json", - theme: "{file:./api_key.txt}", + username: "{file:./api_key.txt}", }) }, }) @@ -1852,7 +1872,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => { directory: tmp.path, fn: async () => { const config = await Config.get() - expect(config.theme).toBe("secret_key_from_file") + expect(config.username).toBe("secret_key_from_file") }, }) } finally { diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts new file mode 100644 index 000000000000..f9de5b041b48 --- /dev/null +++ b/packages/opencode/test/config/tui.test.ts @@ -0,0 +1,510 @@ +import { afterEach, expect, test } from "bun:test" +import path from "path" +import fs from "fs/promises" +import { tmpdir } from "../fixture/fixture" +import { Instance } from "../../src/project/instance" +import { TuiConfig } from "../../src/config/tui" +import { Global } from "../../src/global" +import { Filesystem } from "../../src/util/filesystem" + +const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR! + +afterEach(async () => { + delete process.env.OPENCODE_CONFIG + delete process.env.OPENCODE_TUI_CONFIG + await fs.rm(path.join(Global.Path.config, "tui.json"), { force: true }).catch(() => {}) + await fs.rm(path.join(Global.Path.config, "tui.jsonc"), { force: true }).catch(() => {}) + await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {}) +}) + +test("loads tui config with the same precedence order as server config paths", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(Global.Path.config, "tui.json"), JSON.stringify({ theme: "global" }, null, 2)) + await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ theme: "project" }, null, 2)) + await fs.mkdir(path.join(dir, ".opencode"), { recursive: true }) + await Bun.write( + path.join(dir, ".opencode", "tui.json"), + JSON.stringify({ theme: "local", diff_style: "stacked" }, null, 2), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBe("local") + expect(config.diff_style).toBe("stacked") + }, + }) +}) + +test("migrates tui-specific keys from opencode.json when tui.json does not exist", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify( + { + theme: "migrated-theme", + tui: { scroll_speed: 5 }, + keybinds: { app_exit: "ctrl+q" }, + }, + null, + 2, + ), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBe("migrated-theme") + expect(config.scroll_speed).toBe(5) + expect(config.keybinds?.app_exit).toBe("ctrl+q") + const text = await Filesystem.readText(path.join(tmp.path, "tui.json")) + expect(JSON.parse(text)).toMatchObject({ + theme: "migrated-theme", + scroll_speed: 5, + }) + const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json"))) + expect(server.theme).toBeUndefined() + expect(server.keybinds).toBeUndefined() + expect(server.tui).toBeUndefined() + expect(await Filesystem.exists(path.join(tmp.path, "opencode.json.tui-migration.bak"))).toBe(true) + expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true) + }, + }) +}) + +test("migrates project legacy tui keys even when global tui.json already exists", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(Global.Path.config, "tui.json"), JSON.stringify({ theme: "global" }, null, 2)) + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify( + { + theme: "project-migrated", + tui: { scroll_speed: 2 }, + }, + null, + 2, + ), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBe("project-migrated") + expect(config.scroll_speed).toBe(2) + expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true) + + const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json"))) + expect(server.theme).toBeUndefined() + expect(server.tui).toBeUndefined() + }, + }) +}) + +test("drops unknown legacy tui keys during migration", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify( + { + theme: "migrated-theme", + tui: { scroll_speed: 2, foo: 1 }, + }, + null, + 2, + ), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBe("migrated-theme") + expect(config.scroll_speed).toBe(2) + + const text = await Filesystem.readText(path.join(tmp.path, "tui.json")) + const migrated = JSON.parse(text) + expect(migrated.scroll_speed).toBe(2) + expect(migrated.foo).toBeUndefined() + }, + }) +}) + +test("skips migration when opencode.jsonc is syntactically invalid", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.jsonc"), + `{ + "theme": "broken-theme", + "tui": { "scroll_speed": 2 } + "username": "still-broken" +}`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBeUndefined() + expect(config.scroll_speed).toBeUndefined() + expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(false) + expect(await Filesystem.exists(path.join(tmp.path, "opencode.jsonc.tui-migration.bak"))).toBe(false) + const source = await Filesystem.readText(path.join(tmp.path, "opencode.jsonc")) + expect(source).toContain('"theme": "broken-theme"') + expect(source).toContain('"tui": { "scroll_speed": 2 }') + }, + }) +}) + +test("skips migration when tui.json already exists", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ theme: "legacy" }, null, 2)) + await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ diff_style: "stacked" }, null, 2)) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.diff_style).toBe("stacked") + expect(config.theme).toBeUndefined() + + const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json"))) + expect(server.theme).toBe("legacy") + expect(await Filesystem.exists(path.join(tmp.path, "opencode.json.tui-migration.bak"))).toBe(false) + }, + }) +}) + +test("continues loading tui config when legacy source cannot be stripped", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ theme: "readonly-theme" }, null, 2)) + }, + }) + + const source = path.join(tmp.path, "opencode.json") + await fs.chmod(source, 0o444) + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBe("readonly-theme") + expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true) + + const server = JSON.parse(await Filesystem.readText(source)) + expect(server.theme).toBe("readonly-theme") + }, + }) + } finally { + await fs.chmod(source, 0o644) + } +}) + +test("migration backup preserves JSONC comments", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.jsonc"), + `{ + // top-level comment + "theme": "jsonc-theme", + "tui": { + // nested comment + "scroll_speed": 1.5 + } +}`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await TuiConfig.get() + const backup = await Filesystem.readText(path.join(tmp.path, "opencode.jsonc.tui-migration.bak")) + expect(backup).toContain("// top-level comment") + expect(backup).toContain("// nested comment") + expect(backup).toContain('"theme": "jsonc-theme"') + expect(backup).toContain('"scroll_speed": 1.5') + }, + }) +}) + +test("migrates legacy tui keys across multiple opencode.json levels", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const nested = path.join(dir, "apps", "client") + await fs.mkdir(nested, { recursive: true }) + await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ theme: "root-theme" }, null, 2)) + await Bun.write(path.join(nested, "opencode.json"), JSON.stringify({ theme: "nested-theme" }, null, 2)) + }, + }) + + await Instance.provide({ + directory: path.join(tmp.path, "apps", "client"), + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBe("nested-theme") + expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true) + expect(await Filesystem.exists(path.join(tmp.path, "apps", "client", "tui.json"))).toBe(true) + }, + }) +}) + +test("flattens nested tui key inside tui.json", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "tui.json"), + JSON.stringify({ + theme: "outer", + tui: { scroll_speed: 3, diff_style: "stacked" }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.scroll_speed).toBe(3) + expect(config.diff_style).toBe("stacked") + // top-level keys take precedence over nested tui keys + expect(config.theme).toBe("outer") + }, + }) +}) + +test("top-level keys in tui.json take precedence over nested tui key", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "tui.json"), + JSON.stringify({ + diff_style: "auto", + tui: { diff_style: "stacked", scroll_speed: 2 }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.diff_style).toBe("auto") + expect(config.scroll_speed).toBe(2) + }, + }) +}) + +test("project config takes precedence over OPENCODE_TUI_CONFIG (matches OPENCODE_CONFIG)", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ theme: "project", diff_style: "auto" })) + const custom = path.join(dir, "custom-tui.json") + await Bun.write(custom, JSON.stringify({ theme: "custom", diff_style: "stacked" })) + process.env.OPENCODE_TUI_CONFIG = custom + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + // project tui.json overrides the custom path, same as server config precedence + expect(config.theme).toBe("project") + // project also set diff_style, so that wins + expect(config.diff_style).toBe("auto") + }, + }) +}) + +test("merges keybind overrides across precedence layers", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(Global.Path.config, "tui.json"), JSON.stringify({ keybinds: { app_exit: "ctrl+q" } })) + await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ keybinds: { theme_list: "ctrl+k" } })) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.keybinds?.app_exit).toBe("ctrl+q") + expect(config.keybinds?.theme_list).toBe("ctrl+k") + }, + }) +}) + +test("OPENCODE_TUI_CONFIG provides settings when no project config exists", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const custom = path.join(dir, "custom-tui.json") + await Bun.write(custom, JSON.stringify({ theme: "from-env", diff_style: "stacked" })) + process.env.OPENCODE_TUI_CONFIG = custom + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBe("from-env") + expect(config.diff_style).toBe("stacked") + }, + }) +}) + +test("does not derive tui path from OPENCODE_CONFIG", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const customDir = path.join(dir, "custom") + await fs.mkdir(customDir, { recursive: true }) + await Bun.write(path.join(customDir, "opencode.json"), JSON.stringify({ model: "test/model" })) + await Bun.write(path.join(customDir, "tui.json"), JSON.stringify({ theme: "should-not-load" })) + process.env.OPENCODE_CONFIG = path.join(customDir, "opencode.json") + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBeUndefined() + }, + }) +}) + +test("applies env and file substitutions in tui.json", async () => { + const original = process.env.TUI_THEME_TEST + process.env.TUI_THEME_TEST = "env-theme" + try { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "keybind.txt"), "ctrl+q") + await Bun.write( + path.join(dir, "tui.json"), + JSON.stringify({ + theme: "{env:TUI_THEME_TEST}", + keybinds: { app_exit: "{file:keybind.txt}" }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBe("env-theme") + expect(config.keybinds?.app_exit).toBe("ctrl+q") + }, + }) + } finally { + if (original === undefined) delete process.env.TUI_THEME_TEST + else process.env.TUI_THEME_TEST = original + } +}) + +test("applies file substitutions when first identical token is in a commented line", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "theme.txt"), "resolved-theme") + await Bun.write( + path.join(dir, "tui.jsonc"), + `{ + // "theme": "{file:theme.txt}", + "theme": "{file:theme.txt}" +}`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBe("resolved-theme") + }, + }) +}) + +test("loads managed tui config and gives it highest precedence", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ theme: "project-theme" }, null, 2)) + await fs.mkdir(managedConfigDir, { recursive: true }) + await Bun.write(path.join(managedConfigDir, "tui.json"), JSON.stringify({ theme: "managed-theme" }, null, 2)) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBe("managed-theme") + }, + }) +}) + +test("loads .opencode/tui.json", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await fs.mkdir(path.join(dir, ".opencode"), { recursive: true }) + await Bun.write(path.join(dir, ".opencode", "tui.json"), JSON.stringify({ diff_style: "stacked" }, null, 2)) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.diff_style).toBe("stacked") + }, + }) +}) + +test("gracefully falls back when tui.json has invalid JSON", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "tui.json"), "{ invalid json }") + await fs.mkdir(managedConfigDir, { recursive: true }) + await Bun.write(path.join(managedConfigDir, "tui.json"), JSON.stringify({ theme: "managed-fallback" }, null, 2)) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.theme).toBe("managed-fallback") + expect(config.keybinds).toBeDefined() + }, + }) +}) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 28d5caa02bba..be6c00cf4457 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -991,388 +991,6 @@ export type GlobalEvent = { payload: Event } -/** - * Custom keybind configurations - */ -export type KeybindsConfig = { - /** - * Leader key for keybind combinations - */ - leader?: string - /** - * Exit the application - */ - app_exit?: string - /** - * Open external editor - */ - editor_open?: string - /** - * List available themes - */ - theme_list?: string - /** - * Toggle sidebar - */ - sidebar_toggle?: string - /** - * Toggle session scrollbar - */ - scrollbar_toggle?: string - /** - * Toggle username visibility - */ - username_toggle?: string - /** - * View status - */ - status_view?: string - /** - * Export session to editor - */ - session_export?: string - /** - * Create a new session - */ - session_new?: string - /** - * List all sessions - */ - session_list?: string - /** - * Show session timeline - */ - session_timeline?: string - /** - * Fork session from message - */ - session_fork?: string - /** - * Rename session - */ - session_rename?: string - /** - * Delete session - */ - session_delete?: string - /** - * Delete stash entry - */ - stash_delete?: string - /** - * Open provider list from model dialog - */ - model_provider_list?: string - /** - * Toggle model favorite status - */ - model_favorite_toggle?: string - /** - * Share current session - */ - session_share?: string - /** - * Unshare current session - */ - session_unshare?: string - /** - * Interrupt current session - */ - session_interrupt?: string - /** - * Compact the session - */ - session_compact?: string - /** - * Scroll messages up by one page - */ - messages_page_up?: string - /** - * Scroll messages down by one page - */ - messages_page_down?: string - /** - * Scroll messages up by one line - */ - messages_line_up?: string - /** - * Scroll messages down by one line - */ - messages_line_down?: string - /** - * Scroll messages up by half page - */ - messages_half_page_up?: string - /** - * Scroll messages down by half page - */ - messages_half_page_down?: string - /** - * Navigate to first message - */ - messages_first?: string - /** - * Navigate to last message - */ - messages_last?: string - /** - * Navigate to next message - */ - messages_next?: string - /** - * Navigate to previous message - */ - messages_previous?: string - /** - * Navigate to last user message - */ - messages_last_user?: string - /** - * Copy message - */ - messages_copy?: string - /** - * Undo message - */ - messages_undo?: string - /** - * Redo message - */ - messages_redo?: string - /** - * Toggle code block concealment in messages - */ - messages_toggle_conceal?: string - /** - * Toggle tool details visibility - */ - tool_details?: string - /** - * List available models - */ - model_list?: string - /** - * Next recently used model - */ - model_cycle_recent?: string - /** - * Previous recently used model - */ - model_cycle_recent_reverse?: string - /** - * Next favorite model - */ - model_cycle_favorite?: string - /** - * Previous favorite model - */ - model_cycle_favorite_reverse?: string - /** - * List available commands - */ - command_list?: string - /** - * List agents - */ - agent_list?: string - /** - * Next agent - */ - agent_cycle?: string - /** - * Previous agent - */ - agent_cycle_reverse?: string - /** - * Cycle model variants - */ - variant_cycle?: string - /** - * Clear input field - */ - input_clear?: string - /** - * Paste from clipboard - */ - input_paste?: string - /** - * Submit input - */ - input_submit?: string - /** - * Insert newline in input - */ - input_newline?: string - /** - * Move cursor left in input - */ - input_move_left?: string - /** - * Move cursor right in input - */ - input_move_right?: string - /** - * Move cursor up in input - */ - input_move_up?: string - /** - * Move cursor down in input - */ - input_move_down?: string - /** - * Select left in input - */ - input_select_left?: string - /** - * Select right in input - */ - input_select_right?: string - /** - * Select up in input - */ - input_select_up?: string - /** - * Select down in input - */ - input_select_down?: string - /** - * Move to start of line in input - */ - input_line_home?: string - /** - * Move to end of line in input - */ - input_line_end?: string - /** - * Select to start of line in input - */ - input_select_line_home?: string - /** - * Select to end of line in input - */ - input_select_line_end?: string - /** - * Move to start of visual line in input - */ - input_visual_line_home?: string - /** - * Move to end of visual line in input - */ - input_visual_line_end?: string - /** - * Select to start of visual line in input - */ - input_select_visual_line_home?: string - /** - * Select to end of visual line in input - */ - input_select_visual_line_end?: string - /** - * Move to start of buffer in input - */ - input_buffer_home?: string - /** - * Move to end of buffer in input - */ - input_buffer_end?: string - /** - * Select to start of buffer in input - */ - input_select_buffer_home?: string - /** - * Select to end of buffer in input - */ - input_select_buffer_end?: string - /** - * Delete line in input - */ - input_delete_line?: string - /** - * Delete to end of line in input - */ - input_delete_to_line_end?: string - /** - * Delete to start of line in input - */ - input_delete_to_line_start?: string - /** - * Backspace in input - */ - input_backspace?: string - /** - * Delete character in input - */ - input_delete?: string - /** - * Undo in input - */ - input_undo?: string - /** - * Redo in input - */ - input_redo?: string - /** - * Move word forward in input - */ - input_word_forward?: string - /** - * Move word backward in input - */ - input_word_backward?: string - /** - * Select word forward in input - */ - input_select_word_forward?: string - /** - * Select word backward in input - */ - input_select_word_backward?: string - /** - * Delete word forward in input - */ - input_delete_word_forward?: string - /** - * Delete word backward in input - */ - input_delete_word_backward?: string - /** - * Previous history item - */ - history_previous?: string - /** - * Next history item - */ - history_next?: string - /** - * Next child session - */ - session_child_cycle?: string - /** - * Previous child session - */ - session_child_cycle_reverse?: string - /** - * Go to parent session - */ - session_parent?: string - /** - * Suspend terminal - */ - terminal_suspend?: string - /** - * Toggle terminal title - */ - terminal_title_toggle?: string - /** - * Toggle tips on home screen - */ - tips_toggle?: string - /** - * Toggle thinking blocks visibility - */ - display_thinking?: string -} - /** * Log level */ @@ -1672,34 +1290,7 @@ export type Config = { * JSON schema reference for configuration validation */ $schema?: string - /** - * Theme name to use for the interface - */ - theme?: string - keybinds?: KeybindsConfig logLevel?: LogLevel - /** - * TUI specific settings - */ - tui?: { - /** - * TUI scroll speed - */ - scroll_speed?: number - /** - * Scroll acceleration settings - */ - scroll_acceleration?: { - /** - * Enable scroll acceleration - */ - enabled: boolean - } - /** - * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column - */ - diff_style?: "auto" | "stacked" - } server?: ServerConfig /** * Command configuration, see https://opencode.ai/docs/commands diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index b14a7ccb8f8e..612d4fb8cdd9 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -314,7 +314,7 @@ function configSchema() { hooks: { "astro:build:done": async () => { console.log("generating config schema") - spawnSync("../opencode/script/schema.ts", ["./dist/config.json"]) + spawnSync("../opencode/script/schema.ts", ["./dist/config.json", "./dist/tui.json"]) }, }, } diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index c504f734fa5d..6b1c3dee57e2 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -558,6 +558,7 @@ OpenCode can be configured using environment variables. | `OPENCODE_AUTO_SHARE` | boolean | Automatically share sessions | | `OPENCODE_GIT_BASH_PATH` | string | Path to Git Bash executable on Windows | | `OPENCODE_CONFIG` | string | Path to config file | +| `OPENCODE_TUI_CONFIG` | string | Path to TUI config file | | `OPENCODE_CONFIG_DIR` | string | Path to config directory | | `OPENCODE_CONFIG_CONTENT` | string | Inline json config content | | `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable automatic update checks | diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index eeccde2f7913..038f253274e9 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -14,10 +14,11 @@ OpenCode supports both **JSON** and **JSONC** (JSON with Comments) formats. ```jsonc title="opencode.jsonc" { "$schema": "https://opencode.ai/config.json", - // Theme configuration - "theme": "opencode", "model": "anthropic/claude-sonnet-4-5", "autoupdate": true, + "server": { + "port": 4096, + }, } ``` @@ -34,7 +35,7 @@ Configuration files are **merged together**, not replaced. Configuration files are merged together, not replaced. Settings from the following config locations are combined. Later configs override earlier ones only for conflicting keys. Non-conflicting settings from all configs are preserved. -For example, if your global config sets `theme: "opencode"` and `autoupdate: true`, and your project config sets `model: "anthropic/claude-sonnet-4-5"`, the final configuration will include all three settings. +For example, if your global config sets `autoupdate: true` and your project config sets `model: "anthropic/claude-sonnet-4-5"`, the final configuration will include both settings. --- @@ -95,7 +96,9 @@ You can enable specific servers in your local config: ### Global -Place your global OpenCode config in `~/.config/opencode/opencode.json`. Use global config for user-wide preferences like themes, providers, or keybinds. +Place your global OpenCode config in `~/.config/opencode/opencode.json`. Use global config for user-wide server/runtime preferences like providers, models, and permissions. + +For TUI-specific settings, use `~/.config/opencode/tui.json`. Global config overrides remote organizational defaults. @@ -105,6 +108,8 @@ Global config overrides remote organizational defaults. Add `opencode.json` in your project root. Project config has the highest precedence among standard config files - it overrides both global and remote configs. +For project-specific TUI settings, add `tui.json` alongside it. + :::tip Place project specific config in the root of your project. ::: @@ -146,7 +151,9 @@ The custom directory is loaded after the global config and `.opencode` directori ## Schema -The config file has a schema that's defined in [**`opencode.ai/config.json`**](https://opencode.ai/config.json). +The server/runtime config schema is defined in [**`opencode.ai/config.json`**](https://opencode.ai/config.json). + +TUI config uses [**`opencode.ai/tui.json`**](https://opencode.ai/tui.json). Your editor should be able to validate and autocomplete based on the schema. @@ -154,28 +161,24 @@ Your editor should be able to validate and autocomplete based on the schema. ### TUI -You can configure TUI-specific settings through the `tui` option. +Use a dedicated `tui.json` (or `tui.jsonc`) file for TUI-specific settings. -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", - "tui": { - "scroll_speed": 3, - "scroll_acceleration": { - "enabled": true - }, - "diff_style": "auto" - } + "$schema": "https://opencode.ai/tui.json", + "scroll_speed": 3, + "scroll_acceleration": { + "enabled": true + }, + "diff_style": "auto" } ``` -Available options: +Use `OPENCODE_TUI_CONFIG` to point to a custom TUI config file. -- `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration. **Takes precedence over `scroll_speed`.** -- `scroll_speed` - Custom scroll speed multiplier (default: `3`, minimum: `1`). Ignored if `scroll_acceleration.enabled` is `true`. -- `diff_style` - Control diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows single column. +Legacy `theme`, `keybinds`, and `tui` keys in `opencode.json` are deprecated and automatically migrated when possible. -[Learn more about using the TUI here](/docs/tui). +[Learn more about TUI configuration here](/docs/tui#configure). --- @@ -301,12 +304,12 @@ Bearer tokens (`AWS_BEARER_TOKEN_BEDROCK` or `/connect`) take precedence over pr ### Themes -You can configure the theme you want to use in your OpenCode config through the `theme` option. +Set your UI theme in `tui.json`. -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", - "theme": "" + "$schema": "https://opencode.ai/tui.json", + "theme": "tokyonight" } ``` @@ -406,11 +409,11 @@ You can also define commands using markdown files in `~/.config/opencode/command ### Keybinds -You can customize your keybinds through the `keybinds` option. +Customize keybinds in `tui.json`. -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", + "$schema": "https://opencode.ai/tui.json", "keybinds": {} } ``` diff --git a/packages/web/src/content/docs/keybinds.mdx b/packages/web/src/content/docs/keybinds.mdx index 25fe2a1d9100..95b3d496391d 100644 --- a/packages/web/src/content/docs/keybinds.mdx +++ b/packages/web/src/content/docs/keybinds.mdx @@ -3,11 +3,11 @@ title: Keybinds description: Customize your keybinds. --- -OpenCode has a list of keybinds that you can customize through the OpenCode config. +OpenCode has a list of keybinds that you can customize through `tui.json`. -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", + "$schema": "https://opencode.ai/tui.json", "keybinds": { "leader": "ctrl+x", "app_exit": "ctrl+c,ctrl+d,q", @@ -117,11 +117,11 @@ You don't need to use a leader key for your keybinds but we recommend doing so. ## Disable keybind -You can disable a keybind by adding the key to your config with a value of "none". +You can disable a keybind by adding the key to `tui.json` with a value of "none". -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", + "$schema": "https://opencode.ai/tui.json", "keybinds": { "session_compact": "none" } diff --git a/packages/web/src/content/docs/themes.mdx b/packages/web/src/content/docs/themes.mdx index d37ce3135569..8a7c6a46ac8a 100644 --- a/packages/web/src/content/docs/themes.mdx +++ b/packages/web/src/content/docs/themes.mdx @@ -61,11 +61,11 @@ The system theme is for users who: ## Using a theme -You can select a theme by bringing up the theme select with the `/theme` command. Or you can specify it in your [config](/docs/config). +You can select a theme by bringing up the theme select with the `/theme` command. Or you can specify it in `tui.json`. -```json title="opencode.json" {3} +```json title="tui.json" {3} { - "$schema": "https://opencode.ai/config.json", + "$schema": "https://opencode.ai/tui.json", "theme": "tokyonight" } ``` diff --git a/packages/web/src/content/docs/tui.mdx b/packages/web/src/content/docs/tui.mdx index 1e48d42ccb1e..010e8328f419 100644 --- a/packages/web/src/content/docs/tui.mdx +++ b/packages/web/src/content/docs/tui.mdx @@ -355,24 +355,34 @@ Some editors need command-line arguments to run in blocking mode. The `--wait` f ## Configure -You can customize TUI behavior through your OpenCode config file. +You can customize TUI behavior through `tui.json` (or `tui.jsonc`). -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", - "tui": { - "scroll_speed": 3, - "scroll_acceleration": { - "enabled": true - } - } + "$schema": "https://opencode.ai/tui.json", + "theme": "opencode", + "keybinds": { + "leader": "ctrl+x" + }, + "scroll_speed": 3, + "scroll_acceleration": { + "enabled": true + }, + "diff_style": "auto" } ``` +This is separate from `opencode.json`, which configures server/runtime behavior. + ### Options -- `scroll_acceleration` - Enable macOS-style scroll acceleration for smooth, natural scrolling. When enabled, scroll speed increases with rapid scrolling gestures and stays precise for slower movements. **This setting takes precedence over `scroll_speed` and overrides it when enabled.** -- `scroll_speed` - Controls how fast the TUI scrolls when using scroll commands (minimum: `1`). Defaults to `3`. **Note: This is ignored if `scroll_acceleration.enabled` is set to `true`.** +- `theme` - Sets your UI theme. [Learn more](/docs/themes). +- `keybinds` - Customizes keyboard shortcuts. [Learn more](/docs/keybinds). +- `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration for smooth, natural scrolling. When enabled, scroll speed increases with rapid scrolling gestures and stays precise for slower movements. **This setting takes precedence over `scroll_speed` and overrides it when enabled.** +- `scroll_speed` - Controls how fast the TUI scrolls when using scroll commands (minimum: `0.001`, supports decimal values). Defaults to `3`. **Note: This is ignored if `scroll_acceleration.enabled` is set to `true`.** +- `diff_style` - Controls diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows a single-column layout. + +Use `OPENCODE_TUI_CONFIG` to load a custom TUI config path. --- From 4551282a4b95cd3f7e6ea4b6ba6a83dd747bb3a0 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 25 Feb 2026 22:55:09 +0000 Subject: [PATCH 71/94] chore: generate --- packages/sdk/openapi.json | 511 -------------------------------------- 1 file changed, 511 deletions(-) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 80a4a8d72ae0..0f9c6f0203c2 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8721,483 +8721,6 @@ }, "required": ["directory", "payload"] }, - "KeybindsConfig": { - "description": "Custom keybind configurations", - "type": "object", - "properties": { - "leader": { - "description": "Leader key for keybind combinations", - "default": "ctrl+x", - "type": "string" - }, - "app_exit": { - "description": "Exit the application", - "default": "ctrl+c,ctrl+d,q", - "type": "string" - }, - "editor_open": { - "description": "Open external editor", - "default": "e", - "type": "string" - }, - "theme_list": { - "description": "List available themes", - "default": "t", - "type": "string" - }, - "sidebar_toggle": { - "description": "Toggle sidebar", - "default": "b", - "type": "string" - }, - "scrollbar_toggle": { - "description": "Toggle session scrollbar", - "default": "none", - "type": "string" - }, - "username_toggle": { - "description": "Toggle username visibility", - "default": "none", - "type": "string" - }, - "status_view": { - "description": "View status", - "default": "s", - "type": "string" - }, - "session_export": { - "description": "Export session to editor", - "default": "x", - "type": "string" - }, - "session_new": { - "description": "Create a new session", - "default": "n", - "type": "string" - }, - "session_list": { - "description": "List all sessions", - "default": "l", - "type": "string" - }, - "session_timeline": { - "description": "Show session timeline", - "default": "g", - "type": "string" - }, - "session_fork": { - "description": "Fork session from message", - "default": "none", - "type": "string" - }, - "session_rename": { - "description": "Rename session", - "default": "ctrl+r", - "type": "string" - }, - "session_delete": { - "description": "Delete session", - "default": "ctrl+d", - "type": "string" - }, - "stash_delete": { - "description": "Delete stash entry", - "default": "ctrl+d", - "type": "string" - }, - "model_provider_list": { - "description": "Open provider list from model dialog", - "default": "ctrl+a", - "type": "string" - }, - "model_favorite_toggle": { - "description": "Toggle model favorite status", - "default": "ctrl+f", - "type": "string" - }, - "session_share": { - "description": "Share current session", - "default": "none", - "type": "string" - }, - "session_unshare": { - "description": "Unshare current session", - "default": "none", - "type": "string" - }, - "session_interrupt": { - "description": "Interrupt current session", - "default": "escape", - "type": "string" - }, - "session_compact": { - "description": "Compact the session", - "default": "c", - "type": "string" - }, - "messages_page_up": { - "description": "Scroll messages up by one page", - "default": "pageup,ctrl+alt+b", - "type": "string" - }, - "messages_page_down": { - "description": "Scroll messages down by one page", - "default": "pagedown,ctrl+alt+f", - "type": "string" - }, - "messages_line_up": { - "description": "Scroll messages up by one line", - "default": "ctrl+alt+y", - "type": "string" - }, - "messages_line_down": { - "description": "Scroll messages down by one line", - "default": "ctrl+alt+e", - "type": "string" - }, - "messages_half_page_up": { - "description": "Scroll messages up by half page", - "default": "ctrl+alt+u", - "type": "string" - }, - "messages_half_page_down": { - "description": "Scroll messages down by half page", - "default": "ctrl+alt+d", - "type": "string" - }, - "messages_first": { - "description": "Navigate to first message", - "default": "ctrl+g,home", - "type": "string" - }, - "messages_last": { - "description": "Navigate to last message", - "default": "ctrl+alt+g,end", - "type": "string" - }, - "messages_next": { - "description": "Navigate to next message", - "default": "none", - "type": "string" - }, - "messages_previous": { - "description": "Navigate to previous message", - "default": "none", - "type": "string" - }, - "messages_last_user": { - "description": "Navigate to last user message", - "default": "none", - "type": "string" - }, - "messages_copy": { - "description": "Copy message", - "default": "y", - "type": "string" - }, - "messages_undo": { - "description": "Undo message", - "default": "u", - "type": "string" - }, - "messages_redo": { - "description": "Redo message", - "default": "r", - "type": "string" - }, - "messages_toggle_conceal": { - "description": "Toggle code block concealment in messages", - "default": "h", - "type": "string" - }, - "tool_details": { - "description": "Toggle tool details visibility", - "default": "none", - "type": "string" - }, - "model_list": { - "description": "List available models", - "default": "m", - "type": "string" - }, - "model_cycle_recent": { - "description": "Next recently used model", - "default": "f2", - "type": "string" - }, - "model_cycle_recent_reverse": { - "description": "Previous recently used model", - "default": "shift+f2", - "type": "string" - }, - "model_cycle_favorite": { - "description": "Next favorite model", - "default": "none", - "type": "string" - }, - "model_cycle_favorite_reverse": { - "description": "Previous favorite model", - "default": "none", - "type": "string" - }, - "command_list": { - "description": "List available commands", - "default": "ctrl+p", - "type": "string" - }, - "agent_list": { - "description": "List agents", - "default": "a", - "type": "string" - }, - "agent_cycle": { - "description": "Next agent", - "default": "tab", - "type": "string" - }, - "agent_cycle_reverse": { - "description": "Previous agent", - "default": "shift+tab", - "type": "string" - }, - "variant_cycle": { - "description": "Cycle model variants", - "default": "ctrl+t", - "type": "string" - }, - "input_clear": { - "description": "Clear input field", - "default": "ctrl+c", - "type": "string" - }, - "input_paste": { - "description": "Paste from clipboard", - "default": "ctrl+v", - "type": "string" - }, - "input_submit": { - "description": "Submit input", - "default": "return", - "type": "string" - }, - "input_newline": { - "description": "Insert newline in input", - "default": "shift+return,ctrl+return,alt+return,ctrl+j", - "type": "string" - }, - "input_move_left": { - "description": "Move cursor left in input", - "default": "left,ctrl+b", - "type": "string" - }, - "input_move_right": { - "description": "Move cursor right in input", - "default": "right,ctrl+f", - "type": "string" - }, - "input_move_up": { - "description": "Move cursor up in input", - "default": "up", - "type": "string" - }, - "input_move_down": { - "description": "Move cursor down in input", - "default": "down", - "type": "string" - }, - "input_select_left": { - "description": "Select left in input", - "default": "shift+left", - "type": "string" - }, - "input_select_right": { - "description": "Select right in input", - "default": "shift+right", - "type": "string" - }, - "input_select_up": { - "description": "Select up in input", - "default": "shift+up", - "type": "string" - }, - "input_select_down": { - "description": "Select down in input", - "default": "shift+down", - "type": "string" - }, - "input_line_home": { - "description": "Move to start of line in input", - "default": "ctrl+a", - "type": "string" - }, - "input_line_end": { - "description": "Move to end of line in input", - "default": "ctrl+e", - "type": "string" - }, - "input_select_line_home": { - "description": "Select to start of line in input", - "default": "ctrl+shift+a", - "type": "string" - }, - "input_select_line_end": { - "description": "Select to end of line in input", - "default": "ctrl+shift+e", - "type": "string" - }, - "input_visual_line_home": { - "description": "Move to start of visual line in input", - "default": "alt+a", - "type": "string" - }, - "input_visual_line_end": { - "description": "Move to end of visual line in input", - "default": "alt+e", - "type": "string" - }, - "input_select_visual_line_home": { - "description": "Select to start of visual line in input", - "default": "alt+shift+a", - "type": "string" - }, - "input_select_visual_line_end": { - "description": "Select to end of visual line in input", - "default": "alt+shift+e", - "type": "string" - }, - "input_buffer_home": { - "description": "Move to start of buffer in input", - "default": "home", - "type": "string" - }, - "input_buffer_end": { - "description": "Move to end of buffer in input", - "default": "end", - "type": "string" - }, - "input_select_buffer_home": { - "description": "Select to start of buffer in input", - "default": "shift+home", - "type": "string" - }, - "input_select_buffer_end": { - "description": "Select to end of buffer in input", - "default": "shift+end", - "type": "string" - }, - "input_delete_line": { - "description": "Delete line in input", - "default": "ctrl+shift+d", - "type": "string" - }, - "input_delete_to_line_end": { - "description": "Delete to end of line in input", - "default": "ctrl+k", - "type": "string" - }, - "input_delete_to_line_start": { - "description": "Delete to start of line in input", - "default": "ctrl+u", - "type": "string" - }, - "input_backspace": { - "description": "Backspace in input", - "default": "backspace,shift+backspace", - "type": "string" - }, - "input_delete": { - "description": "Delete character in input", - "default": "ctrl+d,delete,shift+delete", - "type": "string" - }, - "input_undo": { - "description": "Undo in input", - "default": "ctrl+-,super+z", - "type": "string" - }, - "input_redo": { - "description": "Redo in input", - "default": "ctrl+.,super+shift+z", - "type": "string" - }, - "input_word_forward": { - "description": "Move word forward in input", - "default": "alt+f,alt+right,ctrl+right", - "type": "string" - }, - "input_word_backward": { - "description": "Move word backward in input", - "default": "alt+b,alt+left,ctrl+left", - "type": "string" - }, - "input_select_word_forward": { - "description": "Select word forward in input", - "default": "alt+shift+f,alt+shift+right", - "type": "string" - }, - "input_select_word_backward": { - "description": "Select word backward in input", - "default": "alt+shift+b,alt+shift+left", - "type": "string" - }, - "input_delete_word_forward": { - "description": "Delete word forward in input", - "default": "alt+d,alt+delete,ctrl+delete", - "type": "string" - }, - "input_delete_word_backward": { - "description": "Delete word backward in input", - "default": "ctrl+w,ctrl+backspace,alt+backspace", - "type": "string" - }, - "history_previous": { - "description": "Previous history item", - "default": "up", - "type": "string" - }, - "history_next": { - "description": "Next history item", - "default": "down", - "type": "string" - }, - "session_child_cycle": { - "description": "Next child session", - "default": "right", - "type": "string" - }, - "session_child_cycle_reverse": { - "description": "Previous child session", - "default": "left", - "type": "string" - }, - "session_parent": { - "description": "Go to parent session", - "default": "up", - "type": "string" - }, - "terminal_suspend": { - "description": "Suspend terminal", - "default": "ctrl+z", - "type": "string" - }, - "terminal_title_toggle": { - "description": "Toggle terminal title", - "default": "none", - "type": "string" - }, - "tips_toggle": { - "description": "Toggle tips on home screen", - "default": "h", - "type": "string" - }, - "display_thinking": { - "description": "Toggle thinking blocks visibility", - "default": "none", - "type": "string" - } - }, - "additionalProperties": false - }, "LogLevel": { "description": "Log level", "type": "string", @@ -9777,43 +9300,9 @@ "description": "JSON schema reference for configuration validation", "type": "string" }, - "theme": { - "description": "Theme name to use for the interface", - "type": "string" - }, - "keybinds": { - "$ref": "#/components/schemas/KeybindsConfig" - }, "logLevel": { "$ref": "#/components/schemas/LogLevel" }, - "tui": { - "description": "TUI specific settings", - "type": "object", - "properties": { - "scroll_speed": { - "description": "TUI scroll speed", - "type": "number", - "minimum": 0.001 - }, - "scroll_acceleration": { - "description": "Scroll acceleration settings", - "type": "object", - "properties": { - "enabled": { - "description": "Enable scroll acceleration", - "type": "boolean" - } - }, - "required": ["enabled"] - }, - "diff_style": { - "description": "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column", - "type": "string", - "enum": ["auto", "stacked"] - } - } - }, "server": { "$ref": "#/components/schemas/ServerConfig" }, From 444178e079fb41ba2149a1cdfdd3040593715d70 Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Thu, 26 Feb 2026 00:16:39 +0100 Subject: [PATCH 72/94] fix(docs): update schema URL in share configuration examples across multiple languages (#15114) --- packages/web/src/content/docs/ar/share.mdx | 6 +++--- packages/web/src/content/docs/bs/share.mdx | 6 +++--- packages/web/src/content/docs/da/share.mdx | 6 +++--- packages/web/src/content/docs/de/share.mdx | 6 +++--- packages/web/src/content/docs/es/share.mdx | 6 +++--- packages/web/src/content/docs/fr/share.mdx | 6 +++--- packages/web/src/content/docs/it/share.mdx | 6 +++--- packages/web/src/content/docs/ja/share.mdx | 6 +++--- packages/web/src/content/docs/ko/share.mdx | 6 +++--- packages/web/src/content/docs/nb/share.mdx | 6 +++--- packages/web/src/content/docs/pl/share.mdx | 6 +++--- packages/web/src/content/docs/pt-br/share.mdx | 6 +++--- packages/web/src/content/docs/ru/share.mdx | 6 +++--- packages/web/src/content/docs/share.mdx | 6 +++--- packages/web/src/content/docs/th/share.mdx | 6 +++--- packages/web/src/content/docs/tr/share.mdx | 6 +++--- packages/web/src/content/docs/zh-cn/share.mdx | 6 +++--- packages/web/src/content/docs/zh-tw/share.mdx | 6 +++--- 18 files changed, 54 insertions(+), 54 deletions(-) diff --git a/packages/web/src/content/docs/ar/share.mdx b/packages/web/src/content/docs/ar/share.mdx index 535d44dadf83..6d13410458cd 100644 --- a/packages/web/src/content/docs/ar/share.mdx +++ b/packages/web/src/content/docs/ar/share.mdx @@ -41,7 +41,7 @@ description: شارك محادثات OpenCode الخاصة بك. ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ description: شارك محادثات OpenCode الخاصة بك. ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ description: شارك محادثات OpenCode الخاصة بك. ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/bs/share.mdx b/packages/web/src/content/docs/bs/share.mdx index a15e15074349..b0760ee0c138 100644 --- a/packages/web/src/content/docs/bs/share.mdx +++ b/packages/web/src/content/docs/bs/share.mdx @@ -41,7 +41,7 @@ Da eksplicitno postavite rucni nacin u [config datoteci](/docs/config): ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Mozete ukljuciti automatsko dijeljenje za sve nove razgovore tako sto `share` po ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Dijeljenje mozete potpuno iskljuciti tako sto `share` postavite na `"disabled"` ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/da/share.mdx b/packages/web/src/content/docs/da/share.mdx index 1ac2094ca700..80b9f0959e2c 100644 --- a/packages/web/src/content/docs/da/share.mdx +++ b/packages/web/src/content/docs/da/share.mdx @@ -41,7 +41,7 @@ For at eksplisitt angi manuell modus i [konfigurasjonsfilen](/docs/config): ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Du kan aktivere automatisk deling for alle nye samtaler ved at sette alternative ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Du kan deaktivere deling helt ved at sette alternativet `share` til `"disabled"` ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/de/share.mdx b/packages/web/src/content/docs/de/share.mdx index 99fad21099b0..9b7e9284c7ac 100644 --- a/packages/web/src/content/docs/de/share.mdx +++ b/packages/web/src/content/docs/de/share.mdx @@ -43,7 +43,7 @@ Um den manuellen Modus explizit in der [Konfiguration](/docs/config) zu setzen: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -56,7 +56,7 @@ Du kannst automatisches Teilen fuer neue Unterhaltungen aktivieren, indem du in ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -71,7 +71,7 @@ Du kannst Teilen komplett deaktivieren, indem du in der [Konfiguration](/docs/co ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/es/share.mdx b/packages/web/src/content/docs/es/share.mdx index e1c62a031c04..3bb376f38625 100644 --- a/packages/web/src/content/docs/es/share.mdx +++ b/packages/web/src/content/docs/es/share.mdx @@ -41,7 +41,7 @@ Para configurar explícitamente el modo manual en su [archivo de configuración] ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Puede habilitar el uso compartido automático para todas las conversaciones nuev ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Puede desactivar el uso compartido por completo configurando la opción `share` ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/fr/share.mdx b/packages/web/src/content/docs/fr/share.mdx index acc7c03f83e9..e6b067a8c82a 100644 --- a/packages/web/src/content/docs/fr/share.mdx +++ b/packages/web/src/content/docs/fr/share.mdx @@ -41,7 +41,7 @@ Pour définir explicitement le mode manuel dans votre [fichier de configuration] ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Vous pouvez activer le partage automatique pour toutes les nouvelles conversatio ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Vous pouvez désactiver entièrement le partage en définissant l'option `share` ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/it/share.mdx b/packages/web/src/content/docs/it/share.mdx index f9eff6ca9bd5..9b410a6b8654 100644 --- a/packages/web/src/content/docs/it/share.mdx +++ b/packages/web/src/content/docs/it/share.mdx @@ -41,7 +41,7 @@ Per impostare esplicitamente la modalita manuale nel tuo [file di config](/docs/ ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Puoi abilitare la condivisione automatica per tutte le nuove conversazioni impos ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Puoi disabilitare completamente la condivisione impostando l'opzione `share` su ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/ja/share.mdx b/packages/web/src/content/docs/ja/share.mdx index 7995ba9a075e..606e807dc8aa 100644 --- a/packages/web/src/content/docs/ja/share.mdx +++ b/packages/web/src/content/docs/ja/share.mdx @@ -41,7 +41,7 @@ OpenCode は、会話の共有方法を制御する 3 つの共有モードを ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ OpenCode は、会話の共有方法を制御する 3 つの共有モードを ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ OpenCode は、会話の共有方法を制御する 3 つの共有モードを ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/ko/share.mdx b/packages/web/src/content/docs/ko/share.mdx index 55cf6a2c3e39..9e5c6388243e 100644 --- a/packages/web/src/content/docs/ko/share.mdx +++ b/packages/web/src/content/docs/ko/share.mdx @@ -41,7 +41,7 @@ opencode는 대화가 공유되는 방법을 제어하는 세 가지 공유 모 ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ opencode는 대화가 공유되는 방법을 제어하는 세 가지 공유 모 ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ opencode는 대화가 공유되는 방법을 제어하는 세 가지 공유 모 ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/nb/share.mdx b/packages/web/src/content/docs/nb/share.mdx index 370477d1cfc5..ca0cb4829acb 100644 --- a/packages/web/src/content/docs/nb/share.mdx +++ b/packages/web/src/content/docs/nb/share.mdx @@ -41,7 +41,7 @@ For å eksplisitt angi manuell modus i [konfigurasjonsfilen](/docs/config): ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Du kan aktivere automatisk deling for alle nye samtaler ved å sette alternative ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Du kan deaktivere deling helt ved å sette alternativet `share` til `"disabled"` ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/pl/share.mdx b/packages/web/src/content/docs/pl/share.mdx index 463019295a3f..0389267b59ad 100644 --- a/packages/web/src/content/docs/pl/share.mdx +++ b/packages/web/src/content/docs/pl/share.mdx @@ -41,7 +41,7 @@ Aby jawnie ustawić tryb ręczny w [pliku konfiguracyjnym] (./config): ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Możesz włączyć automatyczne udostępnianie dla wszystkich nowych rozmów, us ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Możesz całkowicie wyłączyć udostępnianie, ustawiając opcję `share` na `" ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/pt-br/share.mdx b/packages/web/src/content/docs/pt-br/share.mdx index 5aa0439d068b..166226d6dc28 100644 --- a/packages/web/src/content/docs/pt-br/share.mdx +++ b/packages/web/src/content/docs/pt-br/share.mdx @@ -41,7 +41,7 @@ Para definir explicitamente o modo manual em seu [arquivo de configuração](/do ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Você pode habilitar o compartilhamento automático para todas as novas conversa ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Você pode desativar o compartilhamento completamente definindo a opção `share ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/ru/share.mdx b/packages/web/src/content/docs/ru/share.mdx index c4df3b6a7032..8982afb08dfb 100644 --- a/packages/web/src/content/docs/ru/share.mdx +++ b/packages/web/src/content/docs/ru/share.mdx @@ -41,7 +41,7 @@ opencode поддерживает три режима общего доступ ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ opencode поддерживает три режима общего доступ ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ opencode поддерживает три режима общего доступ ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/share.mdx b/packages/web/src/content/docs/share.mdx index 475ee08d041c..b2c79334097d 100644 --- a/packages/web/src/content/docs/share.mdx +++ b/packages/web/src/content/docs/share.mdx @@ -41,7 +41,7 @@ To explicitly set manual mode in your [config file](/docs/config): ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ You can enable automatic sharing for all new conversations by setting the `share ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ You can disable sharing entirely by setting the `share` option to `"disabled"` i ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/th/share.mdx b/packages/web/src/content/docs/th/share.mdx index 195d7696f9b1..91bfce44172c 100644 --- a/packages/web/src/content/docs/th/share.mdx +++ b/packages/web/src/content/docs/th/share.mdx @@ -41,7 +41,7 @@ OpenCode รองรับโหมดการแชร์สามโหม ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ OpenCode รองรับโหมดการแชร์สามโหม ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ OpenCode รองรับโหมดการแชร์สามโหม ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/tr/share.mdx b/packages/web/src/content/docs/tr/share.mdx index 1b7abfdb7ea0..a0544eb02aa8 100644 --- a/packages/web/src/content/docs/tr/share.mdx +++ b/packages/web/src/content/docs/tr/share.mdx @@ -41,7 +41,7 @@ Manuel modu acikca ayarlamak icin [config dosyaniza](/docs/config) sunu ekleyin: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Tum yeni konusmalar icin otomatik paylasimi acmak isterseniz, [config dosyanizda ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Paylasimi tamamen kapatmak icin [config dosyanizda](/docs/config) `share` degeri ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/zh-cn/share.mdx b/packages/web/src/content/docs/zh-cn/share.mdx index 8a7be16dc91f..a2b34688e4dc 100644 --- a/packages/web/src/content/docs/zh-cn/share.mdx +++ b/packages/web/src/content/docs/zh-cn/share.mdx @@ -41,7 +41,7 @@ OpenCode 支持三种分享模式,用于控制对话的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ OpenCode 支持三种分享模式,用于控制对话的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ OpenCode 支持三种分享模式,用于控制对话的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/zh-tw/share.mdx b/packages/web/src/content/docs/zh-tw/share.mdx index 1512007bc356..58365035b640 100644 --- a/packages/web/src/content/docs/zh-tw/share.mdx +++ b/packages/web/src/content/docs/zh-tw/share.mdx @@ -41,7 +41,7 @@ OpenCode 支援三種分享模式,用於控制對話的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ OpenCode 支援三種分享模式,用於控制對話的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ OpenCode 支援三種分享模式,用於控制對話的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` From b8337cddc4269ba6e72f74c1e1f39aae41f56af3 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:05:08 -0600 Subject: [PATCH 73/94] fix(app): permissions and questions from child sessions (#15105) Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com> --- .../e2e/session/session-composer-dock.spec.ts | 298 +++++++++++++--- packages/app/src/pages/directory-layout.tsx | 11 +- .../composer/session-composer-state.test.ts | 83 +++++ .../composer/session-composer-state.ts | 22 +- .../session/composer/session-request-tree.ts | 45 +++ packages/ui/src/components/message-part.css | 101 +----- packages/ui/src/components/message-part.tsx | 325 +----------------- packages/ui/src/context/data.tsx | 34 +- 8 files changed, 393 insertions(+), 526 deletions(-) create mode 100644 packages/app/src/pages/session/composer/session-composer-state.test.ts create mode 100644 packages/app/src/pages/session/composer/session-request-tree.ts diff --git a/packages/app/e2e/session/session-composer-dock.spec.ts b/packages/app/e2e/session/session-composer-dock.spec.ts index 6bf7714a66d1..e9cfc03e4857 100644 --- a/packages/app/e2e/session/session-composer-dock.spec.ts +++ b/packages/app/e2e/session/session-composer-dock.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { clearSessionDockSeed, seedSessionPermission, seedSessionQuestion, seedSessionTodos } from "../actions" +import { clearSessionDockSeed, seedSessionQuestion, seedSessionTodos } from "../actions" import { permissionDockSelector, promptSelector, @@ -11,11 +11,23 @@ import { } from "../selectors" type Sdk = Parameters[0] - -async function withDockSession(sdk: Sdk, title: string, fn: (session: { id: string; title: string }) => Promise) { - const session = await sdk.session.create({ title }).then((r) => r.data) +type PermissionRule = { permission: string; pattern: string; action: "allow" | "deny" | "ask" } + +async function withDockSession( + sdk: Sdk, + title: string, + fn: (session: { id: string; title: string }) => Promise, + opts?: { permission?: PermissionRule[] }, +) { + const session = await sdk.session + .create(opts?.permission ? { title, permission: opts.permission } : { title }) + .then((r) => r.data) if (!session?.id) throw new Error("Session create did not return an id") - return fn(session) + try { + return await fn(session) + } finally { + await sdk.session.delete({ sessionID: session.id }).catch(() => undefined) + } } test.setTimeout(120_000) @@ -28,6 +40,85 @@ async function withDockSeed(sdk: Sdk, sessionID: string, fn: () => Promise } } +async function clearPermissionDock(page: any, label: RegExp) { + const dock = page.locator(permissionDockSelector) + for (let i = 0; i < 3; i++) { + const count = await dock.count() + if (count === 0) return + await dock.getByRole("button", { name: label }).click() + await page.waitForTimeout(150) + } +} + +async function withMockPermission( + page: any, + request: { + id: string + sessionID: string + permission: string + patterns: string[] + metadata?: Record + always?: string[] + }, + opts: { child?: any } | undefined, + fn: () => Promise, +) { + let pending = [ + { + ...request, + always: request.always ?? ["*"], + metadata: request.metadata ?? {}, + }, + ] + + const list = async (route: any) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(pending), + }) + } + + const reply = async (route: any) => { + const url = new URL(route.request().url()) + const id = url.pathname.split("/").pop() + pending = pending.filter((item) => item.id !== id) + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(true), + }) + } + + await page.route("**/permission", list) + await page.route("**/session/*/permissions/*", reply) + + const sessionList = opts?.child + ? async (route: any) => { + const res = await route.fetch() + const json = await res.json() + const list = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined + if (Array.isArray(list) && !list.some((item) => item?.id === opts.child?.id)) list.push(opts.child) + await route.fulfill({ + status: res.status(), + headers: res.headers(), + contentType: "application/json", + body: JSON.stringify(json), + }) + } + : undefined + + if (sessionList) await page.route("**/session?*", sessionList) + + try { + return await fn() + } finally { + await page.unroute("**/permission", list) + await page.unroute("**/session/*/permissions/*", reply) + if (sessionList) await page.unroute("**/session?*", sessionList) + } +} + test("default dock shows prompt input", async ({ page, sdk, gotoSession }) => { await withDockSession(sdk, "e2e composer dock default", async (session) => { await gotoSession(session.id) @@ -76,72 +167,175 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess test("blocked permission flow supports allow once", async ({ page, sdk, gotoSession }) => { await withDockSession(sdk, "e2e composer dock permission once", async (session) => { - await withDockSeed(sdk, session.id, async () => { - await gotoSession(session.id) - - await seedSessionPermission(sdk, { + await gotoSession(session.id) + await withMockPermission( + page, + { + id: "per_e2e_once", sessionID: session.id, permission: "bash", - patterns: ["README.md"], - description: "Need permission for command", - }) - - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) - - await page - .locator(permissionDockSelector) - .getByRole("button", { name: /allow once/i }) - .click() - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() - }) + patterns: ["/tmp/opencode-e2e-perm-once"], + metadata: { description: "Need permission for command" }, + }, + undefined, + async () => { + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /allow once/i) + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) }) }) test("blocked permission flow supports reject", async ({ page, sdk, gotoSession }) => { await withDockSession(sdk, "e2e composer dock permission reject", async (session) => { - await withDockSeed(sdk, session.id, async () => { - await gotoSession(session.id) - - await seedSessionPermission(sdk, { + await gotoSession(session.id) + await withMockPermission( + page, + { + id: "per_e2e_reject", sessionID: session.id, permission: "bash", - patterns: ["REJECT.md"], - }) - - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) - - await page.locator(permissionDockSelector).getByRole("button", { name: /deny/i }).click() - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() - }) + patterns: ["/tmp/opencode-e2e-perm-reject"], + }, + undefined, + async () => { + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /deny/i) + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) }) }) test("blocked permission flow supports allow always", async ({ page, sdk, gotoSession }) => { await withDockSession(sdk, "e2e composer dock permission always", async (session) => { - await withDockSeed(sdk, session.id, async () => { - await gotoSession(session.id) - - await seedSessionPermission(sdk, { + await gotoSession(session.id) + await withMockPermission( + page, + { + id: "per_e2e_always", sessionID: session.id, permission: "bash", - patterns: ["README.md"], - description: "Need permission for command", + patterns: ["/tmp/opencode-e2e-perm-always"], + metadata: { description: "Need permission for command" }, + }, + undefined, + async () => { + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /allow always/i) + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) + }) +}) + +test("child session question request blocks parent dock and unblocks after submit", async ({ + page, + sdk, + gotoSession, +}) => { + await withDockSession(sdk, "e2e composer dock child question parent", async (session) => { + await gotoSession(session.id) + + const child = await sdk.session + .create({ + title: "e2e composer dock child question", + parentID: session.id, + }) + .then((r) => r.data) + if (!child?.id) throw new Error("Child session create did not return an id") + + try { + await withDockSeed(sdk, child.id, async () => { + await seedSessionQuestion(sdk, { + sessionID: child.id, + questions: [ + { + header: "Child input", + question: "Pick one child option", + options: [ + { label: "Continue", description: "Continue child" }, + { label: "Stop", description: "Stop child" }, + ], + }, + ], + }) + + const dock = page.locator(questionDockSelector) + await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await dock.locator('[data-slot="question-option"]').first().click() + await dock.getByRole("button", { name: /submit/i }).click() + + await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() }) + } finally { + await sdk.session.delete({ sessionID: child.id }).catch(() => undefined) + } + }) +}) - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) +test("child session permission request blocks parent dock and supports allow once", async ({ + page, + sdk, + gotoSession, +}) => { + await withDockSession(sdk, "e2e composer dock child permission parent", async (session) => { + await gotoSession(session.id) - await page - .locator(permissionDockSelector) - .getByRole("button", { name: /allow always/i }) - .click() - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() - }) + const child = await sdk.session + .create({ + title: "e2e composer dock child permission", + parentID: session.id, + }) + .then((r) => r.data) + if (!child?.id) throw new Error("Child session create did not return an id") + + try { + await withMockPermission( + page, + { + id: "per_e2e_child", + sessionID: child.id, + permission: "bash", + patterns: ["/tmp/opencode-e2e-perm-child"], + metadata: { description: "Need child permission" }, + }, + { child }, + async () => { + await page.goto(page.url()) + const dock = page.locator(permissionDockSelector) + await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /allow once/i) + await page.goto(page.url()) + + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) + } finally { + await sdk.session.delete({ sessionID: child.id }).catch(() => undefined) + } }) }) diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 4f1d93ab2829..71b52180f2e7 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -1,12 +1,11 @@ import { createEffect, createMemo, Show, type ParentProps } from "solid-js" import { createStore } from "solid-js/store" import { useNavigate, useParams } from "@solidjs/router" -import { SDKProvider, useSDK } from "@/context/sdk" +import { SDKProvider } from "@/context/sdk" import { SyncProvider, useSync } from "@/context/sync" import { LocalProvider } from "@/context/local" import { DataProvider } from "@opencode-ai/ui/context" -import type { QuestionAnswer } from "@opencode-ai/sdk/v2" import { decode64 } from "@/utils/base64" import { showToast } from "@opencode-ai/ui/toast" import { useLanguage } from "@/context/language" @@ -15,19 +14,11 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { const params = useParams() const navigate = useNavigate() const sync = useSync() - const sdk = useSDK() return ( sdk.client.permission.respond(input)} - onQuestionReply={(input: { requestID: string; answers: QuestionAnswer[] }) => sdk.client.question.reply(input)} - onQuestionReject={(input: { requestID: string }) => sdk.client.question.reject(input)} onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}/session/${sessionID}`)} onSessionHref={(sessionID: string) => `/${params.dir}/session/${sessionID}`} > diff --git a/packages/app/src/pages/session/composer/session-composer-state.test.ts b/packages/app/src/pages/session/composer/session-composer-state.test.ts new file mode 100644 index 000000000000..7b6029eb31b7 --- /dev/null +++ b/packages/app/src/pages/session/composer/session-composer-state.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, test } from "bun:test" +import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client" +import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" + +const session = (input: { id: string; parentID?: string }) => + ({ + id: input.id, + parentID: input.parentID, + }) as Session + +const permission = (id: string, sessionID: string) => + ({ + id, + sessionID, + }) as PermissionRequest + +const question = (id: string, sessionID: string) => + ({ + id, + sessionID, + questions: [], + }) as QuestionRequest + +describe("sessionPermissionRequest", () => { + test("prefers the current session permission", () => { + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + const permissions = { + root: [permission("perm-root", "root")], + child: [permission("perm-child", "child")], + } + + expect(sessionPermissionRequest(sessions, permissions, "root")?.id).toBe("perm-root") + }) + + test("returns a nested child permission", () => { + const sessions = [ + session({ id: "root" }), + session({ id: "child", parentID: "root" }), + session({ id: "grand", parentID: "child" }), + session({ id: "other" }), + ] + const permissions = { + grand: [permission("perm-grand", "grand")], + other: [permission("perm-other", "other")], + } + + expect(sessionPermissionRequest(sessions, permissions, "root")?.id).toBe("perm-grand") + }) + + test("returns undefined without a matching tree permission", () => { + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + const permissions = { + other: [permission("perm-other", "other")], + } + + expect(sessionPermissionRequest(sessions, permissions, "root")).toBeUndefined() + }) +}) + +describe("sessionQuestionRequest", () => { + test("prefers the current session question", () => { + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + const questions = { + root: [question("q-root", "root")], + child: [question("q-child", "child")], + } + + expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-root") + }) + + test("returns a nested child question", () => { + const sessions = [ + session({ id: "root" }), + session({ id: "child", parentID: "root" }), + session({ id: "grand", parentID: "child" }), + ] + const questions = { + grand: [question("q-grand", "grand")], + } + + expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand") + }) +}) diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index 04c6f7e692a5..ed65867ef00e 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -7,14 +7,20 @@ import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" +import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" export function createSessionComposerBlocked() { const params = useParams() const sync = useSync() + const permissionRequest = createMemo(() => + sessionPermissionRequest(sync.data.session, sync.data.permission, params.id), + ) + const questionRequest = createMemo(() => sessionQuestionRequest(sync.data.session, sync.data.question, params.id)) + return createMemo(() => { const id = params.id if (!id) return false - return !!sync.data.permission[id]?.[0] || !!sync.data.question[id]?.[0] + return !!permissionRequest() || !!questionRequest() }) } @@ -26,18 +32,18 @@ export function createSessionComposerState() { const language = useLanguage() const questionRequest = createMemo((): QuestionRequest | undefined => { - const id = params.id - if (!id) return - return sync.data.question[id]?.[0] + return sessionQuestionRequest(sync.data.session, sync.data.question, params.id) }) const permissionRequest = createMemo((): PermissionRequest | undefined => { - const id = params.id - if (!id) return - return sync.data.permission[id]?.[0] + return sessionPermissionRequest(sync.data.session, sync.data.permission, params.id) }) - const blocked = createSessionComposerBlocked() + const blocked = createMemo(() => { + const id = params.id + if (!id) return false + return !!permissionRequest() || !!questionRequest() + }) const todos = createMemo((): Todo[] => { const id = params.id diff --git a/packages/app/src/pages/session/composer/session-request-tree.ts b/packages/app/src/pages/session/composer/session-request-tree.ts new file mode 100644 index 000000000000..f9673e254944 --- /dev/null +++ b/packages/app/src/pages/session/composer/session-request-tree.ts @@ -0,0 +1,45 @@ +import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client" + +function sessionTreeRequest(session: Session[], request: Record, sessionID?: string) { + if (!sessionID) return + + const map = session.reduce((acc, item) => { + if (!item.parentID) return acc + const list = acc.get(item.parentID) + if (list) list.push(item.id) + if (!list) acc.set(item.parentID, [item.id]) + return acc + }, new Map()) + + const seen = new Set([sessionID]) + const ids = [sessionID] + for (const id of ids) { + const list = map.get(id) + if (!list) continue + for (const child of list) { + if (seen.has(child)) continue + seen.add(child) + ids.push(child) + } + } + + const id = ids.find((id) => !!request[id]?.[0]) + if (!id) return + return request[id]?.[0] +} + +export function sessionPermissionRequest( + session: Session[], + request: Record, + sessionID?: string, +) { + return sessionTreeRequest(session, request, sessionID) +} + +export function sessionQuestionRequest( + session: Session[], + request: Record, + sessionID?: string, +) { + return sessionTreeRequest(session, request, sessionID) +} diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index f063076e079b..bea33ff54cf7 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -660,105 +660,6 @@ [data-component="tool-part-wrapper"] { width: 100%; - - &[data-permission="true"], - &[data-question="true"] { - position: sticky; - top: calc(2px + var(--sticky-header-height, 40px)); - bottom: 0px; - z-index: 20; - border-radius: 6px; - border: none; - box-shadow: var(--shadow-xs-border-base); - background-color: var(--surface-raised-base); - overflow: visible; - overflow-anchor: none; - - & > *:first-child { - border-top-left-radius: 6px; - border-top-right-radius: 6px; - overflow: hidden; - } - - & > *:last-child { - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; - overflow: hidden; - } - - [data-component="collapsible"] { - border: none; - } - - [data-component="card"] { - border: none; - } - } - - &[data-permission="true"] { - &::before { - content: ""; - position: absolute; - inset: -1.5px; - top: -5px; - border-radius: 7.5px; - border: 1.5px solid transparent; - background: - linear-gradient(var(--background-base) 0 0) padding-box, - conic-gradient( - from var(--border-angle), - transparent 0deg, - transparent 0deg, - var(--border-warning-strong, var(--border-warning-selected)) 300deg, - var(--border-warning-base) 360deg - ) - border-box; - animation: chase-border 2.5s linear infinite; - pointer-events: none; - z-index: -1; - } - } - - &[data-question="true"] { - background: var(--background-base); - border: 1px solid var(--border-weak-base); - } -} - -@property --border-angle { - syntax: ""; - initial-value: 0deg; - inherits: false; -} - -@keyframes chase-border { - from { - --border-angle: 0deg; - } - - to { - --border-angle: 360deg; - } -} - -[data-component="permission-prompt"] { - display: flex; - flex-direction: column; - padding: 8px 12px; - background-color: var(--surface-raised-strong); - border-radius: 0 0 6px 6px; - - [data-slot="permission-actions"] { - display: flex; - align-items: center; - gap: 8px; - justify-content: flex-end; - - [data-component="button"] { - padding-left: 12px; - padding-right: 12px; - } - } } [data-component="dock-prompt"][data-kind="permission"] { @@ -873,7 +774,7 @@ } } -:is([data-component="question-prompt"], [data-component="dock-prompt"][data-kind="question"]) { +[data-component="dock-prompt"][data-kind="question"] { position: relative; display: flex; flex-direction: column; diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index adba42ce9306..8fbad45bd8a9 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -23,11 +23,9 @@ import { ToolPart, UserMessage, Todo, - QuestionRequest, QuestionAnswer, QuestionInfo, } from "@opencode-ai/sdk/v2" -import { createStore } from "solid-js/store" import { useData } from "../context" import { useDiffComponent } from "../context/diff" import { useCodeComponent } from "../context/code" @@ -37,7 +35,6 @@ import { BasicTool } from "./basic-tool" import { GenericTool } from "./basic-tool" import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" -import { Button } from "./button" import { Card } from "./card" import { Collapsible } from "./collapsible" import { FileIcon } from "./file-icon" @@ -950,7 +947,6 @@ function ToolFileAccordion(props: { path: string; actions?: JSX.Element; childre } PART_MAPPING["tool"] = function ToolPartDisplay(props) { - const data = useData() const i18n = useI18n() const part = props.part as ToolPart if (part.tool === "todowrite" || part.tool === "todoread") return null @@ -959,75 +955,18 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { () => part.tool === "question" && (part.state.status === "pending" || part.state.status === "running"), ) - const permission = createMemo(() => { - const next = data.store.permission?.[props.message.sessionID]?.[0] - if (!next || !next.tool) return undefined - if (next.tool!.callID !== part.callID) return undefined - return next - }) - - const questionRequest = createMemo(() => { - const next = data.store.question?.[props.message.sessionID]?.[0] - if (!next || !next.tool) return undefined - if (next.tool!.callID !== part.callID) return undefined - return next - }) - - const [showPermission, setShowPermission] = createSignal(false) - const [showQuestion, setShowQuestion] = createSignal(false) - - createEffect(() => { - const perm = permission() - if (perm) { - const timeout = setTimeout(() => setShowPermission(true), 50) - onCleanup(() => clearTimeout(timeout)) - } else { - setShowPermission(false) - } - }) - - createEffect(() => { - const question = questionRequest() - if (question) { - const timeout = setTimeout(() => setShowQuestion(true), 50) - onCleanup(() => clearTimeout(timeout)) - } else { - setShowQuestion(false) - } - }) - - const [forceOpen, setForceOpen] = createSignal(false) - createEffect(() => { - if (permission() || questionRequest()) setForceOpen(true) - }) - - const respond = (response: "once" | "always" | "reject") => { - const perm = permission() - if (!perm || !data.respondToPermission) return - data.respondToPermission({ - sessionID: perm.sessionID, - permissionID: perm.id, - response, - }) - } - const emptyInput: Record = {} const emptyMetadata: Record = {} const input = () => part.state?.input ?? emptyInput // @ts-expect-error const partMetadata = () => part.state?.metadata ?? emptyMetadata - const metadata = () => { - const perm = permission() - if (perm?.metadata) return { ...perm.metadata, ...partMetadata() } - return partMetadata() - } const render = ToolRegistry.render(part.tool) ?? GenericTool return ( -
+
{(error) => { @@ -1067,33 +1006,15 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { component={render} input={input()} tool={part.tool} - metadata={metadata()} + metadata={partMetadata()} // @ts-expect-error output={part.state.output} status={part.state.status} hideDetails={props.hideDetails} - forceOpen={forceOpen()} - locked={showPermission() || showQuestion()} defaultOpen={props.defaultOpen} /> - -
-
- - - -
-
-
- {(request) => }
) @@ -1963,245 +1884,3 @@ ToolRegistry.register({ ) }, }) - -function QuestionPrompt(props: { request: QuestionRequest }) { - const data = useData() - const i18n = useI18n() - const questions = createMemo(() => props.request.questions) - const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true) - - const [store, setStore] = createStore({ - tab: 0, - answers: [] as QuestionAnswer[], - custom: [] as string[], - editing: false, - }) - - const question = createMemo(() => questions()[store.tab]) - const confirm = createMemo(() => !single() && store.tab === questions().length) - const options = createMemo(() => question()?.options ?? []) - const input = createMemo(() => store.custom[store.tab] ?? "") - const multi = createMemo(() => question()?.multiple === true) - const customPicked = createMemo(() => { - const value = input() - if (!value) return false - return store.answers[store.tab]?.includes(value) ?? false - }) - - function submit() { - const answers = questions().map((_, i) => store.answers[i] ?? []) - data.replyToQuestion?.({ - requestID: props.request.id, - answers, - }) - } - - function reject() { - data.rejectQuestion?.({ - requestID: props.request.id, - }) - } - - function pick(answer: string, custom: boolean = false) { - const answers = [...store.answers] - answers[store.tab] = [answer] - setStore("answers", answers) - if (custom) { - const inputs = [...store.custom] - inputs[store.tab] = answer - setStore("custom", inputs) - } - if (single()) { - data.replyToQuestion?.({ - requestID: props.request.id, - answers: [[answer]], - }) - return - } - setStore("tab", store.tab + 1) - } - - function toggle(answer: string) { - const existing = store.answers[store.tab] ?? [] - const next = [...existing] - const index = next.indexOf(answer) - if (index === -1) next.push(answer) - if (index !== -1) next.splice(index, 1) - const answers = [...store.answers] - answers[store.tab] = next - setStore("answers", answers) - } - - function selectTab(index: number) { - setStore("tab", index) - setStore("editing", false) - } - - function selectOption(optIndex: number) { - if (optIndex === options().length) { - setStore("editing", true) - return - } - const opt = options()[optIndex] - if (!opt) return - if (multi()) { - toggle(opt.label) - return - } - pick(opt.label) - } - - function handleCustomSubmit(e: Event) { - e.preventDefault() - const value = input().trim() - if (!value) { - setStore("editing", false) - return - } - if (multi()) { - const existing = store.answers[store.tab] ?? [] - const next = [...existing] - if (!next.includes(value)) next.push(value) - const answers = [...store.answers] - answers[store.tab] = next - setStore("answers", answers) - setStore("editing", false) - return - } - pick(value, true) - setStore("editing", false) - } - - return ( -
- -
- - {(q, index) => { - const active = () => index() === store.tab - const answered = () => (store.answers[index()]?.length ?? 0) > 0 - return ( - - ) - }} - - -
-
- - -
-
- {question()?.question} - {multi() ? " " + i18n.t("ui.question.multiHint") : ""} -
-
- - {(opt, i) => { - const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false - return ( - - ) - }} - - - -
- setTimeout(() => el.focus(), 0)} - type="text" - data-slot="custom-input" - placeholder={i18n.t("ui.question.custom.placeholder")} - value={input()} - onInput={(e) => { - const inputs = [...store.custom] - inputs[store.tab] = e.currentTarget.value - setStore("custom", inputs) - }} - /> - - -
-
-
-
-
- - -
-
{i18n.t("ui.messagePart.review.title")}
- - {(q, index) => { - const value = () => store.answers[index()]?.join(", ") ?? "" - const answered = () => Boolean(value()) - return ( -
- {q.question} - - {answered() ? value() : i18n.t("ui.question.review.notAnswered")} - -
- ) - }} -
-
-
- -
- - - - - - - - - -
-
- ) -} diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx index 2c44763f5368..e116199eb233 100644 --- a/packages/ui/src/context/data.tsx +++ b/packages/ui/src/context/data.tsx @@ -1,14 +1,4 @@ -import type { - Message, - Session, - Part, - FileDiff, - SessionStatus, - PermissionRequest, - QuestionRequest, - QuestionAnswer, - ProviderListResponse, -} from "@opencode-ai/sdk/v2" +import type { Message, Session, Part, FileDiff, SessionStatus, ProviderListResponse } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" @@ -24,12 +14,6 @@ type Data = { session_diff_preload?: { [sessionID: string]: PreloadMultiFileDiffResult[] } - permission?: { - [sessionID: string]: PermissionRequest[] - } - question?: { - [sessionID: string]: QuestionRequest[] - } message: { [sessionID: string]: Message[] } @@ -38,16 +22,6 @@ type Data = { } } -export type PermissionRespondFn = (input: { - sessionID: string - permissionID: string - response: "once" | "always" | "reject" -}) => void - -export type QuestionReplyFn = (input: { requestID: string; answers: QuestionAnswer[] }) => void - -export type QuestionRejectFn = (input: { requestID: string }) => void - export type NavigateToSessionFn = (sessionID: string) => void export type SessionHrefFn = (sessionID: string) => string @@ -57,9 +31,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ init: (props: { data: Data directory: string - onPermissionRespond?: PermissionRespondFn - onQuestionReply?: QuestionReplyFn - onQuestionReject?: QuestionRejectFn onNavigateToSession?: NavigateToSessionFn onSessionHref?: SessionHrefFn }) => { @@ -70,9 +41,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ get directory() { return props.directory }, - respondToPermission: props.onPermissionRespond, - replyToQuestion: props.onQuestionReply, - rejectQuestion: props.onQuestionReject, navigateToSession: props.onNavigateToSession, sessionHref: props.onSessionHref, } From c4ea11fef3dc3ac6bd2e3c55d1c8179457eace5d Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 25 Feb 2026 23:06:16 -0500 Subject: [PATCH 74/94] wip: zen --- packages/console/app/src/routes/black/index.tsx | 13 +++++++++---- .../black/{_subscribe => subscribe}/[plan].tsx | 11 +++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) rename packages/console/app/src/routes/black/{_subscribe => subscribe}/[plan].tsx (98%) diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index 382832e8faf2..8bce3cd464f7 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/packages/console/app/src/routes/black/index.tsx @@ -1,16 +1,21 @@ -import { A, useSearchParams } from "@solidjs/router" +import { A, createAsync, query, useSearchParams } from "@solidjs/router" import { Title } from "@solidjs/meta" import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js" import { PlanIcon, plans } from "./common" import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" +import { Resource } from "@opencode-ai/console-resource" -const paused = true +const getPaused = query(async () => { + "use server" + return Resource.App.stage === "production" +}, "black.paused") export default function Black() { const [params] = useSearchParams() const i18n = useI18n() const language = useLanguage() + const paused = createAsync(() => getPaused()) const [selected, setSelected] = createSignal((params.plan as string) || null) const [mounted, setMounted] = createSignal(false) const selectedPlan = createMemo(() => plans.find((p) => p.id === selected())) @@ -44,7 +49,7 @@ export default function Black() { <> {i18n.t("black.title")}
- {i18n.t("black.paused")}

}> + {i18n.t("black.paused")}

}>
@@ -108,7 +113,7 @@ export default function Black() { - +

{i18n.t("black.finePrint.beforeTerms")} ·{" "} {i18n.t("black.finePrint.terms")} diff --git a/packages/console/app/src/routes/black/_subscribe/[plan].tsx b/packages/console/app/src/routes/black/subscribe/[plan].tsx similarity index 98% rename from packages/console/app/src/routes/black/_subscribe/[plan].tsx rename to packages/console/app/src/routes/black/subscribe/[plan].tsx index 644d87d9b325..19b56eabe678 100644 --- a/packages/console/app/src/routes/black/_subscribe/[plan].tsx +++ b/packages/console/app/src/routes/black/subscribe/[plan].tsx @@ -17,6 +17,12 @@ import { Billing } from "@opencode-ai/console-core/billing.js" import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" import { formError } from "~/lib/form-error" +import { Resource } from "@opencode-ai/console-resource" + +const getEnabled = query(async () => { + "use server" + return Resource.App.stage !== "production" +}, "black.subscribe.enabled") const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!) @@ -269,6 +275,7 @@ export default function BlackSubscribe() { const params = useParams() const i18n = useI18n() const language = useLanguage() + const enabled = createAsync(() => getEnabled()) const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"] const plan = planData.id @@ -359,7 +366,7 @@ export default function BlackSubscribe() { } return ( - <> + {i18n.t("black.subscribe.title")}

@@ -472,6 +479,6 @@ export default function BlackSubscribe() { {i18n.t("black.finePrint.terms")}

- +
) } From 392a6d993f5cbb233bc0eeab297919cb21099f2c Mon Sep 17 00:00:00 2001 From: kil-penguin Date: Thu, 26 Feb 2026 15:02:40 +0900 Subject: [PATCH 75/94] fix(desktop): remove interactive shell flag from sidecar spawn to prevent hang on macOS (#15136) Co-authored-by: kil-penguin --- packages/desktop/src-tauri/src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index 0c5dfebaf5e5..af1a45cf3a38 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -320,7 +320,7 @@ pub fn spawn_command( }; let mut cmd = Command::new(shell); - cmd.args(["-il", "-c", &line]); + cmd.args(["-l", "-c", &line]); for (key, value) in envs { cmd.env(key, value); From aae75b3cfb10cdff965fb434c487980b152efdec Mon Sep 17 00:00:00 2001 From: Stefan Date: Thu, 26 Feb 2026 08:59:08 +0200 Subject: [PATCH 76/94] fix(app): middle-click tab close in scrollable tab bar (#15081) Co-authored-by: opencode-agent[bot] --- packages/ui/src/components/tabs.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx index 4836a0864c2a..a9dbea7bc4d0 100644 --- a/packages/ui/src/components/tabs.tsx +++ b/packages/ui/src/components/tabs.tsx @@ -65,6 +65,11 @@ function TabsTrigger(props: ParentProps) { ...(split.classList ?? {}), [split.class ?? ""]: !!split.class, }} + onMouseDown={(e) => { + if (e.button === 1 && split.onMiddleClick) { + e.preventDefault() + } + }} onAuxClick={(e) => { if (e.button === 1 && split.onMiddleClick) { e.preventDefault() From fce811b52f335eb28dc43b49a96f42b2ada751f5 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:55:01 +1000 Subject: [PATCH 77/94] fix: most segfaults on windows with Bun v1.3.10 stable (#15181) --- .github/actions/setup-bun/action.yml | 56 +--------------------------- .github/workflows/publish.yml | 4 +- .github/workflows/sign-cli.yml | 4 +- package.json | 2 +- packages/desktop/scripts/utils.ts | 2 +- packages/opencode/script/build.ts | 7 +++- 6 files changed, 12 insertions(+), 63 deletions(-) diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index e7966cb48c7e..6c632f7e0729 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -1,10 +1,5 @@ name: "Setup Bun" description: "Setup Bun with caching and install dependencies" -inputs: - cross-compile: - description: "Pre-cache canary cross-compile binaries for all targets" - required: false - default: "false" runs: using: "composite" steps: @@ -21,12 +16,13 @@ runs: shell: bash run: | if [ "$RUNNER_ARCH" = "X64" ]; then + V=$(node -p "require('./package.json').packageManager.split('@')[1]") case "$RUNNER_OS" in macOS) OS=darwin ;; Linux) OS=linux ;; Windows) OS=windows ;; esac - echo "url=https://github.com/oven-sh/bun/releases/download/canary/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT" + echo "url=https://github.com/oven-sh/bun/releases/download/bun-v${V}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT" fi - name: Setup Bun @@ -35,54 +31,6 @@ runs: bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }} bun-download-url: ${{ steps.bun-url.outputs.url }} - - name: Pre-cache canary cross-compile binaries - if: inputs.cross-compile == 'true' - shell: bash - run: | - BUN_VERSION=$(bun --revision) - if echo "$BUN_VERSION" | grep -q "canary"; then - SEMVER=$(echo "$BUN_VERSION" | sed 's/^\([0-9]*\.[0-9]*\.[0-9]*\).*/\1/') - echo "Bun version: $BUN_VERSION (semver: $SEMVER)" - CACHE_DIR="$HOME/.bun/install/cache" - mkdir -p "$CACHE_DIR" - TMP_DIR=$(mktemp -d) - for TARGET in linux-aarch64 linux-x64 linux-x64-baseline linux-aarch64-musl linux-x64-musl linux-x64-musl-baseline darwin-aarch64 darwin-x64 windows-x64 windows-x64-baseline; do - DEST="$CACHE_DIR/bun-${TARGET}-v${SEMVER}" - if [ -f "$DEST" ]; then - echo "Already cached: $DEST" - continue - fi - URL="https://github.com/oven-sh/bun/releases/download/canary/bun-${TARGET}.zip" - echo "Downloading $TARGET from $URL" - if curl -sfL -o "$TMP_DIR/bun.zip" "$URL"; then - unzip -qo "$TMP_DIR/bun.zip" -d "$TMP_DIR" - if echo "$TARGET" | grep -q "windows"; then - BIN_NAME="bun.exe" - else - BIN_NAME="bun" - fi - mv "$TMP_DIR/bun-${TARGET}/$BIN_NAME" "$DEST" - chmod +x "$DEST" - rm -rf "$TMP_DIR/bun-${TARGET}" "$TMP_DIR/bun.zip" - echo "Cached: $DEST" - # baseline bun resolves "bun-darwin-x64" to the baseline cache key - # so copy the modern binary there too - if [ "$TARGET" = "darwin-x64" ]; then - BASELINE_DEST="$CACHE_DIR/bun-darwin-x64-baseline-v${SEMVER}" - if [ ! -f "$BASELINE_DEST" ]; then - cp "$DEST" "$BASELINE_DEST" - echo "Cached (baseline alias): $BASELINE_DEST" - fi - fi - else - echo "Skipped: $TARGET (not available)" - fi - done - rm -rf "$TMP_DIR" - else - echo "Not a canary build ($BUN_VERSION), skipping pre-cache" - fi - - name: Install dependencies run: bun install shell: bash diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cca7df5c4ed7..8d4c9038a7e4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -77,8 +77,6 @@ jobs: fetch-tags: true - uses: ./.github/actions/setup-bun - with: - cross-compile: "true" - name: Setup git committer id: committer @@ -90,7 +88,7 @@ jobs: - name: Build id: build run: | - ./packages/opencode/script/build.ts --all + ./packages/opencode/script/build.ts env: OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_RELEASE: ${{ needs.version.outputs.release }} diff --git a/.github/workflows/sign-cli.yml b/.github/workflows/sign-cli.yml index 89176223176d..d9d61fd800eb 100644 --- a/.github/workflows/sign-cli.yml +++ b/.github/workflows/sign-cli.yml @@ -20,12 +20,10 @@ jobs: fetch-tags: true - uses: ./.github/actions/setup-bun - with: - cross-compile: "true" - name: Build run: | - ./packages/opencode/script/build.ts --all + ./packages/opencode/script/build.ts - name: Upload unsigned Windows CLI id: upload_unsigned_windows_cli diff --git a/package.json b/package.json index 2e7c1172aa64..3fd9f306676c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.9", + "packageManager": "bun@1.3.10", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev:desktop": "bun --cwd packages/desktop tauri dev", diff --git a/packages/desktop/scripts/utils.ts b/packages/desktop/scripts/utils.ts index f6ea7009c862..2629eb466c0a 100644 --- a/packages/desktop/scripts/utils.ts +++ b/packages/desktop/scripts/utils.ts @@ -8,7 +8,7 @@ export const SIDECAR_BINARIES: Array<{ rustTarget: string; ocBinary: string; ass }, { rustTarget: "x86_64-apple-darwin", - ocBinary: "opencode-darwin-x64", + ocBinary: "opencode-darwin-x64-baseline", assetExt: "zip", }, { diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 19353b67fe49..34e80d71a081 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -56,7 +56,7 @@ const migrations = await Promise.all( ) console.log(`Loaded ${migrations.length} migrations`) -const singleFlag = process.argv.includes("--single") || (!!process.env.CI && !process.argv.includes("--all")) +const singleFlag = process.argv.includes("--single") const baselineFlag = process.argv.includes("--baseline") const skipInstall = process.argv.includes("--skip-install") @@ -103,6 +103,11 @@ const allTargets: { os: "darwin", arch: "x64", }, + { + os: "darwin", + arch: "x64", + avx2: false, + }, { os: "win32", arch: "x64", From 799b2623cbb1c0f19e045d87c2c8593e83678bc0 Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 26 Feb 2026 08:22:25 +0000 Subject: [PATCH 78/94] release: v1.2.15 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index 2bdab5cb6afb..27a2a9a2b632 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -75,7 +75,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -109,7 +109,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -136,7 +136,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -160,7 +160,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -184,7 +184,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -217,7 +217,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -246,7 +246,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -262,7 +262,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.14", + "version": "1.2.15", "bin": { "opencode": "./bin/opencode", }, @@ -376,7 +376,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -396,7 +396,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.14", + "version": "1.2.15", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -407,7 +407,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -420,7 +420,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -462,7 +462,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "zod": "catalog:", }, @@ -473,7 +473,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 37d2801baf78..446c14e9671e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.14", + "version": "1.2.15", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 05d2309a423d..ad5813ced67d 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.14", + "version": "1.2.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 078d662072da..8f65e0c4578f 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.14", + "version": "1.2.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index ac1c6bfd89a3..6cdf752432cc 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.14", + "version": "1.2.15", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 1b91c7cbe01f..09344f7fa23e 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.14", + "version": "1.2.15", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 2bd9cce9a35e..4fe999e700a7 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.14", + "version": "1.2.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 0cd3ec690831..cc46f7530fb1 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.14", + "version": "1.2.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 436b2e9e191f..e9f246af890f 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.14" +version = "1.2.15" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.15/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.15/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.15/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.15/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.15/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 7a68ef5b9d29..63e50b992113 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.14", + "version": "1.2.15", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index e23d2e41ad3e..9252468153b3 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.14", + "version": "1.2.15", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index c4ed60455abe..e476c41e2fb6 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.14", + "version": "1.2.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 3faee471736b..ffbdf2198248 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.14", + "version": "1.2.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index c61ff7521b34..72ffe20d5e3f 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.14", + "version": "1.2.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 08f46d633bc7..0cdccdc54cc3 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.14", + "version": "1.2.15", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index d389d3ade1b2..36a235639ee9 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.14", + "version": "1.2.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 12bffe86d6b3..daf2ad3480f7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.14", + "version": "1.2.15", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index a661b25d80f0..a041b65223dd 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.14", + "version": "1.2.15", "publisher": "sst-dev", "repository": { "type": "git", From 6b021658ad514255c7398983b088c1636caaa5e4 Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:39:55 +0100 Subject: [PATCH 79/94] fix(app): open in powershell (#15112) --- packages/desktop/src-tauri/src/lib.rs | 15 ++++++++- packages/desktop/src-tauri/src/os/windows.rs | 32 +++++++++++++++++--- packages/desktop/src/bindings.ts | 1 + packages/desktop/src/index.tsx | 11 ++++++- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 71fe8407f029..879732121470 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -179,6 +179,18 @@ fn resolve_app_path(app_name: &str) -> Option { } } +#[tauri::command] +#[specta::specta] +fn open_in_powershell(path: String) -> Result<(), String> { + #[cfg(target_os = "windows")] + { + return os::windows::open_in_powershell(path); + } + + #[cfg(not(target_os = "windows"))] + Err("PowerShell is only supported on Windows".to_string()) +} + #[cfg(target_os = "macos")] fn check_macos_app(app_name: &str) -> bool { // Check common installation locations @@ -373,7 +385,8 @@ fn make_specta_builder() -> tauri_specta::Builder { markdown::parse_markdown_command, check_app_exists, wsl_path, - resolve_app_path + resolve_app_path, + open_in_powershell ]) .events(tauri_specta::collect_events![ LoadingWindowComplete, diff --git a/packages/desktop/src-tauri/src/os/windows.rs b/packages/desktop/src-tauri/src/os/windows.rs index cab265b626bd..a163c4aa7ebb 100644 --- a/packages/desktop/src-tauri/src/os/windows.rs +++ b/packages/desktop/src-tauri/src/os/windows.rs @@ -6,9 +6,12 @@ use std::{ }; use windows_sys::Win32::{ Foundation::ERROR_SUCCESS, - System::Registry::{ - HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, REG_EXPAND_SZ, REG_SZ, RRF_RT_REG_EXPAND_SZ, - RRF_RT_REG_SZ, RegGetValueW, + System::{ + Registry::{ + RegGetValueW, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, REG_EXPAND_SZ, REG_SZ, + RRF_RT_REG_EXPAND_SZ, RRF_RT_REG_SZ, + }, + Threading::{CREATE_NEW_CONSOLE, CREATE_NO_WINDOW}, }, }; @@ -310,7 +313,7 @@ pub fn resolve_windows_app_path(app_name: &str) -> Option { let resolve_where = |query: &str| -> Option { let output = Command::new("where") - .creation_flags(0x08000000) + .creation_flags(CREATE_NO_WINDOW) .arg(query) .output() .ok()?; @@ -437,3 +440,24 @@ pub fn resolve_windows_app_path(app_name: &str) -> Option { None } + +pub fn open_in_powershell(path: String) -> Result<(), String> { + let path = PathBuf::from(path); + let dir = if path.is_dir() { + path + } else if let Some(parent) = path.parent() { + parent.to_path_buf() + } else { + std::env::current_dir() + .map_err(|e| format!("Failed to determine current directory: {e}"))? + }; + + Command::new("powershell.exe") + .creation_flags(CREATE_NEW_CONSOLE) + .current_dir(dir) + .args(["-NoExit"]) + .spawn() + .map_err(|e| format!("Failed to start PowerShell: {e}"))?; + + Ok(()) +} diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts index 6d05bfc56e90..8e1b4127a54f 100644 --- a/packages/desktop/src/bindings.ts +++ b/packages/desktop/src/bindings.ts @@ -18,6 +18,7 @@ export const commands = { checkAppExists: (appName: string) => __TAURI_INVOKE("check_app_exists", { appName }), wslPath: (path: string, mode: "windows" | "linux" | null) => __TAURI_INVOKE("wsl_path", { path, mode }), resolveAppPath: (appName: string) => __TAURI_INVOKE("resolve_app_path", { appName }), + openInPowershell: (path: string) => __TAURI_INVOKE("open_in_powershell", { path }), }; /** Events */ diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 983fe3945605..188a37eb87d2 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -118,7 +118,6 @@ const createPlatform = (): Platform => { async openPath(path: string, app?: string) { const os = ostype() if (os === "windows") { - const resolvedApp = (app && (await commands.resolveAppPath(app))) || app const resolvedPath = await (async () => { if (window.__OPENCODE__?.wsl) { const converted = await commands.wslPath(path, "windows").catch(() => null) @@ -127,6 +126,16 @@ const createPlatform = (): Platform => { return path })() + const resolvedApp = (app && (await commands.resolveAppPath(app))) || app + const isPowershell = (value?: string) => { + if (!value) return false + const name = value.toLowerCase().replaceAll("/", "\\").split("\\").pop() + return name === "powershell" || name === "powershell.exe" + } + if (isPowershell(resolvedApp)) { + await commands.openInPowershell(resolvedPath) + return + } return openerOpenPath(resolvedPath, resolvedApp) } return openerOpenPath(path, app) From bb8a1718a63c2caae9e40c85dd4bdfe34f8012d7 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 26 Feb 2026 18:35:21 +0530 Subject: [PATCH 80/94] fix(desktop): restore shell path env for desktop sidecar (#15211) --- packages/desktop/src-tauri/src/cli.rs | 185 +++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index af1a45cf3a38..97fdba144f4c 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -4,10 +4,13 @@ use process_wrap::tokio::CommandWrap; use process_wrap::tokio::ProcessGroup; #[cfg(windows)] use process_wrap::tokio::{CommandWrapper, JobObject, KillOnDrop}; +use std::collections::HashMap; #[cfg(unix)] use std::os::unix::process::ExitStatusExt; +use std::path::Path; +use std::process::Stdio; use std::sync::Arc; -use std::{process::Stdio, time::Duration}; +use std::time::{Duration, Instant}; use tauri::{AppHandle, Manager, path::BaseDirectory}; use tauri_specta::Event; use tokio::{ @@ -39,6 +42,7 @@ impl CommandWrapper for WinCreationFlags { const CLI_INSTALL_DIR: &str = ".opencode/bin"; const CLI_BINARY_NAME: &str = "opencode"; +const SHELL_ENV_TIMEOUT: Duration = Duration::from_secs(5); #[derive(serde::Deserialize, Debug)] pub struct ServerConfig { @@ -232,6 +236,133 @@ fn shell_escape(input: &str) -> String { escaped } +fn parse_shell_env(stdout: &[u8]) -> HashMap { + String::from_utf8_lossy(stdout) + .split('\0') + .filter_map(|line| { + if line.is_empty() { + return None; + } + + let (key, value) = line.split_once('=')?; + if key.is_empty() { + return None; + } + + Some((key.to_string(), value.to_string())) + }) + .collect() +} + +fn command_output_with_timeout( + mut cmd: std::process::Command, + timeout: Duration, +) -> std::io::Result> { + let mut child = cmd.spawn()?; + let start = Instant::now(); + + loop { + if child.try_wait()?.is_some() { + return child.wait_with_output().map(Some); + } + + if start.elapsed() >= timeout { + let _ = child.kill(); + let _ = child.wait(); + return Ok(None); + } + + std::thread::sleep(Duration::from_millis(25)); + } +} + +enum ShellEnvProbe { + Loaded(HashMap), + Timeout, + Unavailable, +} + +fn probe_shell_env(shell: &str, mode: &str) -> ShellEnvProbe { + let mut cmd = std::process::Command::new(shell); + cmd.args([mode, "-c", "env -0"]); + cmd.stdin(Stdio::null()); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::null()); + let output = match command_output_with_timeout(cmd, SHELL_ENV_TIMEOUT) { + Ok(Some(output)) => output, + Ok(None) => return ShellEnvProbe::Timeout, + Err(error) => { + tracing::debug!(shell, mode, ?error, "Shell env probe failed"); + return ShellEnvProbe::Unavailable; + } + }; + if !output.status.success() { + tracing::debug!(shell, mode, "Shell env probe exited with non-zero status"); + return ShellEnvProbe::Unavailable; + } + let env = parse_shell_env(&output.stdout); + if env.is_empty() { + tracing::debug!(shell, mode, "Shell env probe returned empty env"); + return ShellEnvProbe::Unavailable; + } + + ShellEnvProbe::Loaded(env) +} + +fn is_nushell(shell: &str) -> bool { + let shell_name = Path::new(shell) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(shell) + .to_ascii_lowercase(); + shell_name == "nu" || shell_name == "nu.exe" || shell.to_ascii_lowercase().ends_with("\\nu.exe") +} +fn load_shell_env(shell: &str) -> Option> { + if is_nushell(shell) { + tracing::debug!(shell, "Skipping shell env probe for nushell"); + return None; + } + + match probe_shell_env(shell, "-il") { + ShellEnvProbe::Loaded(env) => { + tracing::info!( + shell, + env_count = env.len(), + "Loaded shell environment with -il" + ); + return Some(env); + } + ShellEnvProbe::Timeout => { + tracing::warn!(shell, "Interactive shell env probe timed out"); + return None; + } + ShellEnvProbe::Unavailable => {} + } + + if let ShellEnvProbe::Loaded(env) = probe_shell_env(shell, "-l") { + tracing::info!( + shell, + env_count = env.len(), + "Loaded shell environment with -l" + ); + return Some(env); + } + tracing::warn!(shell, "Falling back to app environment"); + None +} + +fn merge_shell_env( + shell_env: Option>, + envs: Vec<(String, String)>, +) -> Vec<(String, String)> { + let mut merged = shell_env.unwrap_or_default(); + for (key, value) in envs { + merged.insert(key, value); + } + + merged.into_iter().collect() +} + pub fn spawn_command( app: &tauri::AppHandle, args: &str, @@ -312,6 +443,7 @@ pub fn spawn_command( } else { let sidecar = get_sidecar_path(app); let shell = get_user_shell(); + let envs = merge_shell_env(load_shell_env(&shell), envs); let line = if shell.ends_with("/nu") { format!("^\"{}\" {}", sidecar.display(), args) @@ -556,3 +688,54 @@ async fn read_line CommandEvent + Send + Copy + 'static>( } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + #[test] + fn parse_shell_env_supports_null_delimited_pairs() { + let env = parse_shell_env(b"PATH=/usr/bin:/bin\0FOO=bar=baz\0\0"); + + assert_eq!(env.get("PATH"), Some(&"/usr/bin:/bin".to_string())); + assert_eq!(env.get("FOO"), Some(&"bar=baz".to_string())); + } + + #[test] + fn parse_shell_env_ignores_invalid_entries() { + let env = parse_shell_env(b"INVALID\0=empty\0OK=1\0"); + + assert_eq!(env.len(), 1); + assert_eq!(env.get("OK"), Some(&"1".to_string())); + } + + #[test] + fn merge_shell_env_keeps_explicit_overrides() { + let mut shell_env = HashMap::new(); + shell_env.insert("PATH".to_string(), "/shell/path".to_string()); + shell_env.insert("HOME".to_string(), "/tmp/home".to_string()); + + let merged = merge_shell_env( + Some(shell_env), + vec![ + ("PATH".to_string(), "/desktop/path".to_string()), + ("OPENCODE_CLIENT".to_string(), "desktop".to_string()), + ], + ) + .into_iter() + .collect::>(); + + assert_eq!(merged.get("PATH"), Some(&"/desktop/path".to_string())); + assert_eq!(merged.get("HOME"), Some(&"/tmp/home".to_string())); + assert_eq!(merged.get("OPENCODE_CLIENT"), Some(&"desktop".to_string())); + } + + #[test] + fn is_nushell_handles_path_and_binary_name() { + assert!(is_nushell("nu")); + assert!(is_nushell("/opt/homebrew/bin/nu")); + assert!(is_nushell("C:\\Program Files\\nu.exe")); + assert!(!is_nushell("/bin/zsh")); + } +} From 7453e78b355da70f731a06853c1027bccb8a3ed3 Mon Sep 17 00:00:00 2001 From: David Hill <1879069+iamdavidhill@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:37:33 +0000 Subject: [PATCH 81/94] feat: opencode go provider list (#15203) --- .../src/components/dialog-select-model-unpaid.tsx | 11 +++++++++++ .../app/src/components/dialog-select-provider.tsx | 7 +++++++ packages/app/src/components/settings-providers.tsx | 13 +++++++++++++ packages/app/src/hooks/use-providers.ts | 11 ++++++++++- packages/app/src/i18n/ar.ts | 2 ++ packages/app/src/i18n/br.ts | 2 ++ packages/app/src/i18n/bs.ts | 2 ++ packages/app/src/i18n/da.ts | 2 ++ packages/app/src/i18n/de.ts | 2 ++ packages/app/src/i18n/en.ts | 2 ++ packages/app/src/i18n/es.ts | 2 ++ packages/app/src/i18n/fr.ts | 2 ++ packages/app/src/i18n/ja.ts | 2 ++ packages/app/src/i18n/ko.ts | 2 ++ packages/app/src/i18n/no.ts | 2 ++ packages/app/src/i18n/pl.ts | 2 ++ packages/app/src/i18n/ru.ts | 2 ++ packages/app/src/i18n/th.ts | 2 ++ packages/app/src/i18n/zh.ts | 2 ++ packages/app/src/i18n/zht.ts | 2 ++ .../src/cli/cmd/tui/component/dialog-provider.tsx | 8 ++++---- 21 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index af788d05b03c..5ca29a520a0c 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -97,9 +97,20 @@ export const DialogSelectModelUnpaid: Component = () => {
{i.name} + +
{language.t("dialog.provider.opencode.tagline")}
+
{language.t("dialog.provider.tag.recommended")} + + <> +
+ {language.t("dialog.provider.opencodeGo.tagline")} +
+ {language.t("dialog.provider.tag.recommended")} + +
{language.t("dialog.provider.anthropic.note")}
diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx index 8bbd3054b9a2..76e718bb0011 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -29,6 +29,7 @@ export const DialogSelectProvider: Component = () => { if (id === "anthropic") return language.t("dialog.provider.anthropic.note") if (id === "openai") return language.t("dialog.provider.openai.note") if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note") + if (id === "opencode-go") return language.t("dialog.provider.opencodeGo.tagline") } return ( @@ -70,6 +71,9 @@ export const DialogSelectProvider: Component = () => {
{i.name} + +
{language.t("dialog.provider.opencode.tagline")}
+
{language.t("settings.providers.tag.custom")} @@ -77,6 +81,9 @@ export const DialogSelectProvider: Component = () => { {language.t("dialog.provider.tag.recommended")} {(value) =>
{value()}
}
+ + {language.t("dialog.provider.tag.recommended")} +
)} diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index d1837ee607db..55a25ca0c4b7 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -187,9 +187,22 @@ export const SettingsProviders: Component = () => {
{item.name} + + + {language.t("dialog.provider.opencode.tagline")} + + {language.t("dialog.provider.tag.recommended")} + + <> + + {language.t("dialog.provider.opencodeGo.tagline")} + + {language.t("dialog.provider.tag.recommended")} + +
{(key) => {language.t(key())}} diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index 502364afdf3a..9ef5272ef548 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -3,7 +3,16 @@ import { decode64 } from "@/utils/base64" import { useParams } from "@solidjs/router" import { createMemo } from "solid-js" -export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] +export const popularProviders = [ + "opencode", + "opencode-go", + "anthropic", + "github-copilot", + "openai", + "google", + "openrouter", + "vercel", +] const popularProviderSet = new Set(popularProviders) export function useProviders() { diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 91a16b3b8532..e8964a664649 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -91,6 +91,8 @@ export const dict = { "dialog.provider.group.other": "آخر", "dialog.provider.tag.recommended": "موصى به", "dialog.provider.opencode.note": "نماذج مختارة تتضمن Claude و GPT و Gemini والمزيد", + "dialog.provider.opencode.tagline": "نماذج موثوقة ومحسنة", + "dialog.provider.opencodeGo.tagline": "اشتراك منخفض التكلفة للجميع", "dialog.provider.anthropic.note": "اتصل باستخدام Claude Pro/Max أو مفتاح API", "dialog.provider.copilot.note": "اتصل باستخدام Copilot أو مفتاح API", "dialog.provider.openai.note": "اتصل باستخدام ChatGPT Pro/Plus أو مفتاح API", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 7682a12b6972..f23668a0d875 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -91,6 +91,8 @@ export const dict = { "dialog.provider.group.other": "Outro", "dialog.provider.tag.recommended": "Recomendado", "dialog.provider.opencode.note": "Modelos selecionados incluindo Claude, GPT, Gemini e mais", + "dialog.provider.opencode.tagline": "Modelos otimizados e confiáveis", + "dialog.provider.opencodeGo.tagline": "Assinatura de baixo custo para todos", "dialog.provider.anthropic.note": "Conectar com Claude Pro/Max ou chave de API", "dialog.provider.copilot.note": "Conectar com Copilot ou chave de API", "dialog.provider.openai.note": "Conectar com ChatGPT Pro/Plus ou chave de API", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index cb0274042ed0..6951f9db1f8e 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -99,6 +99,8 @@ export const dict = { "dialog.provider.group.other": "Ostalo", "dialog.provider.tag.recommended": "Preporučeno", "dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge", + "dialog.provider.opencode.tagline": "Pouzdani optimizovani modeli", + "dialog.provider.opencodeGo.tagline": "Povoljna pretplata za sve", "dialog.provider.anthropic.note": "Direktan pristup Claude modelima, uključujući Pro i Max", "dialog.provider.copilot.note": "AI modeli za pomoć pri kodiranju putem GitHub Copilot", "dialog.provider.openai.note": "GPT modeli za brze, sposobne opšte AI zadatke", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 30cc555eb1a0..b870fb51a44b 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -99,6 +99,8 @@ export const dict = { "dialog.provider.group.other": "Andre", "dialog.provider.tag.recommended": "Anbefalet", "dialog.provider.opencode.note": "Udvalgte modeller inklusive Claude, GPT, Gemini og flere", + "dialog.provider.opencode.tagline": "Pålidelige optimerede modeller", + "dialog.provider.opencodeGo.tagline": "Billigt abonnement for alle", "dialog.provider.anthropic.note": "Direkte adgang til Claude-modeller, inklusive Pro og Max", "dialog.provider.copilot.note": "AI-modeller til kodningsassistance via GitHub Copilot", "dialog.provider.openai.note": "GPT-modeller til hurtige, kompetente generelle AI-opgaver", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 3a7bbe927727..24d00a6813aa 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -95,6 +95,8 @@ export const dict = { "dialog.provider.group.other": "Andere", "dialog.provider.tag.recommended": "Empfohlen", "dialog.provider.opencode.note": "Kuratierte Modelle inklusive Claude, GPT, Gemini und mehr", + "dialog.provider.opencode.tagline": "Zuverlässige, optimierte Modelle", + "dialog.provider.opencodeGo.tagline": "Kostengünstiges Abo für alle", "dialog.provider.anthropic.note": "Mit Claude Pro/Max oder API-Schlüssel verbinden", "dialog.provider.copilot.note": "Mit Copilot oder API-Schlüssel verbinden", "dialog.provider.openai.note": "Mit ChatGPT Pro/Plus oder API-Schlüssel verbinden", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 0b4388ceb19a..bea29aa352e7 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -99,6 +99,8 @@ export const dict = { "dialog.provider.group.other": "Other", "dialog.provider.tag.recommended": "Recommended", "dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more", + "dialog.provider.opencode.tagline": "Reliable optimized models", + "dialog.provider.opencodeGo.tagline": "Low cost subscription for everyone", "dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max", "dialog.provider.copilot.note": "AI models for coding assistance via GitHub Copilot", "dialog.provider.openai.note": "GPT models for fast, capable general AI tasks", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 3566226d7bfd..30c52c928fbd 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -99,6 +99,8 @@ export const dict = { "dialog.provider.group.other": "Otro", "dialog.provider.tag.recommended": "Recomendado", "dialog.provider.opencode.note": "Modelos seleccionados incluyendo Claude, GPT, Gemini y más", + "dialog.provider.opencode.tagline": "Modelos optimizados y fiables", + "dialog.provider.opencodeGo.tagline": "Suscripción económica para todos", "dialog.provider.anthropic.note": "Acceso directo a modelos Claude, incluyendo Pro y Max", "dialog.provider.copilot.note": "Modelos de IA para asistencia de codificación a través de GitHub Copilot", "dialog.provider.openai.note": "Modelos GPT para tareas de IA generales rápidas y capaces", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index c961f060e1fd..3b690937e8a4 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -91,6 +91,8 @@ export const dict = { "dialog.provider.group.other": "Autre", "dialog.provider.tag.recommended": "Recommandé", "dialog.provider.opencode.note": "Modèles sélectionnés incluant Claude, GPT, Gemini et plus", + "dialog.provider.opencode.tagline": "Modèles optimisés et fiables", + "dialog.provider.opencodeGo.tagline": "Abonnement abordable pour tous", "dialog.provider.anthropic.note": "Connectez-vous avec Claude Pro/Max ou une clé API", "dialog.provider.copilot.note": "Connectez-vous avec Copilot ou une clé API", "dialog.provider.openai.note": "Connectez-vous avec ChatGPT Pro/Plus ou une clé API", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 7a62c9de2716..c8a949e7822e 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -91,6 +91,8 @@ export const dict = { "dialog.provider.group.other": "その他", "dialog.provider.tag.recommended": "推奨", "dialog.provider.opencode.note": "Claude, GPT, Geminiなどを含む厳選されたモデル", + "dialog.provider.opencode.tagline": "信頼性の高い最適化モデル", + "dialog.provider.opencodeGo.tagline": "すべての人に低価格のサブスクリプション", "dialog.provider.anthropic.note": "Claude Pro/MaxまたはAPIキーで接続", "dialog.provider.copilot.note": "CopilotまたはAPIキーで接続", "dialog.provider.openai.note": "ChatGPT Pro/PlusまたはAPIキーで接続", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 8967c71cff7d..d5cedc7deaac 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -95,6 +95,8 @@ export const dict = { "dialog.provider.group.other": "기타", "dialog.provider.tag.recommended": "추천", "dialog.provider.opencode.note": "Claude, GPT, Gemini 등을 포함한 엄선된 모델", + "dialog.provider.opencode.tagline": "신뢰할 수 있는 최적화 모델", + "dialog.provider.opencodeGo.tagline": "모두를 위한 저렴한 구독", "dialog.provider.anthropic.note": "Claude Pro/Max 또는 API 키로 연결", "dialog.provider.copilot.note": "Copilot 또는 API 키로 연결", "dialog.provider.openai.note": "ChatGPT Pro/Plus 또는 API 키로 연결", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 3fbe75716d0c..02a73def023d 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -102,6 +102,8 @@ export const dict = { "dialog.provider.group.other": "Andre", "dialog.provider.tag.recommended": "Anbefalt", "dialog.provider.opencode.note": "Utvalgte modeller inkludert Claude, GPT, Gemini og mer", + "dialog.provider.opencode.tagline": "Pålitelige, optimaliserte modeller", + "dialog.provider.opencodeGo.tagline": "Rimelig abonnement for alle", "dialog.provider.anthropic.note": "Direkte tilgang til Claude-modeller, inkludert Pro og Max", "dialog.provider.copilot.note": "AI-modeller for kodeassistanse via GitHub Copilot", "dialog.provider.openai.note": "GPT-modeller for raske, dyktige generelle AI-oppgaver", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index d8ae150d7cb5..587698e68936 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -91,6 +91,8 @@ export const dict = { "dialog.provider.group.other": "Inne", "dialog.provider.tag.recommended": "Zalecane", "dialog.provider.opencode.note": "Wyselekcjonowane modele, w tym Claude, GPT, Gemini i inne", + "dialog.provider.opencode.tagline": "Niezawodne, zoptymalizowane modele", + "dialog.provider.opencodeGo.tagline": "Tania subskrypcja dla każdego", "dialog.provider.anthropic.note": "Bezpośredni dostęp do modeli Claude, w tym Pro i Max", "dialog.provider.copilot.note": "Modele AI do pomocy w kodowaniu przez GitHub Copilot", "dialog.provider.openai.note": "Modele GPT do szybkich i wszechstronnych zadań AI", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index a7d328924a41..4dc5007a6e93 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -99,6 +99,8 @@ export const dict = { "dialog.provider.group.other": "Другие", "dialog.provider.tag.recommended": "Рекомендуемые", "dialog.provider.opencode.note": "Отобранные модели, включая Claude, GPT, Gemini и другие", + "dialog.provider.opencode.tagline": "Надежные оптимизированные модели", + "dialog.provider.opencodeGo.tagline": "Доступная подписка для всех", "dialog.provider.anthropic.note": "Прямой доступ к моделям Claude, включая Pro и Max", "dialog.provider.copilot.note": "ИИ-модели для помощи в кодировании через GitHub Copilot", "dialog.provider.openai.note": "Модели GPT для быстрых и мощных задач общего ИИ", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 1e9773bf021c..831cfe598f39 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -99,6 +99,8 @@ export const dict = { "dialog.provider.group.other": "อื่น ๆ", "dialog.provider.tag.recommended": "แนะนำ", "dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ", + "dialog.provider.opencode.tagline": "โมเดลที่เชื่อถือได้และปรับให้เหมาะสม", + "dialog.provider.opencodeGo.tagline": "การสมัครสมาชิกราคาประหยัดสำหรับทุกคน", "dialog.provider.anthropic.note": "เข้าถึงโมเดล Claude โดยตรง รวมถึง Pro และ Max", "dialog.provider.copilot.note": "โมเดล AI สำหรับการช่วยเหลือในการเขียนโค้ดผ่าน GitHub Copilot", "dialog.provider.openai.note": "โมเดล GPT สำหรับงาน AI ทั่วไปที่รวดเร็วและมีความสามารถ", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 62c7bb9ff233..9cda1058481f 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -126,6 +126,8 @@ export const dict = { "dialog.provider.group.other": "其他", "dialog.provider.tag.recommended": "推荐", "dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接", + "dialog.provider.opencode.tagline": "可靠的优化模型", + "dialog.provider.opencodeGo.tagline": "适合所有人的低成本订阅", "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接", "dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接", "dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index cb8f068f63b7..69f963085ede 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -103,6 +103,8 @@ export const dict = { "dialog.provider.group.other": "其他", "dialog.provider.tag.recommended": "推薦", "dialog.provider.opencode.note": "精選模型,包含 Claude、GPT、Gemini 等等", + "dialog.provider.opencode.tagline": "可靠的優化模型", + "dialog.provider.opencodeGo.tagline": "適合所有人的低成本訂閱", "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 金鑰連線", "dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 金鑰連線", "dialog.provider.copilot.note": "使用 Copilot 或 API 金鑰連線", diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index d88dfdd86f17..7bf189f0902a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -16,9 +16,9 @@ import { useToast } from "../ui/toast" const PROVIDER_PRIORITY: Record = { opencode: 0, - openai: 1, - "github-copilot": 2, - "opencode-go": 3, + "opencode-go": 1, + openai: 2, + "github-copilot": 3, anthropic: 4, google: 5, } @@ -38,7 +38,7 @@ export function createDialogProviderOptions() { opencode: "(Recommended)", anthropic: "(Claude Max or API key)", openai: "(ChatGPT Plus/Pro or API key)", - "opencode-go": "(Low cost)", + "opencode-go": "Low cost subscription for everyone", }[provider.id], category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", async onSelect() { From 05ac0a73e15d7a6d230e0a76230104bbe3f5df22 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:51:58 -0600 Subject: [PATCH 82/94] fix(app): simplify review layout (#14953) Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com> --- packages/app/src/pages/session.tsx | 23 +- .../src/pages/session/session-side-panel.tsx | 229 +++++++++--------- 2 files changed, 111 insertions(+), 141 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 2e440a6b036c..6751f4186f7c 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -416,7 +416,7 @@ export default function Page() { ) const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") - const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened()) + const reviewTab = createMemo(() => isDesktop()) const fileTreeTab = () => layout.fileTree.tab() const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value) @@ -700,33 +700,12 @@ export default function Page() { const active = tabs().active() const tab = active === "review" || (!active && hasReview()) ? "changes" : "all" layout.fileTree.setTab(tab) - return } - - if (fileTreeTab() !== "changes") return - tabs().setActive("review") }, { defer: true }, ), ) - createEffect(() => { - if (!isDesktop()) return - if (!layout.fileTree.opened()) return - if (fileTreeTab() !== "all") return - - const active = tabs().active() - if (active && active !== "review") return - - const first = openedTabs()[0] - if (first) { - tabs().setActive(first) - return - } - - if (contextOpen()) tabs().setActive("context") - }) - createEffect(() => { const id = params.id if (!id) return diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 07b18f3146d5..efcc7c95cc05 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -47,7 +47,7 @@ export function SessionSidePanel(props: { const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const open = createMemo(() => isDesktop() && (view().reviewPanel.opened() || layout.fileTree.opened())) - const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened()) + const reviewTab = createMemo(() => isDesktop()) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) @@ -202,133 +202,124 @@ export function SessionSidePanel(props: { >
- - - - -
- { - const stop = createFileTabListSync({ el, contextOpen }) - onCleanup(stop) - }} - > - - -
-
{language.t("session.tab.review")}
- -
- {reviewCount()} -
-
-
-
-
- - - tabs().close("context")} - aria-label={language.t("common.closeTab")} - /> - - } - hideCloseButton - onMiddleClick={() => tabs().close("context")} - > -
- -
{language.t("session.tab.context")}
+ + + + +
+ { + const stop = createFileTabListSync({ el, contextOpen }) + onCleanup(stop) + }} + > + + +
+
{language.t("session.tab.review")}
+ +
+ {reviewCount()}
- -
- - {(tab) => } - - - + +
+
+
+ + - dialog.show(() => ) - } - aria-label={language.t("command.file.open")} + class="h-5 w-5" + onClick={() => tabs().close("context")} + aria-label={language.t("common.closeTab")} /> - - -
-
- - - - {props.reviewPanel()} - + + } + hideCloseButton + onMiddleClick={() => tabs().close("context")} + > +
+ +
{language.t("session.tab.context")}
+
+
- - - -
-
- -
- {language.t("session.files.selectToOpen")} -
-
+ + {(tab) => } + + + + dialog.show(() => )} + aria-label={language.t("command.file.open")} + /> + + + +
+ + + + {props.reviewPanel()} + + + + + +
+
+ +
+ {language.t("session.files.selectToOpen")}
- - +
+
+
+
- - - -
- -
-
-
+ + + +
+ +
+
+
- - {(tab) => } - -
- - - {(tab) => { - const path = createMemo(() => file.pathFromTab(tab)) - return ( -
- {(p) => } -
- ) - }} -
-
-
- } - > - {props.reviewPanel()} - + + {(tab) => } + + + + + {(tab) => { + const path = createMemo(() => file.pathFromTab(tab)) + return ( +
+ {(p) => } +
+ ) + }} +
+
+
From b4d0090e005e7355b3e8f2594c0ad8538fffe75b Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:53:36 -0600 Subject: [PATCH 83/94] chore: fix flaky test --- .../app/e2e/projects/projects-switch.spec.ts | 22 +++--- packages/app/src/pages/layout.tsx | 42 +++++++++- packages/app/src/pages/layout/helpers.test.ts | 76 ++++++++++++++++++- packages/app/src/pages/layout/helpers.ts | 5 ++ 4 files changed, 131 insertions(+), 14 deletions(-) diff --git a/packages/app/e2e/projects/projects-switch.spec.ts b/packages/app/e2e/projects/projects-switch.spec.ts index f17557a800a8..74b3890888f6 100644 --- a/packages/app/e2e/projects/projects-switch.spec.ts +++ b/packages/app/e2e/projects/projects-switch.spec.ts @@ -9,7 +9,7 @@ import { sessionIDFromUrl, } from "../actions" import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors" -import { createSdk, dirSlug } from "../utils" +import { createSdk, dirSlug, sessionPath } from "../utils" function slugFromUrl(url: string) { return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? "" @@ -51,7 +51,6 @@ test("switching back to a project opens the latest workspace session", async ({ const other = await createTestProject() const otherSlug = dirSlug(other) - const stamp = Date.now() let rootDir: string | undefined let workspaceDir: string | undefined let sessionID: string | undefined @@ -80,6 +79,7 @@ test("switching back to a project opens the latest workspace session", async ({ const workspaceSlug = slugFromUrl(page.url()) workspaceDir = base64Decode(workspaceSlug) + if (!workspaceDir) throw new Error(`Failed to decode workspace slug: ${workspaceSlug}`) await openSidebar(page) const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first() @@ -92,15 +92,14 @@ test("switching back to a project opens the latest workspace session", async ({ await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`)) - const prompt = page.locator(promptSelector) - await expect(prompt).toBeVisible() - await prompt.fill(`project switch remembers workspace ${stamp}`) - await prompt.press("Enter") - - await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("") - const created = sessionIDFromUrl(page.url()) - if (!created) throw new Error(`Failed to parse session id from URL: ${page.url()}`) + const created = await createSdk(workspaceDir) + .session.create() + .then((x) => x.data?.id) + if (!created) throw new Error(`Failed to create session for workspace: ${workspaceDir}`) sessionID = created + + await page.goto(sessionPath(workspaceDir, created)) + await expect(page.locator(promptSelector)).toBeVisible() await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`)) await openSidebar(page) @@ -114,7 +113,8 @@ test("switching back to a project opens the latest workspace session", async ({ await expect(rootButton).toBeVisible() await rootButton.click() - await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`)) + await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe(created) + await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`)) }, { extra: [other] }, ) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 62094a6e4282..cb194052d1e0 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -61,6 +61,7 @@ import { displayName, errorMessage, getDraggableId, + latestRootSession, sortedRootSessions, syncWorkspaceOrder, workspaceKey, @@ -1093,14 +1094,51 @@ export default function Layout(props: ParentProps) { return meta?.worktree ?? directory } - function navigateToProject(directory: string | undefined) { + async function navigateToProject(directory: string | undefined) { if (!directory) return const root = projectRoot(directory) server.projects.touch(root) + const project = layout.projects.list().find((item) => item.worktree === root) + const dirs = Array.from(new Set([root, ...(store.workspaceOrder[root] ?? []), ...(project?.sandboxes ?? [])])) + const openSession = async (target: { directory: string; id: string }) => { + const resolved = await globalSDK.client.session + .get({ sessionID: target.id }) + .then((x) => x.data) + .catch(() => undefined) + const next = resolved?.directory ? resolved : target + setStore("lastProjectSession", root, { directory: next.directory, id: next.id, at: Date.now() }) + navigateWithSidebarReset(`/${base64Encode(next.directory)}/session/${next.id}`) + } const projectSession = store.lastProjectSession[root] if (projectSession?.id) { - navigateWithSidebarReset(`/${base64Encode(projectSession.directory)}/session/${projectSession.id}`) + await openSession(projectSession) + return + } + + const latest = latestRootSession( + dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]), + Date.now(), + ) + if (latest) { + await openSession(latest) + return + } + + const fetched = latestRootSession( + await Promise.all( + dirs.map(async (item) => ({ + path: { directory: item }, + session: await globalSDK.client.session + .list({ directory: item }) + .then((x) => x.data ?? []) + .catch(() => []), + })), + ), + Date.now(), + ) + if (fetched) { + await openSession(fetched) return } diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts index 83d8f4748aba..7627d9ba17c6 100644 --- a/packages/app/src/pages/layout/helpers.test.ts +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -1,6 +1,25 @@ import { describe, expect, test } from "bun:test" +import { type Session } from "@opencode-ai/sdk/v2/client" import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links" -import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers" +import { + displayName, + errorMessage, + getDraggableId, + latestRootSession, + syncWorkspaceOrder, + workspaceKey, +} from "./helpers" + +const session = (input: Partial & Pick) => + ({ + title: "", + version: "v2", + parentID: undefined, + messageCount: 0, + permissions: { session: {}, share: {} }, + time: { created: 0, updated: 0, archived: undefined }, + ...input, + }) as Session describe("layout deep links", () => { test("parses open-project deep links", () => { @@ -73,6 +92,61 @@ describe("layout workspace helpers", () => { expect(result).toEqual(["/root", "/c", "/b"]) }) + test("finds the latest root session across workspaces", () => { + const result = latestRootSession( + [ + { + path: { directory: "/root" }, + session: [session({ id: "root", directory: "/root", time: { created: 1, updated: 1, archived: undefined } })], + }, + { + path: { directory: "/workspace" }, + session: [ + session({ + id: "workspace", + directory: "/workspace", + time: { created: 2, updated: 2, archived: undefined }, + }), + ], + }, + ], + 120_000, + ) + + expect(result?.id).toBe("workspace") + }) + + test("ignores archived and child sessions when finding latest root session", () => { + const result = latestRootSession( + [ + { + path: { directory: "/workspace" }, + session: [ + session({ + id: "archived", + directory: "/workspace", + time: { created: 10, updated: 10, archived: 10 }, + }), + session({ + id: "child", + directory: "/workspace", + parentID: "parent", + time: { created: 20, updated: 20, archived: undefined }, + }), + session({ + id: "root", + directory: "/workspace", + time: { created: 30, updated: 30, archived: undefined }, + }), + ], + }, + ], + 120_000, + ) + + expect(result?.id).toBe("root") + }) + test("extracts draggable id safely", () => { expect(getDraggableId({ draggable: { id: "x" } })).toBe("x") expect(getDraggableId({ draggable: { id: 42 } })).toBeUndefined() diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 6a1e7c0123d8..be4297fbe914 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -28,6 +28,11 @@ export const isRootVisibleSession = (session: Session, directory: string) => export const sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).sort(sortSessions(now)) +export const latestRootSession = (stores: { session: Session[]; path: { directory: string } }[], now: number) => + stores + .flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory))) + .sort(sortSessions(now))[0] + export const childMapByParent = (sessions: Session[]) => { const map = new Map() for (const session of sessions) { From 96ca0de3bc1c22d8ad3ce91b7f068facdaf4851d Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 26 Feb 2026 11:17:15 -0500 Subject: [PATCH 84/94] wip: zen --- packages/console/app/src/routes/zen/util/handler.ts | 1 + packages/console/function/src/log-processor.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 1719625839ce..a6aee5368e3c 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -92,6 +92,7 @@ export async function handler( const stickyProvider = await stickyTracker?.get() const authInfo = await authenticate(modelInfo) const billingSource = validateBilling(authInfo, modelInfo) + logger.metric({ source: billingSource }) const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => { const providerInfo = selectProvider( diff --git a/packages/console/function/src/log-processor.ts b/packages/console/function/src/log-processor.ts index 327fc930b72e..f8b2cf5270b6 100644 --- a/packages/console/function/src/log-processor.ts +++ b/packages/console/function/src/log-processor.ts @@ -13,7 +13,11 @@ export default { url.pathname !== "/zen/v1/chat/completions" && url.pathname !== "/zen/v1/messages" && url.pathname !== "/zen/v1/responses" && - !url.pathname.startsWith("/zen/v1/models/") + !url.pathname.startsWith("/zen/v1/models/") && + url.pathname !== "/zen/go/v1/chat/completions" && + url.pathname !== "/zen/go/v1/messages" && + url.pathname !== "/zen/go/v1/responses" && + !url.pathname.startsWith("/zen/go/v1/models/") ) return From 08f056d412e424eb48842dc99d0955be4e9874ae Mon Sep 17 00:00:00 2001 From: Niu Shuai Date: Fri, 27 Feb 2026 00:59:45 +0800 Subject: [PATCH 85/94] docs: Sync zh_CN docs with English Version (#15228) --- .../src/content/docs/zh-cn/custom-tools.mdx | 26 ++++++ packages/web/src/content/docs/zh-cn/lsp.mdx | 1 + .../web/src/content/docs/zh-cn/plugins.mdx | 4 + .../web/src/content/docs/zh-cn/providers.mdx | 10 +++ packages/web/src/content/docs/zh-cn/tui.mdx | 2 +- packages/web/src/content/docs/zh-cn/zen.mdx | 88 +++++++++++-------- 6 files changed, 91 insertions(+), 40 deletions(-) diff --git a/packages/web/src/content/docs/zh-cn/custom-tools.mdx b/packages/web/src/content/docs/zh-cn/custom-tools.mdx index 81a90a2bcb57..8b44a0450c2a 100644 --- a/packages/web/src/content/docs/zh-cn/custom-tools.mdx +++ b/packages/web/src/content/docs/zh-cn/custom-tools.mdx @@ -79,6 +79,32 @@ export const multiply = tool({ --- +#### 与内置工具的名称冲突 + +自定义工具通过工具名称进行索引。如果自定义工具使用了与内置工具相同的名称,则优先使用自定义工具。 + +例如,这个文件取代了内置的bash工具: + +```ts title=".opencode/tools/bash.ts" +import { tool } from "@opencode-ai/plugin" + +export default tool({ + description: "Restricted bash wrapper", + args: { + command: tool.schema.string(), + }, + async execute(args) { + return `blocked: ${args.command}` + }, +}) +``` + +:::note +除非你有意替换内置工具,否则最好用独特的名字。如果你想禁用内置工具但不想覆盖它,使用 [权限](/docs/permissions). +::: + +--- + ### 参数 你可以使用 `tool.schema`(即 [Zod](https://zod.dev))来定义参数类型。 diff --git a/packages/web/src/content/docs/zh-cn/lsp.mdx b/packages/web/src/content/docs/zh-cn/lsp.mdx index 57b812190215..59dd7082a1e0 100644 --- a/packages/web/src/content/docs/zh-cn/lsp.mdx +++ b/packages/web/src/content/docs/zh-cn/lsp.mdx @@ -27,6 +27,7 @@ OpenCode 内置了多种适用于主流语言的 LSP 服务器: | gopls | .go | 需要 `go` 命令可用 | | hls | .hs, .lhs | 需要 `haskell-language-server-wrapper` 命令可用 | | jdtls | .java | 需要已安装 `Java SDK (version 21+)` | +| julials | .jl | 需要安装 `julia` and `LanguageServer.jl` | | kotlin-ls | .kt, .kts | 为 Kotlin 项目自动安装 | | lua-ls | .lua | 为 Lua 项目自动安装 | | nixd | .nix | 需要 `nixd` 命令可用 | diff --git a/packages/web/src/content/docs/zh-cn/plugins.mdx b/packages/web/src/content/docs/zh-cn/plugins.mdx index 0df6d1ee6591..e8a8bd70cbc3 100644 --- a/packages/web/src/content/docs/zh-cn/plugins.mdx +++ b/packages/web/src/content/docs/zh-cn/plugins.mdx @@ -307,6 +307,10 @@ export const CustomToolsPlugin: Plugin = async (ctx) => { 你的自定义工具将与内置工具一起在 OpenCode 中可用。 +:::note +如果插件工具与内置工具使用相同的名称,则优先使用插件工具。 +::: + --- ### 日志记录 diff --git a/packages/web/src/content/docs/zh-cn/providers.mdx b/packages/web/src/content/docs/zh-cn/providers.mdx index ccc2bf7d406b..9c1616876d72 100644 --- a/packages/web/src/content/docs/zh-cn/providers.mdx +++ b/packages/web/src/content/docs/zh-cn/providers.mdx @@ -131,6 +131,8 @@ OpenCode Zen 是由 OpenCode 团队提供的模型列表,这些模型已经过 2. 使用以下方法之一**配置身份验证**: + *** + #### 环境变量(快速上手) 运行 opencode 时设置以下环境变量之一: @@ -153,6 +155,8 @@ OpenCode Zen 是由 OpenCode 团队提供的模型列表,这些模型已经过 export AWS_REGION=us-east-1 ``` + *** + #### 配置文件(推荐) 如需项目级别或持久化的配置,请使用 `opencode.json`: @@ -180,6 +184,8 @@ OpenCode Zen 是由 OpenCode 团队提供的模型列表,这些模型已经过 配置文件中的选项优先级高于环境变量。 ::: + *** + #### 进阶:VPC 端点 如果你使用 Bedrock 的 VPC 端点: @@ -203,12 +209,16 @@ OpenCode Zen 是由 OpenCode 团队提供的模型列表,这些模型已经过 `endpoint` 选项是通用 `baseURL` 选项的别名,使用了 AWS 特有的术语。如果同时指定了 `endpoint` 和 `baseURL`,则 `endpoint` 优先。 ::: + *** + #### 认证方式 - **`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`**:在 AWS 控制台中创建 IAM 用户并生成访问密钥 - **`AWS_PROFILE`**:使用 `~/.aws/credentials` 中的命名配置文件。需要先通过 `aws configure --profile my-profile` 或 `aws sso login` 进行配置 - **`AWS_BEARER_TOKEN_BEDROCK`**:从 Amazon Bedrock 控制台生成长期 API 密钥 - **`AWS_WEB_IDENTITY_TOKEN_FILE` / `AWS_ROLE_ARN`**:适用于 EKS IRSA(服务账户的 IAM 角色)或其他支持 OIDC 联合的 Kubernetes 环境。使用服务账户注解时,Kubernetes 会自动注入这些环境变量。 + *** + #### 认证优先级 Amazon Bedrock 使用以下认证优先级: diff --git a/packages/web/src/content/docs/zh-cn/tui.mdx b/packages/web/src/content/docs/zh-cn/tui.mdx index e34c088cb3a9..df8ce38fecc3 100644 --- a/packages/web/src/content/docs/zh-cn/tui.mdx +++ b/packages/web/src/content/docs/zh-cn/tui.mdx @@ -234,7 +234,7 @@ How is auth handled in @packages/functions/src/api/index.ts? 列出可用主题。 ```bash frame="none" -/theme +/themes ``` **快捷键:** `ctrl+x t` diff --git a/packages/web/src/content/docs/zh-cn/zen.mdx b/packages/web/src/content/docs/zh-cn/zen.mdx index 39358c417007..e3fe35e8672b 100644 --- a/packages/web/src/content/docs/zh-cn/zen.mdx +++ b/packages/web/src/content/docs/zh-cn/zen.mdx @@ -64,19 +64,22 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。 | GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.1 | minimax-m2.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 Free | glm-5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 4.7 | glm-4.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -104,42 +107,47 @@ https://opencode.ai/zen/v1/models 我们支持按量付费模式。以下是**每 100 万 Token** 的价格。 -| 模型 | 输入 | 输出 | 缓存读取 | 缓存写入 | -| -------------------------------- | ------ | ------ | -------- | -------- | -| Big Pickle | 免费 | 免费 | 免费 | - | -| MiniMax M2.5 Free | 免费 | 免费 | 免费 | - | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | -| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - | -| GLM 5 | $1.00 | $3.20 | $0.20 | - | -| GLM 4.7 | $0.60 | $2.20 | $0.10 | - | -| GLM 4.6 | $0.60 | $2.20 | $0.10 | - | -| Kimi K2.5 Free | 免费 | 免费 | 免费 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - | -| Kimi K2 Thinking | $0.40 | $2.50 | - | - | -| Kimi K2 | $0.40 | $2.50 | - | - | -| Qwen3 Coder 480B | $0.45 | $1.50 | - | - | -| Claude Sonnet 4.5 (≤ 200K Token) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4.5 (> 200K Token) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K Token) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K Token) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 | -| Claude Opus 4.6 (≤ 200K Token) | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.6 (> 200K Token) | $10.00 | $37.50 | $1.00 | $12.50 | -| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | -| Gemini 3 Pro (≤ 200K Token) | $2.00 | $12.00 | $0.20 | - | -| Gemini 3 Pro (> 200K Token) | $4.00 | $18.00 | $0.40 | - | -| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.2 | $1.75 | $14.00 | $0.175 | - | -| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - | -| GPT 5.1 | $1.07 | $8.50 | $0.107 | - | -| GPT 5.1 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5.1 Codex Max | $1.25 | $10.00 | $0.125 | - | -| GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | -| GPT 5 | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | 免费 | 免费 | 免费 | - | +| 模型 | 输入 | 输出 | 缓存读取 | 缓存写入 | +| --------------------------------- | ------ | ------ | -------- | -------- | +| Big Pickle | 免费 | 免费 | 免费 | - | +| MiniMax M2.5 Free | 免费 | 免费 | 免费 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - | +| GLM 5 Free | Free | Free | Free | - | +| GLM 5 | $1.00 | $3.20 | $0.20 | - | +| GLM 4.7 | $0.60 | $2.20 | $0.10 | - | +| GLM 4.6 | $0.60 | $2.20 | $0.10 | - | +| Kimi K2.5 Free | 免费 | 免费 | 免费 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - | +| Kimi K2 Thinking | $0.40 | $2.50 | - | - | +| Kimi K2 | $0.40 | $2.50 | - | - | +| Qwen3 Coder 480B | $0.45 | $1.50 | - | - | +| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 | +| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 | +| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | +| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 4.6 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | +| Claude Sonnet 4.6 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | +| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | +| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | +| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | +| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | +| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | +| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 | +| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | +| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | +| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | +| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | +| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | +| GPT 5.2 | $1.75 | $14.00 | $0.175 | - | +| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - | +| GPT 5.1 | $1.07 | $8.50 | $0.107 | - | +| GPT 5.1 Codex | $1.07 | $8.50 | $0.107 | - | +| GPT 5.1 Codex Max | $1.25 | $10.00 | $0.125 | - | +| GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | +| GPT 5 | $1.07 | $8.50 | $0.107 | - | +| GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | +| GPT 5 Nano | 免费 | 免费 | 免费 | - | 你可能会在使用记录中看到 _Claude Haiku 3.5_。这是一个[低成本模型](/docs/config/#models),用于生成会话标题。 @@ -149,6 +157,7 @@ https://opencode.ai/zen/v1/models 免费模型说明: +- GLM 5 Free 在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 - Kimi K2.5 Free 在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 - MiniMax M2.5 Free 在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 - Big Pickle 是一个隐身模型,在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 @@ -178,6 +187,7 @@ https://opencode.ai/zen/v1/models 我们所有的模型都托管在美国。我们的提供商遵循零保留政策,不会将你的数据用于模型训练,但以下情况除外: - Big Pickle:在免费期间,收集的数据可能会被用于改进模型。 +- GLM 5 Free:在免费期间,收集的数据可能会被用于改进模型。 - Kimi K2.5 Free:在免费期间,收集的数据可能会被用于改进模型。 - MiniMax M2.5 Free:在免费期间,收集的数据可能会被用于改进模型。 - OpenAI API:请求会根据 [OpenAI 数据政策](https://platform.openai.com/docs/guides/your-data)保留 30 天。 From 5745ee87ba9847e32c07fd364c52a6fad23bb55e Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:00:11 +0100 Subject: [PATCH 86/94] refactor(desktop): enhance project tile interaction with suppress hover functionality (#15214) --- .../app/src/pages/layout/sidebar-project.tsx | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index e19e6f430f0d..3c3652e38f36 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -1,4 +1,5 @@ -import { createEffect, createMemo, createSignal, For, Show, type Accessor, type JSX } from "solid-js" +import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" +import { createStore } from "solid-js/store" import { base64Encode } from "@opencode-ai/util/encode" import { Button } from "@opencode-ai/ui/button" import { ContextMenu } from "@opencode-ai/ui/context-menu" @@ -7,7 +8,7 @@ import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { createSortable } from "@thisbeyond/solid-dnd" -import { type LocalProject } from "@/context/layout" +import { useLayout, type LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { useNotification } from "@/context/notification" @@ -60,6 +61,7 @@ const ProjectTile = (props: { selected: Accessor active: Accessor overlay: Accessor + suppressHover: Accessor dirs: Accessor onProjectMouseEnter: (worktree: string, event: MouseEvent) => void onProjectMouseLeave: (worktree: string) => void @@ -71,9 +73,11 @@ const ProjectTile = (props: { closeProject: (directory: string) => void setMenu: (value: boolean) => void setOpen: (value: boolean) => void + setSuppressHover: (value: boolean) => void language: ReturnType }): JSX.Element => { const notification = useNotification() + const layout = useLayout() const unseenCount = createMemo(() => props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), ) @@ -107,17 +111,28 @@ const ProjectTile = (props: { }} onMouseEnter={(event: MouseEvent) => { if (!props.overlay()) return + if (props.suppressHover()) return props.onProjectMouseEnter(props.project.worktree, event) }} onMouseLeave={() => { + if (props.suppressHover()) props.setSuppressHover(false) if (!props.overlay()) return props.onProjectMouseLeave(props.project.worktree) }} onFocus={() => { if (!props.overlay()) return + if (props.suppressHover()) return props.onProjectFocus(props.project.worktree) }} - onClick={() => props.navigateToProject(props.project.worktree)} + onClick={() => { + if (props.selected()) { + props.setSuppressHover(true) + layout.sidebar.toggle() + return + } + props.setSuppressHover(false) + props.navigateToProject(props.project.worktree) + }} onBlur={() => props.setOpen(false)} > @@ -278,16 +293,19 @@ export const SortableProject = (props: { const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2)) const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project)) const dirs = createMemo(() => props.ctx.workspaceIds(props.project)) - const [open, setOpen] = createSignal(false) - const [menu, setMenu] = createSignal(false) + const [state, setState] = createStore({ + open: false, + menu: false, + suppressHover: false, + }) const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened()) const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened()) const active = createMemo(() => projectTileActive({ - menu: menu(), + menu: state.menu, preview: preview(), - open: open(), + open: state.open, overlay: overlay(), hoverProject: props.ctx.hoverProject(), worktree: props.project.worktree, @@ -296,8 +314,14 @@ export const SortableProject = (props: { createEffect(() => { if (preview()) return - if (!open()) return - setOpen(false) + if (!state.open) return + setState("open", false) + }) + + createEffect(() => { + if (!selected()) return + if (!state.open) return + setState("open", false) }) const label = (directory: string) => { @@ -328,6 +352,7 @@ export const SortableProject = (props: { selected={selected} active={active} overlay={overlay} + suppressHover={() => state.suppressHover} dirs={dirs} onProjectMouseEnter={props.ctx.onProjectMouseEnter} onProjectMouseLeave={props.ctx.onProjectMouseLeave} @@ -337,8 +362,9 @@ export const SortableProject = (props: { toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces} workspacesEnabled={props.ctx.workspacesEnabled} closeProject={props.ctx.closeProject} - setMenu={setMenu} - setOpen={setOpen} + setMenu={(value) => setState("menu", value)} + setOpen={(value) => setState("open", value)} + setSuppressHover={(value) => setState("suppressHover", value)} language={language} /> ) @@ -346,17 +372,18 @@ export const SortableProject = (props: { return ( // @ts-ignore
- + { - if (menu()) return - setOpen(value) + if (state.menu) return + if (value && state.suppressHover) return + setState("open", value) if (value) props.ctx.setHoverSession(undefined) }} > @@ -371,7 +398,7 @@ export const SortableProject = (props: { projectChildren={projectChildren} workspaceSessions={workspaceSessions} workspaceChildren={workspaceChildren} - setOpen={setOpen} + setOpen={(value) => setState("open", value)} ctx={props.ctx} language={language} /> From 7e6a007c351fa86a3a38973489b47d79ea32c877 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:23:20 -0600 Subject: [PATCH 87/94] feat(app): auto-accept all permissions mode --- packages/app/src/components/prompt-input.tsx | 19 ++++++--- packages/app/src/context/permission.tsx | 43 ++++++++++++-------- packages/app/src/i18n/ar.ts | 12 +++--- packages/app/src/i18n/br.ts | 12 +++--- packages/app/src/i18n/bs.ts | 12 +++--- packages/app/src/i18n/da.ts | 12 +++--- packages/app/src/i18n/de.ts | 12 +++--- packages/app/src/i18n/en.ts | 12 +++--- packages/app/src/i18n/es.ts | 12 +++--- packages/app/src/i18n/fr.ts | 14 +++---- packages/app/src/i18n/ja.ts | 12 +++--- packages/app/src/i18n/ko.ts | 12 +++--- packages/app/src/i18n/no.ts | 12 +++--- packages/app/src/i18n/pl.ts | 12 +++--- packages/app/src/i18n/ru.ts | 12 +++--- packages/app/src/i18n/th.ts | 12 +++--- packages/app/src/i18n/zh.ts | 12 +++--- packages/app/src/i18n/zht.ts | 12 +++--- 18 files changed, 134 insertions(+), 122 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index adfd592f8d01..9174133acd24 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1048,6 +1048,11 @@ export const PromptInput: Component = (props) => { } const variants = createMemo(() => ["default", ...local.model.variant.list()]) + const accepting = createMemo(() => { + const id = params.id + if (!id) return false + return permission.isAutoAccepting(id, sdk.directory) + }) return (
@@ -1233,7 +1238,9 @@ export const PromptInput: Component = (props) => { diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx index 988723834f95..ccfda5e698c0 100644 --- a/packages/app/src/context/permission.tsx +++ b/packages/app/src/context/permission.tsx @@ -16,10 +16,6 @@ type PermissionRespondFn = (input: { directory?: string }) => void -function shouldAutoAccept(perm: PermissionRequest) { - return perm.permission === "edit" -} - function isNonAllowRule(rule: unknown) { if (!rule) return false if (typeof rule === "string") return rule !== "allow" @@ -40,10 +36,7 @@ function hasPermissionPromptRules(permission: unknown) { if (Array.isArray(permission)) return false const config = permission as Record - if (isNonAllowRule(config.edit)) return true - if (isNonAllowRule(config.write)) return true - - return false + return Object.values(config).some(isNonAllowRule) } export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({ @@ -61,9 +54,25 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple }) const [store, setStore, _, ready] = persisted( - Persist.global("permission", ["permission.v3"]), + { + ...Persist.global("permission", ["permission.v3"]), + migrate(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return value + + const data = value as Record + if (data.autoAccept) return value + + return { + ...data, + autoAccept: + typeof data.autoAcceptEdits === "object" && data.autoAcceptEdits && !Array.isArray(data.autoAcceptEdits) + ? data.autoAcceptEdits + : {}, + } + }, + }, createStore({ - autoAcceptEdits: {} as Record, + autoAccept: {} as Record, }), ) @@ -112,7 +121,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple function isAutoAccepting(sessionID: string, directory?: string) { const key = acceptKey(sessionID, directory) - return store.autoAcceptEdits[key] ?? store.autoAcceptEdits[sessionID] ?? false + return store.autoAccept[key] ?? store.autoAccept[sessionID] ?? false } function bumpEnableVersion(sessionID: string, directory?: string) { @@ -128,7 +137,6 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple const perm = event.properties if (!isAutoAccepting(perm.sessionID, e.name)) return - if (!shouldAutoAccept(perm)) return respondOnce(perm, e.name) }) @@ -139,8 +147,8 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple const version = bumpEnableVersion(sessionID, directory) setStore( produce((draft) => { - draft.autoAcceptEdits[key] = true - delete draft.autoAcceptEdits[sessionID] + draft.autoAccept[key] = true + delete draft.autoAccept[sessionID] }), ) @@ -152,7 +160,6 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple for (const perm of x.data ?? []) { if (!perm?.id) continue if (perm.sessionID !== sessionID) continue - if (!shouldAutoAccept(perm)) continue respondOnce(perm, directory) } }) @@ -164,8 +171,8 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple const key = directory ? acceptKey(sessionID, directory) : undefined setStore( produce((draft) => { - if (key) delete draft.autoAcceptEdits[key] - delete draft.autoAcceptEdits[sessionID] + if (key) delete draft.autoAccept[key] + delete draft.autoAccept[sessionID] }), ) } @@ -174,7 +181,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple ready, respond, autoResponds(permission: PermissionRequest, directory?: string) { - return isAutoAccepting(permission.sessionID, directory) && shouldAutoAccept(permission) + return isAutoAccepting(permission.sessionID, directory) }, isAutoAccepting, toggleAutoAccept(sessionID: string, directory: string) { diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index e8964a664649..0046a8bc4508 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -65,8 +65,8 @@ export const dict = { "command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي", "command.prompt.mode.shell": "Shell", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا", - "command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا", + "command.permissions.autoaccept.enable": "قبول الأذونات تلقائيًا", + "command.permissions.autoaccept.disable": "إيقاف قبول الأذونات تلقائيًا", "command.workspace.toggle": "تبديل مساحات العمل", "command.workspace.toggle.description": "تمكين أو تعطيل مساحات العمل المتعددة في الشريط الجانبي", "command.session.undo": "تراجع", @@ -366,10 +366,10 @@ export const dict = { "toast.workspace.enabled.description": "الآن يتم عرض عدة worktrees في الشريط الجانبي", "toast.workspace.disabled.title": "تم تعطيل مساحات العمل", "toast.workspace.disabled.description": "يتم عرض worktree الرئيسي فقط في الشريط الجانبي", - "toast.permissions.autoaccept.on.title": "قبول التعديلات تلقائيًا", - "toast.permissions.autoaccept.on.description": "سيتم الموافقة تلقائيًا على أذونات التحرير والكتابة", - "toast.permissions.autoaccept.off.title": "توقف قبول التعديلات تلقائيًا", - "toast.permissions.autoaccept.off.description": "ستتطلب أذونات التحرير والكتابة موافقة", + "toast.permissions.autoaccept.on.title": "يتم قبول الأذونات تلقائيًا", + "toast.permissions.autoaccept.on.description": "ستتم الموافقة على طلبات الأذونات تلقائيًا", + "toast.permissions.autoaccept.off.title": "تم إيقاف قبول الأذونات تلقائيًا", + "toast.permissions.autoaccept.off.description": "ستتطلب طلبات الأذونات موافقة", "toast.model.none.title": "لم يتم تحديد نموذج", "toast.model.none.description": "قم بتوصيل موفر لتلخيص هذه الجلسة", "toast.file.loadFailed.title": "فشل تحميل الملف", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index f23668a0d875..0d41ba7fcac4 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -65,8 +65,8 @@ export const dict = { "command.model.variant.cycle.description": "Mudar para o próximo nível de esforço", "command.prompt.mode.shell": "Shell", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Aceitar edições automaticamente", - "command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente", + "command.permissions.autoaccept.enable": "Aceitar permissões automaticamente", + "command.permissions.autoaccept.disable": "Parar de aceitar permissões automaticamente", "command.workspace.toggle": "Alternar espaços de trabalho", "command.workspace.toggle.description": "Habilitar ou desabilitar múltiplos espaços de trabalho na barra lateral", "command.session.undo": "Desfazer", @@ -367,10 +367,10 @@ export const dict = { "toast.workspace.enabled.description": "Várias worktrees agora são exibidas na barra lateral", "toast.workspace.disabled.title": "Espaços de trabalho desativados", "toast.workspace.disabled.description": "Apenas a worktree principal é exibida na barra lateral", - "toast.permissions.autoaccept.on.title": "Aceitando edições automaticamente", - "toast.permissions.autoaccept.on.description": "Permissões de edição e escrita serão aprovadas automaticamente", - "toast.permissions.autoaccept.off.title": "Parou de aceitar edições automaticamente", - "toast.permissions.autoaccept.off.description": "Permissões de edição e escrita exigirão aprovação", + "toast.permissions.autoaccept.on.title": "Aceitando permissões automaticamente", + "toast.permissions.autoaccept.on.description": "Solicitações de permissão serão aprovadas automaticamente", + "toast.permissions.autoaccept.off.title": "Parou de aceitar permissões automaticamente", + "toast.permissions.autoaccept.off.description": "Solicitações de permissão exigirão aprovação", "toast.model.none.title": "Nenhum modelo selecionado", "toast.model.none.description": "Conecte um provedor para resumir esta sessão", "toast.file.loadFailed.title": "Falha ao carregar arquivo", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 6951f9db1f8e..a34d857b9671 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -71,8 +71,8 @@ export const dict = { "command.model.variant.cycle.description": "Prebaci na sljedeći nivo", "command.prompt.mode.shell": "Shell", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Automatski prihvataj izmjene", - "command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje izmjena", + "command.permissions.autoaccept.enable": "Automatski prihvati dozvole", + "command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje dozvola", "command.workspace.toggle": "Prikaži/sakrij radne prostore", "command.workspace.toggle.description": "Omogući ili onemogući više radnih prostora u bočnoj traci", "command.session.undo": "Poništi", @@ -405,10 +405,10 @@ export const dict = { "toast.workspace.disabled.title": "Radni prostori onemogućeni", "toast.workspace.disabled.description": "Samo glavni worktree se prikazuje u bočnoj traci", - "toast.permissions.autoaccept.on.title": "Automatsko prihvatanje izmjena", - "toast.permissions.autoaccept.on.description": "Dozvole za izmjene i pisanje biće automatski odobrene", - "toast.permissions.autoaccept.off.title": "Zaustavljeno automatsko prihvatanje izmjena", - "toast.permissions.autoaccept.off.description": "Dozvole za izmjene i pisanje zahtijevaće odobrenje", + "toast.permissions.autoaccept.on.title": "Automatsko prihvatanje dozvola", + "toast.permissions.autoaccept.on.description": "Zahtjevi za dozvole će biti automatski odobreni", + "toast.permissions.autoaccept.off.title": "Zaustavljeno automatsko prihvatanje dozvola", + "toast.permissions.autoaccept.off.description": "Zahtjevi za dozvole će zahtijevati odobrenje", "toast.model.none.title": "Nije odabran model", "toast.model.none.description": "Poveži provajdera da sažmeš ovu sesiju", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index b870fb51a44b..3df23bd433ad 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -71,8 +71,8 @@ export const dict = { "command.model.variant.cycle.description": "Skift til næste indsatsniveau", "command.prompt.mode.shell": "Shell", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Accepter ændringer automatisk", - "command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer", + "command.permissions.autoaccept.enable": "Accepter tilladelser automatisk", + "command.permissions.autoaccept.disable": "Stop med at acceptere tilladelser automatisk", "command.workspace.toggle": "Skift arbejdsområder", "command.workspace.toggle.description": "Aktiver eller deaktiver flere arbejdsområder i sidebjælken", "command.session.undo": "Fortryd", @@ -398,10 +398,10 @@ export const dict = { "toast.theme.title": "Tema skiftet", "toast.scheme.title": "Farveskema", - "toast.permissions.autoaccept.on.title": "Accepterer ændringer automatisk", - "toast.permissions.autoaccept.on.description": "Redigerings- og skrivetilladelser vil automatisk blive godkendt", - "toast.permissions.autoaccept.off.title": "Stoppede automatisk accept af ændringer", - "toast.permissions.autoaccept.off.description": "Redigerings- og skrivetilladelser vil kræve godkendelse", + "toast.permissions.autoaccept.on.title": "Accepterer tilladelser automatisk", + "toast.permissions.autoaccept.on.description": "Anmodninger om tilladelse godkendes automatisk", + "toast.permissions.autoaccept.off.title": "Stoppet med at acceptere tilladelser automatisk", + "toast.permissions.autoaccept.off.description": "Anmodninger om tilladelse vil kræve godkendelse", "toast.workspace.enabled.title": "Arbejdsområder aktiveret", "toast.workspace.enabled.description": "Flere worktrees vises nu i sidepanelet", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 24d00a6813aa..ce48a1953470 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -69,8 +69,8 @@ export const dict = { "command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln", "command.prompt.mode.shell": "Shell", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren", - "command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen", + "command.permissions.autoaccept.enable": "Berechtigungen automatisch akzeptieren", + "command.permissions.autoaccept.disable": "Automatische Akzeptanz von Berechtigungen stoppen", "command.workspace.toggle": "Arbeitsbereiche umschalten", "command.workspace.toggle.description": "Mehrere Arbeitsbereiche in der Seitenleiste aktivieren oder deaktivieren", "command.session.undo": "Rückgängig", @@ -374,10 +374,10 @@ export const dict = { "toast.workspace.enabled.description": "Mehrere Worktrees werden jetzt in der Seitenleiste angezeigt", "toast.workspace.disabled.title": "Arbeitsbereiche deaktiviert", "toast.workspace.disabled.description": "Nur der Haupt-Worktree wird in der Seitenleiste angezeigt", - "toast.permissions.autoaccept.on.title": "Änderungen werden automatisch akzeptiert", - "toast.permissions.autoaccept.on.description": "Bearbeitungs- und Schreibrechte werden automatisch genehmigt", - "toast.permissions.autoaccept.off.title": "Automatische Annahme von Änderungen gestoppt", - "toast.permissions.autoaccept.off.description": "Bearbeitungs- und Schreibrechte erfordern Genehmigung", + "toast.permissions.autoaccept.on.title": "Berechtigungen werden automatisch akzeptiert", + "toast.permissions.autoaccept.on.description": "Berechtigungsanfragen werden automatisch genehmigt", + "toast.permissions.autoaccept.off.title": "Automatische Akzeptanz von Berechtigungen gestoppt", + "toast.permissions.autoaccept.off.description": "Berechtigungsanfragen erfordern eine Genehmigung", "toast.model.none.title": "Kein Modell ausgewählt", "toast.model.none.description": "Verbinden Sie einen Anbieter, um diese Sitzung zusammenzufassen", "toast.file.loadFailed.title": "Datei konnte nicht geladen werden", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index bea29aa352e7..0b7a2e2808b8 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -71,8 +71,8 @@ export const dict = { "command.model.variant.cycle.description": "Switch to the next effort level", "command.prompt.mode.shell": "Shell", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Auto-accept edits", - "command.permissions.autoaccept.disable": "Stop auto-accepting edits", + "command.permissions.autoaccept.enable": "Auto-accept permissions", + "command.permissions.autoaccept.disable": "Stop auto-accepting permissions", "command.workspace.toggle": "Toggle workspaces", "command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar", "command.session.undo": "Undo", @@ -404,10 +404,10 @@ export const dict = { "toast.workspace.disabled.title": "Workspaces disabled", "toast.workspace.disabled.description": "Only the main worktree is shown in the sidebar", - "toast.permissions.autoaccept.on.title": "Auto-accepting edits", - "toast.permissions.autoaccept.on.description": "Edit and write permissions will be automatically approved", - "toast.permissions.autoaccept.off.title": "Stopped auto-accepting edits", - "toast.permissions.autoaccept.off.description": "Edit and write permissions will require approval", + "toast.permissions.autoaccept.on.title": "Auto-accepting permissions", + "toast.permissions.autoaccept.on.description": "Permission requests will be automatically approved", + "toast.permissions.autoaccept.off.title": "Stopped auto-accepting permissions", + "toast.permissions.autoaccept.off.description": "Permission requests will require approval", "toast.model.none.title": "No model selected", "toast.model.none.description": "Connect a provider to summarize this session", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 30c52c928fbd..de490cbe9045 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -71,8 +71,8 @@ export const dict = { "command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo", "command.prompt.mode.shell": "Shell", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente", - "command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente", + "command.permissions.autoaccept.enable": "Aceptar permisos automáticamente", + "command.permissions.autoaccept.disable": "Dejar de aceptar permisos automáticamente", "command.workspace.toggle": "Alternar espacios de trabajo", "command.workspace.toggle.description": "Habilitar o deshabilitar múltiples espacios de trabajo en la barra lateral", "command.session.undo": "Deshacer", @@ -405,10 +405,10 @@ export const dict = { "toast.workspace.disabled.title": "Espacios de trabajo deshabilitados", "toast.workspace.disabled.description": "Solo se muestra el worktree principal en la barra lateral", - "toast.permissions.autoaccept.on.title": "Aceptando ediciones automáticamente", - "toast.permissions.autoaccept.on.description": "Los permisos de edición y escritura serán aprobados automáticamente", - "toast.permissions.autoaccept.off.title": "Se dejó de aceptar ediciones automáticamente", - "toast.permissions.autoaccept.off.description": "Los permisos de edición y escritura requerirán aprobación", + "toast.permissions.autoaccept.on.title": "Aceptando permisos automáticamente", + "toast.permissions.autoaccept.on.description": "Las solicitudes de permisos se aprobarán automáticamente", + "toast.permissions.autoaccept.off.title": "Se dejó de aceptar permisos automáticamente", + "toast.permissions.autoaccept.off.description": "Las solicitudes de permisos requerirán aprobación", "toast.model.none.title": "Ningún modelo seleccionado", "toast.model.none.description": "Conecta un proveedor para resumir esta sesión", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 3b690937e8a4..5e197b4fb417 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -65,8 +65,8 @@ export const dict = { "command.model.variant.cycle.description": "Passer au niveau d'effort suivant", "command.prompt.mode.shell": "Shell", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Accepter automatiquement les modifications", - "command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications", + "command.permissions.autoaccept.enable": "Accepter automatiquement les permissions", + "command.permissions.autoaccept.disable": "Arrêter d'accepter automatiquement les permissions", "command.workspace.toggle": "Basculer les espaces de travail", "command.workspace.toggle.description": "Activer ou désactiver plusieurs espaces de travail dans la barre latérale", "command.session.undo": "Annuler", @@ -368,12 +368,10 @@ export const dict = { "toast.workspace.enabled.description": "Plusieurs worktrees sont désormais affichés dans la barre latérale", "toast.workspace.disabled.title": "Espaces de travail désactivés", "toast.workspace.disabled.description": "Seul le worktree principal est affiché dans la barre latérale", - "toast.permissions.autoaccept.on.title": "Acceptation auto des modifications", - "toast.permissions.autoaccept.on.description": - "Les permissions de modification et d'écriture seront automatiquement approuvées", - "toast.permissions.autoaccept.off.title": "Arrêt acceptation auto des modifications", - "toast.permissions.autoaccept.off.description": - "Les permissions de modification et d'écriture nécessiteront une approbation", + "toast.permissions.autoaccept.on.title": "Acceptation automatique des permissions", + "toast.permissions.autoaccept.on.description": "Les demandes de permission seront approuvées automatiquement", + "toast.permissions.autoaccept.off.title": "Acceptation automatique des permissions arrêtée", + "toast.permissions.autoaccept.off.description": "Les demandes de permission nécessiteront une approbation", "toast.model.none.title": "Aucun modèle sélectionné", "toast.model.none.description": "Connectez un fournisseur pour résumer cette session", "toast.file.loadFailed.title": "Échec du chargement du fichier", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index c8a949e7822e..30f27c197d65 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -65,8 +65,8 @@ export const dict = { "command.model.variant.cycle.description": "次の思考レベルに切り替え", "command.prompt.mode.shell": "シェル", "command.prompt.mode.normal": "プロンプト", - "command.permissions.autoaccept.enable": "編集を自動承認", - "command.permissions.autoaccept.disable": "編集の自動承認を停止", + "command.permissions.autoaccept.enable": "権限を自動承認する", + "command.permissions.autoaccept.disable": "権限の自動承認を停止する", "command.workspace.toggle": "ワークスペースを切り替え", "command.workspace.toggle.description": "サイドバーでの複数のワークスペースの有効化・無効化", "command.session.undo": "元に戻す", @@ -366,10 +366,10 @@ export const dict = { "toast.workspace.enabled.description": "サイドバーに複数のワークツリーが表示されます", "toast.workspace.disabled.title": "ワークスペースが無効になりました", "toast.workspace.disabled.description": "サイドバーにはメインのワークツリーのみが表示されます", - "toast.permissions.autoaccept.on.title": "編集を自動承認中", - "toast.permissions.autoaccept.on.description": "編集と書き込みの権限は自動的に承認されます", - "toast.permissions.autoaccept.off.title": "編集の自動承認を停止しました", - "toast.permissions.autoaccept.off.description": "編集と書き込みの権限には承認が必要です", + "toast.permissions.autoaccept.on.title": "権限を自動承認しています", + "toast.permissions.autoaccept.on.description": "権限の要求は自動的に承認されます", + "toast.permissions.autoaccept.off.title": "権限の自動承認を停止しました", + "toast.permissions.autoaccept.off.description": "権限の要求には承認が必要になります", "toast.model.none.title": "モデルが選択されていません", "toast.model.none.description": "このセッションを要約するにはプロバイダーを接続してください", "toast.file.loadFailed.title": "ファイルの読み込みに失敗しました", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index d5cedc7deaac..da6cde9eabfc 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -69,8 +69,8 @@ export const dict = { "command.model.variant.cycle.description": "다음 생각 수준으로 전환", "command.prompt.mode.shell": "셸", "command.prompt.mode.normal": "프롬프트", - "command.permissions.autoaccept.enable": "편집 자동 수락", - "command.permissions.autoaccept.disable": "편집 자동 수락 중지", + "command.permissions.autoaccept.enable": "권한 자동 수락", + "command.permissions.autoaccept.disable": "권한 자동 수락 중지", "command.workspace.toggle": "작업 공간 전환", "command.workspace.toggle.description": "사이드바에서 다중 작업 공간 활성화 또는 비활성화", "command.session.undo": "실행 취소", @@ -369,10 +369,10 @@ export const dict = { "toast.workspace.enabled.description": "이제 사이드바에 여러 작업 트리가 표시됩니다", "toast.workspace.disabled.title": "작업 공간 비활성화됨", "toast.workspace.disabled.description": "사이드바에 메인 작업 트리만 표시됩니다", - "toast.permissions.autoaccept.on.title": "편집 자동 수락 중", - "toast.permissions.autoaccept.on.description": "편집 및 쓰기 권한이 자동으로 승인됩니다", - "toast.permissions.autoaccept.off.title": "편집 자동 수락 중지됨", - "toast.permissions.autoaccept.off.description": "편집 및 쓰기 권한 승인이 필요합니다", + "toast.permissions.autoaccept.on.title": "권한 자동 수락 중", + "toast.permissions.autoaccept.on.description": "권한 요청이 자동으로 승인됩니다", + "toast.permissions.autoaccept.off.title": "권한 자동 수락 중지됨", + "toast.permissions.autoaccept.off.description": "권한 요청에 승인이 필요합니다", "toast.model.none.title": "선택된 모델 없음", "toast.model.none.description": "이 세션을 요약하려면 공급자를 연결하세요", "toast.file.loadFailed.title": "파일 로드 실패", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 02a73def023d..bc04695d30bc 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -74,8 +74,8 @@ export const dict = { "command.model.variant.cycle.description": "Bytt til neste innsatsnivå", "command.prompt.mode.shell": "Shell", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Godta endringer automatisk", - "command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk", + "command.permissions.autoaccept.enable": "Aksepter tillatelser automatisk", + "command.permissions.autoaccept.disable": "Stopp automatisk akseptering av tillatelser", "command.workspace.toggle": "Veksle arbeidsområder", "command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar", "command.session.undo": "Angre", @@ -406,10 +406,10 @@ export const dict = { "toast.workspace.disabled.title": "Arbeidsområder deaktivert", "toast.workspace.disabled.description": "Kun hoved-worktree vises i sidefeltet", - "toast.permissions.autoaccept.on.title": "Godtar endringer automatisk", - "toast.permissions.autoaccept.on.description": "Redigerings- og skrivetillatelser vil bli godkjent automatisk", - "toast.permissions.autoaccept.off.title": "Sluttet å godta endringer automatisk", - "toast.permissions.autoaccept.off.description": "Redigerings- og skrivetillatelser vil kreve godkjenning", + "toast.permissions.autoaccept.on.title": "Aksepterer tillatelser automatisk", + "toast.permissions.autoaccept.on.description": "Forespørsler om tillatelse vil bli godkjent automatisk", + "toast.permissions.autoaccept.off.title": "Stoppet automatisk akseptering av tillatelser", + "toast.permissions.autoaccept.off.description": "Forespørsler om tillatelse vil kreve godkjenning", "toast.model.none.title": "Ingen modell valgt", "toast.model.none.description": "Koble til en leverandør for å oppsummere denne sesjonen", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 587698e68936..0be46f095c9d 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -65,8 +65,8 @@ export const dict = { "command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku", "command.prompt.mode.shell": "Terminal", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji", - "command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji", + "command.permissions.autoaccept.enable": "Automatycznie akceptuj uprawnienia", + "command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie uprawnień", "command.workspace.toggle": "Przełącz przestrzenie robocze", "command.workspace.toggle.description": "Włącz lub wyłącz wiele przestrzeni roboczych na pasku bocznym", "command.session.undo": "Cofnij", @@ -367,10 +367,10 @@ export const dict = { "toast.workspace.enabled.description": "Kilka worktree jest teraz wyświetlanych na pasku bocznym", "toast.workspace.disabled.title": "Przestrzenie robocze wyłączone", "toast.workspace.disabled.description": "Tylko główny worktree jest wyświetlany na pasku bocznym", - "toast.permissions.autoaccept.on.title": "Automatyczne akceptowanie edycji", - "toast.permissions.autoaccept.on.description": "Uprawnienia do edycji i zapisu będą automatycznie zatwierdzane", - "toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie edycji", - "toast.permissions.autoaccept.off.description": "Uprawnienia do edycji i zapisu będą wymagały zatwierdzenia", + "toast.permissions.autoaccept.on.title": "Automatyczne akceptowanie uprawnień", + "toast.permissions.autoaccept.on.description": "Żądania uprawnień będą automatycznie zatwierdzane", + "toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie uprawnień", + "toast.permissions.autoaccept.off.description": "Żądania uprawnień będą wymagały zatwierdzenia", "toast.model.none.title": "Nie wybrano modelu", "toast.model.none.description": "Połącz dostawcę, aby podsumować tę sesję", "toast.file.loadFailed.title": "Nie udało się załadować pliku", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 4dc5007a6e93..cbb916a59464 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -71,8 +71,8 @@ export const dict = { "command.model.variant.cycle.description": "Переключиться к следующему уровню усилий", "command.prompt.mode.shell": "Оболочка", "command.prompt.mode.normal": "Промпт", - "command.permissions.autoaccept.enable": "Авто-принятие изменений", - "command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений", + "command.permissions.autoaccept.enable": "Автоматически принимать разрешения", + "command.permissions.autoaccept.disable": "Остановить автоматическое принятие разрешений", "command.workspace.toggle": "Переключить рабочие пространства", "command.workspace.toggle.description": "Включить или отключить несколько рабочих пространств в боковой панели", "command.session.undo": "Отменить", @@ -400,10 +400,10 @@ export const dict = { "toast.theme.title": "Тема переключена", "toast.scheme.title": "Цветовая схема", - "toast.permissions.autoaccept.on.title": "Авто-принятие изменений", - "toast.permissions.autoaccept.on.description": "Разрешения на редактирование и запись будут автоматически одобрены", - "toast.permissions.autoaccept.off.title": "Авто-принятие остановлено", - "toast.permissions.autoaccept.off.description": "Редактирование и запись потребуют подтверждения", + "toast.permissions.autoaccept.on.title": "Разрешения принимаются автоматически", + "toast.permissions.autoaccept.on.description": "Запросы на разрешения будут одобряться автоматически", + "toast.permissions.autoaccept.off.title": "Автоматическое принятие разрешений остановлено", + "toast.permissions.autoaccept.off.description": "Запросы на разрешения будут требовать одобрения", "toast.workspace.enabled.title": "Рабочие пространства включены", "toast.workspace.enabled.description": "В боковой панели теперь отображаются несколько рабочих деревьев", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 831cfe598f39..c6a33dc676a2 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -71,8 +71,8 @@ export const dict = { "command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป", "command.prompt.mode.shell": "เชลล์", "command.prompt.mode.normal": "พรอมต์", - "command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ", - "command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ", + "command.permissions.autoaccept.enable": "ยอมรับสิทธิ์โดยอัตโนมัติ", + "command.permissions.autoaccept.disable": "หยุดยอมรับสิทธิ์โดยอัตโนมัติ", "command.workspace.toggle": "สลับพื้นที่ทำงาน", "command.workspace.toggle.description": "เปิดหรือปิดใช้งานพื้นที่ทำงานหลายรายการในแถบด้านข้าง", "command.session.undo": "ยกเลิก", @@ -403,10 +403,10 @@ export const dict = { "toast.workspace.disabled.title": "ปิดใช้งานพื้นที่ทำงานแล้ว", "toast.workspace.disabled.description": "จะแสดงเฉพาะ worktree หลักในแถบด้านข้าง", - "toast.permissions.autoaccept.on.title": "กำลังยอมรับการแก้ไขโดยอัตโนมัติ", - "toast.permissions.autoaccept.on.description": "สิทธิ์การแก้ไขและจะได้รับเขียนการอนุมัติโดยอัตโนมัติ", - "toast.permissions.autoaccept.off.title": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ", - "toast.permissions.autoaccept.off.description": "สิทธิ์การแก้ไขและเขียนจะต้องได้รับการอนุมัติ", + "toast.permissions.autoaccept.on.title": "กำลังยอมรับสิทธิ์โดยอัตโนมัติ", + "toast.permissions.autoaccept.on.description": "คำขอสิทธิ์จะได้รับการอนุมัติโดยอัตโนมัติ", + "toast.permissions.autoaccept.off.title": "หยุดยอมรับสิทธิ์โดยอัตโนมัติแล้ว", + "toast.permissions.autoaccept.off.description": "คำขอสิทธิ์จะต้องได้รับการอนุมัติ", "toast.model.none.title": "ไม่ได้เลือกโมเดล", "toast.model.none.description": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 9cda1058481f..0b5739083696 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -96,8 +96,8 @@ export const dict = { "command.prompt.mode.shell": "Shell", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "自动接受编辑", - "command.permissions.autoaccept.disable": "停止自动接受编辑", + "command.permissions.autoaccept.enable": "自动接受权限", + "command.permissions.autoaccept.disable": "停止自动接受权限", "command.workspace.toggle": "切换工作区", "command.workspace.toggle.description": "在侧边栏启用或禁用多个工作区", @@ -415,10 +415,10 @@ export const dict = { "toast.workspace.enabled.description": "侧边栏现在显示多个工作树", "toast.workspace.disabled.title": "工作区已禁用", "toast.workspace.disabled.description": "侧边栏只显示主工作树", - "toast.permissions.autoaccept.on.title": "自动接受编辑", - "toast.permissions.autoaccept.on.description": "编辑和写入权限将自动获批", - "toast.permissions.autoaccept.off.title": "已停止自动接受编辑", - "toast.permissions.autoaccept.off.description": "编辑和写入权限将需要手动批准", + "toast.permissions.autoaccept.on.title": "正在自动接受权限", + "toast.permissions.autoaccept.on.description": "权限请求将被自动批准", + "toast.permissions.autoaccept.off.title": "已停止自动接受权限", + "toast.permissions.autoaccept.off.description": "权限请求将需要批准", "toast.model.none.title": "未选择模型", "toast.model.none.description": "请先连接提供商以总结此会话", "toast.file.loadFailed.title": "加载文件失败", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 69f963085ede..4e4509e201cc 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -75,8 +75,8 @@ export const dict = { "command.model.variant.cycle.description": "切換到下一個強度等級", "command.prompt.mode.shell": "Shell", "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "自動接受編輯", - "command.permissions.autoaccept.disable": "停止自動接受編輯", + "command.permissions.autoaccept.enable": "自動接受權限", + "command.permissions.autoaccept.disable": "停止自動接受權限", "command.workspace.toggle": "切換工作區", "command.workspace.toggle.description": "在側邊欄啟用或停用多個工作區", "command.session.undo": "復原", @@ -402,10 +402,10 @@ export const dict = { "toast.workspace.disabled.title": "工作區已停用", "toast.workspace.disabled.description": "側邊欄只顯示主工作樹", - "toast.permissions.autoaccept.on.title": "自動接受編輯", - "toast.permissions.autoaccept.on.description": "編輯和寫入權限將自動獲准", - "toast.permissions.autoaccept.off.title": "已停止自動接受編輯", - "toast.permissions.autoaccept.off.description": "編輯和寫入權限將需要手動批准", + "toast.permissions.autoaccept.on.title": "正在自動接受權限", + "toast.permissions.autoaccept.on.description": "權限請求將被自動批准", + "toast.permissions.autoaccept.off.title": "已停止自動接受權限", + "toast.permissions.autoaccept.off.description": "權限請求將需要批准", "toast.model.none.title": "未選擇模型", "toast.model.none.description": "請先連線提供者以總結此工作階段", From 9312867565e1de0f51a7ea8d43c5957afa4c90e5 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:39:45 -0600 Subject: [PATCH 88/94] feat(app): new tabs styling (#15284) Co-authored-by: David Hill --- .../session/session-sortable-tab.tsx | 1 + packages/app/src/pages/session/review-tab.tsx | 33 +--- .../src/pages/session/session-side-panel.tsx | 11 +- packages/ui/src/components/tabs.css | 178 ++++++++++++++++-- packages/ui/src/components/tabs.tsx | 2 + 5 files changed, 174 insertions(+), 51 deletions(-) diff --git a/packages/app/src/components/session/session-sortable-tab.tsx b/packages/app/src/components/session/session-sortable-tab.tsx index b94e7a8e96cf..c1e2da71291d 100644 --- a/packages/app/src/components/session/session-sortable-tab.tsx +++ b/packages/app/src/components/session/session-sortable-tab.tsx @@ -46,6 +46,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v title={language.t("common.closeTab")} keybind={command.keybind("tab.close")} placement="bottom" + gutter={10} > { - const node = button - if (!node) return - - const scroll = node.parentElement - if (!scroll) return - - const handler = () => { - const rect = node.getBoundingClientRect() - const scrollRect = scroll.getBoundingClientRect() - setState("stuck", rect.right >= scrollRect.right && scroll.scrollWidth > scroll.clientWidth) - } - - scroll.addEventListener("scroll", handler, { passive: true }) - const observer = new ResizeObserver(handler) - observer.observe(scroll) - handler() - onCleanup(() => { - scroll.removeEventListener("scroll", handler) - observer.disconnect() - }) - }) - return ( -
+
{props.children}
) diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index efcc7c95cc05..5c8efff38129 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -219,13 +219,11 @@ export function SessionSidePanel(props: { }} > - +
{language.t("session.tab.review")}
-
- {reviewCount()} -
+
{reviewCount()}
@@ -234,7 +232,7 @@ export function SessionSidePanel(props: { + dialog.show(() => )} aria-label={language.t("command.file.open")} /> @@ -312,7 +311,7 @@ export function SessionSidePanel(props: { {(tab) => { const path = createMemo(() => file.pathFromTab(tab)) return ( -
+
{(p) => }
) diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index 03df9cd84eae..43b74cf33e17 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -1,4 +1,9 @@ [data-component="tabs"] { + --tabs-bar-height: 48px; + --tabs-compact-pill-height: 24px; + --tabs-compact-pill-radius: 6px; + --tabs-compact-pill-padding-x: 4px; + width: 100%; height: 100%; display: flex; @@ -93,17 +98,6 @@ outline: none; box-shadow: none; } - &:has([data-hidden]) { - [data-slot="tabs-trigger-close-button"] { - opacity: 0; - } - - &:hover { - [data-slot="tabs-trigger-close-button"] { - opacity: 1; - } - } - } &:has([data-selected]) { color: var(--text-strong); background-color: transparent; @@ -112,6 +106,7 @@ opacity: 1; } } + &:hover:not(:disabled):not([data-selected]) { color: var(--text-strong); } @@ -140,6 +135,118 @@ } } + #review-panel &[data-variant="normal"][data-orientation="horizontal"] { + background-color: var(--background-stronger); + + [data-slot="tabs-list"] { + height: var(--tabs-bar-height); + padding-left: 12px; + padding-right: 0; + --tabs-review-gap: 16px; + --tabs-review-fade: 16px; + gap: var(--tabs-review-gap); + background-color: var(--background-stronger); + border-bottom: 1px solid var(--border-weak-base); + + &::after { + display: none; + } + + > .sticky { + border-bottom: none; + background-color: var(--background-stronger); + + &::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: calc(var(--tabs-review-fade) * -1); + width: var(--tabs-review-fade); + pointer-events: none; + background: linear-gradient(90deg, transparent, var(--background-stronger)); + } + } + } + + [data-slot="tabs-trigger-wrapper"] { + height: var(--tabs-compact-pill-height); + margin-block: calc((var(--tabs-bar-height) - var(--tabs-compact-pill-height)) / 2); + max-width: 320px; + padding-inline: var(--tabs-compact-pill-padding-x); + box-sizing: border-box; + border: 1px solid transparent; + border-radius: var(--tabs-compact-pill-radius); + background-color: transparent; + gap: 8px; + color: var(--text-weak); + transition: + color 120ms ease, + background-color 120ms ease, + border-color 120ms ease; + + &::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: calc((var(--tabs-compact-pill-height) - var(--tabs-bar-height)) / 2); + height: 1px; + background-color: var(--text-strong); + opacity: 0; + transform: scaleX(0.75); + transform-origin: center; + transition: + opacity 120ms ease, + transform 120ms ease; + } + + &[data-value="review"] { + padding-left: 8px; + padding-right: 8px; + } + + [data-slot="tabs-trigger"] { + height: 100%; + padding: 0 !important; + } + + &:has([data-slot="tabs-trigger-close-button"]) { + padding-right: 5px; + [data-slot="tabs-trigger"] { + padding-right: 0 !important; + } + } + + &:has([data-selected]) { + color: var(--text-strong); + background-color: var(--surface-base-active); + border-color: var(--border-weak-base); + + &::after { + opacity: 1; + transform: scaleX(1); + } + } + + [data-component="file-icon"] { + filter: grayscale(1) !important; + transition: filter 120ms ease; + } + + &:has([data-selected]) { + [data-component="file-icon"] { + filter: grayscale(0) !important; + } + } + + &:hover:not(:disabled):not(:has([data-selected])) { + color: var(--text-base); + background-color: var(--surface-base-hover); + } + } + } + &[data-variant="alt"] { [data-slot="tabs-list"] { padding-left: 24px; @@ -282,9 +389,15 @@ } [data-slot="tabs-trigger-wrapper"] { - height: 24px; - border-radius: 6px; + height: var(--tabs-compact-pill-height); + border-radius: var(--tabs-compact-pill-radius); color: var(--text-weak); + box-sizing: border-box; + border: 1px solid transparent; + transition: + color 120ms ease, + background-color 120ms ease, + border-color 120ms ease; &:not(:has([data-selected])):hover:not(:disabled) { color: var(--text-base); @@ -292,6 +405,7 @@ &:has([data-selected]) { color: var(--text-strong); + border-color: var(--border-weak-base); } } } @@ -459,3 +573,41 @@ } } } + +[data-component="tabs-drag-preview"] { + position: relative; + display: flex; + align-items: center; + height: var(--tabs-bar-height, 48px); + max-width: 320px; + padding-inline: var(--tabs-compact-pill-padding-x, 4px); + overflow: hidden; + color: var(--text-strong); + opacity: 0.6; +} + +[data-component="tabs-drag-preview"]::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: calc((var(--tabs-bar-height, 48px) - var(--tabs-compact-pill-height, 24px)) / 2); + height: var(--tabs-compact-pill-height, 24px); + border: 1px solid var(--border-weak-base); + border-radius: var(--tabs-compact-pill-radius, 6px); + background-color: var(--surface-base-active); +} + +[data-component="tabs-drag-preview"]::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 1px; + background-color: var(--text-strong); +} + +[data-component="tabs-drag-preview"] > * { + position: relative; +} diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx index a9dbea7bc4d0..396504dd7283 100644 --- a/packages/ui/src/components/tabs.tsx +++ b/packages/ui/src/components/tabs.tsx @@ -61,6 +61,7 @@ function TabsTrigger(props: ParentProps) { return (
) { {split.children} From 270d084cb114cc16ef15cd6f5bbe10bb768d65de Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 27 Feb 2026 03:11:58 +0530 Subject: [PATCH 89/94] fix(ui): avoid truncating workspace paths in assistant text (#14584) --- packages/ui/src/components/message-part.tsx | 23 +++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 8fbad45bd8a9..0f67d683f6a1 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -145,17 +145,22 @@ function createThrottledValue(getValue: () => string) { return value } -function relativizeProjectPaths(text: string, directory?: string) { - if (!text) return "" - if (!directory) return text - if (directory === "/") return text - if (directory === "\\") return text - return text.split(directory).join("") +function relativizeProjectPath(path: string, directory?: string) { + if (!path) return "" + if (!directory) return path + if (directory === "/") return path + if (directory === "\\") return path + if (path === directory) return "" + + const separator = directory.includes("\\") ? "\\" : "/" + const prefix = directory.endsWith(separator) ? directory : directory + separator + if (!path.startsWith(prefix)) return path + return path.slice(directory.length) } function getDirectory(path: string | undefined) { const data = useData() - return relativizeProjectPaths(_getDirectory(path), data.directory) + return relativizeProjectPath(_getDirectory(path), data.directory) } import type { IconProps } from "./icon" @@ -1066,7 +1071,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { return items.filter((x) => !!x).join(" \u00B7 ") }) - const displayText = () => relativizeProjectPaths((part.text ?? "").trim(), data.directory) + const displayText = () => (part.text ?? "").trim() const throttledText = createThrottledValue(displayText) const isLastTextPart = createMemo(() => { const last = (data.store.part?.[props.message.id] ?? []) @@ -1168,7 +1173,7 @@ ToolRegistry.register({
- {i18n.t("ui.tool.loaded")} {relativizeProjectPaths(filepath, data.directory)} + {i18n.t("ui.tool.loaded")} {relativizeProjectPath(filepath, data.directory)}
)} From a0b3bbffd5459b81fcb59dbe30f0fd62cc8ec414 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 27 Feb 2026 03:12:24 +0530 Subject: [PATCH 90/94] fix(ui): prevent filename and diff count overlap in session changes (#14773) --- packages/ui/src/components/session-review.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index ec048d009bb4..fae181e20ced 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -99,6 +99,7 @@ align-items: center; justify-content: space-between; width: 100%; + min-width: 0; gap: 20px; } @@ -115,9 +116,12 @@ align-items: center; flex-grow: 1; min-width: 0; + overflow: hidden; } [data-slot="session-review-directory"] { + flex: 1 1 auto; + min-width: 0; color: var(--text-base); text-overflow: ellipsis; overflow: hidden; @@ -129,6 +133,11 @@ [data-slot="session-review-filename"] { color: var(--text-strong); flex-shrink: 0; + max-width: 100%; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } [data-slot="session-review-view-button"] { @@ -163,6 +172,7 @@ gap: 16px; align-items: center; justify-content: flex-end; + margin-left: auto; } [data-slot="session-review-diff-chevron"] { From 8c484a05b894be0a88c130badb2353d02d1deb23 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:50:15 -0600 Subject: [PATCH 91/94] fix(app): terminal issues --- packages/opencode/src/pty/index.ts | 104 ++++++----------------------- 1 file changed, 19 insertions(+), 85 deletions(-) diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index 33083485b5f6..dee3fbc54298 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -23,60 +23,6 @@ export namespace Pty { close: (code?: number, reason?: string) => void } - type Subscriber = { - id: number - token: unknown - } - - const sockets = new WeakMap() - const owners = new WeakMap() - let socketCounter = 0 - - const tagSocket = (ws: Socket) => { - if (!ws || typeof ws !== "object") return - const next = (socketCounter = (socketCounter + 1) % Number.MAX_SAFE_INTEGER) - sockets.set(ws, next) - return next - } - - const token = (ws: Socket) => { - const data = ws.data - if (data === undefined) return - if (data === null) return - if (typeof data !== "object") return data - - const id = (data as { connId?: unknown }).connId - if (typeof id === "number" || typeof id === "string") return id - - const href = (data as { href?: unknown }).href - if (typeof href === "string") return href - - const url = (data as { url?: unknown }).url - if (typeof url === "string") return url - if (url && typeof url === "object") { - const href = (url as { href?: unknown }).href - if (typeof href === "string") return href - return url - } - - const events = (data as { events?: unknown }).events - if (typeof events === "number" || typeof events === "string") return events - if (events && typeof events === "object") { - const id = (events as { connId?: unknown }).connId - if (typeof id === "number" || typeof id === "string") return id - - const id2 = (events as { connection?: unknown }).connection - if (typeof id2 === "number" || typeof id2 === "string") return id2 - - const id3 = (events as { id?: unknown }).id - if (typeof id3 === "number" || typeof id3 === "string") return id3 - - return events - } - - return data - } - // WebSocket control frame: 0x00 + UTF-8 JSON. const meta = (cursor: number) => { const json = JSON.stringify({ cursor }) @@ -141,7 +87,7 @@ export namespace Pty { buffer: string bufferCursor: number cursor: number - subscribers: Map + subscribers: Map } const state = Instance.state( @@ -151,9 +97,9 @@ export namespace Pty { try { session.process.kill() } catch {} - for (const ws of session.subscribers.keys()) { + for (const [key, ws] of session.subscribers.entries()) { try { - ws.close() + if (ws.data === key) ws.close() } catch { // ignore } @@ -224,26 +170,21 @@ export namespace Pty { ptyProcess.onData((chunk) => { session.cursor += chunk.length - for (const [ws, sub] of session.subscribers) { + for (const [key, ws] of session.subscribers.entries()) { if (ws.readyState !== 1) { - session.subscribers.delete(ws) - continue - } - - if (typeof ws === "object" && sockets.get(ws) !== sub.id) { - session.subscribers.delete(ws) + session.subscribers.delete(key) continue } - if (token(ws) !== sub.token) { - session.subscribers.delete(ws) + if (ws.data !== key) { + session.subscribers.delete(key) continue } try { ws.send(chunk) } catch { - session.subscribers.delete(ws) + session.subscribers.delete(key) } } @@ -256,9 +197,9 @@ export namespace Pty { ptyProcess.onExit(({ exitCode }) => { log.info("session exited", { id, exitCode }) session.info.status = "exited" - for (const ws of session.subscribers.keys()) { + for (const [key, ws] of session.subscribers.entries()) { try { - ws.close() + if (ws.data === key) ws.close() } catch { // ignore } @@ -291,9 +232,9 @@ export namespace Pty { try { session.process.kill() } catch {} - for (const ws of session.subscribers.keys()) { + for (const [key, ws] of session.subscribers.entries()) { try { - ws.close() + if (ws.data === key) ws.close() } catch { // ignore } @@ -325,23 +266,16 @@ export namespace Pty { } log.info("client connected to session", { id }) - const socketId = tagSocket(ws) - if (socketId === undefined) { - ws.close() - return - } - - const previous = owners.get(ws) - if (previous && previous !== id) { - state().get(previous)?.subscribers.delete(ws) - } + // Use ws.data as the unique key for this connection lifecycle. + // If ws.data is undefined, fallback to ws object. + const connectionKey = ws.data && typeof ws.data === "object" ? ws.data : ws - owners.set(ws, id) - session.subscribers.set(ws, { id: socketId, token: token(ws) }) + // Optionally cleanup if the key somehow exists + session.subscribers.delete(connectionKey) + session.subscribers.set(connectionKey, ws) const cleanup = () => { - session.subscribers.delete(ws) - if (owners.get(ws) === id) owners.delete(ws) + session.subscribers.delete(connectionKey) } const start = session.bufferCursor From 05d77b7d473d8828671ede90fec1f4da75106738 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:05:04 -0600 Subject: [PATCH 92/94] chore: storybook (#15285) Co-authored-by: David Hill --- bun.lock | 203 +++++++++++- packages/storybook/.gitignore | 3 + packages/storybook/.storybook/main.ts | 37 +++ packages/storybook/.storybook/manager.ts | 11 + packages/storybook/.storybook/preview.tsx | 106 ++++++ packages/storybook/.storybook/theme-tool.ts | 21 ++ packages/storybook/debug-storybook.log | 307 ++++++++++++++++++ packages/storybook/package.json | 28 ++ packages/storybook/tsconfig.json | 16 + packages/ui/package.json | 1 + .../ui/src/assets/icons/provider/302ai.svg | 7 + .../ui/src/assets/icons/provider/berget.svg | 3 + .../icons/provider/cloudferro-sherlock.svg | 5 + .../ui/src/assets/icons/provider/firmware.svg | 18 + .../ui/src/assets/icons/provider/gitlab.svg | 3 + .../ui/src/assets/icons/provider/jiekou.svg | 4 + .../ui/src/assets/icons/provider/kilo.svg | 4 + .../icons/provider/kuae-cloud-coding-plan.svg | 3 + .../ui/src/assets/icons/provider/meganova.svg | 7 + .../icons/provider/minimax-cn-coding-plan.svg | 24 ++ .../icons/provider/minimax-coding-plan.svg | 24 ++ .../ui/src/assets/icons/provider/moark.svg | 3 + .../ui/src/assets/icons/provider/nova.svg | 3 + .../src/assets/icons/provider/novita-ai.svg | 10 + .../assets/icons/provider/privatemode-ai.svg | 5 + .../src/assets/icons/provider/qihang-ai.svg | 9 + .../ui/src/assets/icons/provider/qiniu-ai.svg | 7 + .../ui/src/assets/icons/provider/stackit.svg | 4 + .../ui/src/assets/icons/provider/stepfun.svg | 24 ++ .../ui/src/assets/icons/provider/vivgrid.svg | 4 + .../ui/src/components/accordion.stories.tsx | 149 +++++++++ .../ui/src/components/app-icon.stories.tsx | 69 ++++ packages/ui/src/components/avatar.stories.tsx | 76 +++++ .../ui/src/components/basic-tool.stories.tsx | 133 ++++++++ packages/ui/src/components/button.stories.tsx | 108 ++++++ packages/ui/src/components/card.stories.tsx | 90 +++++ .../ui/src/components/checkbox.stories.tsx | 71 ++++ packages/ui/src/components/code.stories.tsx | 70 ++++ .../ui/src/components/collapsible.stories.tsx | 86 +++++ .../src/components/context-menu.stories.tsx | 113 +++++++ packages/ui/src/components/dialog.stories.tsx | 173 ++++++++++ .../src/components/diff-changes.stories.tsx | 81 +++++ .../ui/src/components/diff-ssr.stories.tsx | 97 ++++++ packages/ui/src/components/diff.stories.tsx | 96 ++++++ .../ui/src/components/dock-prompt.stories.tsx | 62 ++++ .../src/components/dropdown-menu.stories.tsx | 97 ++++++ .../ui/src/components/favicon.stories.tsx | 49 +++ .../ui/src/components/file-icon.stories.tsx | 94 ++++++ packages/ui/src/components/font.stories.tsx | 48 +++ .../ui/src/components/hover-card.stories.tsx | 70 ++++ .../ui/src/components/icon-button.stories.tsx | 74 +++++ packages/ui/src/components/icon.stories.tsx | 170 ++++++++++ .../src/components/image-preview.stories.tsx | 59 ++++ .../src/components/inline-input.stories.tsx | 50 +++ .../ui/src/components/keybind.stories.tsx | 43 +++ .../src/components/line-comment.stories.tsx | 115 +++++++ packages/ui/src/components/list.stories.tsx | 170 ++++++++++ packages/ui/src/components/logo.stories.tsx | 57 ++++ .../ui/src/components/markdown.stories.tsx | 53 +++ .../ui/src/components/message-nav.stories.tsx | 7 + .../src/components/message-part.stories.tsx | 7 + .../ui/src/components/popover.stories.tsx | 87 +++++ .../components/progress-circle.stories.tsx | 59 ++++ .../ui/src/components/progress.stories.tsx | 67 ++++ .../src/components/provider-icon.stories.tsx | 69 ++++ .../src/components/provider-icons/sprite.svg | 247 ++++++++++++++ .../ui/src/components/provider-icons/types.ts | 20 ++ .../ui/src/components/radio-group.stories.tsx | 92 ++++++ .../src/components/resize-handle.stories.tsx | 156 +++++++++ packages/ui/src/components/select.stories.tsx | 113 +++++++ .../src/components/session-review.stories.tsx | 7 + .../src/components/session-turn.stories.tsx | 7 + .../ui/src/components/spinner.stories.tsx | 53 +++ .../sticky-accordion-header.stories.tsx | 54 +++ packages/ui/src/components/switch.stories.tsx | 68 ++++ packages/ui/src/components/tabs.stories.tsx | 179 ++++++++++ packages/ui/src/components/tag.stories.tsx | 58 ++++ .../ui/src/components/text-field.stories.tsx | 111 +++++++ .../src/components/text-shimmer.stories.tsx | 59 ++++ packages/ui/src/components/toast.stories.tsx | 138 ++++++++ .../ui/src/components/tooltip.stories.tsx | 64 ++++ .../ui/src/components/typewriter.stories.tsx | 51 +++ packages/ui/src/storybook/fixtures.ts | 51 +++ packages/ui/src/storybook/scaffold.tsx | 62 ++++ packages/ui/tsconfig.json | 3 +- 85 files changed, 5407 insertions(+), 9 deletions(-) create mode 100644 packages/storybook/.gitignore create mode 100644 packages/storybook/.storybook/main.ts create mode 100644 packages/storybook/.storybook/manager.ts create mode 100644 packages/storybook/.storybook/preview.tsx create mode 100644 packages/storybook/.storybook/theme-tool.ts create mode 100644 packages/storybook/debug-storybook.log create mode 100644 packages/storybook/package.json create mode 100644 packages/storybook/tsconfig.json create mode 100644 packages/ui/src/assets/icons/provider/302ai.svg create mode 100644 packages/ui/src/assets/icons/provider/berget.svg create mode 100644 packages/ui/src/assets/icons/provider/cloudferro-sherlock.svg create mode 100644 packages/ui/src/assets/icons/provider/firmware.svg create mode 100644 packages/ui/src/assets/icons/provider/gitlab.svg create mode 100644 packages/ui/src/assets/icons/provider/jiekou.svg create mode 100644 packages/ui/src/assets/icons/provider/kilo.svg create mode 100644 packages/ui/src/assets/icons/provider/kuae-cloud-coding-plan.svg create mode 100644 packages/ui/src/assets/icons/provider/meganova.svg create mode 100644 packages/ui/src/assets/icons/provider/minimax-cn-coding-plan.svg create mode 100644 packages/ui/src/assets/icons/provider/minimax-coding-plan.svg create mode 100644 packages/ui/src/assets/icons/provider/moark.svg create mode 100644 packages/ui/src/assets/icons/provider/nova.svg create mode 100644 packages/ui/src/assets/icons/provider/novita-ai.svg create mode 100644 packages/ui/src/assets/icons/provider/privatemode-ai.svg create mode 100644 packages/ui/src/assets/icons/provider/qihang-ai.svg create mode 100644 packages/ui/src/assets/icons/provider/qiniu-ai.svg create mode 100644 packages/ui/src/assets/icons/provider/stackit.svg create mode 100644 packages/ui/src/assets/icons/provider/stepfun.svg create mode 100644 packages/ui/src/assets/icons/provider/vivgrid.svg create mode 100644 packages/ui/src/components/accordion.stories.tsx create mode 100644 packages/ui/src/components/app-icon.stories.tsx create mode 100644 packages/ui/src/components/avatar.stories.tsx create mode 100644 packages/ui/src/components/basic-tool.stories.tsx create mode 100644 packages/ui/src/components/button.stories.tsx create mode 100644 packages/ui/src/components/card.stories.tsx create mode 100644 packages/ui/src/components/checkbox.stories.tsx create mode 100644 packages/ui/src/components/code.stories.tsx create mode 100644 packages/ui/src/components/collapsible.stories.tsx create mode 100644 packages/ui/src/components/context-menu.stories.tsx create mode 100644 packages/ui/src/components/dialog.stories.tsx create mode 100644 packages/ui/src/components/diff-changes.stories.tsx create mode 100644 packages/ui/src/components/diff-ssr.stories.tsx create mode 100644 packages/ui/src/components/diff.stories.tsx create mode 100644 packages/ui/src/components/dock-prompt.stories.tsx create mode 100644 packages/ui/src/components/dropdown-menu.stories.tsx create mode 100644 packages/ui/src/components/favicon.stories.tsx create mode 100644 packages/ui/src/components/file-icon.stories.tsx create mode 100644 packages/ui/src/components/font.stories.tsx create mode 100644 packages/ui/src/components/hover-card.stories.tsx create mode 100644 packages/ui/src/components/icon-button.stories.tsx create mode 100644 packages/ui/src/components/icon.stories.tsx create mode 100644 packages/ui/src/components/image-preview.stories.tsx create mode 100644 packages/ui/src/components/inline-input.stories.tsx create mode 100644 packages/ui/src/components/keybind.stories.tsx create mode 100644 packages/ui/src/components/line-comment.stories.tsx create mode 100644 packages/ui/src/components/list.stories.tsx create mode 100644 packages/ui/src/components/logo.stories.tsx create mode 100644 packages/ui/src/components/markdown.stories.tsx create mode 100644 packages/ui/src/components/message-nav.stories.tsx create mode 100644 packages/ui/src/components/message-part.stories.tsx create mode 100644 packages/ui/src/components/popover.stories.tsx create mode 100644 packages/ui/src/components/progress-circle.stories.tsx create mode 100644 packages/ui/src/components/progress.stories.tsx create mode 100644 packages/ui/src/components/provider-icon.stories.tsx create mode 100644 packages/ui/src/components/radio-group.stories.tsx create mode 100644 packages/ui/src/components/resize-handle.stories.tsx create mode 100644 packages/ui/src/components/select.stories.tsx create mode 100644 packages/ui/src/components/session-review.stories.tsx create mode 100644 packages/ui/src/components/session-turn.stories.tsx create mode 100644 packages/ui/src/components/spinner.stories.tsx create mode 100644 packages/ui/src/components/sticky-accordion-header.stories.tsx create mode 100644 packages/ui/src/components/switch.stories.tsx create mode 100644 packages/ui/src/components/tabs.stories.tsx create mode 100644 packages/ui/src/components/tag.stories.tsx create mode 100644 packages/ui/src/components/text-field.stories.tsx create mode 100644 packages/ui/src/components/text-shimmer.stories.tsx create mode 100644 packages/ui/src/components/toast.stories.tsx create mode 100644 packages/ui/src/components/tooltip.stories.tsx create mode 100644 packages/ui/src/components/typewriter.stories.tsx create mode 100644 packages/ui/src/storybook/fixtures.ts create mode 100644 packages/ui/src/storybook/scaffold.tsx diff --git a/bun.lock b/bun.lock index 27a2a9a2b632..f9f48eddd0e7 100644 --- a/bun.lock +++ b/bun.lock @@ -418,6 +418,27 @@ "typescript": "catalog:", }, }, + "packages/storybook": { + "name": "@opencode-ai/storybook", + "devDependencies": { + "@opencode-ai/ui": "workspace:*", + "@solidjs/meta": "catalog:", + "@storybook/addon-a11y": "^10.2.10", + "@storybook/addon-docs": "^10.2.10", + "@storybook/addon-links": "^10.2.10", + "@storybook/addon-onboarding": "^10.2.10", + "@storybook/addon-vitest": "^10.2.10", + "@tsconfig/node22": "catalog:", + "@types/node": "catalog:", + "@types/react": "18.0.25", + "react": "18.2.0", + "solid-js": "catalog:", + "storybook": "^10.2.10", + "storybook-solidjs-vite": "^10.0.9", + "typescript": "catalog:", + "vite": "catalog:", + }, + }, "packages/ui": { "name": "@opencode-ai/ui", "version": "1.2.15", @@ -1136,6 +1157,8 @@ "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="], + "@joshwooding/vite-plugin-react-docgen-typescript": ["@joshwooding/vite-plugin-react-docgen-typescript@0.6.4", "", { "dependencies": { "glob": "^13.0.1", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "typescript": ">= 4.3.x", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-6PyZBYKnnVNqOSB0YFly+62R7dmov8segT27A+RVTBVd4iAE6kbW9QBJGlyR2yG4D4ohzhZSTIu7BK1UTtmFFA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -1208,6 +1231,8 @@ "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], @@ -1302,6 +1327,8 @@ "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], + "@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"], + "@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"], "@opencode-ai/util": ["@opencode-ai/util@workspace:packages/util"], @@ -1774,6 +1801,26 @@ "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@storybook/addon-a11y": ["@storybook/addon-a11y@10.2.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-1S9pDXgvbHhBStGarCvfJ3/rfcaiAcQHRhuM3Nk4WGSIYtC1LCSRuzYdDYU0aNRpdCbCrUA7kUCbqvIE3tH+3Q=="], + + "@storybook/addon-docs": ["@storybook/addon-docs@10.2.10", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.2.10", "@storybook/icons": "^2.0.1", "@storybook/react-dom-shim": "10.2.10", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-2wIYtdvZIzPbQ5194M5Igpy8faNbQ135nuO5ZaZ2VuttqGr+IJcGnDP42zYwbAsGs28G8ohpkbSgIzVyJWUhPQ=="], + + "@storybook/addon-links": ["@storybook/addon-links@10.2.10", "", { "dependencies": { "@storybook/global": "^5.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.2.10" }, "optionalPeers": ["react"] }, "sha512-oo9Xx4/2OVJtptXKpqH4ySri7ZuBdiSOXlZVGejEfLa0Jeajlh/KIlREpGvzPPOqUVT7dSddWzBjJmJUyQC3ew=="], + + "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.2.10", "", { "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-DkzZQTXHp99SpHMIQ5plbbHcs4EWVzWhLXlW+icA8sBlKo5Bwj540YcOApKbqB0m/OzWprsznwN7Kv4vfvHu4w=="], + + "@storybook/addon-vitest": ["@storybook/addon-vitest@10.2.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", "storybook": "^10.2.10", "vitest": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-U2oHw+Ar+Xd06wDTB74VlujhIIW89OHThpJjwgqgM6NWrOC/XLllJ53ILFDyREBkMwpBD7gJQIoQpLEcKBIEhw=="], + + "@storybook/builder-vite": ["@storybook/builder-vite@10.2.10", "", { "dependencies": { "@storybook/csf-plugin": "10.2.10", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.2.10", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-Wd6CYL7LvRRNiXMz977x9u/qMm7nmMw/7Dow2BybQo+Xbfy1KhVjIoZ/gOiG515zpojSozctNrJUbM0+jH1jwg=="], + + "@storybook/csf-plugin": ["@storybook/csf-plugin@10.2.10", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.2.10", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-aFvgaNDAnKMjuyhPK5ialT22pPqMN0XfPBNPeeNVPYztngkdKBa8WFqF/umDd47HxAjebq+vn6uId1xHyOHH3g=="], + + "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], + + "@storybook/icons": ["@storybook/icons@2.0.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg=="], + + "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.2.10", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.2.10" } }, "sha512-TmBrhyLHn8B8rvDHKk5uW5BqzO1M1T+fqFNWg88NIAJOoyX4Uc90FIJjDuN1OJmWKGwB5vLmPwaKBYsTe1yS+w=="], + "@stripe/stripe-js": ["@stripe/stripe-js@8.6.1", "", {}, "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA=="], "@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="], @@ -1866,6 +1913,12 @@ "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + "@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -1876,6 +1929,8 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -2010,7 +2065,7 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], @@ -2020,7 +2075,7 @@ "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], - "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], @@ -2116,6 +2171,8 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], "astro": ["astro@5.7.13", "", { "dependencies": { "@astrojs/compiler": "^2.11.0", "@astrojs/internal-helpers": "0.6.1", "@astrojs/markdown-remark": "6.3.1", "@astrojs/telemetry": "3.2.1", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.6.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.1.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", "shiki": "^3.2.1", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.6.0", "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-cRGq2llKOhV3XMcYwQpfBIUcssN6HEK5CRbcMxAfd9OcFhvWE7KUy50zLioAZVVl3AqgUTJoNTlmZfD2eG0G1w=="], @@ -2144,6 +2201,8 @@ "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], + "axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="], + "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], @@ -2258,7 +2317,7 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="], @@ -2274,6 +2333,8 @@ "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + "cheerio": ["cheerio@1.0.0-rc.12", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "htmlparser2": "^8.0.1", "parse5": "^7.0.0", "parse5-htmlparser2-tree-adapter": "^7.0.0" } }, "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q=="], "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], @@ -2368,6 +2429,8 @@ "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -2388,6 +2451,8 @@ "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], @@ -2440,6 +2505,8 @@ "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -2834,6 +2901,8 @@ "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], @@ -3074,6 +3143,8 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], @@ -3084,6 +3155,8 @@ "luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], @@ -3234,6 +3307,8 @@ "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + "miniflare": ["miniflare@4.20251118.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251118.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-uLSAE/DvOm392fiaig4LOaatxLjM7xzIniFRG5Y3yF9IduOYLLK/pkCPQNCgKQH3ou0YJRHnTN+09LPfqYNTQQ=="], "minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], @@ -3426,6 +3501,8 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], @@ -3492,6 +3569,8 @@ "pretty": ["pretty@2.0.0", "", { "dependencies": { "condense-newlines": "^0.2.1", "extend-shallow": "^2.0.1", "js-beautify": "^1.6.12" } }, "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w=="], + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], @@ -3534,8 +3613,12 @@ "react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], + "react-docgen-typescript": ["react-docgen-typescript@2.4.0", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg=="], + "react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], "react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], @@ -3560,6 +3643,8 @@ "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], @@ -3568,6 +3653,8 @@ "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], @@ -3760,7 +3847,7 @@ "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -3808,6 +3895,10 @@ "stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="], + "storybook": ["storybook@10.2.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-N4U42qKgzMHS7DjqLz5bY4P7rnvJtYkWFCyKspZr3FhPUuy6CWOae3aYC2BjXkHrdug0Jyta6VxFTuB1tYUKhg=="], + + "storybook-solidjs-vite": ["storybook-solidjs-vite@10.0.9", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.1", "@storybook/builder-vite": "^10.0.0", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.8" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-n6MwWCL9mK/qIaUutE9vhGB0X1I1hVnKin2NL+iVC5oXfAiuaABVZlr/1oEeEypsgCdyDOcbEbhJmDWmaqGpPw=="], + "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], "streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="], @@ -3834,6 +3925,8 @@ "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="], "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], @@ -3896,6 +3989,8 @@ "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + "titleize": ["titleize@4.0.0", "", {}, "sha512-ZgUJ1K83rhdu7uh7EHAC2BgY5DzoX8V5rTvoWI4vFysggi6YjLe5gUXABPWAU7VkvGP7P/0YiWq+dcPeYDsf1g=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -3920,6 +4015,8 @@ "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], @@ -4020,6 +4117,8 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + "unstorage": ["unstorage@2.0.0-alpha.5", "", { "peerDependencies": { "@azure/app-configuration": "^1.9.0", "@azure/cosmos": "^4.7.0", "@azure/data-tables": "^13.3.1", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.29.1", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.12.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.35.6", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.8.2", "lru-cache": "^11.2.2", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-Sj8btci21Twnd6M+N+MHhjg3fVn6lAPElPmvFTe0Y/wR0WImErUdA1PzlAaUavHylJ7uDiFwlZDQKm0elG4b7g=="], "unzip-stream": ["unzip-stream@0.3.4", "", { "dependencies": { "binary": "^0.3.0", "mkdirp": "^0.5.1" } }, "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw=="], @@ -4032,6 +4131,8 @@ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], @@ -4106,6 +4207,8 @@ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], @@ -4260,6 +4363,8 @@ "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.10", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.5", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.19.0", "smol-toml": "^1.5.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A=="], + "@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "@astrojs/sitemap/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@astrojs/solid-js/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], @@ -4488,6 +4593,8 @@ "@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], + "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], @@ -4632,8 +4739,18 @@ "@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -4692,8 +4809,6 @@ "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "condense-newlines/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], @@ -4714,6 +4829,8 @@ "esbuild-plugin-copy/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], @@ -4814,6 +4931,10 @@ "postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + "pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -4842,12 +4963,16 @@ "sitemap/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], - "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], "sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], + "storybook/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + + "storybook/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -4880,6 +5005,10 @@ "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "vitest/@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + + "vitest/@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + "vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], @@ -5210,6 +5339,8 @@ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="], @@ -5304,6 +5435,60 @@ "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "storybook/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "storybook/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "storybook/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "storybook/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "storybook/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "storybook/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "storybook/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "storybook/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "storybook/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "storybook/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "storybook/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "storybook/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "storybook/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "storybook/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "storybook/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "storybook/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "storybook/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "storybook/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "storybook/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "storybook/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "storybook/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "storybook/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "storybook/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "storybook/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "storybook/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "storybook/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], @@ -5372,6 +5557,8 @@ "vite-plugin-icons-spritesheet/glob/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="], + "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], diff --git a/packages/storybook/.gitignore b/packages/storybook/.gitignore new file mode 100644 index 000000000000..b122737adfe0 --- /dev/null +++ b/packages/storybook/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +storybook-static/ +.storybook-cache/ diff --git a/packages/storybook/.storybook/main.ts b/packages/storybook/.storybook/main.ts new file mode 100644 index 000000000000..6c850858a553 --- /dev/null +++ b/packages/storybook/.storybook/main.ts @@ -0,0 +1,37 @@ +import { defineMain } from "storybook-solidjs-vite" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const here = path.dirname(fileURLToPath(import.meta.url)) +const ui = path.resolve(here, "../../ui") + +export default defineMain({ + framework: { + name: "storybook-solidjs-vite", + options: {}, + }, + addons: [ + "@storybook/addon-onboarding", + "@storybook/addon-docs", + "@storybook/addon-links", + "@storybook/addon-a11y", + "@storybook/addon-vitest", + ], + stories: ["../../ui/src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + async viteFinal(config) { + const { mergeConfig, searchForWorkspaceRoot } = await import("vite") + return mergeConfig(config, { + resolve: { + dedupe: ["solid-js", "solid-js/web", "@solidjs/meta"], + }, + worker: { + format: "es", + }, + server: { + fs: { + allow: [searchForWorkspaceRoot(process.cwd()), ui], + }, + }, + }) + }, +}) diff --git a/packages/storybook/.storybook/manager.ts b/packages/storybook/.storybook/manager.ts new file mode 100644 index 000000000000..9af9ba0a8280 --- /dev/null +++ b/packages/storybook/.storybook/manager.ts @@ -0,0 +1,11 @@ +import { addons, types } from "storybook/manager-api" +import { ThemeTool } from "./theme-tool" + +addons.register("opencode/theme-toggle", () => { + addons.add("opencode/theme-toggle/tool", { + type: types.TOOL, + title: "Theme", + match: ({ viewMode }) => viewMode === "story" || viewMode === "docs", + render: ThemeTool, + }) +}) diff --git a/packages/storybook/.storybook/preview.tsx b/packages/storybook/.storybook/preview.tsx new file mode 100644 index 000000000000..cb5ee4329bb0 --- /dev/null +++ b/packages/storybook/.storybook/preview.tsx @@ -0,0 +1,106 @@ +import "@opencode-ai/ui/styles" + +import { createEffect, onCleanup, onMount } from "solid-js" +import addonA11y from "@storybook/addon-a11y" +import addonDocs from "@storybook/addon-docs" +import { MetaProvider } from "@solidjs/meta" +import { addons } from "storybook/preview-api" +import { GLOBALS_UPDATED } from "storybook/internal/core-events" +import { createJSXDecorator, definePreview } from "storybook-solidjs-vite" +import { Code } from "@opencode-ai/ui/code" +import { CodeComponentProvider } from "@opencode-ai/ui/context/code" +import { DialogProvider } from "@opencode-ai/ui/context/dialog" +import { DiffComponentProvider } from "@opencode-ai/ui/context/diff" +import { MarkedProvider } from "@opencode-ai/ui/context/marked" +import { Diff } from "@opencode-ai/ui/diff" +import { ThemeProvider, useTheme, type ColorScheme } from "@opencode-ai/ui/theme" +import { Font } from "@opencode-ai/ui/font" + +function resolveScheme(value: unknown): ColorScheme { + if (value === "light" || value === "dark" || value === "system") return value + return "system" +} + +const channel = addons.getChannel() + +const Scheme = (props: { value?: unknown }) => { + const theme = useTheme() + const apply = (value?: unknown) => { + theme.setColorScheme(resolveScheme(value)) + } + createEffect(() => { + apply(props.value) + }) + createEffect(() => { + const root = document.documentElement + root.classList.remove("light", "dark") + root.classList.add(theme.mode()) + }) + onMount(() => { + const handler = (event: { globals?: Record }) => { + apply(event.globals?.theme) + } + channel.on(GLOBALS_UPDATED, handler) + onCleanup(() => channel.off(GLOBALS_UPDATED, handler)) + }) + return null +} + +const frame = createJSXDecorator((Story, context) => { + const override = context.parameters?.themes?.themeOverride + const selected = context.globals?.theme + const pick = override === "light" || override === "dark" ? override : selected + const scheme = resolveScheme(pick) + return ( + + + + + + + + +
+ +
+
+
+
+
+
+
+ ) +}) + +export default definePreview({ + addons: [addonDocs(), addonA11y()], + decorators: [frame], + globalTypes: { + theme: { + name: "Theme", + description: "Global theme", + defaultValue: "light", + }, + }, + parameters: { + actions: { + argTypesRegex: "^on.*", + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + a11y: { + test: "todo", + }, + }, +}) diff --git a/packages/storybook/.storybook/theme-tool.ts b/packages/storybook/.storybook/theme-tool.ts new file mode 100644 index 000000000000..3dac777cd7df --- /dev/null +++ b/packages/storybook/.storybook/theme-tool.ts @@ -0,0 +1,21 @@ +import { createElement } from "react" +import { useGlobals } from "storybook/manager-api" +import { ToggleButton } from "storybook/internal/components" + +export function ThemeTool() { + const [globals, updateGlobals] = useGlobals() + const mode = globals.theme === "dark" ? "dark" : "light" + const toggle = () => { + const next = mode === "dark" ? "light" : "dark" + updateGlobals({ theme: next }) + } + return createElement( + ToggleButton, + { + title: "Toggle theme", + active: mode === "dark", + onClick: toggle, + }, + mode === "dark" ? "Dark" : "Light", + ) +} diff --git a/packages/storybook/debug-storybook.log b/packages/storybook/debug-storybook.log new file mode 100644 index 000000000000..e13d40c8e8ec --- /dev/null +++ b/packages/storybook/debug-storybook.log @@ -0,0 +1,307 @@ +[14:25:48.462] [INFO] storybook v10.2.10 +[14:25:48.749] [DEBUG] Getting package.json info for /Users/davidhill/Documents/Local/opencode/packages/storybook/package.json... +[14:25:48.997] [INFO] Starting... +[14:25:49.095] [DEBUG] Starting preview.. +[14:25:49.098] [WARN] 🚨 Unable to index files: +- ./../ui/src/components/accordion.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/accordion.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/app-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/app-icon.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/avatar.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/avatar.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/basic-tool.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/basic-tool.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/checkbox.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/checkbox.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/code.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/code.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/collapsible.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/collapsible.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/context-menu.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/context-menu.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/dialog.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dialog.stories.tsx (line 10, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/diff-changes.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff-changes.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/diff-ssr.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff-ssr.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/diff.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/dock-prompt.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dock-prompt.stories.tsx (line 15, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/dropdown-menu.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dropdown-menu.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/favicon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/favicon.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/file-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/file-icon.stories.tsx (line 13, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/font.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/font.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/hover-card.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/hover-card.stories.tsx (line 13, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/icon-button.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/icon-button.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/icon.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/image-preview.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/image-preview.stories.tsx (line 13, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/inline-input.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/inline-input.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/keybind.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/keybind.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/line-comment.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/line-comment.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/list.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/list.stories.tsx (line 15, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/logo.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/logo.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/markdown.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/markdown.stories.tsx (line 12, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/message-nav.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/message-nav.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/message-part.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/message-part.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/popover.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/popover.stories.tsx (line 16, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/progress-circle.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/progress-circle.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/progress.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/progress.stories.tsx (line 15, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/provider-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/provider-icon.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/radio-group.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/radio-group.stories.tsx (line 13, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/resize-handle.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/resize-handle.stories.tsx (line 17, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/select.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/select.stories.tsx (line 16, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/session-review.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-review.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/session-turn.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-turn.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/spinner.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/spinner.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/sticky-accordion-header.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/sticky-accordion-header.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/switch.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/switch.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/tabs.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tabs.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/tag.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tag.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/text-field.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/text-field.stories.tsx (line 14, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/text-shimmer.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/text-shimmer.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/toast.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/toast.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/tooltip.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tooltip.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/typewriter.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/typewriter.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +[14:25:49.109] [ERROR] Failed to build the preview +[14:25:49.110] [ERROR] Error: Unable to index files: +- ./../ui/src/components/accordion.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/accordion.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/app-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/app-icon.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/avatar.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/avatar.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/basic-tool.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/basic-tool.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/checkbox.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/checkbox.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/code.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/code.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/collapsible.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/collapsible.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/context-menu.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/context-menu.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/dialog.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dialog.stories.tsx (line 10, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/diff-changes.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff-changes.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/diff-ssr.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff-ssr.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/diff.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/dock-prompt.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dock-prompt.stories.tsx (line 15, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/dropdown-menu.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dropdown-menu.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/favicon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/favicon.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/file-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/file-icon.stories.tsx (line 13, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/font.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/font.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/hover-card.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/hover-card.stories.tsx (line 13, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/icon-button.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/icon-button.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/icon.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/image-preview.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/image-preview.stories.tsx (line 13, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/inline-input.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/inline-input.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/keybind.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/keybind.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/line-comment.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/line-comment.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/list.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/list.stories.tsx (line 15, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/logo.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/logo.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/markdown.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/markdown.stories.tsx (line 12, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/message-nav.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/message-nav.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/message-part.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/message-part.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/popover.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/popover.stories.tsx (line 16, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/progress-circle.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/progress-circle.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/progress.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/progress.stories.tsx (line 15, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/provider-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/provider-icon.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/radio-group.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/radio-group.stories.tsx (line 13, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/resize-handle.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/resize-handle.stories.tsx (line 17, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/select.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/select.stories.tsx (line 16, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/session-review.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-review.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/session-turn.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-turn.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/spinner.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/spinner.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/sticky-accordion-header.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/sticky-accordion-header.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/switch.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/switch.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/tabs.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tabs.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/tag.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tag.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/text-field.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/text-field.stories.tsx (line 14, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/text-shimmer.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/text-shimmer.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/toast.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/toast.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/tooltip.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tooltip.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export +- ./../ui/src/components/typewriter.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/typewriter.stories.tsx (line 6, col 0) + +More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export + at _StoryIndexGenerator.getIndexAndStats (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/core-server/index.js:6085:15) + at async _StoryIndexGenerator.getIndex (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/core-server/index.js:6074:13) + at async getOptimizeDeps (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/@storybook+builder-vite@10.2.10+a2a25316dbcddd7f/node_modules/@storybook/builder-vite/dist/index.js:1862:15) + at async createViteServer (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/@storybook+builder-vite@10.2.10+a2a25316dbcddd7f/node_modules/@storybook/builder-vite/dist/index.js:1888:19) + at async Module.start (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/@storybook+builder-vite@10.2.10+a2a25316dbcddd7f/node_modules/@storybook/builder-vite/dist/index.js:1923:17) + at async storybookDevServer (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/core-server/index.js:7241:83) + at async buildOrThrow (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/core-server/index.js:4504:12) + at async buildDevStandalone (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/core-server/index.js:7611:66) + at async withTelemetry (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/_node-chunks/chunk-S3MWHNYJ.js:218:12) + at async dev (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/bin/core.js:2734:3) +[14:25:49.118] [WARN] Broken build, fix the error above. +You may need to refresh the browser. \ No newline at end of file diff --git a/packages/storybook/package.json b/packages/storybook/package.json new file mode 100644 index 000000000000..2ab92bd5f81a --- /dev/null +++ b/packages/storybook/package.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@opencode-ai/storybook", + "private": true, + "type": "module", + "scripts": { + "storybook": "storybook dev -p 6006", + "build": "storybook build" + }, + "devDependencies": { + "@opencode-ai/ui": "workspace:*", + "@solidjs/meta": "catalog:", + "@storybook/addon-a11y": "^10.2.10", + "@storybook/addon-docs": "^10.2.10", + "@storybook/addon-links": "^10.2.10", + "@storybook/addon-onboarding": "^10.2.10", + "@storybook/addon-vitest": "^10.2.10", + "@tsconfig/node22": "catalog:", + "@types/node": "catalog:", + "@types/react": "18.0.25", + "react": "18.2.0", + "solid-js": "catalog:", + "storybook": "^10.2.10", + "storybook-solidjs-vite": "^10.0.9", + "typescript": "catalog:", + "vite": "catalog:" + } +} diff --git a/packages/storybook/tsconfig.json b/packages/storybook/tsconfig.json new file mode 100644 index 000000000000..68ae315d2bfe --- /dev/null +++ b/packages/storybook/tsconfig.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + "lib": ["es2023", "dom", "dom.iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "strict": true, + "types": ["vite/client", "node"] + }, + "include": [".storybook/**/*.ts", ".storybook/**/*.tsx"] +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 0cdccdc54cc3..b2f9bb40111b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,6 +4,7 @@ "type": "module", "license": "MIT", "exports": { + "./package.json": "./package.json", "./*": "./src/components/*.tsx", "./i18n/*": "./src/i18n/*.ts", "./pierre": "./src/pierre/index.ts", diff --git a/packages/ui/src/assets/icons/provider/302ai.svg b/packages/ui/src/assets/icons/provider/302ai.svg new file mode 100644 index 000000000000..46f2e4315e02 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/302ai.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/packages/ui/src/assets/icons/provider/berget.svg b/packages/ui/src/assets/icons/provider/berget.svg new file mode 100644 index 000000000000..831547a59ed0 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/berget.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/ui/src/assets/icons/provider/cloudferro-sherlock.svg b/packages/ui/src/assets/icons/provider/cloudferro-sherlock.svg new file mode 100644 index 000000000000..6f09a794e6ce --- /dev/null +++ b/packages/ui/src/assets/icons/provider/cloudferro-sherlock.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/ui/src/assets/icons/provider/firmware.svg b/packages/ui/src/assets/icons/provider/firmware.svg new file mode 100644 index 000000000000..baa524ba2d42 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/firmware.svg @@ -0,0 +1,18 @@ + + + + + + diff --git a/packages/ui/src/assets/icons/provider/gitlab.svg b/packages/ui/src/assets/icons/provider/gitlab.svg new file mode 100644 index 000000000000..eef04ace2b09 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/gitlab.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/icons/provider/jiekou.svg b/packages/ui/src/assets/icons/provider/jiekou.svg new file mode 100644 index 000000000000..7fe6378e561c --- /dev/null +++ b/packages/ui/src/assets/icons/provider/jiekou.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui/src/assets/icons/provider/kilo.svg b/packages/ui/src/assets/icons/provider/kilo.svg new file mode 100644 index 000000000000..0a761347a8e2 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/kilo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui/src/assets/icons/provider/kuae-cloud-coding-plan.svg b/packages/ui/src/assets/icons/provider/kuae-cloud-coding-plan.svg new file mode 100644 index 000000000000..3d0d0c455737 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/kuae-cloud-coding-plan.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/icons/provider/meganova.svg b/packages/ui/src/assets/icons/provider/meganova.svg new file mode 100644 index 000000000000..ab294f1e173c --- /dev/null +++ b/packages/ui/src/assets/icons/provider/meganova.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/ui/src/assets/icons/provider/minimax-cn-coding-plan.svg b/packages/ui/src/assets/icons/provider/minimax-cn-coding-plan.svg new file mode 100644 index 000000000000..086e9aa1fca1 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/minimax-cn-coding-plan.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/ui/src/assets/icons/provider/minimax-coding-plan.svg b/packages/ui/src/assets/icons/provider/minimax-coding-plan.svg new file mode 100644 index 000000000000..086e9aa1fca1 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/minimax-coding-plan.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/ui/src/assets/icons/provider/moark.svg b/packages/ui/src/assets/icons/provider/moark.svg new file mode 100644 index 000000000000..dc84a9191c78 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/moark.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/icons/provider/nova.svg b/packages/ui/src/assets/icons/provider/nova.svg new file mode 100644 index 000000000000..9fcae228c0ae --- /dev/null +++ b/packages/ui/src/assets/icons/provider/nova.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/icons/provider/novita-ai.svg b/packages/ui/src/assets/icons/provider/novita-ai.svg new file mode 100644 index 000000000000..ac537b8dd424 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/novita-ai.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/ui/src/assets/icons/provider/privatemode-ai.svg b/packages/ui/src/assets/icons/provider/privatemode-ai.svg new file mode 100644 index 000000000000..edb5a6d76481 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/privatemode-ai.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/ui/src/assets/icons/provider/qihang-ai.svg b/packages/ui/src/assets/icons/provider/qihang-ai.svg new file mode 100644 index 000000000000..3b356637a121 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/qihang-ai.svg @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/packages/ui/src/assets/icons/provider/qiniu-ai.svg b/packages/ui/src/assets/icons/provider/qiniu-ai.svg new file mode 100644 index 000000000000..858560f9ffe7 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/qiniu-ai.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/ui/src/assets/icons/provider/stackit.svg b/packages/ui/src/assets/icons/provider/stackit.svg new file mode 100644 index 000000000000..0d78b781acf6 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/stackit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui/src/assets/icons/provider/stepfun.svg b/packages/ui/src/assets/icons/provider/stepfun.svg new file mode 100644 index 000000000000..086e9aa1fca1 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/stepfun.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/ui/src/assets/icons/provider/vivgrid.svg b/packages/ui/src/assets/icons/provider/vivgrid.svg new file mode 100644 index 000000000000..928fa3ff1ed2 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/vivgrid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui/src/components/accordion.stories.tsx b/packages/ui/src/components/accordion.stories.tsx new file mode 100644 index 000000000000..c53b6d3da9a8 --- /dev/null +++ b/packages/ui/src/components/accordion.stories.tsx @@ -0,0 +1,149 @@ +// @ts-nocheck +import { createEffect, createSignal } from "solid-js" +import * as mod from "./accordion" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Accordion for collapsible content sections with optional multi-open behavior. + +Use one trigger per item; keep content concise. + +### API +- Root supports Kobalte Accordion props: \`value\`, \`multiple\`, \`collapsible\`, \`onChange\`. +- Compose with \`Accordion.Item\`, \`Header\`, \`Trigger\`, \`Content\`. + +### Variants and states +- Single or multiple open items. +- Collapsible or fixed-open behavior. + +### Behavior +- Controlled via \`value\`/\`onChange\` when provided. + +### Accessibility +- TODO: confirm keyboard navigation from Kobalte Accordion. + +### Theming/tokens +- Uses \`data-component="accordion"\` and slot data attributes. + +` + +const story = create({ title: "UI/Accordion", mod }) +export default { + title: "UI/Accordion", + id: "components-accordion", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} +export const Basic = { + args: { + collapsible: true, + multiple: false, + value: "first", + }, + argTypes: { + collapsible: { control: "boolean" }, + multiple: { control: "boolean" }, + value: { + control: "select", + options: ["first", "second", "none"], + mapping: { + none: undefined, + }, + }, + }, + render: (props) => { + const [value, setValue] = createSignal(props.value) + createEffect(() => { + setValue(props.value) + }) + + const current = () => { + if (props.multiple) { + if (Array.isArray(value())) return value() + if (value()) return [value()] + return [] + } + + if (Array.isArray(value())) return value()[0] + return value() + } + + return ( +
+ + + + First + + +
Accordion content.
+
+
+ + + Second + + +
More content.
+
+
+
+
+ ) + }, +} + +export const Multiple = { + args: { + collapsible: true, + multiple: true, + value: ["first", "second"], + }, + render: (props) => ( + + + + First + + +
Accordion content.
+
+
+ + + Second + + +
More content.
+
+
+
+ ), +} + +export const NonCollapsible = { + args: { + collapsible: false, + multiple: false, + value: "first", + }, + render: (props) => ( + + + + First + + +
Accordion content.
+
+
+
+ ), +} diff --git a/packages/ui/src/components/app-icon.stories.tsx b/packages/ui/src/components/app-icon.stories.tsx new file mode 100644 index 000000000000..24460b6da228 --- /dev/null +++ b/packages/ui/src/components/app-icon.stories.tsx @@ -0,0 +1,69 @@ +// @ts-nocheck +import { iconNames } from "./app-icons/types" +import * as mod from "./app-icon" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Application icon renderer for known editor/terminal apps. + +Use in provider or app selection lists. + +### API +- Required: \`id\` (app icon name). +- Accepts standard img props except \`src\`. + +### Variants and states +- Auto-switches themed icons when available. + +### Behavior +- Watches color scheme changes to swap themed assets. + +### Accessibility +- Provide \`alt\` text when the icon conveys meaning. + +### Theming/tokens +- Uses \`data-component="app-icon"\`. + +` + +const story = create({ title: "UI/AppIcon", mod, args: { id: "vscode" } }) +export default { + title: "UI/AppIcon", + id: "components-app-icon", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + id: { + control: "select", + options: iconNames, + }, + }, +} + +export const Basic = story.Basic + +export const AllIcons = { + render: () => ( +
+ {iconNames.map((id) => ( +
+ +
{id}
+
+ ))} +
+ ), +} diff --git a/packages/ui/src/components/avatar.stories.tsx b/packages/ui/src/components/avatar.stories.tsx new file mode 100644 index 000000000000..044224ae8c33 --- /dev/null +++ b/packages/ui/src/components/avatar.stories.tsx @@ -0,0 +1,76 @@ +// @ts-nocheck +import * as mod from "./avatar" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +User avatar with image fallback to initials. + +Use in user lists and headers. + +### API +- Required: \`fallback\` string. +- Optional: \`src\`, \`background\`, \`foreground\`, \`size\`. + +### Variants and states +- Sizes: small, normal, large. +- Image vs fallback state. + +### Behavior +- Uses grapheme-aware fallback rendering. + +### Accessibility +- TODO: provide alt text when using images; currently image is decorative. + +### Theming/tokens +- Uses \`data-component="avatar"\` with size and image state attributes. + +` + +const story = create({ title: "UI/Avatar", mod, args: { fallback: "A" } }) + +export default { + title: "UI/Avatar", + id: "components-avatar", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + size: { + control: "select", + options: ["small", "normal", "large"], + }, + }, +} + +export const Basic = story.Basic + +export const WithImage = { + args: { + src: "https://placehold.co/80x80/png", + fallback: "J", + }, +} + +export const Sizes = { + render: () => ( +
+ + + +
+ ), +} + +export const CustomColors = { + args: { + fallback: "C", + background: "#1f2a44", + foreground: "#f2f5ff", + }, +} diff --git a/packages/ui/src/components/basic-tool.stories.tsx b/packages/ui/src/components/basic-tool.stories.tsx new file mode 100644 index 000000000000..9d9d97acfebd --- /dev/null +++ b/packages/ui/src/components/basic-tool.stories.tsx @@ -0,0 +1,133 @@ +// @ts-nocheck +import { createSignal } from "solid-js" +import * as mod from "./basic-tool" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Expandable tool panel with a structured trigger and optional details. + +Use structured triggers for consistent layout; custom triggers allowed. + +### API +- Required: \`icon\` and \`trigger\` (structured or custom JSX). +- Optional: \`status\`, \`defaultOpen\`, \`forceOpen\`, \`defer\`, \`locked\`. + +### Variants and states +- Pending/running status animates the title via TextShimmer. + +### Behavior +- Uses Collapsible; can defer content rendering until open. +- Locked state prevents closing. + +### Accessibility +- TODO: confirm trigger semantics and aria labeling. + +### Theming/tokens +- Uses \`data-component="tool-trigger"\` and related slots. + +` + +const story = create({ + title: "UI/Basic Tool", + mod, + args: { + icon: "mcp", + defaultOpen: true, + trigger: { + title: "Basic Tool", + subtitle: "Example subtitle", + args: ["--flag", "value"], + }, + children: "Details content", + }, +}) + +export default { + title: "UI/Basic Tool", + id: "components-basic-tool", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const Pending = { + args: { + status: "pending", + trigger: { + title: "Running tool", + subtitle: "Working...", + }, + children: "Progress details", + }, +} + +export const Locked = { + args: { + locked: true, + trigger: { + title: "Locked tool", + subtitle: "Cannot close", + }, + children: "Locked details", + }, +} + +export const Deferred = { + args: { + defer: true, + defaultOpen: false, + trigger: { + title: "Deferred tool", + subtitle: "Content mounts on open", + }, + children: "Deferred content", + }, +} + +export const ForceOpen = { + args: { + forceOpen: true, + trigger: { + title: "Forced open", + subtitle: "Cannot close", + }, + children: "Forced content", + }, +} + +export const HideDetails = { + args: { + hideDetails: true, + trigger: { + title: "Summary only", + subtitle: "Details hidden", + }, + children: "Hidden content", + }, +} + +export const SubtitleAction = { + render: () => { + const [message, setMessage] = createSignal("Subtitle not clicked") + return ( +
+
{message()}
+ setMessage("Subtitle clicked")} + > + Subtitle action details + +
+ ) + }, +} diff --git a/packages/ui/src/components/button.stories.tsx b/packages/ui/src/components/button.stories.tsx new file mode 100644 index 000000000000..24fad5c8a0fa --- /dev/null +++ b/packages/ui/src/components/button.stories.tsx @@ -0,0 +1,108 @@ +// @ts-nocheck +import { Button } from "./button" + +const docs = `### Overview +Primary action button with size, variant, and optional icon support. + +Use \`IconButton\` for icon-only actions. + +### API +- \`variant\`: "primary" | "secondary" | "ghost". +- \`size\`: "small" | "normal" | "large". +- \`icon\`: Icon name for a leading icon. +- Inherits Kobalte Button props and native button attributes. + +### Variants and states +- Variants: primary, secondary, ghost. +- States: disabled. + +### Behavior +- Renders an Icon when \`icon\` is set. + +### Accessibility +- Provide clear label text; use \`aria-label\` for icon-only buttons. + +### Theming/tokens +- Uses \`data-component="button"\` with size/variant data attributes. + +` + +export default { + title: "UI/Button", + id: "components-button", + component: Button, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + args: { + children: "Button", + variant: "secondary", + size: "normal", + }, + argTypes: { + variant: { + control: "select", + options: ["primary", "secondary", "ghost"], + }, + size: { + control: "select", + options: ["small", "normal", "large"], + }, + icon: { + control: "select", + options: ["none", "check", "plus", "arrow-right"], + mapping: { + none: undefined, + }, + }, + }, +} + +export const Primary = { + args: { + variant: "primary", + }, +} + +export const Secondary = {} + +export const Ghost = { + args: { + variant: "ghost", + }, +} + +export const WithIcon = { + args: { + children: "Continue", + icon: "arrow-right", + }, +} + +export const Disabled = { + args: { + variant: "primary", + disabled: true, + }, +} + +export const Sizes = { + render: () => ( +
+ + + +
+ ), +} diff --git a/packages/ui/src/components/card.stories.tsx b/packages/ui/src/components/card.stories.tsx new file mode 100644 index 000000000000..befb2d34fc7a --- /dev/null +++ b/packages/ui/src/components/card.stories.tsx @@ -0,0 +1,90 @@ +// @ts-nocheck +import { Card } from "./card" +import { Button } from "./button" + +const docs = `### Overview +Surface container for grouping related content and actions. + +Pair with \`Button\` or \`Tag\` for quick actions. + +### API +- Optional: \`variant\` (normal, error, warning, success, info). +- Accepts standard div props. + +### Variants and states +- Semantic variants for status-driven messaging. + +### Behavior +- Pure presentational container. + +### Accessibility +- Provide headings or aria labels when used in isolation. + +### Theming/tokens +- Uses \`data-component="card"\` with variant data attributes. + +` + +export default { + title: "UI/Card", + id: "components-card", + component: Card, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + args: { + variant: "normal", + }, + argTypes: { + variant: { + control: "select", + options: ["normal", "error", "warning", "success", "info"], + }, + }, + render: (props: { variant?: "normal" | "error" | "warning" | "success" | "info" }) => { + return ( + +
+
+
Card title
+
Small supporting text.
+
+ +
+
+ ) + }, +} + +export const Normal = {} + +export const Error = { + args: { + variant: "error", + }, +} + +export const Warning = { + args: { + variant: "warning", + }, +} + +export const Success = { + args: { + variant: "success", + }, +} + +export const Info = { + args: { + variant: "info", + }, +} diff --git a/packages/ui/src/components/checkbox.stories.tsx b/packages/ui/src/components/checkbox.stories.tsx new file mode 100644 index 000000000000..ceb09f103e54 --- /dev/null +++ b/packages/ui/src/components/checkbox.stories.tsx @@ -0,0 +1,71 @@ +// @ts-nocheck +import { Icon } from "./icon" +import * as mod from "./checkbox" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Checkbox control for multi-select or agreement inputs. + +Use in forms and multi-select lists. + +### API +- Uses Kobalte Checkbox props (\`checked\`, \`defaultChecked\`, \`onChange\`). +- Optional: \`hideLabel\`, \`description\`, \`icon\`. +- Children render as the label. + +### Variants and states +- Checked/unchecked, indeterminate, disabled (via Kobalte). + +### Behavior +- Controlled or uncontrolled usage. + +### Accessibility +- TODO: confirm aria attributes from Kobalte. + +### Theming/tokens +- Uses \`data-component="checkbox"\` and related slots. + +` + +const story = create({ title: "UI/Checkbox", mod, args: { children: "Checkbox", defaultChecked: true } }) +export default { + title: "UI/Checkbox", + id: "components-checkbox", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const States = { + render: () => ( +
+ Checked + Unchecked + Disabled + With description +
+ ), +} + +export const CustomIcon = { + render: () => ( + } defaultChecked> + Custom icon + + ), +} + +export const HiddenLabel = { + args: { + children: "Hidden label", + hideLabel: true, + }, +} diff --git a/packages/ui/src/components/code.stories.tsx b/packages/ui/src/components/code.stories.tsx new file mode 100644 index 000000000000..992fa6302424 --- /dev/null +++ b/packages/ui/src/components/code.stories.tsx @@ -0,0 +1,70 @@ +// @ts-nocheck +import * as mod from "./code" +import { create } from "../storybook/scaffold" +import { code } from "../storybook/fixtures" + +const docs = `### Overview +Syntax-highlighted code viewer with selection support and large-file virtualization. + +Use alongside \`LineComment\` and \`Diff\` in review workflows. + +### API +- Required: \`file\` with file name + contents. +- Optional: \`language\`, \`annotations\`, \`selectedLines\`, \`commentedLines\`. +- Optional callbacks: \`onRendered\`, \`onLineSelectionEnd\`. + +### Variants and states +- Supports large-file virtualization automatically. + +### Behavior +- Re-renders when \`file\` or rendering options change. +- Optional line selection integrates with selection callbacks. + +### Accessibility +- TODO: confirm keyboard find and selection behavior. + +### Theming/tokens +- Uses \`data-component="code"\` and Pierre CSS variables from \`styleVariables\`. + +` + +const story = create({ + title: "UI/Code", + mod, + args: { + file: code, + language: "ts", + }, +}) + +export default { + title: "UI/Code", + id: "components-code", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const SelectedLines = { + args: { + enableLineSelection: true, + selectedLines: { start: 2, end: 4 }, + }, +} + +export const CommentedLines = { + args: { + commentedLines: [ + { start: 1, end: 1 }, + { start: 5, end: 6 }, + ], + }, +} diff --git a/packages/ui/src/components/collapsible.stories.tsx b/packages/ui/src/components/collapsible.stories.tsx new file mode 100644 index 000000000000..67883b229974 --- /dev/null +++ b/packages/ui/src/components/collapsible.stories.tsx @@ -0,0 +1,86 @@ +// @ts-nocheck +import * as mod from "./collapsible" + +const docs = `### Overview +Toggleable content region with optional arrow indicator. + +Compose \`Collapsible.Trigger\`, \`Collapsible.Content\`, and \`Collapsible.Arrow\`. + +### API +- Root accepts Kobalte Collapsible props (\`open\`, \`defaultOpen\`, \`onOpenChange\`). +- \`variant\` controls styling ("normal" | "ghost"). + +### Variants and states +- Normal and ghost variants. +- Open/closed states. + +### Behavior +- Trigger toggles the content visibility. + +### Accessibility +- TODO: confirm ARIA attributes provided by Kobalte. + +### Theming/tokens +- Uses \`data-component="collapsible"\` and slots for trigger/content/arrow. + +` + +export default { + title: "UI/Collapsible", + id: "components-collapsible", + component: mod.Collapsible, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + variant: { + control: "select", + options: ["normal", "ghost"], + }, + }, +} + +export const Basic = { + args: { + variant: "normal", + defaultOpen: true, + }, + render: (props) => ( + + +
+ Details + +
+
+ +
Optional details sit here.
+
+
+ ), +} + +export const Ghost = { + args: { + variant: "ghost", + defaultOpen: false, + }, + render: (props) => ( + + +
+ Ghost trigger + +
+
+ +
Ghost content.
+
+
+ ), +} diff --git a/packages/ui/src/components/context-menu.stories.tsx b/packages/ui/src/components/context-menu.stories.tsx new file mode 100644 index 000000000000..bee5a5596554 --- /dev/null +++ b/packages/ui/src/components/context-menu.stories.tsx @@ -0,0 +1,113 @@ +// @ts-nocheck +import * as mod from "./context-menu" + +const docs = `### Overview +Context menu for right-click interactions with composable items and submenus. + +Use \`ItemLabel\` and \`ItemDescription\` for rich items. + +### API +- Root accepts Kobalte ContextMenu props (\`open\`, \`defaultOpen\`, \`onOpenChange\`). +- Compose \`Trigger\`, \`Content\`, \`Item\`, \`Separator\`, and optional \`Sub\` sections. + +### Variants and states +- Supports grouped sections and nested submenus. + +### Behavior +- Opens on context menu gesture over the trigger element. + +### Accessibility +- TODO: confirm keyboard and focus behavior from Kobalte. + +### Theming/tokens +- Uses \`data-component="context-menu"\` and slot attributes for styling. + +` + +export default { + title: "UI/ContextMenu", + id: "components-context-menu", + component: mod.ContextMenu, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => ( + + +
+ Right click (or open) here +
+
+ + + + Actions + + Copy + + + Paste + + + + + More + + + Duplicate + + + Move + + + + + +
+ ), +} + +export const CheckboxRadio = { + render: () => ( + + +
+ Right click (or open) here +
+
+ + + Show line numbers + Wrap lines + + + Compact + Comfortable + + + +
+ ), +} diff --git a/packages/ui/src/components/dialog.stories.tsx b/packages/ui/src/components/dialog.stories.tsx new file mode 100644 index 000000000000..60cd0a1c1919 --- /dev/null +++ b/packages/ui/src/components/dialog.stories.tsx @@ -0,0 +1,173 @@ +// @ts-nocheck +import { onMount } from "solid-js" +import * as mod from "./dialog" +import { Button } from "./button" +import { useDialog } from "../context/dialog" + +const docs = `### Overview +Dialog content wrapper used with the DialogProvider for modal flows. + +Provide concise title/description and keep body focused. + +### API +- Optional: \`title\`, \`description\`, \`action\`. +- \`size\`: normal | large | x-large. +- \`fit\` and \`transition\` control layout and animation. + +### Variants and states +- Sizes and optional header/action controls. + +### Behavior +- Intended to be rendered via \`useDialog().show\`. + +### Accessibility +- TODO: confirm focus trapping and aria attributes from Kobalte Dialog. + +### Theming/tokens +- Uses \`data-component="dialog"\` and slot attributes. + +` + +export default { + title: "UI/Dialog", + id: "components-dialog", + component: mod.Dialog, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => { + const dialog = useDialog() + const open = () => + dialog.show(() => ( + + Dialog body content. + + )) + + onMount(open) + + return ( + + ) + }, +} + +export const Sizes = { + render: () => { + const dialog = useDialog() + return ( +
+ + + +
+ ) + }, +} + +export const Transition = { + render: () => { + const dialog = useDialog() + return ( + + ) + }, +} + +export const CustomAction = { + render: () => { + const dialog = useDialog() + return ( + } + > + Dialog body content. + + )) + } + > + Open action dialog + + ) + }, +} + +export const Fit = { + render: () => { + const dialog = useDialog() + return ( + + ) + }, +} diff --git a/packages/ui/src/components/diff-changes.stories.tsx b/packages/ui/src/components/diff-changes.stories.tsx new file mode 100644 index 000000000000..fe0ba6eb4f2e --- /dev/null +++ b/packages/ui/src/components/diff-changes.stories.tsx @@ -0,0 +1,81 @@ +// @ts-nocheck +import * as mod from "./diff-changes" +import { create } from "../storybook/scaffold" +import { changes } from "../storybook/fixtures" + +const docs = `### Overview +Summarize additions/deletions as text or compact bars. + +Pair with \`Diff\`/\`DiffSSR\` to contextualize a change set. + +### API +- Required: \`changes\` as { additions, deletions } or an array of those objects. +- Optional: \`variant\` ("default" | "bars"). + +### Variants and states +- Default text summary or bar visualization. +- Handles zero-change state (renders nothing in default variant). + +### Behavior +- Aggregates arrays into total additions/deletions. + +### Accessibility +- Ensure surrounding context conveys meaning of the counts/bars. + +### Theming/tokens +- Uses \`data-component="diff-changes"\` and diff color tokens. + +` + +const story = create({ + title: "UI/DiffChanges", + mod, + args: { + changes, + variant: "default", + }, +}) + +export default { + title: "UI/DiffChanges", + id: "components-diff-changes", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + variant: { + control: "select", + options: ["default", "bars"], + }, + }, +} + +export const Default = story.Basic + +export const Bars = { + args: { + variant: "bars", + }, +} + +export const MultipleFiles = { + args: { + changes: [ + { additions: 4, deletions: 1 }, + { additions: 8, deletions: 3 }, + { additions: 2, deletions: 0 }, + ], + }, +} + +export const Zero = { + args: { + changes: { additions: 0, deletions: 0 }, + }, +} diff --git a/packages/ui/src/components/diff-ssr.stories.tsx b/packages/ui/src/components/diff-ssr.stories.tsx new file mode 100644 index 000000000000..d1adce280664 --- /dev/null +++ b/packages/ui/src/components/diff-ssr.stories.tsx @@ -0,0 +1,97 @@ +// @ts-nocheck +import { preloadMultiFileDiff } from "@pierre/diffs/ssr" +import { createResource, Show } from "solid-js" +import * as mod from "./diff-ssr" +import { createDefaultOptions } from "../pierre" +import { WorkerPoolProvider } from "../context/worker-pool" +import { getWorkerPools } from "../pierre/worker" +import { diff } from "../storybook/fixtures" + +const docs = `### Overview +Server-rendered diff hydration component for preloaded Pierre diff output. + +Use alongside server routes that preload diffs. +Pair with \`DiffChanges\` for summaries. + +### API +- Required: \`before\`, \`after\`, and \`preloadedDiff\` from \`preloadMultiFileDiff\`. +- Optional: \`diffStyle\`, \`annotations\`, \`selectedLines\`, \`commentedLines\`. + +### Variants and states +- Unified/split styles (preloaded must match the style used during preload). + +### Behavior +- Hydrates pre-rendered diff HTML into a live diff instance. +- Requires a worker pool provider for syntax highlighting. + +### Accessibility +- TODO: confirm keyboard behavior from the Pierre diff engine. + +### Theming/tokens +- Uses \`data-component="diff"\` with Pierre CSS variables and theme tokens. + +` + +const load = async () => { + return preloadMultiFileDiff({ + oldFile: diff.before, + newFile: diff.after, + options: createDefaultOptions("unified"), + }) +} + +const loadSplit = async () => { + return preloadMultiFileDiff({ + oldFile: diff.before, + newFile: diff.after, + options: createDefaultOptions("split"), + }) +} + +export default { + title: "UI/DiffSSR", + id: "components-diff-ssr", + component: mod.Diff, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => { + const [data] = createResource(load) + return ( + + Loading pre-rendered diff...
}> + {(preloaded) => ( +
+ +
+ )} + + + ) + }, +} + +export const Split = { + render: () => { + const [data] = createResource(loadSplit) + return ( + + Loading pre-rendered diff...
}> + {(preloaded) => ( +
+ +
+ )} +
+ + ) + }, +} diff --git a/packages/ui/src/components/diff.stories.tsx b/packages/ui/src/components/diff.stories.tsx new file mode 100644 index 000000000000..03bf4a0e0f0f --- /dev/null +++ b/packages/ui/src/components/diff.stories.tsx @@ -0,0 +1,96 @@ +// @ts-nocheck +import * as mod from "./diff" +import { create } from "../storybook/scaffold" +import { diff } from "../storybook/fixtures" + +const docs = `### Overview +Render a code diff with OpenCode styling using the Pierre diff engine. + +Pair with \`DiffChanges\` for summary counts. +Use \`LineComment\` or external UI for annotation workflows. + +### API +- Required: \`before\` and \`after\` file contents (name + contents). +- Optional: \`diffStyle\` ("unified" | "split"), \`annotations\`, \`selectedLines\`, \`commentedLines\`. +- Optional interaction: \`enableLineSelection\`, \`onLineSelectionEnd\`. +- Passes through Pierre FileDiff options (see component source). + +### Variants and states +- Unified and split diff styles. +- Optional line selection + commented line highlighting. + +### Behavior +- Re-renders when \`before\`/\`after\` or diff options change. +- Line selection uses mouse drag/selection when enabled. + +### Accessibility +- TODO: confirm keyboard behavior from the Pierre diff engine. +- Provide surrounding labels or headings when used as a standalone view. + +### Theming/tokens +- Uses \`data-component="diff"\` and Pierre CSS variables from \`styleVariables\`. +- Colors derive from theme tokens (diff add/delete, background, text). + +` + +const story = create({ + title: "UI/Diff", + mod, + args: { + before: diff.before, + after: diff.after, + diffStyle: "unified", + }, +}) + +export default { + title: "UI/Diff", + id: "components-diff", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + diffStyle: { + control: "select", + options: ["unified", "split"], + }, + enableLineSelection: { + control: "boolean", + }, + }, +} + +export const Unified = story.Basic + +export const Split = { + args: { + diffStyle: "split", + }, +} + +export const Selectable = { + args: { + enableLineSelection: true, + }, +} + +export const SelectedLines = { + args: { + selectedLines: { start: 2, end: 4 }, + }, +} + +export const CommentedLines = { + args: { + commentedLines: [ + { start: 1, end: 1 }, + { start: 4, end: 4 }, + ], + }, +} diff --git a/packages/ui/src/components/dock-prompt.stories.tsx b/packages/ui/src/components/dock-prompt.stories.tsx new file mode 100644 index 000000000000..de017a1ba261 --- /dev/null +++ b/packages/ui/src/components/dock-prompt.stories.tsx @@ -0,0 +1,62 @@ +// @ts-nocheck +import * as mod from "./dock-prompt" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Docked prompt layout for questions and permission requests. + +Use with form controls or confirmation buttons in the footer. + +### API +- Required: \`kind\` (question | permission), \`header\`, \`children\`, \`footer\`. +- Optional: \`ref\` for measuring or focus management. + +### Variants and states +- Question and permission layouts (data attributes). + +### Behavior +- Pure layout component; behavior handled by parent. + +### Accessibility +- Ensure header and footer content provide clear context and actions. + +### Theming/tokens +- Uses \`data-component="dock-prompt"\` with kind data attribute. + +` + +const story = create({ + title: "UI/DockPrompt", + mod, + args: { + kind: "question", + header: "Header", + children: "Prompt content", + footer: "Footer", + }, +}) + +export default { + title: "UI/DockPrompt", + id: "components-dock-prompt", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const Permission = { + args: { + kind: "permission", + header: "Allow access?", + children: "This action needs permission to proceed.", + footer: "Approve or deny", + }, +} diff --git a/packages/ui/src/components/dropdown-menu.stories.tsx b/packages/ui/src/components/dropdown-menu.stories.tsx new file mode 100644 index 000000000000..7a2d644fac3c --- /dev/null +++ b/packages/ui/src/components/dropdown-menu.stories.tsx @@ -0,0 +1,97 @@ +// @ts-nocheck +import * as mod from "./dropdown-menu" +import { Button } from "./button" + +const docs = `### Overview +Dropdown menu built on Kobalte with composable items, groups, and submenus. + +Use \`DropdownMenu.ItemLabel\`/\`ItemDescription\` for richer rows. + +### API +- Root accepts Kobalte DropdownMenu props (\`open\`, \`defaultOpen\`, \`onOpenChange\`). +- Compose with \`Trigger\`, \`Content\`, \`Item\`, \`Separator\`, and optional \`Sub\` sections. + +### Variants and states +- Supports item groups, separators, and nested submenus. + +### Behavior +- Menu opens from trigger and renders in a portal by default. + +### Accessibility +- TODO: confirm keyboard navigation from Kobalte. + +### Theming/tokens +- Uses \`data-component="dropdown-menu"\` and slot attributes for styling. + +` + +export default { + title: "UI/DropdownMenu", + id: "components-dropdown-menu", + component: mod.DropdownMenu, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => ( + + + Open menu + + + + + Actions + + New file + + + Rename + Shift+R + + + + + More options + + + Duplicate + + + Move + + + + + + + ), +} + +export const CheckboxRadio = { + render: () => ( + + + Open menu + + + + Show line numbers + Wrap lines + + + Compact + Comfortable + + + + + ), +} diff --git a/packages/ui/src/components/favicon.stories.tsx b/packages/ui/src/components/favicon.stories.tsx new file mode 100644 index 000000000000..a693c0f4607a --- /dev/null +++ b/packages/ui/src/components/favicon.stories.tsx @@ -0,0 +1,49 @@ +// @ts-nocheck +import * as mod from "./favicon" + +const docs = `### Overview +Injects favicon and app icon meta tags for the document head. + +Render once near the app root (head management). + +### API +- No props. + +### Variants and states +- Single configuration. + +### Behavior +- Registers link and meta tags via Solid Meta components. + +### Accessibility +- Not applicable. + +### Theming/tokens +- Not applicable. + +` + +export default { + title: "UI/Favicon", + id: "components-favicon", + component: mod.Favicon, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => ( +
+ +
+ Head tags are injected for favicon and app icons. +
+
+ ), +} diff --git a/packages/ui/src/components/file-icon.stories.tsx b/packages/ui/src/components/file-icon.stories.tsx new file mode 100644 index 000000000000..937328502f7c --- /dev/null +++ b/packages/ui/src/components/file-icon.stories.tsx @@ -0,0 +1,94 @@ +// @ts-nocheck +import * as mod from "./file-icon" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +File and folder icon renderer based on file name and extension. + +Use in file trees and lists. + +### API +- Required: \`node\` with \`path\` and \`type\`. +- Optional: \`expanded\` (for folders), \`mono\` for monochrome rendering. + +### Variants and states +- Folder vs file icons; expanded folder variant. + +### Behavior +- Maps file names and extensions to sprite icons. + +### Accessibility +- Provide adjacent text labels for filenames; icons are decorative. + +### Theming/tokens +- Uses \`data-component="file-icon"\` and sprite-based styling. + +` + +const story = create({ + title: "UI/FileIcon", + mod, + args: { + node: { path: "package.json", type: "file" }, + mono: true, + }, +}) + +export default { + title: "UI/FileIcon", + id: "components-file-icon", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const Folder = { + args: { + node: { path: "src", type: "directory" }, + expanded: true, + mono: false, + }, +} + +export const Samples = { + render: () => { + const items = [ + { path: "README.md", type: "file" }, + { path: "package.json", type: "file" }, + { path: "tsconfig.json", type: "file" }, + { path: "index.ts", type: "file" }, + { path: "styles.css", type: "file" }, + { path: "logo.svg", type: "file" }, + { path: "photo.png", type: "file" }, + { path: "Dockerfile", type: "file" }, + { path: ".env", type: "file" }, + { path: "src", type: "directory" }, + { path: "public", type: "directory" }, + ] as const + + return ( +
+ {items.map((node) => ( +
+ +
{node.path}
+
+ ))} +
+ ) + }, +} diff --git a/packages/ui/src/components/font.stories.tsx b/packages/ui/src/components/font.stories.tsx new file mode 100644 index 000000000000..153a2c8dc9b7 --- /dev/null +++ b/packages/ui/src/components/font.stories.tsx @@ -0,0 +1,48 @@ +// @ts-nocheck +import * as mod from "./font" + +const docs = `### Overview +Loads OpenCode typography assets and mono nerd fonts. + +Render once at the app root or Storybook preview. + +### API +- No props. + +### Variants and states +- Fonts include sans and multiple mono families. + +### Behavior +- Injects @font-face rules and preload links into the document head. + +### Accessibility +- Not applicable. + +### Theming/tokens +- Provides font families used by theme tokens. + +` + +export default { + title: "UI/Font", + id: "components-font", + component: mod.Font, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => ( +
+ +
OpenCode Sans Sample
+
OpenCode Mono Sample
+
+ ), +} diff --git a/packages/ui/src/components/hover-card.stories.tsx b/packages/ui/src/components/hover-card.stories.tsx new file mode 100644 index 000000000000..3f5cf1028184 --- /dev/null +++ b/packages/ui/src/components/hover-card.stories.tsx @@ -0,0 +1,70 @@ +// @ts-nocheck +import { createSignal } from "solid-js" +import * as mod from "./hover-card" + +const docs = `### Overview +Hover-triggered card for lightweight previews and metadata. + +Use for short summaries; avoid dense interactive controls. + +### API +- Required: \`trigger\` element. +- Children render inside the hover card body. + +### Variants and states +- None; content and trigger are fully composable. + +### Behavior +- Opens on hover/focus over the trigger. + +### Accessibility +- TODO: confirm focus and hover intent behavior from Kobalte. + +### Theming/tokens +- Uses \`data-component="hover-card-content"\` and slots for styling. + +` + +export default { + title: "UI/HoverCard", + id: "components-hover-card", + component: mod.HoverCard, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => ( + Hover me}> +
+
Preview
+
Short supporting text.
+
+
+ ), +} + +export const InlineMount = { + render: () => { + const [mount, setMount] = createSignal(undefined) + return ( +
+ Hover me} + > +
+
Mounted inside
+
Uses custom mount node.
+
+
+
+ ) + }, +} diff --git a/packages/ui/src/components/icon-button.stories.tsx b/packages/ui/src/components/icon-button.stories.tsx new file mode 100644 index 000000000000..9782759f823a --- /dev/null +++ b/packages/ui/src/components/icon-button.stories.tsx @@ -0,0 +1,74 @@ +// @ts-nocheck +import * as mod from "./icon-button" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Compact icon-only button with size and variant control. + +Use \`Button\` for text labels and primary actions. + +### API +- Required: \`icon\` icon name. +- Optional: \`size\`, \`iconSize\`, \`variant\`. +- Inherits Kobalte Button props and native button attributes. + +### Variants and states +- Variants: primary, secondary, ghost. +- Sizes: small, normal, large. + +### Behavior +- Icon size adapts to button size unless overridden. + +### Accessibility +- Provide \`aria-label\` when there is no visible text. + +### Theming/tokens +- Uses \`data-component="icon-button"\` and size/variant data attributes. + +` + +const story = create({ title: "UI/IconButton", mod, args: { icon: "check", "aria-label": "Icon" } }) +export default { + title: "UI/IconButton", + id: "components-icon-button", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const Sizes = { + render: () => ( +
+ + + +
+ ), +} + +export const Variants = { + render: () => ( +
+ + + +
+ ), +} + +export const IconSizeOverride = { + render: () => ( +
+ + +
+ ), +} diff --git a/packages/ui/src/components/icon.stories.tsx b/packages/ui/src/components/icon.stories.tsx new file mode 100644 index 000000000000..1986d747722b --- /dev/null +++ b/packages/ui/src/components/icon.stories.tsx @@ -0,0 +1,170 @@ +// @ts-nocheck +import * as mod from "./icon" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Inline icon renderer using the built-in OpenCode icon set. + +Use with \`Button\`, \`IconButton\`, and menu items. + +### API +- Required: \`name\` (icon key). +- Optional: \`size\` (small | normal | medium | large). +- Accepts standard SVG props. + +### Variants and states +- Size variants only. + +### Behavior +- Uses an internal SVG path map. + +### Accessibility +- Icons are aria-hidden by default; wrap with accessible text when needed. + +### Theming/tokens +- Uses \`data-component="icon"\` with size data attributes. + +` + +const names = [ + "align-right", + "arrow-up", + "arrow-left", + "arrow-right", + "archive", + "bubble-5", + "prompt", + "brain", + "bullet-list", + "check-small", + "chevron-down", + "chevron-left", + "chevron-right", + "chevron-grabber-vertical", + "chevron-double-right", + "circle-x", + "close", + "close-small", + "checklist", + "console", + "expand", + "collapse", + "code", + "code-lines", + "circle-ban-sign", + "edit-small-2", + "eye", + "enter", + "folder", + "file-tree", + "file-tree-active", + "magnifying-glass", + "plus-small", + "plus", + "new-session", + "pencil-line", + "mcp", + "glasses", + "magnifying-glass-menu", + "window-cursor", + "task", + "stop", + "layout-left", + "layout-left-partial", + "layout-left-full", + "layout-right", + "layout-right-partial", + "layout-right-full", + "square-arrow-top-right", + "open-file", + "speech-bubble", + "comment", + "folder-add-left", + "github", + "discord", + "layout-bottom", + "layout-bottom-partial", + "layout-bottom-full", + "dot-grid", + "circle-check", + "copy", + "check", + "photo", + "share", + "download", + "menu", + "server", + "branch", + "edit", + "help", + "settings-gear", + "dash", + "cloud-upload", + "trash", + "sliders", + "keyboard", + "selector", + "arrow-down-to-line", + "warning", + "link", + "providers", + "models", +] + +const story = create({ title: "UI/Icon", mod, args: { name: "check" } }) + +export default { + title: "UI/Icon", + id: "components-icon", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + name: { + control: "select", + options: names, + }, + size: { + control: "select", + options: ["small", "normal", "medium", "large"], + }, + }, +} + +export const Basic = story.Basic + +export const Sizes = { + render: () => ( +
+ + + + +
+ ), +} + +export const Gallery = { + render: () => ( +
+ {names.map((name) => ( +
+ +
{name}
+
+ ))} +
+ ), +} diff --git a/packages/ui/src/components/image-preview.stories.tsx b/packages/ui/src/components/image-preview.stories.tsx new file mode 100644 index 000000000000..f0a00c782519 --- /dev/null +++ b/packages/ui/src/components/image-preview.stories.tsx @@ -0,0 +1,59 @@ +// @ts-nocheck +import { onMount } from "solid-js" +import * as mod from "./image-preview" +import { Button } from "./button" +import { useDialog } from "../context/dialog" + +const docs = `### Overview +Image preview content intended to render inside the dialog stack. + +Use for full-size image inspection; keep images optimized. + +### API +- Required: \`src\`. +- Optional: \`alt\` text. + +### Variants and states +- Single layout with close action. + +### Behavior +- Intended to be used via \`useDialog().show\`. + +### Accessibility +- Uses localized aria-label for close button. + +### Theming/tokens +- Uses \`data-component="image-preview"\` and slot attributes. + +` + +export default { + title: "UI/ImagePreview", + id: "components-image-preview", + component: mod.ImagePreview, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => { + const dialog = useDialog() + const src = "https://placehold.co/640x360/png" + + const open = () => dialog.show(() => ) + + onMount(open) + + return ( + + ) + }, +} diff --git a/packages/ui/src/components/inline-input.stories.tsx b/packages/ui/src/components/inline-input.stories.tsx new file mode 100644 index 000000000000..e364c896315f --- /dev/null +++ b/packages/ui/src/components/inline-input.stories.tsx @@ -0,0 +1,50 @@ +// @ts-nocheck +import * as mod from "./inline-input" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Compact inline input for short values. + +Use inside text or table rows for quick edits. + +### API +- Optional: \`width\` to set a fixed width. +- Accepts standard input props. + +### Variants and states +- No built-in variants; style via class or width. + +### Behavior +- Uses inline width when provided. + +### Accessibility +- Provide a label or aria-label when used standalone. + +### Theming/tokens +- Uses \`data-component="inline-input"\`. + +` + +const story = create({ title: "UI/InlineInput", mod, args: { placeholder: "Type...", value: "Inline" } }) +export default { + title: "UI/InlineInput", + id: "components-inline-input", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const FixedWidth = { + args: { + value: "80px", + width: "80px", + }, +} diff --git a/packages/ui/src/components/keybind.stories.tsx b/packages/ui/src/components/keybind.stories.tsx new file mode 100644 index 000000000000..a458a53a7421 --- /dev/null +++ b/packages/ui/src/components/keybind.stories.tsx @@ -0,0 +1,43 @@ +// @ts-nocheck +import * as mod from "./keybind" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Keyboard shortcut pill for displaying keybindings. + +Pair with menu items or command palettes. + +### API +- Children render the key sequence text. +- Accepts standard span props. + +### Variants and states +- Single visual style. + +### Behavior +- Presentational only. + +### Accessibility +- Ensure text conveys the shortcut (e.g., "Cmd+K"). + +### Theming/tokens +- Uses \`data-component="keybind"\`. + +` + +const story = create({ title: "UI/Keybind", mod, args: { children: "Cmd+K" } }) +export default { + title: "UI/Keybind", + id: "components-keybind", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic diff --git a/packages/ui/src/components/line-comment.stories.tsx b/packages/ui/src/components/line-comment.stories.tsx new file mode 100644 index 000000000000..c48674e3c74b --- /dev/null +++ b/packages/ui/src/components/line-comment.stories.tsx @@ -0,0 +1,115 @@ +// @ts-nocheck +import { createSignal } from "solid-js" +import * as mod from "./line-comment" + +const docs = `### Overview +Inline comment anchor and editor for code review or annotation flows. + +Pair with \`Diff\` or \`Code\` to align comments to lines. + +### API +- \`LineCommentAnchor\`: position with \`top\`, control \`open\`, render custom children. +- \`LineComment\`: convenience wrapper for displaying comment + selection label. +- \`LineCommentEditor\`: controlled textarea with submit/cancel handlers. + +### Variants and states +- Default display and editor display variants. + +### Behavior +- Anchor positions relative to a containing element. +- Editor submits on Enter (Shift+Enter for newline). + +### Accessibility +- TODO: confirm ARIA labeling for comment button and editor textarea. + +### Theming/tokens +- Uses \`data-component="line-comment"\` and related slots. + +` + +export default { + title: "UI/LineComment", + id: "components-line-comment", + component: mod.LineComment, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Default = { + render: () => ( +
+
12 | const total = sum(values)
+
13 | return total / values.length
+ +
+ ), +} + +export const Editor = { + render: () => { + const [value, setValue] = createSignal("Add context for this change.") + return ( +
+
40 | if (values.length === 0) return 0
+ setValue("")} + onSubmit={(next) => setValue(next)} + /> +
+ ) + }, +} + +export const AnchorOnly = { + render: () => ( +
+
20 | const ready = true
+ +
Anchor content
+
+
+ ), +} diff --git a/packages/ui/src/components/list.stories.tsx b/packages/ui/src/components/list.stories.tsx new file mode 100644 index 000000000000..280f323c0bd4 --- /dev/null +++ b/packages/ui/src/components/list.stories.tsx @@ -0,0 +1,170 @@ +// @ts-nocheck +import * as mod from "./list" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Filterable list with keyboard navigation and optional search input. + +Use within panels or popovers where keyboard navigation is expected. + +### API +- Required: \`items\` and \`key\`. +- Required: \`children\` render function for items. +- Optional: \`search\`, \`filterKeys\`, \`groupBy\`, \`onSelect\`, \`onKeyEvent\`. + +### Variants and states +- Optional search bar and group headers. + +### Behavior +- Uses fuzzy search when \`search\` is enabled. +- Keyboard navigation via arrow keys; Enter selects. + +### Accessibility +- TODO: confirm ARIA roles for list items and search input. + +### Theming/tokens +- Uses \`data-component="list"\` and data slots for structure. + +` + +const story = create({ + title: "UI/List", + mod, + args: { + items: ["One", "Two", "Three", "Four"], + key: (x: string) => x, + children: (x: string) => x, + search: true, + }, +}) + +export default { + title: "UI/List", + id: "components-list", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const Grouped = { + render: () => { + const items = [ + { id: "a1", title: "Alpha", group: "Group A" }, + { id: "a2", title: "Bravo", group: "Group A" }, + { id: "b1", title: "Delta", group: "Group B" }, + ] + return ( + item.id} groupBy={(item) => item.group} search={true}> + {(item) => item.title} + + ) + }, +} + +export const Empty = { + render: () => ( + item} search={true}> + {(item) => item} + + ), +} + +export const WithAdd = { + render: () => ( + item} + search={true} + add={{ + render: () => ( + + ), + }} + > + {(item) => item} + + ), +} + +export const Divider = { + render: () => ( + item} divider={true}> + {(item) => item} + + ), +} + +export const ActiveIcon = { + render: () => ( + item} activeIcon="chevron-right"> + {(item) => item} + + ), +} + +export const NoSearch = { + render: () => ( + item} search={false}> + {(item) => item} + + ), +} + +export const SearchOptions = { + render: () => ( + item} + search={{ + placeholder: "Filter...", + hideIcon: true, + action: , + }} + > + {(item) => item} + + ), +} + +export const ItemWrapper = { + render: () => ( + item} + itemWrapper={(item, node) => ( +
{node}
+ )} + > + {(item) => item} +
+ ), +} + +export const GroupHeader = { + render: () => { + const items = [ + { id: "a1", title: "Alpha", group: "Group A" }, + { id: "b1", title: "Beta", group: "Group B" }, + ] + return ( + item.id} + groupBy={(item) => item.group} + groupHeader={(group) => {group.category}} + > + {(item) => item.title} + + ) + }, +} diff --git a/packages/ui/src/components/logo.stories.tsx b/packages/ui/src/components/logo.stories.tsx new file mode 100644 index 000000000000..3f5dd9cef738 --- /dev/null +++ b/packages/ui/src/components/logo.stories.tsx @@ -0,0 +1,57 @@ +// @ts-nocheck +import * as mod from "./logo" + +const docs = `### Overview +OpenCode logo assets: mark, splash, and wordmark. + +Use Mark for compact spaces, Logo for headers, Splash for hero sections. + +### API +- \`Mark\`, \`Splash\`, and \`Logo\` components accept standard SVG props. + +### Variants and states +- Multiple logo variants for different contexts. + +### Behavior +- Pure SVG rendering. + +### Accessibility +- Provide title/aria-label when logos convey meaning. + +### Theming/tokens +- Uses theme color tokens via CSS variables. + +` + +export default { + title: "UI/Logo", + id: "components-logo", + component: mod.Logo, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => ( +
+
+
Mark
+ +
+
+
Splash
+ +
+
+
Logo
+ +
+
+ ), +} diff --git a/packages/ui/src/components/markdown.stories.tsx b/packages/ui/src/components/markdown.stories.tsx new file mode 100644 index 000000000000..cae429486967 --- /dev/null +++ b/packages/ui/src/components/markdown.stories.tsx @@ -0,0 +1,53 @@ +// @ts-nocheck +import * as mod from "./markdown" +import { create } from "../storybook/scaffold" +import { markdown } from "../storybook/fixtures" + +const docs = `### Overview +Render sanitized Markdown with code blocks, inline code, and safe links. + +Pair with \`Code\` for standalone code views. + +### API +- Required: \`text\` Markdown string. +- Uses the Marked context provider for parsing and sanitization. + +### Variants and states +- Code blocks include copy buttons when rendered. + +### Behavior +- Sanitizes HTML and auto-converts inline URL code to links. +- Adds copy buttons to code blocks. + +### Accessibility +- Copy buttons include aria-labels from i18n. +- TODO: confirm link target behavior in sanitized output. + +### Theming/tokens +- Uses \`data-component="markdown"\` and related slots for styling. + +` + +const story = create({ + title: "UI/Markdown", + mod, + args: { + text: markdown, + }, +}) + +export default { + title: "UI/Markdown", + id: "components-markdown", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic diff --git a/packages/ui/src/components/message-nav.stories.tsx b/packages/ui/src/components/message-nav.stories.tsx new file mode 100644 index 000000000000..7ce31a7beda9 --- /dev/null +++ b/packages/ui/src/components/message-nav.stories.tsx @@ -0,0 +1,7 @@ +// @ts-nocheck +import * as mod from "./message-nav" +import { create } from "../storybook/scaffold" + +const story = create({ title: "UI/MessageNav", mod }) +export default { title: "UI/MessageNav", id: "components-message-nav", component: story.meta.component } +export const Basic = story.Basic diff --git a/packages/ui/src/components/message-part.stories.tsx b/packages/ui/src/components/message-part.stories.tsx new file mode 100644 index 000000000000..28489dc7b1b2 --- /dev/null +++ b/packages/ui/src/components/message-part.stories.tsx @@ -0,0 +1,7 @@ +// @ts-nocheck +import * as mod from "./message-part" +import { create } from "../storybook/scaffold" + +const story = create({ title: "UI/MessagePart", mod }) +export default { title: "UI/MessagePart", id: "components-message-part", component: story.meta.component } +export const Basic = story.Basic diff --git a/packages/ui/src/components/popover.stories.tsx b/packages/ui/src/components/popover.stories.tsx new file mode 100644 index 000000000000..e5117b451b15 --- /dev/null +++ b/packages/ui/src/components/popover.stories.tsx @@ -0,0 +1,87 @@ +// @ts-nocheck +import { createSignal } from "solid-js" +import * as mod from "./popover" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Composable popover with optional title, description, and close button. + +Use for small contextual details; avoid long forms. + +### API +- \`trigger\` and \`children\` define the anchor and content. +- Optional: \`title\`, \`description\`, \`portal\`, \`open\`, \`defaultOpen\`. + +### Variants and states +- Supports controlled and uncontrolled open state. + +### Behavior +- Closes on outside click or Escape by default. + +### Accessibility +- TODO: confirm focus management from Kobalte. + +### Theming/tokens +- Uses \`data-component="popover-content"\` and related slots. + +` + +const story = create({ + title: "UI/Popover", + mod, + args: { + trigger: "Open popover", + title: "Popover", + description: "Optional description", + defaultOpen: true, + children: "Popover content", + }, +}) + +export default { + title: "UI/Popover", + id: "components-popover", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const NoHeader = { + args: { + title: undefined, + description: undefined, + children: "Popover body only", + }, +} + +export const Inline = { + args: { + portal: false, + defaultOpen: true, + }, +} + +export const Controlled = { + render: () => { + const [open, setOpen] = createSignal(true) + return ( + + Controlled content + + ) + }, +} diff --git a/packages/ui/src/components/progress-circle.stories.tsx b/packages/ui/src/components/progress-circle.stories.tsx new file mode 100644 index 000000000000..5bc23c3108c3 --- /dev/null +++ b/packages/ui/src/components/progress-circle.stories.tsx @@ -0,0 +1,59 @@ +// @ts-nocheck +import * as mod from "./progress-circle" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Circular progress indicator for compact loading states. + +Pair with labels for clarity in dashboards. + +### API +- Required: \`percentage\` (0-100). +- Optional: \`size\`, \`strokeWidth\`. + +### Variants and states +- Single visual style; size and stroke width adjust appearance. + +### Behavior +- Percentage is clamped between 0 and 100. + +### Accessibility +- Use alongside text or aria-live messaging for progress context. + +### Theming/tokens +- Uses \`data-component="progress-circle"\` with background/progress slots. + +` + +const story = create({ title: "UI/ProgressCircle", mod, args: { percentage: 65, size: 48 } }) + +export default { + title: "UI/ProgressCircle", + id: "components-progress-circle", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + percentage: { + control: { type: "range", min: 0, max: 100, step: 1 }, + }, + }, +} + +export const Basic = story.Basic + +export const States = { + render: () => ( +
+ + + +
+ ), +} diff --git a/packages/ui/src/components/progress.stories.tsx b/packages/ui/src/components/progress.stories.tsx new file mode 100644 index 000000000000..2ee322343496 --- /dev/null +++ b/packages/ui/src/components/progress.stories.tsx @@ -0,0 +1,67 @@ +// @ts-nocheck +import * as mod from "./progress" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Linear progress indicator with optional label and value display. + +Use in forms, uploads, or background tasks. + +### API +- \`value\` and \`maxValue\` control progress. +- Optional: \`showValueLabel\`, \`hideLabel\`. +- Children provide the label text. + +### Variants and states +- Supports indeterminate state via Kobalte props (if provided). + +### Behavior +- Uses Kobalte Progress for value calculation. + +### Accessibility +- TODO: confirm ARIA attributes from Kobalte. + +### Theming/tokens +- Uses \`data-component="progress"\` with track/fill slots. + +` + +const story = create({ + title: "UI/Progress", + mod, + args: { + value: 60, + maxValue: 100, + children: "Progress", + showValueLabel: true, + }, +}) + +export default { + title: "UI/Progress", + id: "components-progress", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const NoLabel = { + args: { + children: "", + hideLabel: true, + showValueLabel: false, + value: 30, + }, +} + +export const Indeterminate = { + render: () => Loading, +} diff --git a/packages/ui/src/components/provider-icon.stories.tsx b/packages/ui/src/components/provider-icon.stories.tsx new file mode 100644 index 000000000000..e7fc39967bf1 --- /dev/null +++ b/packages/ui/src/components/provider-icon.stories.tsx @@ -0,0 +1,69 @@ +// @ts-nocheck +import { iconNames } from "./provider-icons/types" +import * as mod from "./provider-icon" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Provider icon sprite renderer for model/provider badges. + +Use in model pickers or provider lists. + +### API +- Required: \`id\` (provider icon name). +- Accepts standard SVG props. + +### Variants and states +- Single visual style; size via CSS. + +### Behavior +- Renders from the provider SVG sprite sheet. + +### Accessibility +- Provide accessible text nearby when the icon conveys meaning. + +### Theming/tokens +- Uses \`data-component="provider-icon"\`. + +` + +const story = create({ title: "UI/ProviderIcon", mod, args: { id: "openai" } }) +export default { + title: "UI/ProviderIcon", + id: "components-provider-icon", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + id: { + control: "select", + options: iconNames, + }, + }, +} + +export const Basic = story.Basic + +export const AllIcons = { + render: () => ( +
+ {iconNames.map((id) => ( +
+ +
{id}
+
+ ))} +
+ ), +} diff --git a/packages/ui/src/components/provider-icons/sprite.svg b/packages/ui/src/components/provider-icons/sprite.svg index 88406fa8c3ce..29d22461d055 100644 --- a/packages/ui/src/components/provider-icons/sprite.svg +++ b/packages/ui/src/components/provider-icons/sprite.svg @@ -87,6 +87,18 @@ fill="currentColor" > + + + + @@ -175,6 +187,36 @@ fill="currentColor" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/components/provider-icons/types.ts b/packages/ui/src/components/provider-icons/types.ts index 89fbc0625f57..bafa7ffaf042 100644 --- a/packages/ui/src/components/provider-icons/types.ts +++ b/packages/ui/src/components/provider-icons/types.ts @@ -10,6 +10,7 @@ export const iconNames = [ "xai", "wandb", "vultr", + "vivgrid", "vercel", "venice", "v0", @@ -17,11 +18,16 @@ export const iconNames = [ "togetherai", "synthetic", "submodel", + "stepfun", + "stackit", "siliconflow", "siliconflow-cn", "scaleway", "sap-ai-core", "requesty", + "qiniu-ai", + "qihang-ai", + "privatemode-ai", "poe", "perplexity", "ovhcloud", @@ -30,19 +36,28 @@ export const iconNames = [ "openai", "ollama-cloud", "nvidia", + "novita-ai", + "nova", "nebius", "nano-gpt", "morph", "moonshotai", "moonshotai-cn", "modelscope", + "moark", "mistral", "minimax", + "minimax-coding-plan", "minimax-cn", + "minimax-cn-coding-plan", + "meganova", "lucidquery", "lmstudio", "llama", + "kuae-cloud-coding-plan", "kimi-for-coding", + "kilo", + "jiekou", "io-net", "inference", "inception", @@ -53,9 +68,11 @@ export const iconNames = [ "google", "google-vertex", "google-vertex-anthropic", + "gitlab", "github-models", "github-copilot", "friendli", + "firmware", "fireworks-ai", "fastrouter", "deepseek", @@ -64,8 +81,10 @@ export const iconNames = [ "cohere", "cloudflare-workers-ai", "cloudflare-ai-gateway", + "cloudferro-sherlock", "chutes", "cerebras", + "berget", "baseten", "bailing", "azure", @@ -76,6 +95,7 @@ export const iconNames = [ "alibaba-cn", "aihubmix", "abacus", + "302ai", ] as const export type IconName = (typeof iconNames)[number] diff --git a/packages/ui/src/components/radio-group.stories.tsx b/packages/ui/src/components/radio-group.stories.tsx new file mode 100644 index 000000000000..4900ead8467d --- /dev/null +++ b/packages/ui/src/components/radio-group.stories.tsx @@ -0,0 +1,92 @@ +// @ts-nocheck +import * as mod from "./radio-group" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Segmented radio group for choosing a single option. + +Use for view toggles or mode selection. + +### API +- Required: \`options\`. +- Optional: \`current\`, \`defaultValue\`, \`value\`, \`label\`, \`onSelect\`. +- Optional layout: \`size\`, \`fill\`, \`pad\`. + +### Variants and states +- Size variants: small, medium. +- Optional fill and padding behavior. + +### Behavior +- Maps options to segmented items and manages selection. + +### Accessibility +- TODO: confirm role/aria attributes from Kobalte SegmentedControl. + +### Theming/tokens +- Uses \`data-component="radio-group"\` with size/pad data attributes. + +` + +const story = create({ + title: "UI/RadioGroup", + mod, + args: { + options: ["One", "Two", "Three"], + defaultValue: "One", + }, +}) + +export default { + title: "UI/RadioGroup", + id: "components-radio-group", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + size: { + control: "select", + options: ["small", "medium"], + }, + pad: { + control: "select", + options: ["none", "normal"], + }, + fill: { + control: "boolean", + }, + }, +} + +export const Basic = story.Basic + +export const Sizes = { + render: () => ( +
+ + +
+ ), +} + +export const Filled = { + args: { + fill: true, + pad: "none", + }, +} + +export const CustomLabels = { + render: () => ( + (value === "list" ? "List view" : "Grid view")} + /> + ), +} diff --git a/packages/ui/src/components/resize-handle.stories.tsx b/packages/ui/src/components/resize-handle.stories.tsx new file mode 100644 index 000000000000..474cf71e2d10 --- /dev/null +++ b/packages/ui/src/components/resize-handle.stories.tsx @@ -0,0 +1,156 @@ +// @ts-nocheck +import { createSignal } from "solid-js" +import * as mod from "./resize-handle" + +const docs = `### Overview +Drag handle for resizing panels or split views. + +Use alongside resizable panels and split layouts. + +### API +- Required: \`direction\`, \`size\`, \`min\`, \`max\`, \`onResize\`. +- Optional: \`edge\`, \`onCollapse\`, \`collapseThreshold\`. + +### Variants and states +- Horizontal and vertical directions. + +### Behavior +- Drag updates size and calls \`onResize\` with clamped values. + +### Accessibility +- TODO: provide keyboard resizing guidance if needed. + +### Theming/tokens +- Uses \`data-component="resize-handle"\` with direction/edge data attributes. + +` + +export default { + title: "UI/ResizeHandle", + id: "components-resize-handle", + component: mod.ResizeHandle, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => { + const [size, setSize] = createSignal(240) + return ( +
+
Size: {size()}px
+
+ +
+ ) + }, +} + +export const Vertical = { + render: () => { + const [size, setSize] = createSignal(180) + return ( +
+
Size: {size()}px
+
+ +
+ ) + }, +} + +export const Collapse = { + render: () => { + const [size, setSize] = createSignal(200) + const [collapsed, setCollapsed] = createSignal(false) + return ( +
+
+ {collapsed() ? "Collapsed" : `Size: ${size()}px`} +
+
+ { + setCollapsed(false) + setSize(next) + }} + onCollapse={() => setCollapsed(true)} + style="height:24px;border:1px dashed color-mix(in oklab, var(--text-base) 20%, transparent)" + /> +
+ ) + }, +} + +export const EdgeStart = { + render: () => { + const [size, setSize] = createSignal(240) + return ( +
+
Size: {size()}px
+
+ +
+ ) + }, +} diff --git a/packages/ui/src/components/select.stories.tsx b/packages/ui/src/components/select.stories.tsx new file mode 100644 index 000000000000..1ee00ab851e4 --- /dev/null +++ b/packages/ui/src/components/select.stories.tsx @@ -0,0 +1,113 @@ +// @ts-nocheck +import * as mod from "./select" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Select menu for choosing a single option with optional grouping. + +Use \`children\` to customize option rendering. + +### API +- Required: \`options\`. +- Optional: \`current\`, \`placeholder\`, \`value\`, \`label\`, \`groupBy\`. +- Accepts Button props for the trigger (\`variant\`, \`size\`). + +### Variants and states +- Trigger supports "settings" style via \`triggerVariant\`. + +### Behavior +- Uses Kobalte Select with optional item highlight callbacks. + +### Accessibility +- TODO: confirm keyboard navigation and aria attributes from Kobalte. + +### Theming/tokens +- Uses \`data-component="select"\` with slot attributes. + +` + +const story = create({ + title: "UI/Select", + mod, + args: { + options: ["One", "Two", "Three"], + current: "One", + placeholder: "Choose...", + variant: "secondary", + size: "normal", + }, +}) + +export default { + title: "UI/Select", + id: "components-select", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + triggerVariant: { + control: "select", + options: ["settings", undefined], + }, + }, +} + +export const Basic = story.Basic + +export const Grouped = { + render: () => { + const options = [ + { id: "alpha", label: "Alpha", group: "Group A" }, + { id: "bravo", label: "Bravo", group: "Group A" }, + { id: "delta", label: "Delta", group: "Group B" }, + ] + return ( + item.id} + label={(item) => item.label} + groupBy={(item) => item.group} + placeholder="Choose..." + variant="secondary" + /> + ) + }, +} + +export const SettingsTrigger = { + args: { + triggerVariant: "settings", + }, +} + +export const CustomRender = { + render: () => ( + + {(item) => {item}} + + ), +} + +export const CustomTriggerStyle = { + args: { + triggerStyle: { "min-width": "180px", "justify-content": "space-between" }, + }, +} + +export const Disabled = { + args: { + disabled: true, + }, +} diff --git a/packages/ui/src/components/session-review.stories.tsx b/packages/ui/src/components/session-review.stories.tsx new file mode 100644 index 000000000000..7ab1eb203892 --- /dev/null +++ b/packages/ui/src/components/session-review.stories.tsx @@ -0,0 +1,7 @@ +// @ts-nocheck +import * as mod from "./session-review" +import { create } from "../storybook/scaffold" + +const story = create({ title: "UI/SessionReview", mod }) +export default { title: "UI/SessionReview", id: "components-session-review", component: story.meta.component } +export const Basic = story.Basic diff --git a/packages/ui/src/components/session-turn.stories.tsx b/packages/ui/src/components/session-turn.stories.tsx new file mode 100644 index 000000000000..927402c8db77 --- /dev/null +++ b/packages/ui/src/components/session-turn.stories.tsx @@ -0,0 +1,7 @@ +// @ts-nocheck +import * as mod from "./session-turn" +import { create } from "../storybook/scaffold" + +const story = create({ title: "UI/SessionTurn", mod }) +export default { title: "UI/SessionTurn", id: "components-session-turn", component: story.meta.component } +export const Basic = story.Basic diff --git a/packages/ui/src/components/spinner.stories.tsx b/packages/ui/src/components/spinner.stories.tsx new file mode 100644 index 000000000000..be6106d14867 --- /dev/null +++ b/packages/ui/src/components/spinner.stories.tsx @@ -0,0 +1,53 @@ +// @ts-nocheck +import * as mod from "./spinner" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Animated loading indicator for inline or page-level loading states. + +Use with \`Button\` or in empty states. + +### API +- Accepts standard SVG props (class, style). + +### Variants and states +- Single default animation style. + +### Behavior +- Animation is CSS-driven via data attributes. + +### Accessibility +- Use alongside text or aria-live regions to convey loading state. + +### Theming/tokens +- Uses \`data-component="spinner"\` for styling hooks. + +` + +const story = create({ title: "UI/Spinner", mod }) + +export default { + title: "UI/Spinner", + id: "components-spinner", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const Sizes = { + render: () => ( +
+ + + +
+ ), +} diff --git a/packages/ui/src/components/sticky-accordion-header.stories.tsx b/packages/ui/src/components/sticky-accordion-header.stories.tsx new file mode 100644 index 000000000000..3f033562677e --- /dev/null +++ b/packages/ui/src/components/sticky-accordion-header.stories.tsx @@ -0,0 +1,54 @@ +// @ts-nocheck +import { Accordion } from "./accordion" +import * as mod from "./sticky-accordion-header" + +const docs = `### Overview +Sticky accordion header wrapper for persistent section labels. + +Use only inside \`Accordion.Item\` with \`Accordion.Trigger\`. + +### API +- Accepts standard header props and children. + +### Variants and states +- Inherits accordion states. + +### Behavior +- Renders inside an Accordion item header. + +### Accessibility +- TODO: confirm semantics from Accordion.Header usage. + +### Theming/tokens +- Uses \`data-component="sticky-accordion-header"\`. + +` + +export default { + title: "UI/StickyAccordionHeader", + id: "components-sticky-accordion-header", + component: mod.StickyAccordionHeader, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => ( + + + + Sticky header + + +
Accordion content.
+
+
+
+ ), +} diff --git a/packages/ui/src/components/switch.stories.tsx b/packages/ui/src/components/switch.stories.tsx new file mode 100644 index 000000000000..540e91e3654d --- /dev/null +++ b/packages/ui/src/components/switch.stories.tsx @@ -0,0 +1,68 @@ +// @ts-nocheck +import * as mod from "./switch" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Toggle control for binary settings. + +Use in settings panels or forms. + +### API +- Uses Kobalte Switch props (\`checked\`, \`defaultChecked\`, \`onChange\`). +- Optional: \`hideLabel\`, \`description\`. +- Children render as the label. + +### Variants and states +- Checked/unchecked, disabled states. + +### Behavior +- Controlled or uncontrolled usage via Kobalte props. + +### Accessibility +- TODO: confirm aria attributes from Kobalte. + +### Theming/tokens +- Uses \`data-component="switch"\` and slot attributes. + +` + +const story = create({ + title: "UI/Switch", + mod, + args: { defaultChecked: true, children: "Enable notifications" }, +}) + +export default { + title: "UI/Switch", + id: "components-switch", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const States = { + render: () => ( +
+ Enabled + Disabled + Disabled switch + With description +
+ ), +} + +export const HiddenLabel = { + args: { + children: "Hidden label", + hideLabel: true, + defaultChecked: true, + }, +} diff --git a/packages/ui/src/components/tabs.stories.tsx b/packages/ui/src/components/tabs.stories.tsx new file mode 100644 index 000000000000..8e09764dcc64 --- /dev/null +++ b/packages/ui/src/components/tabs.stories.tsx @@ -0,0 +1,179 @@ +// @ts-nocheck +import { IconButton } from "./icon-button" +import { createSignal } from "solid-js" +import * as mod from "./tabs" + +const docs = `### Overview +Tabbed navigation for switching between related panels. + +Compose \`Tabs.List\` + \`Tabs.Trigger\` + \`Tabs.Content\`. + +### API +- Root accepts Kobalte Tabs props (\`value\`, \`defaultValue\`, \`onChange\`). +- \`variant\` sets visual style: normal, alt, pill, settings. +- \`orientation\` supports horizontal or vertical layouts. +- Trigger supports \`closeButton\`, \`hideCloseButton\`, and \`onMiddleClick\`. + +### Variants and states +- Normal, alt, pill, settings variants. +- Horizontal and vertical orientations. + +### Behavior +- Uses Kobalte Tabs for roving focus and selection management. + +### Accessibility +- TODO: confirm keyboard interactions from Kobalte Tabs. + +### Theming/tokens +- Uses \`data-component="tabs"\` with variant/orientation data attributes. + +` + +export default { + title: "UI/Tabs", + id: "components-tabs", + component: mod.Tabs, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + variant: { + control: "select", + options: ["normal", "alt", "pill", "settings"], + }, + orientation: { + control: "select", + options: ["horizontal", "vertical"], + }, + }, +} + +export const Basic = { + args: { + variant: "normal", + orientation: "horizontal", + defaultValue: "overview", + }, + render: (props) => ( + + + Overview + Details + Activity + + Overview content + Details content + Activity content + + ), +} + +export const Settings = { + args: { + variant: "settings", + orientation: "horizontal", + defaultValue: "general", + }, + render: (props) => ( + + + General + Appearance + + General settings + Appearance settings + + ), +} + +export const Alt = { + args: { + variant: "alt", + orientation: "horizontal", + defaultValue: "first", + }, + render: (props) => ( + + + First + Second + + Alt content + Alt content 2 + + ), +} + +export const Vertical = { + args: { + variant: "pill", + orientation: "vertical", + defaultValue: "alpha", + }, + render: (props) => ( + + + Alpha + Beta + + Alpha content + Beta content + + ), +} + +export const Closable = { + args: { + variant: "normal", + orientation: "horizontal", + defaultValue: "tab-1", + }, + render: (props) => ( + + + } + > + Tab 1 + + Tab 2 + + Closable content + Standard content + + ), +} + +export const MiddleClick = { + args: { + variant: "normal", + orientation: "horizontal", + defaultValue: "tab-1", + }, + render: (props) => { + const [message, setMessage] = createSignal("Middle click a tab") + return ( +
+
{message()}
+ + + setMessage("Middle clicked tab-1")}> + Tab 1 + + setMessage("Middle clicked tab-2")}> + Tab 2 + + + Tab 1 content + Tab 2 content + +
+ ) + }, +} diff --git a/packages/ui/src/components/tag.stories.tsx b/packages/ui/src/components/tag.stories.tsx new file mode 100644 index 000000000000..73ae880ba1c2 --- /dev/null +++ b/packages/ui/src/components/tag.stories.tsx @@ -0,0 +1,58 @@ +// @ts-nocheck +import * as mod from "./tag" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Small label tag for metadata and status chips. + +Use alongside headings or lists for quick metadata. + +### API +- Optional: \`size\` (normal | large). +- Accepts standard span props. + +### Variants and states +- Size variants only. + +### Behavior +- Inline element; size controls padding and font size via CSS. + +### Accessibility +- Ensure text conveys meaning; avoid color-only distinction. + +### Theming/tokens +- Uses \`data-component="tag"\` with size data attributes. + +` + +const story = create({ title: "UI/Tag", mod, args: { children: "Tag" } }) +export default { + title: "UI/Tag", + id: "components-tag", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, + argTypes: { + size: { + control: "select", + options: ["normal", "large"], + }, + }, +} + +export const Basic = story.Basic + +export const Sizes = { + render: () => ( +
+ Normal + Large +
+ ), +} diff --git a/packages/ui/src/components/text-field.stories.tsx b/packages/ui/src/components/text-field.stories.tsx new file mode 100644 index 000000000000..73f9006607b9 --- /dev/null +++ b/packages/ui/src/components/text-field.stories.tsx @@ -0,0 +1,111 @@ +// @ts-nocheck +import * as mod from "./text-field" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Text input with label, description, and optional copy-to-clipboard action. + +Pair with \`Tooltip\` and \`IconButton\` for copy affordance (built in). + +### API +- Supports Kobalte TextField props: \`value\`, \`defaultValue\`, \`onChange\`, \`disabled\`, \`readOnly\`. +- Optional: \`label\`, \`description\`, \`error\`, \`variant\`, \`copyable\`, \`multiline\`. + +### Variants and states +- Normal and ghost variants. +- Supports multiline textarea. + +### Behavior +- When \`copyable\` is true, clicking copies the current value. + +### Accessibility +- Label is hidden when \`hideLabel\` is true (sr-only). + +### Theming/tokens +- Uses \`data-component="input"\` with slot attributes for styling. + +` + +const story = create({ + title: "UI/TextField", + mod, + args: { + label: "Label", + placeholder: "Type here...", + defaultValue: "Hello", + }, +}) + +export default { + title: "UI/TextField", + id: "components-text-field", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const Variants = { + render: () => ( +
+ + +
+ ), +} + +export const Multiline = { + args: { + label: "Description", + multiline: true, + defaultValue: "Line one\nLine two", + }, +} + +export const Copyable = { + args: { + label: "Invite link", + defaultValue: "https://example.com/invite/abc", + copyable: true, + copyKind: "link", + }, +} + +export const Error = { + args: { + label: "Email", + defaultValue: "invalid@", + error: "Enter a valid email address", + }, +} + +export const Disabled = { + args: { + label: "Disabled", + defaultValue: "Readonly", + disabled: true, + }, +} + +export const ReadOnly = { + args: { + label: "Read only", + defaultValue: "Read only value", + readOnly: true, + }, +} + +export const HiddenLabel = { + args: { + label: "Hidden label", + hideLabel: true, + placeholder: "Hidden label", + }, +} diff --git a/packages/ui/src/components/text-shimmer.stories.tsx b/packages/ui/src/components/text-shimmer.stories.tsx new file mode 100644 index 000000000000..4b6de34c2e9f --- /dev/null +++ b/packages/ui/src/components/text-shimmer.stories.tsx @@ -0,0 +1,59 @@ +// @ts-nocheck +import * as mod from "./text-shimmer" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Animated shimmer effect for loading text placeholders. + +Use for pending states inside buttons or list rows. + +### API +- Required: \`text\` string. +- Optional: \`as\`, \`active\`, \`stepMs\`, \`durationMs\`. + +### Variants and states +- Active/inactive state via \`active\`. + +### Behavior +- Characters animate with staggered delays. + +### Accessibility +- Uses \`aria-label\` with the full text. + +### Theming/tokens +- Uses \`data-component="text-shimmer"\` and CSS custom properties for timing. + +` + +const story = create({ title: "UI/TextShimmer", mod, args: { text: "Loading..." } }) + +export default { + title: "UI/TextShimmer", + id: "components-text-shimmer", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const Inactive = { + args: { + text: "Static text", + active: false, + }, +} + +export const CustomTiming = { + args: { + text: "Custom timing", + stepMs: 80, + durationMs: 1800, + }, +} diff --git a/packages/ui/src/components/toast.stories.tsx b/packages/ui/src/components/toast.stories.tsx new file mode 100644 index 000000000000..ef9cbb68ef28 --- /dev/null +++ b/packages/ui/src/components/toast.stories.tsx @@ -0,0 +1,138 @@ +// @ts-nocheck +import * as mod from "./toast" +import { Button } from "./button" + +const docs = `### Overview +Toast notifications with optional icons, actions, and progress. + +Use brief titles/descriptions; limit actions to 1-2. + +### API +- Use \`showToast\` or \`showPromiseToast\` to trigger toasts. +- Render \`Toast.Region\` once per page. +- \`Toast\` subcomponents compose the structure. + +### Variants and states +- Variants: default, success, error, loading. +- Optional actions and persistent toasts. + +### Behavior +- Toasts render in a portal and auto-dismiss unless persistent. + +### Accessibility +- TODO: confirm aria-live behavior from Kobalte Toast. + +### Theming/tokens +- Uses \`data-component="toast"\` and slot data attributes. + +` + +export default { + title: "UI/Toast", + id: "components-toast", + component: mod.Toast, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = { + render: () => ( +
+ + + +
+ ), +} + +export const Actions = { + render: () => ( +
+ + +
+ ), +} + +export const Promise = { + render: () => ( +
+ + +
+ ), +} + +export const Loading = { + render: () => ( +
+ + +
+ ), +} diff --git a/packages/ui/src/components/tooltip.stories.tsx b/packages/ui/src/components/tooltip.stories.tsx new file mode 100644 index 000000000000..efe11d92efb9 --- /dev/null +++ b/packages/ui/src/components/tooltip.stories.tsx @@ -0,0 +1,64 @@ +// @ts-nocheck +import * as mod from "./tooltip" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Tooltip for contextual hints and keybind callouts. + +Use for short hints; avoid long descriptions. + +### API +- Required: \`value\` (tooltip content). +- Optional: \`inactive\`, \`forceOpen\`, placement props from Kobalte. + +### Variants and states +- Supports keybind-style tooltip via \`TooltipKeybind\`. + +### Behavior +- Opens on hover/focus; can be forced open. + +### Accessibility +- TODO: confirm trigger semantics and focus behavior. + +### Theming/tokens +- Uses \`data-component="tooltip"\` and related slots. + +` + +const story = create({ title: "UI/Tooltip", mod, args: { value: "Tooltip", children: "Hover me" } }) + +export default { + title: "UI/Tooltip", + id: "components-tooltip", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const Keybind = { + render: () => ( + + Hover for keybind + + ), +} + +export const ForcedOpen = { + args: { + forceOpen: true, + }, +} + +export const Inactive = { + args: { + inactive: true, + }, +} diff --git a/packages/ui/src/components/typewriter.stories.tsx b/packages/ui/src/components/typewriter.stories.tsx new file mode 100644 index 000000000000..880ca7489434 --- /dev/null +++ b/packages/ui/src/components/typewriter.stories.tsx @@ -0,0 +1,51 @@ +// @ts-nocheck +import * as mod from "./typewriter" +import { create } from "../storybook/scaffold" + +const docs = `### Overview +Animated typewriter text effect for short inline messages. + +Use for short status lines; avoid long paragraphs. + +### API +- Optional: \`text\` string; if absent, nothing is rendered. +- Optional: \`as\` to change the rendered element. + +### Variants and states +- Single animation style with cursor blink. + +### Behavior +- Types one character at a time with randomized delays. + +### Accessibility +- TODO: confirm if cursor should be aria-hidden in all contexts. + +### Theming/tokens +- Uses \`blinking-cursor\` class for cursor styling. + +` + +const story = create({ title: "UI/Typewriter", mod, args: { text: "Typewriter text" } }) + +export default { + title: "UI/Typewriter", + id: "components-typewriter", + component: story.meta.component, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: docs, + }, + }, + }, +} + +export const Basic = story.Basic + +export const Inline = { + args: { + text: "Inline typewriter", + as: "span", + }, +} diff --git a/packages/ui/src/storybook/fixtures.ts b/packages/ui/src/storybook/fixtures.ts new file mode 100644 index 000000000000..59d4129709a7 --- /dev/null +++ b/packages/ui/src/storybook/fixtures.ts @@ -0,0 +1,51 @@ +export const diff = { + before: { + name: "src/greet.ts", + contents: `export function greet(name: string) { + return \`Hello, \${name}!\` +} +`, + }, + after: { + name: "src/greet.ts", + contents: `export function greet(name: string, excited = false) { + const message = \`Hello, \${name}!\` + return excited ? \`\${message}!!\` : message +} +`, + }, +} + +export const code = { + name: "src/calc.ts", + contents: `export function sum(values: number[]) { + return values.reduce((total, value) => total + value, 0) +} + +export function average(values: number[]) { + if (values.length === 0) return 0 + return sum(values) / values.length +} +`, +} + +export const markdown = [ + "# Markdown", + "", + "Use **Markdown** for rich text.", + "", + "## Highlights", + "- Headings, lists, and code blocks", + "- Inline `code` and links", + "", + "```ts", + "export const value = 42", + "```", + "", + "More at https://example.com/docs", +].join("\n") + +export const changes = { + additions: 18, + deletions: 6, +} diff --git a/packages/ui/src/storybook/scaffold.tsx b/packages/ui/src/storybook/scaffold.tsx new file mode 100644 index 000000000000..2512aa09be56 --- /dev/null +++ b/packages/ui/src/storybook/scaffold.tsx @@ -0,0 +1,62 @@ +import { ErrorBoundary, type ValidComponent } from "solid-js" +import { Dynamic } from "solid-js/web" + +function fn(value: unknown): value is (...args: never[]) => unknown { + return typeof value === "function" +} + +function pick(mod: Record, name?: string) { + if (name && fn(mod[name])) return mod[name] + if (fn(mod.default)) return mod.default + + const preferred = Object.keys(mod) + .filter((k) => k[0] && k[0] === k[0].toUpperCase()) + .find((k) => fn(mod[k])) + if (preferred) return mod[preferred] + + const first = Object.keys(mod).find((k) => fn(mod[k])) + if (first) return mod[first] + + return () => { + return ( +
+
Missing component export.
+
Exports: {Object.keys(mod).join(", ") || "(none)"}
+
+ ) + } +} + +export function create(input: { + title: string + mod: Record + name?: string + args?: Record +}) { + const component = pick(input.mod, input.name) as unknown as ValidComponent + + return { + meta: { + title: input.title, + component, + }, + Basic: { + args: input.args ?? {}, + render: (args: Record) => { + return ( + { + return ( +
+                  {String(err)}
+                
+ ) + }} + > + +
+ ) + }, + }, + } +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 832f7cbf8164..25f6a3dcc45c 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -17,5 +17,6 @@ // Type Checking & Safety "strict": true, "types": ["vite/client", "bun"] - } + }, + "exclude": ["**/*.stories.*", "**/*.mdx"] } From 9736fce8fcede6947bc05736b4ff383404e389a2 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 26 Feb 2026 22:18:33 +0000 Subject: [PATCH 93/94] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 426f484f0313..eaba0d8f0c07 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-3hfy6nfEnGq4J6inH0pXANw05oas+81iuayn7J0pj9c=", - "aarch64-linux": "sha256-dxWaLtzSeI5NfHwB6u0K10yxoA0ESz/r+zTEQ3FdKFY=", - "aarch64-darwin": "sha256-kkK4rj4g0j2jJFXVmVH7CJcXlI8Dj/KmL/VC3iE4Z+8=", - "x86_64-darwin": "sha256-jt51irxZd48kb0BItd8InP7lfsELUh0unVYO2es+a98=" + "x86_64-linux": "sha256-dZoLhWe4smBsOF7WczMySLXSAB1YRO1vfhiOCL1rBf0=", + "aarch64-linux": "sha256-J7nIz1xuVZEHun5WRZkYRySz29B0A8g5g0RRxnIWTYU=", + "aarch64-darwin": "sha256-R2PuhX+EjUBuLE8MF0G0fcUwNaU+5n6V6uVeK89ulzw=", + "x86_64-darwin": "sha256-Bvzfz9TsTpYriZNLSLgpNcNb+BgtkgpjoWqdOtF2IBg=" } } From d9a81df99aae4b27d64ec569c3e2f2359532c79e Mon Sep 17 00:00:00 2001 From: Cole Leavitt Date: Thu, 26 Feb 2026 17:18:42 -0700 Subject: [PATCH 94/94] fix(tui): resolve streaming freeze from GC pressure and event flooding Batch ALL high-frequency streaming events (delta, status, part, message, todo, diff) into a unified 100ms flush window to reduce store mutations and GC pressure. Previously only delta events were debounced while 8 other event types triggered immediate renders, causing full markdown re-parse per token and triggering Bun's JSC GC madvise spin loop. - sync.tsx: unified batch flush for all streaming events - sdk.tsx: increase event batch window from 16ms to 50ms - processor.ts: fix stream termination (for-await never exited on finish) - db.ts: handle fire-and-forget effect Promise rejections - mcp/index.ts: add timeout to client.listTools() calls --- .../opencode/src/cli/cmd/tui/context/sdk.tsx | 6 +- .../opencode/src/cli/cmd/tui/context/sync.tsx | 224 ++++++++++++------ packages/opencode/src/mcp/index.ts | 4 +- packages/opencode/src/session/processor.ts | 5 +- packages/opencode/src/storage/db.ts | 4 +- 5 files changed, 166 insertions(+), 77 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 7fa7e05c3d25..104ccb8be62b 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -52,10 +52,10 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ const elapsed = Date.now() - last if (timer) return - // If we just flushed recently (within 16ms), batch this with future events + // If we just flushed recently (within 50ms), batch this with future events // Otherwise, process immediately to avoid latency - if (elapsed < 16) { - timer = setTimeout(flush, 16) + if (elapsed < 50) { + timer = setTimeout(flush, 50) return } flush() diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 269ed7ae0bd1..d0328178c0d2 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -29,6 +29,135 @@ import { batch, onMount } from "solid-js" import { Log } from "@/util/log" import type { Path } from "@opencode-ai/sdk" +// Streaming event batcher: batch ALL high-frequency events during streaming +// to reduce store mutations and avoid GC pressure from markdown re-parsing. +// Events are buffered and flushed every BATCH_FLUSH_MS in a single batch() call. +const BATCH_FLUSH_MS = 100 +const pendingDeltas = new Map>>() +const pendingStatus = new Map() +const pendingMessages = new Map() +const pendingParts = new Map() +const pendingTodos = new Map() +const pendingDiffs = new Map() +let batchTimer: Timer | undefined + +function scheduleBatchFlush(setStore: any, store: any) { + if (batchTimer) return + batchTimer = setTimeout(() => { + batchTimer = undefined + // Snapshot and clear all pending state + const deltas = new Map(pendingDeltas) + const statuses = new Map(pendingStatus) + const messages = new Map(pendingMessages) + const parts = new Map(pendingParts) + const todos = new Map(pendingTodos) + const diffs = new Map(pendingDiffs) + pendingDeltas.clear() + pendingStatus.clear() + pendingMessages.clear() + pendingParts.clear() + pendingTodos.clear() + pendingDiffs.clear() + + batch(() => { + // Flush session statuses + for (const [sessionID, status] of statuses) { + setStore("session_status", sessionID, status) + } + + // Flush todos + for (const [sessionID, todoList] of todos) { + setStore("todo", sessionID, todoList) + } + + // Flush diffs + for (const [sessionID, diff] of diffs) { + setStore("session_diff", sessionID, diff) + } + + // Flush message updates + for (const [, info] of messages) { + const existing = store.message[info.sessionID] + if (!existing) { + setStore("message", info.sessionID, [info]) + continue + } + const result = Binary.search(existing, info.id, (m: any) => m.id) + if (result.found) { + setStore("message", info.sessionID, result.index, reconcile(info)) + continue + } + setStore( + "message", + info.sessionID, + produce((draft: any[]) => { + draft.splice(result.index, 0, info) + }), + ) + const updated = store.message[info.sessionID] + if (updated.length > 100) { + const oldest = updated[0] + setStore( + "message", + info.sessionID, + produce((draft: any[]) => { + draft.shift() + }), + ) + setStore( + "part", + produce((draft: any) => { + delete draft[oldest.id] + }), + ) + } + } + + // Flush part updates + for (const [, part] of parts) { + const existing = store.part[part.messageID] + if (!existing) { + setStore("part", part.messageID, [part]) + continue + } + const result = Binary.search(existing, part.id, (p: any) => p.id) + if (result.found) { + setStore("part", part.messageID, result.index, reconcile(part)) + continue + } + setStore( + "part", + part.messageID, + produce((draft: any[]) => { + draft.splice(result.index, 0, part) + }), + ) + } + + // Flush deltas (accumulated text appends) + for (const [msgID, partMap] of deltas) { + const existing = store.part[msgID] + if (!existing) continue + setStore( + "part", + msgID, + produce((draft: any[]) => { + for (const [pID, fields] of partMap) { + const idx = Binary.search(draft, pID, (p: any) => p.id) + if (!idx.found) continue + const p = draft[idx.index] + for (const [f, d] of fields) { + const key = f as keyof typeof p + ;(p[key] as string) = ((p[key] as string | undefined) ?? "") + d + } + } + }), + ) + } + }) + }, BATCH_FLUSH_MS) +} + export const { use: useSync, provider: SyncProvider } = createSimpleContext({ name: "Sync", init: () => { @@ -186,11 +315,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ } case "todo.updated": - setStore("todo", event.properties.sessionID, event.properties.todos) + pendingTodos.set(event.properties.sessionID, event.properties.todos) + scheduleBatchFlush(setStore, store) break case "session.diff": - setStore("session_diff", event.properties.sessionID, event.properties.diff) + pendingDiffs.set(event.properties.sessionID, event.properties.diff) + scheduleBatchFlush(setStore, store) break case "session.deleted": { @@ -221,47 +352,14 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ } case "session.status": { - setStore("session_status", event.properties.sessionID, event.properties.status) + pendingStatus.set(event.properties.sessionID, event.properties.status) + scheduleBatchFlush(setStore, store) break } case "message.updated": { - const messages = store.message[event.properties.info.sessionID] - if (!messages) { - setStore("message", event.properties.info.sessionID, [event.properties.info]) - break - } - const result = Binary.search(messages, event.properties.info.id, (m) => m.id) - if (result.found) { - setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info)) - break - } - setStore( - "message", - event.properties.info.sessionID, - produce((draft) => { - draft.splice(result.index, 0, event.properties.info) - }), - ) - const updated = store.message[event.properties.info.sessionID] - if (updated.length > 100) { - const oldest = updated[0] - batch(() => { - setStore( - "message", - event.properties.info.sessionID, - produce((draft) => { - draft.shift() - }), - ) - setStore( - "part", - produce((draft) => { - delete draft[oldest.id] - }), - ) - }) - } + pendingMessages.set(event.properties.info.id, event.properties.info) + scheduleBatchFlush(setStore, store) break } case "message.removed": { @@ -279,45 +377,31 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ break } case "message.part.updated": { - const parts = store.part[event.properties.part.messageID] - if (!parts) { - setStore("part", event.properties.part.messageID, [event.properties.part]) - break - } - const result = Binary.search(parts, event.properties.part.id, (p) => p.id) - if (result.found) { - setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part)) - break - } - setStore( - "part", - event.properties.part.messageID, - produce((draft) => { - draft.splice(result.index, 0, event.properties.part) - }), - ) + pendingDeltas.get(event.properties.part.messageID)?.delete(event.properties.part.id) + pendingParts.set(event.properties.part.id, event.properties.part) + scheduleBatchFlush(setStore, store) break } case "message.part.delta": { - const parts = store.part[event.properties.messageID] - if (!parts) break - const result = Binary.search(parts, event.properties.partID, (p) => p.id) - if (!result.found) break - setStore( - "part", - event.properties.messageID, - produce((draft) => { - const part = draft[result.index] - const field = event.properties.field as keyof typeof part - const existing = part[field] as string | undefined - ;(part[field] as string) = (existing ?? "") + event.properties.delta - }), - ) + const { messageID, partID, field, delta } = event.properties + let byMessage = pendingDeltas.get(messageID) + if (!byMessage) { + byMessage = new Map() + pendingDeltas.set(messageID, byMessage) + } + let byPart = byMessage.get(partID) + if (!byPart) { + byPart = new Map() + byMessage.set(partID, byPart) + } + byPart.set(field, (byPart.get(field) ?? "") + delta) + scheduleBatchFlush(setStore, store) break } case "message.part.removed": { + pendingDeltas.get(event.properties.messageID)?.delete(event.properties.partID) const parts = store.part[event.properties.messageID] const result = Binary.search(parts, event.properties.partID, (p) => p.id) if (result.found) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 3c29fe03d30a..f8ba0d78714a 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -577,7 +577,9 @@ export namespace MCP { const toolsResults = await Promise.all( connectedClients.map(async ([clientName, client]) => { - const toolsResult = await client.listTools().catch((e) => { + const mcpEntry = config[clientName] + const timeout = (isMcpConfigured(mcpEntry) ? mcpEntry.timeout : undefined) ?? defaultTimeout ?? DEFAULT_TIMEOUT + const toolsResult = await withTimeout(client.listTools(), timeout).catch((e) => { log.error("failed to get tools", { clientName, error: e.message }) const failedStatus = { status: "failed" as const, diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index e7532d20073b..d154c6c80c00 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -50,6 +50,7 @@ export namespace SessionProcessor { try { let currentText: MessageV2.TextPart | undefined let reasoningMap: Record = {} + let finished = false const stream = await LLM.stream(streamInput) for await (const value of stream.fullStream) { @@ -337,6 +338,8 @@ export namespace SessionProcessor { break case "finish": + log.info("stream finish event received") + finished = true break default: @@ -345,7 +348,7 @@ export namespace SessionProcessor { }) continue } - if (needsCompaction) break + if (needsCompaction || finished) break } } catch (e: any) { log.error("process", { diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index f29aac18d163..b71f67d82c2c 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -122,7 +122,7 @@ export namespace Database { if (err instanceof Context.NotFound) { const effects: (() => void | Promise)[] = [] const result = ctx.provide({ effects, tx: Client() }, () => callback(Client())) - for (const effect of effects) effect() + for (const effect of effects) Promise.resolve(effect()).catch((e) => log.error("effect failed", { error: e })) return result } throw err @@ -146,7 +146,7 @@ export namespace Database { const result = Client().transaction((tx) => { return ctx.provide({ tx, effects }, () => callback(tx)) }) - for (const effect of effects) effect() + for (const effect of effects) Promise.resolve(effect()).catch((e) => log.error("effect failed", { error: e })) return result } throw err