From 626fa1462bb139d02a9d34a5864af8bd746aa2da Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:55:46 -0500 Subject: [PATCH 001/192] fix: make home/end keys work in menu list modal windows (resolves #7190) (#8347) --- packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index f7d7306d015..f1cdaaa5292 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -161,6 +161,8 @@ export function DialogSelect(props: DialogSelectProps) { if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1) if (evt.name === "pageup") move(-10) if (evt.name === "pagedown") move(10) + if (evt.name === "home") moveTo(0) + if (evt.name === "end") moveTo(flat().length - 1) if (evt.name === "return") { const option = selected() if (option) { From 578239e0d081372b4d0fdc7c7cb8a5e54b56e0ca Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 16 Jan 2026 16:13:28 -0600 Subject: [PATCH 002/192] chore: cleanup transform code a tad --- packages/opencode/src/provider/transform.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 4566fc1de2b..023b00dd304 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -26,9 +26,7 @@ export namespace ProviderTransform { msgs = msgs.map((msg) => { if (msg.providerOptions) { for (const options of Object.values(msg.providerOptions)) { - if (options && typeof options === "object") { - delete options["itemId"] - } + delete options["itemId"] } } if (!Array.isArray(msg.content)) { @@ -37,9 +35,7 @@ export namespace ProviderTransform { const content = msg.content.map((part) => { if (part.providerOptions) { for (const options of Object.values(part.providerOptions)) { - if (options && typeof options === "object") { - delete options["itemId"] - } + delete options["itemId"] } } return part From 8cddc9ea555039743c210392b63550781810f8ac Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 16 Jan 2026 22:14:23 +0000 Subject: [PATCH 003/192] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 7ddfca3168a..27349018083 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index e5dde4633ba..f3b12aa8c9f 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From 08b94a6890276d1e654696c3be3274521d99b508 Mon Sep 17 00:00:00 2001 From: Seth Carlton Date: Fri, 16 Jan 2026 16:19:17 -0600 Subject: [PATCH 004/192] fix: keep primary model after subagent runs (#8951) --- packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 96b9e8ffd57..145fa9da0c3 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -145,9 +145,9 @@ export function Prompt(props: PromptProps) { const isPrimaryAgent = local.agent.list().some((x) => x.name === msg.agent) if (msg.agent && isPrimaryAgent) { local.agent.set(msg.agent) + if (msg.model) local.model.set(msg.model) + if (msg.variant) local.model.variant.set(msg.variant) } - if (msg.model) local.model.set(msg.model) - if (msg.variant) local.model.variant.set(msg.variant) } }) From c25155586cd57b8db68a479c714c44f2bfbf667b Mon Sep 17 00:00:00 2001 From: Akshar Patel <123344143+AksharP5@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:42:27 -0500 Subject: [PATCH 005/192] fix: open help dialog with tui/open-help route (#8596) --- packages/opencode/src/server/routes/tui.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/server/routes/tui.ts b/packages/opencode/src/server/routes/tui.ts index be371c1e09e..0577429dd74 100644 --- a/packages/opencode/src/server/routes/tui.ts +++ b/packages/opencode/src/server/routes/tui.ts @@ -119,7 +119,9 @@ export const TuiRoutes = lazy(() => }, }), async (c) => { - // TODO: open dialog + await Bus.publish(TuiEvent.CommandExecute, { + command: "help.show", + }) return c.json(true) }, ) From cbe1c81470564a8f5042047d7422219426e78030 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 16 Jan 2026 17:46:32 -0500 Subject: [PATCH 006/192] wip: black --- .../console/app/src/routes/stripe/webhook.ts | 9 +- .../migrations/0053_gigantic_hardball.sql | 1 + .../core/migrations/meta/0053_snapshot.json | 1316 +++++++++++++++++ .../core/migrations/meta/_journal.json | 9 +- packages/console/core/script/black-gift.ts | 112 ++ ...{onboard-zen-black.ts => black-onboard.ts} | 11 +- .../console/core/script/black-transfer.ts | 6 +- packages/console/core/script/lookup-user.ts | 18 +- .../console/core/src/schema/billing.sql.ts | 13 +- 9 files changed, 1474 insertions(+), 21 deletions(-) create mode 100644 packages/console/core/migrations/0053_gigantic_hardball.sql create mode 100644 packages/console/core/migrations/meta/0053_snapshot.json create mode 100644 packages/console/core/script/black-gift.ts rename packages/console/core/script/{onboard-zen-black.ts => black-onboard.ts} (95%) diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts index 3422d9dd65d..4c3430193e4 100644 --- a/packages/console/app/src/routes/stripe/webhook.ts +++ b/packages/console/app/src/routes/stripe/webhook.ts @@ -183,7 +183,12 @@ export async function POST(input: APIEvent) { .set({ customerID, subscriptionID, - subscriptionCouponID: couponID, + subscription: { + status: "subscribed", + coupon: couponID, + seats: 1, + plan: "200", + }, paymentMethodID: paymentMethod.id, paymentMethodLast4: paymentMethod.card?.last4 ?? null, paymentMethodType: paymentMethod.type, @@ -408,7 +413,7 @@ export async function POST(input: APIEvent) { await Database.transaction(async (tx) => { await tx .update(BillingTable) - .set({ subscriptionID: null, subscriptionCouponID: null }) + .set({ subscriptionID: null, subscription: null }) .where(eq(BillingTable.workspaceID, workspaceID)) await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID)) diff --git a/packages/console/core/migrations/0053_gigantic_hardball.sql b/packages/console/core/migrations/0053_gigantic_hardball.sql new file mode 100644 index 00000000000..72d43135f44 --- /dev/null +++ b/packages/console/core/migrations/0053_gigantic_hardball.sql @@ -0,0 +1 @@ +ALTER TABLE `billing` ADD `subscription` json; \ No newline at end of file diff --git a/packages/console/core/migrations/meta/0053_snapshot.json b/packages/console/core/migrations/meta/0053_snapshot.json new file mode 100644 index 00000000000..fba46b14b6e --- /dev/null +++ b/packages/console/core/migrations/meta/0053_snapshot.json @@ -0,0 +1,1316 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "32a0c40b-a269-4ad1-a5a0-52b1f18932aa", + "prevId": "00774acd-a1e5-49c0-b296-cacc9506a566", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "account_id_pk": { + "name": "account_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "auth": { + "name": "auth", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "enum('email','github','google')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "provider": { + "name": "provider", + "columns": [ + "provider", + "subject" + ], + "isUnique": true + }, + "account_id": { + "name": "account_id", + "columns": [ + "account_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "auth_id_pk": { + "name": "auth_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "benchmark": { + "name": "benchmark", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent": { + "name": "agent", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "mediumtext", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "time_created": { + "name": "time_created", + "columns": [ + "time_created" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "benchmark_id_pk": { + "name": "benchmark_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "billing": { + "name": "billing", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_type": { + "name": "payment_method_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_last4": { + "name": "payment_method_last4", + "type": "varchar(4)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "balance": { + "name": "balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload": { + "name": "reload", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_trigger": { + "name": "reload_trigger", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_amount": { + "name": "reload_amount", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_error": { + "name": "reload_error", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_error": { + "name": "time_reload_error", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_locked_till": { + "name": "time_reload_locked_till", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription": { + "name": "subscription", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "varchar(28)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_coupon_id": { + "name": "subscription_coupon_id", + "type": "varchar(28)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_plan": { + "name": "subscription_plan", + "type": "enum('20','100','200')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_subscription_booked": { + "name": "time_subscription_booked", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_customer_id": { + "name": "global_customer_id", + "columns": [ + "customer_id" + ], + "isUnique": true + }, + "global_subscription_id": { + "name": "global_subscription_id", + "columns": [ + "subscription_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "billing_workspace_id_id_pk": { + "name": "billing_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "payment": { + "name": "payment", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_id": { + "name": "payment_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_refunded": { + "name": "time_refunded", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enrichment": { + "name": "enrichment", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "payment_workspace_id_id_pk": { + "name": "payment_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "subscription": { + "name": "subscription", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rolling_usage": { + "name": "rolling_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fixed_usage": { + "name": "fixed_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_rolling_updated": { + "name": "time_rolling_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_fixed_updated": { + "name": "time_fixed_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "workspace_user_id": { + "name": "workspace_user_id", + "columns": [ + "workspace_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "subscription_workspace_id_id_pk": { + "name": "subscription_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "usage": { + "name": "usage", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_5m_tokens": { + "name": "cache_write_5m_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_1h_tokens": { + "name": "cache_write_1h_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_id": { + "name": "key_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enrichment": { + "name": "enrichment", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "usage_time_created": { + "name": "usage_time_created", + "columns": [ + "workspace_id", + "time_created" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_workspace_id_id_pk": { + "name": "usage_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip_rate_limit": { + "name": "ip_rate_limit", + "columns": { + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval": { + "name": "interval", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ip_rate_limit_ip_interval_pk": { + "name": "ip_rate_limit_ip_interval_pk", + "columns": [ + "ip", + "interval" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip": { + "name": "ip", + "columns": { + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage": { + "name": "usage", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ip_ip_pk": { + "name": "ip_ip_pk", + "columns": [ + "ip" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "key": { + "name": "key", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_used": { + "name": "time_used", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_key": { + "name": "global_key", + "columns": [ + "key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "key_workspace_id_id_pk": { + "name": "key_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "model": { + "name": "model", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "model_workspace_model": { + "name": "model_workspace_model", + "columns": [ + "workspace_id", + "model" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_workspace_id_id_pk": { + "name": "model_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "provider": { + "name": "provider", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "workspace_provider": { + "name": "workspace_provider", + "columns": [ + "workspace_id", + "provider" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "provider_workspace_id_id_pk": { + "name": "provider_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_seen": { + "name": "time_seen", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('admin','member')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_account_id": { + "name": "user_account_id", + "columns": [ + "workspace_id", + "account_id" + ], + "isUnique": true + }, + "user_email": { + "name": "user_email", + "columns": [ + "workspace_id", + "email" + ], + "isUnique": true + }, + "global_account_id": { + "name": "global_account_id", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "global_email": { + "name": "global_email", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_workspace_id_id_pk": { + "name": "user_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "slug": { + "name": "slug", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workspace_id": { + "name": "workspace_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/console/core/migrations/meta/_journal.json b/packages/console/core/migrations/meta/_journal.json index cdf4f63906d..89ffd242376 100644 --- a/packages/console/core/migrations/meta/_journal.json +++ b/packages/console/core/migrations/meta/_journal.json @@ -372,6 +372,13 @@ "when": 1768343920467, "tag": "0052_aromatic_agent_zero", "breakpoints": true + }, + { + "idx": 53, + "version": "5", + "when": 1768599366758, + "tag": "0053_gigantic_hardball", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/console/core/script/black-gift.ts b/packages/console/core/script/black-gift.ts new file mode 100644 index 00000000000..3fbf210ab5c --- /dev/null +++ b/packages/console/core/script/black-gift.ts @@ -0,0 +1,112 @@ +import { Billing } from "../src/billing.js" +import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js" +import { UserTable } from "../src/schema/user.sql.js" +import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js" +import { Identifier } from "../src/identifier.js" +import { centsToMicroCents } from "../src/util/price.js" +import { AuthTable } from "../src/schema/auth.sql.js" + +const plan = "200" +const workspaceID = process.argv[2] +const seats = parseInt(process.argv[3]) + +console.log(`Gifting ${seats} seats of Black to workspace ${workspaceID}`) + +if (!workspaceID || !seats) throw new Error("Usage: bun foo.ts ") + +// Get workspace user +const users = await Database.use((tx) => + tx + .select({ + id: UserTable.id, + role: UserTable.role, + email: AuthTable.subject, + }) + .from(UserTable) + .leftJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email"))) + .where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))), +) +if (users.length === 0) throw new Error(`Error: No users found in workspace ${workspaceID}`) +if (users.length !== seats) + throw new Error(`Error: Workspace ${workspaceID} has ${users.length} users, expected ${seats}`) +const adminUser = users.find((user) => user.role === "admin") +if (!adminUser) throw new Error(`Error: No admin user found in workspace ${workspaceID}`) +if (!adminUser.email) throw new Error(`Error: Admin user ${adminUser.id} has no email`) + +// Get Billing +const billing = await Database.use((tx) => + tx + .select({ + customerID: BillingTable.customerID, + subscriptionID: BillingTable.subscriptionID, + }) + .from(BillingTable) + .where(eq(BillingTable.workspaceID, workspaceID)) + .then((rows) => rows[0]), +) +if (!billing) throw new Error(`Error: Workspace ${workspaceID} has no billing record`) +if (billing.subscriptionID) throw new Error(`Error: Workspace ${workspaceID} already has a subscription`) + +// Look up the Stripe customer by email +const customerID = + billing.customerID ?? + (await (() => + Billing.stripe() + .customers.create({ + email: adminUser.email, + metadata: { + workspaceID, + }, + }) + .then((customer) => customer.id))()) +console.log(`Customer ID: ${customerID}`) + +const couponID = "JAIr0Pe1" +const subscription = await Billing.stripe().subscriptions.create({ + customer: customerID!, + items: [ + { + price: `price_1SmfyI2StuRr0lbXovxJNeZn`, + discounts: [{ coupon: couponID }], + quantity: 2, + }, + ], +}) +console.log(`Subscription ID: ${subscription.id}`) + +await Database.transaction(async (tx) => { + // Set customer id, subscription id, and payment method on workspace billing + await tx + .update(BillingTable) + .set({ + customerID, + subscriptionID: subscription.id, + subscription: { status: "subscribed", coupon: couponID, seats, plan }, + }) + .where(eq(BillingTable.workspaceID, workspaceID)) + + // Create a row in subscription table + for (const user of users) { + await tx.insert(SubscriptionTable).values({ + workspaceID, + id: Identifier.create("subscription"), + userID: user.id, + }) + } + // + // // Create a row in payments table + // await tx.insert(PaymentTable).values({ + // workspaceID, + // id: Identifier.create("payment"), + // amount: centsToMicroCents(amountInCents), + // customerID, + // invoiceID, + // paymentID, + // enrichment: { + // type: "subscription", + // couponID, + // }, + // }) +}) + +console.log(`done`) diff --git a/packages/console/core/script/onboard-zen-black.ts b/packages/console/core/script/black-onboard.ts similarity index 95% rename from packages/console/core/script/onboard-zen-black.ts rename to packages/console/core/script/black-onboard.ts index 3ee8809739d..77e5b779e35 100644 --- a/packages/console/core/script/onboard-zen-black.ts +++ b/packages/console/core/script/black-onboard.ts @@ -12,7 +12,7 @@ const email = process.argv[3] console.log(`Onboarding workspace ${workspaceID} for email ${email}`) if (!workspaceID || !email) { - console.error("Usage: bun onboard-zen-black.ts ") + console.error("Usage: bun foo.ts ") process.exit(1) } @@ -50,7 +50,7 @@ const existingSubscription = await Database.use((tx) => tx .select({ workspaceID: BillingTable.workspaceID }) .from(BillingTable) - .where(eq(BillingTable.subscriptionID, subscriptionID)) + .where(sql`JSON_EXTRACT(${BillingTable.subscription}, '$.id') = ${subscriptionID}`) .then((rows) => rows[0]), ) if (existingSubscription) { @@ -128,10 +128,15 @@ await Database.transaction(async (tx) => { .set({ customerID, subscriptionID, - subscriptionCouponID: couponID, paymentMethodID, paymentMethodLast4, paymentMethodType, + subscription: { + status: "subscribed", + coupon: couponID, + seats: 1, + plan: "200", + }, }) .where(eq(BillingTable.workspaceID, workspaceID)) diff --git a/packages/console/core/script/black-transfer.ts b/packages/console/core/script/black-transfer.ts index a7947fe7223..e962ba5d361 100644 --- a/packages/console/core/script/black-transfer.ts +++ b/packages/console/core/script/black-transfer.ts @@ -18,7 +18,7 @@ const fromBilling = await Database.use((tx) => .select({ customerID: BillingTable.customerID, subscriptionID: BillingTable.subscriptionID, - subscriptionCouponID: BillingTable.subscriptionCouponID, + subscription: BillingTable.subscription, paymentMethodID: BillingTable.paymentMethodID, paymentMethodType: BillingTable.paymentMethodType, paymentMethodLast4: BillingTable.paymentMethodLast4, @@ -119,7 +119,7 @@ await Database.transaction(async (tx) => { .set({ customerID: fromPrevPayment.customerID, subscriptionID: null, - subscriptionCouponID: null, + subscription: null, paymentMethodID: fromPrevPaymentMethods.data[0].id, paymentMethodLast4: fromPrevPaymentMethods.data[0].card?.last4 ?? null, paymentMethodType: fromPrevPaymentMethods.data[0].type, @@ -131,7 +131,7 @@ await Database.transaction(async (tx) => { .set({ customerID: fromBilling.customerID, subscriptionID: fromBilling.subscriptionID, - subscriptionCouponID: fromBilling.subscriptionCouponID, + subscription: fromBilling.subscription, paymentMethodID: fromBilling.paymentMethodID, paymentMethodLast4: fromBilling.paymentMethodLast4, paymentMethodType: fromBilling.paymentMethodType, diff --git a/packages/console/core/script/lookup-user.ts b/packages/console/core/script/lookup-user.ts index b3a104457ff..3dc5e7a968c 100644 --- a/packages/console/core/script/lookup-user.ts +++ b/packages/console/core/script/lookup-user.ts @@ -55,8 +55,9 @@ if (identifier.startsWith("wrk_")) { ), ) - // Get all payments for these workspaces - await Promise.all(users.map((u: { workspaceID: string }) => printWorkspace(u.workspaceID))) + for (const user of users) { + await printWorkspace(user.workspaceID) + } } async function printWorkspace(workspaceID: string) { @@ -114,11 +115,11 @@ async function printWorkspace(workspaceID: string) { balance: BillingTable.balance, customerID: BillingTable.customerID, reload: BillingTable.reload, + subscriptionID: BillingTable.subscriptionID, subscription: { - id: BillingTable.subscriptionID, - couponID: BillingTable.subscriptionCouponID, plan: BillingTable.subscriptionPlan, booked: BillingTable.timeSubscriptionBooked, + enrichment: BillingTable.subscription, }, }) .from(BillingTable) @@ -128,8 +129,13 @@ async function printWorkspace(workspaceID: string) { rows.map((row) => ({ ...row, balance: `$${(row.balance / 100000000).toFixed(2)}`, - subscription: row.subscription.id - ? `Subscribed ${row.subscription.couponID ? `(coupon: ${row.subscription.couponID}) ` : ""}` + subscription: row.subscriptionID + ? [ + `Black ${row.subscription.enrichment!.plan}`, + row.subscription.enrichment!.seats > 1 ? `X ${row.subscription.enrichment!.seats} seats` : "", + row.subscription.enrichment!.coupon ? `(coupon: ${row.subscription.enrichment!.coupon})` : "", + `(ref: ${row.subscriptionID})`, + ].join(" ") : row.subscription.booked ? `Waitlist ${row.subscription.plan} plan` : undefined, diff --git a/packages/console/core/src/schema/billing.sql.ts b/packages/console/core/src/schema/billing.sql.ts index f1300f8498b..56b2ef33b9f 100644 --- a/packages/console/core/src/schema/billing.sql.ts +++ b/packages/console/core/src/schema/billing.sql.ts @@ -21,16 +21,17 @@ export const BillingTable = mysqlTable( reloadError: varchar("reload_error", { length: 255 }), timeReloadError: utc("time_reload_error"), timeReloadLockedTill: utc("time_reload_locked_till"), + subscription: json("subscription").$type<{ + status: "subscribed" + coupon?: string + seats: number + plan: "20" | "100" | "200" + }>(), subscriptionID: varchar("subscription_id", { length: 28 }), - subscriptionCouponID: varchar("subscription_coupon_id", { length: 28 }), subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const), timeSubscriptionBooked: utc("time_subscription_booked"), }, - (table) => [ - ...workspaceIndexes(table), - uniqueIndex("global_customer_id").on(table.customerID), - uniqueIndex("global_subscription_id").on(table.subscriptionID), - ], + (table) => [...workspaceIndexes(table), uniqueIndex("global_customer_id").on(table.customerID)], ) export const SubscriptionTable = mysqlTable( From f96c4badd8d491291de7a5f9b06a948f353aa3d4 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 16 Jan 2026 17:48:26 -0500 Subject: [PATCH 007/192] wip: black --- .../migrations/0054_numerous_annihilus.sql | 1 + .../core/migrations/meta/0054_snapshot.json | 1309 +++++++++++++++++ .../core/migrations/meta/_journal.json | 7 + .../console/core/src/schema/billing.sql.ts | 6 +- 4 files changed, 1322 insertions(+), 1 deletion(-) create mode 100644 packages/console/core/migrations/0054_numerous_annihilus.sql create mode 100644 packages/console/core/migrations/meta/0054_snapshot.json diff --git a/packages/console/core/migrations/0054_numerous_annihilus.sql b/packages/console/core/migrations/0054_numerous_annihilus.sql new file mode 100644 index 00000000000..299847db64f --- /dev/null +++ b/packages/console/core/migrations/0054_numerous_annihilus.sql @@ -0,0 +1 @@ +ALTER TABLE `billing` DROP COLUMN `subscription_coupon_id`; \ No newline at end of file diff --git a/packages/console/core/migrations/meta/0054_snapshot.json b/packages/console/core/migrations/meta/0054_snapshot.json new file mode 100644 index 00000000000..429d4a2af1e --- /dev/null +++ b/packages/console/core/migrations/meta/0054_snapshot.json @@ -0,0 +1,1309 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "a0ade64b-b735-4a70-8d39-ebd84bc9e924", + "prevId": "32a0c40b-a269-4ad1-a5a0-52b1f18932aa", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "account_id_pk": { + "name": "account_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "auth": { + "name": "auth", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "enum('email','github','google')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "provider": { + "name": "provider", + "columns": [ + "provider", + "subject" + ], + "isUnique": true + }, + "account_id": { + "name": "account_id", + "columns": [ + "account_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "auth_id_pk": { + "name": "auth_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "benchmark": { + "name": "benchmark", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent": { + "name": "agent", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "mediumtext", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "time_created": { + "name": "time_created", + "columns": [ + "time_created" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "benchmark_id_pk": { + "name": "benchmark_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "billing": { + "name": "billing", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_type": { + "name": "payment_method_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_last4": { + "name": "payment_method_last4", + "type": "varchar(4)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "balance": { + "name": "balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload": { + "name": "reload", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_trigger": { + "name": "reload_trigger", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_amount": { + "name": "reload_amount", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_error": { + "name": "reload_error", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_error": { + "name": "time_reload_error", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_locked_till": { + "name": "time_reload_locked_till", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription": { + "name": "subscription", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "varchar(28)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_plan": { + "name": "subscription_plan", + "type": "enum('20','100','200')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_subscription_booked": { + "name": "time_subscription_booked", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_customer_id": { + "name": "global_customer_id", + "columns": [ + "customer_id" + ], + "isUnique": true + }, + "global_subscription_id": { + "name": "global_subscription_id", + "columns": [ + "subscription_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "billing_workspace_id_id_pk": { + "name": "billing_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "payment": { + "name": "payment", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_id": { + "name": "payment_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_refunded": { + "name": "time_refunded", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enrichment": { + "name": "enrichment", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "payment_workspace_id_id_pk": { + "name": "payment_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "subscription": { + "name": "subscription", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rolling_usage": { + "name": "rolling_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fixed_usage": { + "name": "fixed_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_rolling_updated": { + "name": "time_rolling_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_fixed_updated": { + "name": "time_fixed_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "workspace_user_id": { + "name": "workspace_user_id", + "columns": [ + "workspace_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "subscription_workspace_id_id_pk": { + "name": "subscription_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "usage": { + "name": "usage", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_5m_tokens": { + "name": "cache_write_5m_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_1h_tokens": { + "name": "cache_write_1h_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_id": { + "name": "key_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enrichment": { + "name": "enrichment", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "usage_time_created": { + "name": "usage_time_created", + "columns": [ + "workspace_id", + "time_created" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_workspace_id_id_pk": { + "name": "usage_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip_rate_limit": { + "name": "ip_rate_limit", + "columns": { + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval": { + "name": "interval", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ip_rate_limit_ip_interval_pk": { + "name": "ip_rate_limit_ip_interval_pk", + "columns": [ + "ip", + "interval" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip": { + "name": "ip", + "columns": { + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage": { + "name": "usage", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ip_ip_pk": { + "name": "ip_ip_pk", + "columns": [ + "ip" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "key": { + "name": "key", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_used": { + "name": "time_used", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_key": { + "name": "global_key", + "columns": [ + "key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "key_workspace_id_id_pk": { + "name": "key_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "model": { + "name": "model", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "model_workspace_model": { + "name": "model_workspace_model", + "columns": [ + "workspace_id", + "model" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_workspace_id_id_pk": { + "name": "model_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "provider": { + "name": "provider", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "workspace_provider": { + "name": "workspace_provider", + "columns": [ + "workspace_id", + "provider" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "provider_workspace_id_id_pk": { + "name": "provider_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_seen": { + "name": "time_seen", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('admin','member')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_account_id": { + "name": "user_account_id", + "columns": [ + "workspace_id", + "account_id" + ], + "isUnique": true + }, + "user_email": { + "name": "user_email", + "columns": [ + "workspace_id", + "email" + ], + "isUnique": true + }, + "global_account_id": { + "name": "global_account_id", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "global_email": { + "name": "global_email", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_workspace_id_id_pk": { + "name": "user_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "slug": { + "name": "slug", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workspace_id": { + "name": "workspace_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/console/core/migrations/meta/_journal.json b/packages/console/core/migrations/meta/_journal.json index 89ffd242376..13bac50a306 100644 --- a/packages/console/core/migrations/meta/_journal.json +++ b/packages/console/core/migrations/meta/_journal.json @@ -379,6 +379,13 @@ "when": 1768599366758, "tag": "0053_gigantic_hardball", "breakpoints": true + }, + { + "idx": 54, + "version": "5", + "when": 1768603665356, + "tag": "0054_numerous_annihilus", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/console/core/src/schema/billing.sql.ts b/packages/console/core/src/schema/billing.sql.ts index 56b2ef33b9f..9f05919f240 100644 --- a/packages/console/core/src/schema/billing.sql.ts +++ b/packages/console/core/src/schema/billing.sql.ts @@ -31,7 +31,11 @@ export const BillingTable = mysqlTable( subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const), timeSubscriptionBooked: utc("time_subscription_booked"), }, - (table) => [...workspaceIndexes(table), uniqueIndex("global_customer_id").on(table.customerID)], + (table) => [ + ...workspaceIndexes(table), + uniqueIndex("global_customer_id").on(table.customerID), + uniqueIndex("global_subscription_id").on(table.subscriptionID), + ], ) export const SubscriptionTable = mysqlTable( From 632f20558a99ed0f5fccda2dc896da28cb86a987 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 16 Jan 2026 22:49:19 +0000 Subject: [PATCH 008/192] chore: generate --- .../core/migrations/meta/0053_snapshot.json | 134 ++++-------------- .../core/migrations/meta/0054_snapshot.json | 134 ++++-------------- .../core/migrations/meta/_journal.json | 2 +- 3 files changed, 61 insertions(+), 209 deletions(-) diff --git a/packages/console/core/migrations/meta/0053_snapshot.json b/packages/console/core/migrations/meta/0053_snapshot.json index fba46b14b6e..75a2cb7c929 100644 --- a/packages/console/core/migrations/meta/0053_snapshot.json +++ b/packages/console/core/migrations/meta/0053_snapshot.json @@ -43,9 +43,7 @@ "compositePrimaryKeys": { "account_id_pk": { "name": "account_id_pk", - "columns": [ - "id" - ] + "columns": ["id"] } }, "uniqueConstraints": {}, @@ -109,17 +107,12 @@ "indexes": { "provider": { "name": "provider", - "columns": [ - "provider", - "subject" - ], + "columns": ["provider", "subject"], "isUnique": true }, "account_id": { "name": "account_id", - "columns": [ - "account_id" - ], + "columns": ["account_id"], "isUnique": false } }, @@ -127,9 +120,7 @@ "compositePrimaryKeys": { "auth_id_pk": { "name": "auth_id_pk", - "columns": [ - "id" - ] + "columns": ["id"] } }, "uniqueConstraints": {}, @@ -193,9 +184,7 @@ "indexes": { "time_created": { "name": "time_created", - "columns": [ - "time_created" - ], + "columns": ["time_created"], "isUnique": false } }, @@ -203,9 +192,7 @@ "compositePrimaryKeys": { "benchmark_id_pk": { "name": "benchmark_id_pk", - "columns": [ - "id" - ] + "columns": ["id"] } }, "uniqueConstraints": {}, @@ -388,16 +375,12 @@ "indexes": { "global_customer_id": { "name": "global_customer_id", - "columns": [ - "customer_id" - ], + "columns": ["customer_id"], "isUnique": true }, "global_subscription_id": { "name": "global_subscription_id", - "columns": [ - "subscription_id" - ], + "columns": ["subscription_id"], "isUnique": true } }, @@ -405,10 +388,7 @@ "compositePrimaryKeys": { "billing_workspace_id_id_pk": { "name": "billing_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -502,10 +482,7 @@ "compositePrimaryKeys": { "payment_workspace_id_id_pk": { "name": "payment_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -590,10 +567,7 @@ "indexes": { "workspace_user_id": { "name": "workspace_user_id", - "columns": [ - "workspace_id", - "user_id" - ], + "columns": ["workspace_id", "user_id"], "isUnique": true } }, @@ -601,10 +575,7 @@ "compositePrimaryKeys": { "subscription_workspace_id_id_pk": { "name": "subscription_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -731,10 +702,7 @@ "indexes": { "usage_time_created": { "name": "usage_time_created", - "columns": [ - "workspace_id", - "time_created" - ], + "columns": ["workspace_id", "time_created"], "isUnique": false } }, @@ -742,10 +710,7 @@ "compositePrimaryKeys": { "usage_workspace_id_id_pk": { "name": "usage_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -781,10 +746,7 @@ "compositePrimaryKeys": { "ip_rate_limit_ip_interval_pk": { "name": "ip_rate_limit_ip_interval_pk", - "columns": [ - "ip", - "interval" - ] + "columns": ["ip", "interval"] } }, "uniqueConstraints": {}, @@ -836,9 +798,7 @@ "compositePrimaryKeys": { "ip_ip_pk": { "name": "ip_ip_pk", - "columns": [ - "ip" - ] + "columns": ["ip"] } }, "uniqueConstraints": {}, @@ -916,9 +876,7 @@ "indexes": { "global_key": { "name": "global_key", - "columns": [ - "key" - ], + "columns": ["key"], "isUnique": true } }, @@ -926,10 +884,7 @@ "compositePrimaryKeys": { "key_workspace_id_id_pk": { "name": "key_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -986,10 +941,7 @@ "indexes": { "model_workspace_model": { "name": "model_workspace_model", - "columns": [ - "workspace_id", - "model" - ], + "columns": ["workspace_id", "model"], "isUnique": true } }, @@ -997,10 +949,7 @@ "compositePrimaryKeys": { "model_workspace_id_id_pk": { "name": "model_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -1064,10 +1013,7 @@ "indexes": { "workspace_provider": { "name": "workspace_provider", - "columns": [ - "workspace_id", - "provider" - ], + "columns": ["workspace_id", "provider"], "isUnique": true } }, @@ -1075,10 +1021,7 @@ "compositePrimaryKeys": { "provider_workspace_id_id_pk": { "name": "provider_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -1191,32 +1134,22 @@ "indexes": { "user_account_id": { "name": "user_account_id", - "columns": [ - "workspace_id", - "account_id" - ], + "columns": ["workspace_id", "account_id"], "isUnique": true }, "user_email": { "name": "user_email", - "columns": [ - "workspace_id", - "email" - ], + "columns": ["workspace_id", "email"], "isUnique": true }, "global_account_id": { "name": "global_account_id", - "columns": [ - "account_id" - ], + "columns": ["account_id"], "isUnique": false }, "global_email": { "name": "global_email", - "columns": [ - "email" - ], + "columns": ["email"], "isUnique": false } }, @@ -1224,10 +1157,7 @@ "compositePrimaryKeys": { "user_workspace_id_id_pk": { "name": "user_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -1284,9 +1214,7 @@ "indexes": { "slug": { "name": "slug", - "columns": [ - "slug" - ], + "columns": ["slug"], "isUnique": true } }, @@ -1294,9 +1222,7 @@ "compositePrimaryKeys": { "workspace_id": { "name": "workspace_id", - "columns": [ - "id" - ] + "columns": ["id"] } }, "uniqueConstraints": {}, @@ -1313,4 +1239,4 @@ "tables": {}, "indexes": {} } -} \ No newline at end of file +} diff --git a/packages/console/core/migrations/meta/0054_snapshot.json b/packages/console/core/migrations/meta/0054_snapshot.json index 429d4a2af1e..a1e3851d857 100644 --- a/packages/console/core/migrations/meta/0054_snapshot.json +++ b/packages/console/core/migrations/meta/0054_snapshot.json @@ -43,9 +43,7 @@ "compositePrimaryKeys": { "account_id_pk": { "name": "account_id_pk", - "columns": [ - "id" - ] + "columns": ["id"] } }, "uniqueConstraints": {}, @@ -109,17 +107,12 @@ "indexes": { "provider": { "name": "provider", - "columns": [ - "provider", - "subject" - ], + "columns": ["provider", "subject"], "isUnique": true }, "account_id": { "name": "account_id", - "columns": [ - "account_id" - ], + "columns": ["account_id"], "isUnique": false } }, @@ -127,9 +120,7 @@ "compositePrimaryKeys": { "auth_id_pk": { "name": "auth_id_pk", - "columns": [ - "id" - ] + "columns": ["id"] } }, "uniqueConstraints": {}, @@ -193,9 +184,7 @@ "indexes": { "time_created": { "name": "time_created", - "columns": [ - "time_created" - ], + "columns": ["time_created"], "isUnique": false } }, @@ -203,9 +192,7 @@ "compositePrimaryKeys": { "benchmark_id_pk": { "name": "benchmark_id_pk", - "columns": [ - "id" - ] + "columns": ["id"] } }, "uniqueConstraints": {}, @@ -381,16 +368,12 @@ "indexes": { "global_customer_id": { "name": "global_customer_id", - "columns": [ - "customer_id" - ], + "columns": ["customer_id"], "isUnique": true }, "global_subscription_id": { "name": "global_subscription_id", - "columns": [ - "subscription_id" - ], + "columns": ["subscription_id"], "isUnique": true } }, @@ -398,10 +381,7 @@ "compositePrimaryKeys": { "billing_workspace_id_id_pk": { "name": "billing_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -495,10 +475,7 @@ "compositePrimaryKeys": { "payment_workspace_id_id_pk": { "name": "payment_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -583,10 +560,7 @@ "indexes": { "workspace_user_id": { "name": "workspace_user_id", - "columns": [ - "workspace_id", - "user_id" - ], + "columns": ["workspace_id", "user_id"], "isUnique": true } }, @@ -594,10 +568,7 @@ "compositePrimaryKeys": { "subscription_workspace_id_id_pk": { "name": "subscription_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -724,10 +695,7 @@ "indexes": { "usage_time_created": { "name": "usage_time_created", - "columns": [ - "workspace_id", - "time_created" - ], + "columns": ["workspace_id", "time_created"], "isUnique": false } }, @@ -735,10 +703,7 @@ "compositePrimaryKeys": { "usage_workspace_id_id_pk": { "name": "usage_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -774,10 +739,7 @@ "compositePrimaryKeys": { "ip_rate_limit_ip_interval_pk": { "name": "ip_rate_limit_ip_interval_pk", - "columns": [ - "ip", - "interval" - ] + "columns": ["ip", "interval"] } }, "uniqueConstraints": {}, @@ -829,9 +791,7 @@ "compositePrimaryKeys": { "ip_ip_pk": { "name": "ip_ip_pk", - "columns": [ - "ip" - ] + "columns": ["ip"] } }, "uniqueConstraints": {}, @@ -909,9 +869,7 @@ "indexes": { "global_key": { "name": "global_key", - "columns": [ - "key" - ], + "columns": ["key"], "isUnique": true } }, @@ -919,10 +877,7 @@ "compositePrimaryKeys": { "key_workspace_id_id_pk": { "name": "key_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -979,10 +934,7 @@ "indexes": { "model_workspace_model": { "name": "model_workspace_model", - "columns": [ - "workspace_id", - "model" - ], + "columns": ["workspace_id", "model"], "isUnique": true } }, @@ -990,10 +942,7 @@ "compositePrimaryKeys": { "model_workspace_id_id_pk": { "name": "model_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -1057,10 +1006,7 @@ "indexes": { "workspace_provider": { "name": "workspace_provider", - "columns": [ - "workspace_id", - "provider" - ], + "columns": ["workspace_id", "provider"], "isUnique": true } }, @@ -1068,10 +1014,7 @@ "compositePrimaryKeys": { "provider_workspace_id_id_pk": { "name": "provider_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -1184,32 +1127,22 @@ "indexes": { "user_account_id": { "name": "user_account_id", - "columns": [ - "workspace_id", - "account_id" - ], + "columns": ["workspace_id", "account_id"], "isUnique": true }, "user_email": { "name": "user_email", - "columns": [ - "workspace_id", - "email" - ], + "columns": ["workspace_id", "email"], "isUnique": true }, "global_account_id": { "name": "global_account_id", - "columns": [ - "account_id" - ], + "columns": ["account_id"], "isUnique": false }, "global_email": { "name": "global_email", - "columns": [ - "email" - ], + "columns": ["email"], "isUnique": false } }, @@ -1217,10 +1150,7 @@ "compositePrimaryKeys": { "user_workspace_id_id_pk": { "name": "user_workspace_id_id_pk", - "columns": [ - "workspace_id", - "id" - ] + "columns": ["workspace_id", "id"] } }, "uniqueConstraints": {}, @@ -1277,9 +1207,7 @@ "indexes": { "slug": { "name": "slug", - "columns": [ - "slug" - ], + "columns": ["slug"], "isUnique": true } }, @@ -1287,9 +1215,7 @@ "compositePrimaryKeys": { "workspace_id": { "name": "workspace_id", - "columns": [ - "id" - ] + "columns": ["id"] } }, "uniqueConstraints": {}, @@ -1306,4 +1232,4 @@ "tables": {}, "indexes": {} } -} \ No newline at end of file +} diff --git a/packages/console/core/migrations/meta/_journal.json b/packages/console/core/migrations/meta/_journal.json index 13bac50a306..dd0957e51ca 100644 --- a/packages/console/core/migrations/meta/_journal.json +++ b/packages/console/core/migrations/meta/_journal.json @@ -388,4 +388,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} From 3d095e7fe7087057467d29e78a579ff02d28da14 Mon Sep 17 00:00:00 2001 From: b3nw Date: Fri, 16 Jan 2026 16:57:17 -0600 Subject: [PATCH 009/192] fix: centralize OSC 52 clipboard support for SSH sessions (#8974) --- packages/opencode/src/cli/cmd/tui/app.tsx | 10 ---------- .../src/cli/cmd/tui/routes/session/index.tsx | 5 ----- packages/opencode/src/cli/cmd/tui/ui/dialog.tsx | 5 ----- .../opencode/src/cli/cmd/tui/util/clipboard.ts | 16 ++++++++++++++++ 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 1fea3f4b305..2ec1fb703f9 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -200,11 +200,6 @@ function App() { renderer.console.onCopySelection = async (text: string) => { if (!text || text.length === 0) return - const base64 = Buffer.from(text).toString("base64") - const osc52 = `\x1b]52;c;${base64}\x07` - const finalOsc52 = process.env["TMUX"] ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 - // @ts-expect-error writeOut is not in type definitions - renderer.writeOut(finalOsc52) await Clipboard.copy(text) .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) .catch(toast.error) @@ -627,11 +622,6 @@ function App() { } const text = renderer.getSelection()?.getSelectedText() if (text && text.length > 0) { - const base64 = Buffer.from(text).toString("base64") - const osc52 = `\x1b]52;c;${base64}\x07` - const finalOsc52 = process.env["TMUX"] ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 - /* @ts-expect-error */ - renderer.writeOut(finalOsc52) await Clipboard.copy(text) .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) .catch(toast.error) 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 d91363954a1..1d64a2ff156 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -697,11 +697,6 @@ export function Session() { return } - const base64 = Buffer.from(text).toString("base64") - const osc52 = `\x1b]52;c;${base64}\x07` - const finalOsc52 = process.env["TMUX"] ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 - /* @ts-expect-error */ - renderer.writeOut(finalOsc52) Clipboard.copy(text) .then(() => toast.show({ message: "Message copied to clipboard!", variant: "success" })) .catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" })) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index 4477d301562..57375ba09db 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -141,11 +141,6 @@ export function DialogProvider(props: ParentProps) { onMouseUp={async () => { const text = renderer.getSelection()?.getSelectedText() if (text && text.length > 0) { - const base64 = Buffer.from(text).toString("base64") - const osc52 = `\x1b]52;c;${base64}\x07` - const finalOsc52 = process.env["TMUX"] ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 - /* @ts-expect-error */ - renderer.writeOut(finalOsc52) await Clipboard.copy(text) .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) .catch(toast.error) diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 9c91cf3055a..2526f41714c 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -5,6 +5,21 @@ import { lazy } from "../../../../util/lazy.js" import { tmpdir } from "os" import path from "path" +/** + * Writes text to clipboard via OSC 52 escape sequence. + * This allows clipboard operations to work over SSH by having + * the terminal emulator handle the clipboard locally. + */ +function writeOsc52(text: string): void { + if (!process.stdout.isTTY) return + const base64 = Buffer.from(text).toString("base64") + const osc52 = `\x1b]52;c;${base64}\x07` + // tmux and screen require DCS passthrough wrapping + const passthrough = process.env["TMUX"] || process.env["STY"] + const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 + process.stdout.write(sequence) +} + export namespace Clipboard { export interface Content { data: string @@ -123,6 +138,7 @@ export namespace Clipboard { }) export async function copy(text: string): Promise { + writeOsc52(text) await getCopyMethod()(text) } } From aca1eb6b5b1f74396a9856fa2c413a79a55ca69f Mon Sep 17 00:00:00 2001 From: Caleb Norton Date: Fri, 16 Jan 2026 16:59:07 -0600 Subject: [PATCH 010/192] fix(nix): add desktop application entry (#8972) --- nix/desktop.nix | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/nix/desktop.nix b/nix/desktop.nix index 4b659413aaa..9fb73b56316 100644 --- a/nix/desktop.nix +++ b/nix/desktop.nix @@ -15,6 +15,8 @@ cargo, rustc, makeBinaryWrapper, + copyDesktopItems, + makeDesktopItem, nodejs, jq, }: @@ -57,12 +59,28 @@ rustPlatform.buildRustPackage rec { pkg-config bun makeBinaryWrapper + copyDesktopItems cargo rustc nodejs jq ]; + # based on packages/desktop/src-tauri/release/appstream.metainfo.xml + desktopItems = lib.optionals stdenv.isLinux [ + (makeDesktopItem { + name = "ai.opencode.opencode"; + desktopName = "OpenCode"; + comment = "Open source AI coding agent"; + exec = "opencode-desktop"; + icon = "opencode"; + terminal = false; + type = "Application"; + categories = [ "Development" "IDE" ]; + startupWMClass = "opencode"; + }) + ]; + buildInputs = [ openssl ] @@ -121,6 +139,10 @@ rustPlatform.buildRustPackage rec { # It looks for them in the location specified in tauri.conf.json. postInstall = lib.optionalString stdenv.isLinux '' + # Install icon + mkdir -p $out/share/icons/hicolor/128x128/apps + cp ../../../packages/desktop/src-tauri/icons/prod/128x128.png $out/share/icons/hicolor/128x128/apps/opencode.png + # Wrap the binary to ensure it finds the libraries wrapProgram $out/bin/opencode-desktop \ --prefix LD_LIBRARY_PATH : ${ From 6e020ef9ef2b02576b26116a5c4bc7d7bc1c9c5b Mon Sep 17 00:00:00 2001 From: Caleb Norton Date: Fri, 16 Jan 2026 16:59:34 -0600 Subject: [PATCH 011/192] chore: cleanup nix (#8964) --- .gitignore | 1 + flake.lock | 6 +++--- flake.nix | 28 +++------------------------- 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 75fa054a5e4..78a77f81982 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ opencode.json a.out target .scripts +.direnv/ # Local dev files opencode-dev diff --git a/flake.lock b/flake.lock index 58bdca6bf6a..5ef276f0a08 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1768395095, - "narHash": "sha256-ZhuYJbwbZT32QA95tSkXd9zXHcdZj90EzHpEXBMabaw=", + "lastModified": 1768302833, + "narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "13868c071cc73a5e9f610c47d7bb08e5da64fdd5", + "rev": "61db79b0c6b838d9894923920b612048e1201926", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 4219a7e8e10..32614640ad3 100644 --- a/flake.nix +++ b/flake.nix @@ -7,6 +7,7 @@ outputs = { + self, nixpkgs, ... }: @@ -107,33 +108,10 @@ }; in { - default = opencodePkg; + default = self.packages.${system}.opencode; + opencode = opencodePkg; desktop = desktopPkg; } ); - - apps = forEachSystem ( - system: - let - pkgs = pkgsFor system; - in - { - opencode-dev = { - type = "app"; - meta = { - description = "Nix devshell shell for OpenCode"; - runtimeInputs = [ pkgs.bun ]; - }; - program = "${ - pkgs.writeShellApplication { - name = "opencode-dev"; - text = '' - exec bun run dev "$@" - ''; - } - }/bin/opencode-dev"; - }; - } - ); }; } From c325aa11423f0fdadb80c334469d6ea1b86225e1 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Fri, 16 Jan 2026 20:00:56 -0300 Subject: [PATCH 012/192] fix(desktop): Stream bash output + strip-asni (#8961) --- bun.lock | 1 + packages/ui/package.json | 1 + packages/ui/src/components/message-part.tsx | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index e71d700e1cb..9cda088153c 100644 --- a/bun.lock +++ b/bun.lock @@ -424,6 +424,7 @@ "shiki": "catalog:", "solid-js": "catalog:", "solid-list": "catalog:", + "strip-ansi": "7.1.2", "virtua": "catalog:", }, "devDependencies": { diff --git a/packages/ui/package.json b/packages/ui/package.json index ef6eec23ac8..0b490591c35 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -59,6 +59,7 @@ "shiki": "catalog:", "solid-js": "catalog:", "solid-list": "catalog:", + "strip-ansi": "7.1.2", "virtua": "catalog:" } } diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index e1a34a3241a..165f46f6c50 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -10,6 +10,7 @@ import { onCleanup, type JSX, } from "solid-js" +import stripAnsi from "strip-ansi" import { Dynamic } from "solid-js/web" import { AgentPart, @@ -926,7 +927,7 @@ ToolRegistry.register({ >
From 55224d64a2f39294af2382d14de1e469d95417c3 Mon Sep 17 00:00:00 2001 From: Github Action Date: Fri, 16 Jan 2026 23:01:30 +0000 Subject: [PATCH 013/192] Update flake.lock --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 5ef276f0a08..2bfad510e7b 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1768302833, - "narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=", + "lastModified": 1768456270, + "narHash": "sha256-NgaL2CCiUR6nsqUIY4yxkzz07iQUlUCany44CFv+OxY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "61db79b0c6b838d9894923920b612048e1201926", + "rev": "f4606b01b39e09065df37905a2133905246db9ed", "type": "github" }, "original": { From 43a9c503899a36d3c1d89f6f4dbf15192f5ce9d6 Mon Sep 17 00:00:00 2001 From: Github Action Date: Fri, 16 Jan 2026 23:02:26 +0000 Subject: [PATCH 014/192] Update node_modules hash (x86_64-linux) --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 255e44fe366..e9c7eed2a93 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,6 +1,6 @@ { "nodeModules": { - "x86_64-linux": "sha256-07XxcHLuToM4QfWVyaPLACxjPZ93ZM7gtpX2o08Lp18=", + "x86_64-linux": "sha256-4zchRpxzvHnPMcwumgL9yaX0deIXS5IGPp131eYsSvg=", "aarch64-linux": "sha256-0Im52dLeZ0ZtaPJr/U4m7+IRtOfziHNJI/Bu/V6cPho=", "aarch64-darwin": "sha256-U2UvE70nM0OI0VhIku8qnX+ptPbA+Q/y1BGXbFMcyt4=", "x86_64-darwin": "sha256-CpZFHBMPJSib2Vqs6oC8HQjQtviPUMa/qezHAe22N/A=" From 5c13b209aad88a2533e9181d8aa8b4b3b8408a16 Mon Sep 17 00:00:00 2001 From: Github Action Date: Fri, 16 Jan 2026 23:11:48 +0000 Subject: [PATCH 015/192] Update node_modules hash (x86_64-darwin) --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index e9c7eed2a93..9e03c17381b 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -3,6 +3,6 @@ "x86_64-linux": "sha256-4zchRpxzvHnPMcwumgL9yaX0deIXS5IGPp131eYsSvg=", "aarch64-linux": "sha256-0Im52dLeZ0ZtaPJr/U4m7+IRtOfziHNJI/Bu/V6cPho=", "aarch64-darwin": "sha256-U2UvE70nM0OI0VhIku8qnX+ptPbA+Q/y1BGXbFMcyt4=", - "x86_64-darwin": "sha256-CpZFHBMPJSib2Vqs6oC8HQjQtviPUMa/qezHAe22N/A=" + "x86_64-darwin": "sha256-WYaP44PWRGtoG1DIuUJUH4DvuaCuFhlJZ9fPzGsiIfE=" } } From 1aedb265dd6a9370f2bbf96a3db4a7e5166c22e5 Mon Sep 17 00:00:00 2001 From: Github Action Date: Fri, 16 Jan 2026 23:16:53 +0000 Subject: [PATCH 016/192] Update node_modules hash (aarch64-darwin) --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 9e03c17381b..41fb0260278 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -2,7 +2,7 @@ "nodeModules": { "x86_64-linux": "sha256-4zchRpxzvHnPMcwumgL9yaX0deIXS5IGPp131eYsSvg=", "aarch64-linux": "sha256-0Im52dLeZ0ZtaPJr/U4m7+IRtOfziHNJI/Bu/V6cPho=", - "aarch64-darwin": "sha256-U2UvE70nM0OI0VhIku8qnX+ptPbA+Q/y1BGXbFMcyt4=", + "aarch64-darwin": "sha256-86d/G1q6xiHSSlm+/irXoKLb/yLQbV348uuSrBV70+Q=", "x86_64-darwin": "sha256-WYaP44PWRGtoG1DIuUJUH4DvuaCuFhlJZ9fPzGsiIfE=" } } From 08ef97b162d75b5824f6de3a8a4b6553a13853bf Mon Sep 17 00:00:00 2001 From: Amir Hasanbasic <43892661+hamir-suspect@users.noreply.github.com> Date: Sat, 17 Jan 2026 00:23:18 +0100 Subject: [PATCH 017/192] fix(opencode): add oauth polling safety margin in copilot device authentication (#8986) --- packages/opencode/src/plugin/copilot.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/plugin/copilot.ts b/packages/opencode/src/plugin/copilot.ts index 17ce9debc7d..776b70fe91a 100644 --- a/packages/opencode/src/plugin/copilot.ts +++ b/packages/opencode/src/plugin/copilot.ts @@ -3,6 +3,9 @@ import { Installation } from "@/installation" import { iife } from "@/util/iife" const CLIENT_ID = "Ov23li8tweQw6odWQebz" +// Add a small safety buffer when polling to avoid hitting the server +// slightly too early due to clock skew / timer drift. +const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000 // 3 seconds function normalizeDomain(url: string) { return url.replace(/^https?:\/\//, "").replace(/\/$/, "") @@ -204,6 +207,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { const data = (await response.json()) as { access_token?: string error?: string + interval?: number } if (data.access_token) { @@ -230,13 +234,29 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { } if (data.error === "authorization_pending") { - await new Promise((resolve) => setTimeout(resolve, deviceData.interval * 1000)) + await Bun.sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS) + continue + } + + if (data.error === "slow_down") { + // Based on the RFC spec, we must add 5 seconds to our current polling interval. + // (See https://www.rfc-editor.org/rfc/rfc8628#section-3.5) + let newInterval = (deviceData.interval + 5) * 1000 + + // GitHub OAuth API may return the new interval in seconds in the response. + // We should try to use that if provided with safety margin. + const serverInterval = data.interval + if (serverInterval && typeof serverInterval === 'number' && serverInterval > 0) { + newInterval = serverInterval * 1000 + } + + await Bun.sleep(newInterval + OAUTH_POLLING_SAFETY_MARGIN_MS) continue } if (data.error) return { type: "failed" as const } - await new Promise((resolve) => setTimeout(resolve, deviceData.interval * 1000)) + await Bun.sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS) continue } }, From 80020ade2e0063459859dbca262c72c10cba6f9b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 16 Jan 2026 23:23:57 +0000 Subject: [PATCH 018/192] chore: generate --- packages/opencode/src/plugin/copilot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/plugin/copilot.ts b/packages/opencode/src/plugin/copilot.ts index 776b70fe91a..932b3fd6aff 100644 --- a/packages/opencode/src/plugin/copilot.ts +++ b/packages/opencode/src/plugin/copilot.ts @@ -246,7 +246,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { // GitHub OAuth API may return the new interval in seconds in the response. // We should try to use that if provided with safety margin. const serverInterval = data.interval - if (serverInterval && typeof serverInterval === 'number' && serverInterval > 0) { + if (serverInterval && typeof serverInterval === "number" && serverInterval > 0) { newInterval = serverInterval * 1000 } From e8746ddb1d16b2fb49d80c5ad5ad874c0b82abcf Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 16 Jan 2026 18:48:16 -0500 Subject: [PATCH 019/192] zen: fix opus unicode characters closes #8967 --- .../console/app/src/routes/zen/util/provider/anthropic.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/provider/anthropic.ts b/packages/console/app/src/routes/zen/util/provider/anthropic.ts index 2546ad3ef15..a5f92a29acf 100644 --- a/packages/console/app/src/routes/zen/util/provider/anthropic.ts +++ b/packages/console/app/src/routes/zen/util/provider/anthropic.ts @@ -65,7 +65,6 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => buffer = newBuffer const messages = [] - while (buffer.length >= 4) { // first 4 bytes are the total length (big-endian) const totalLength = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint32(0, false) @@ -121,7 +120,9 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => const parsedDataResult = JSON.parse(data) delete parsedDataResult.p - const bytes = atob(parsedDataResult.bytes) + const binary = atob(parsedDataResult.bytes) + const uint8 = Uint8Array.from(binary, (c) => c.charCodeAt(0)) + const bytes = decoder.decode(uint8) const eventName = JSON.parse(bytes).type messages.push([`event: ${eventName}`, "\n", `data: ${bytes}`, "\n\n"].join("")) } From cad415872e6a71a39fd46468abcc1737e76606b7 Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 16 Jan 2026 23:36:57 +0000 Subject: [PATCH 020/192] fix: recent sessions gutter --- packages/app/src/pages/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 5e291449f46..bc62c70232f 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1381,7 +1381,7 @@ export default function Layout(props: ParentProps) { return ( // @ts-ignore
- +
Recent sessions
From d645e8bbe18a3559d83de4d59c342e3a00ed8822 Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 16 Jan 2026 23:37:36 +0000 Subject: [PATCH 021/192] fix: (desktop) command palette width --- packages/ui/src/components/dialog.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/dialog.css b/packages/ui/src/components/dialog.css index a2f279f203a..5c7cdc233bf 100644 --- a/packages/ui/src/components/dialog.css +++ b/packages/ui/src/components/dialog.css @@ -24,7 +24,7 @@ [data-slot="dialog-container"] { position: relative; z-index: 50; - width: min(calc(100vw - 16px), 480px); + width: min(calc(100vw - 16px), 640px); height: min(calc(100vh - 16px), 512px); display: flex; flex-direction: column; From 1250486ddfef6f2db353af4977cd7f4654aaf72f Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 16 Jan 2026 23:43:41 +0000 Subject: [PATCH 022/192] feat: add Keybind component for displaying keyboard shortcuts --- packages/ui/src/components/keybind.css | 18 ++++++++++++++++++ packages/ui/src/components/keybind.tsx | 20 ++++++++++++++++++++ packages/ui/src/styles/index.css | 1 + 3 files changed, 39 insertions(+) create mode 100644 packages/ui/src/components/keybind.css create mode 100644 packages/ui/src/components/keybind.tsx diff --git a/packages/ui/src/components/keybind.css b/packages/ui/src/components/keybind.css new file mode 100644 index 00000000000..1a9e5dce43e --- /dev/null +++ b/packages/ui/src/components/keybind.css @@ -0,0 +1,18 @@ +[data-component="keybind"] { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + height: 20px; + padding: 0 8px; + border-radius: 2px; + background: var(--surface-base); + box-shadow: var(--shadow-xxs-border); + + /* text-12-medium */ + font-family: var(--font-family-sans); + font-size: 12px; + font-weight: var(--font-weight-medium); + line-height: 1; + color: var(--text-weak); +} diff --git a/packages/ui/src/components/keybind.tsx b/packages/ui/src/components/keybind.tsx new file mode 100644 index 00000000000..a0fa0483fd9 --- /dev/null +++ b/packages/ui/src/components/keybind.tsx @@ -0,0 +1,20 @@ +import type { ComponentProps, ParentProps } from "solid-js" + +export interface KeybindProps extends ParentProps { + class?: string + classList?: ComponentProps<"span">["classList"] +} + +export function Keybind(props: KeybindProps) { + return ( + + {props.children} + + ) +} diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index 8ab4d6ca4d0..b4b0883aeb0 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -24,6 +24,7 @@ @import "../components/icon.css" layer(components); @import "../components/icon-button.css" layer(components); @import "../components/image-preview.css" layer(components); +@import "../components/keybind.css" layer(components); @import "../components/text-field.css" layer(components); @import "../components/inline-input.css" layer(components); @import "../components/list.css" layer(components); From b18fb16e9c649081319d25c22de94bdac8993706 Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 16 Jan 2026 23:43:45 +0000 Subject: [PATCH 023/192] refactor: use Keybind component in titlebar search button --- .../app/src/components/session/session-header.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 4b083771fb8..cf49734b7a5 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -16,6 +16,7 @@ import { Button } from "@opencode-ai/ui/button" 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" export function SessionHeader() { const globalSDK = useGlobalSDK() @@ -64,16 +65,7 @@ export function SessionHeader() {
- - {(keybind) => ( - - {keybind()} - - )} - + {(keybind) => {keybind()}} )} From 54e52896a4eeb40f1e61d80c20c3b01c7dd8ad86 Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 16 Jan 2026 23:46:57 +0000 Subject: [PATCH 024/192] refactor: use Keybind component in search modal list --- packages/app/src/components/dialog-select-file.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 3b80c2687f1..0f37e798963 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -1,6 +1,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { FileIcon } from "@opencode-ai/ui/file-icon" +import { Keybind } from "@opencode-ai/ui/keybind" import { List } from "@opencode-ai/ui/list" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { useParams } from "@solidjs/router" @@ -169,7 +170,7 @@ export function DialogSelectFile() {
- {formatKeybind(item.keybind ?? "")} + {formatKeybind(item.keybind ?? "")}
From 69215d456c545a6923c13cc183fcc8a027be72d7 Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 16 Jan 2026 23:49:34 +0000 Subject: [PATCH 025/192] fix: display arrow keys as symbols in keybind formatting --- packages/app/src/context/command.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 3c640d8e9fa..a93ffc02454 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -104,7 +104,15 @@ export function formatKeybind(config: string): string { if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta") if (kb.key) { - const displayKey = kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1) + const arrows: Record = { + arrowup: "↑", + arrowdown: "↓", + arrowleft: "←", + arrowright: "→", + } + const displayKey = + arrows[kb.key.toLowerCase()] ?? + (kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1)) parts.push(displayKey) } From d1b93616f707a780208b8a5cad2984e8c2861926 Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 16 Jan 2026 23:51:45 +0000 Subject: [PATCH 026/192] fix: increase keybind border-radius in search modal --- packages/app/src/components/dialog-select-file.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 0f37e798963..a8d105f130b 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -170,7 +170,7 @@ export function DialogSelectFile() { - {formatKeybind(item.keybind ?? "")} + {formatKeybind(item.keybind ?? "")} From ab705dacfac1132e25ddf7ff358c4879ff6e9917 Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 16 Jan 2026 23:55:04 +0000 Subject: [PATCH 027/192] fix: add left padding to command items in search modal --- packages/app/src/components/dialog-select-file.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index a8d105f130b..3e177b08888 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -162,7 +162,7 @@ export function DialogSelectFile() { } > -
+
{item.title} From f8f1f46a4fb759466ef477bad10663e678900632 Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 16 Jan 2026 23:56:17 +0000 Subject: [PATCH 028/192] fix: adjust command item left padding in search modal --- packages/app/src/components/dialog-select-file.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 3e177b08888..0adb35fc848 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -162,7 +162,7 @@ export function DialogSelectFile() {
} > -
+
{item.title} From 6f78a71fa7d87faf8820b6f68ceb849c1a371967 Mon Sep 17 00:00:00 2001 From: David Hill Date: Fri, 16 Jan 2026 23:59:42 +0000 Subject: [PATCH 029/192] feat: add hideIcon and class options to List search, customize search modal input --- packages/app/src/components/dialog-select-file.tsx | 2 +- packages/ui/src/components/list.tsx | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 0adb35fc848..1057e83a2da 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -136,7 +136,7 @@ export function DialogSelectFile() { return ( item.id} diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index dc83db53a47..fd618fbdd2c 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -8,6 +8,8 @@ import { TextField } from "./text-field" export interface ListSearchProps { placeholder?: string autofocus?: boolean + hideIcon?: boolean + class?: string } export interface ListProps extends FilteredListProps { @@ -146,9 +148,11 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) return (
-
+
- + + + Date: Sat, 17 Jan 2026 00:00:41 +0000 Subject: [PATCH 030/192] fix: reduce command item left padding in search modal --- packages/app/src/components/dialog-select-file.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 1057e83a2da..621b2fb2196 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -162,7 +162,7 @@ export function DialogSelectFile() {
} > -
+
{item.title} From d23c21023ababec01f2a80285f0f20fef1570d8e Mon Sep 17 00:00:00 2001 From: David Hill Date: Sat, 17 Jan 2026 00:28:44 +0000 Subject: [PATCH 031/192] fix: refine search modal styling and list component --- packages/app/src/components/dialog-select-file.tsx | 6 +++--- packages/ui/src/components/list.css | 11 +++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 621b2fb2196..dee87b5588a 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -134,14 +134,14 @@ export function DialogSelectFile() { }) return ( - + item.id} filterKeys={["title", "description", "category"]} - groupBy={(item) => (grouped() ? item.category : "")} + groupBy={(item) => item.category} onMove={handleMove} onSelect={handleSelect} > diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css index 651f5ef971e..03a77df0587 100644 --- a/packages/ui/src/components/list.css +++ b/packages/ui/src/components/list.css @@ -56,11 +56,14 @@ width: 20px; height: 20px; background-color: transparent; + opacity: 0.5; + transition: opacity 0.15s ease; &:hover:not(:disabled), &:focus:not(:disabled), &:active:not(:disabled) { background-color: transparent; + opacity: 0.7; } &:hover:not(:disabled) [data-slot="icon-svg"] { @@ -125,10 +128,14 @@ display: flex; flex-direction: column; + &:last-child { + padding-bottom: 12px; + } + [data-slot="list-header"] { display: flex; z-index: 10; - padding: 0 12px 8px 8px; + padding: 8px 12px 8px 12px; justify-content: space-between; align-items: center; align-self: stretch; @@ -136,7 +143,7 @@ position: sticky; top: 0; - color: var(--text-base); + color: var(--text-weak); /* text-14-medium */ font-family: var(--font-family-sans); From ef7ef6538e347c8d7052b641e3bcb8a95099f92e Mon Sep 17 00:00:00 2001 From: David Hill Date: Sat, 17 Jan 2026 00:30:27 +0000 Subject: [PATCH 032/192] fix: limit search modal max-height to 480px --- packages/app/src/components/dialog-select-file.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index dee87b5588a..432e531e192 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -134,7 +134,7 @@ export function DialogSelectFile() { }) return ( - + Date: Sat, 17 Jan 2026 00:35:32 +0000 Subject: [PATCH 033/192] fix: remove the secondary text from commands --- packages/app/src/pages/session.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index ca5e73a9be9..aad0b596cb9 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -419,7 +419,6 @@ export default function Page() { { id: "session.new", title: "New session", - description: "Create a new session", category: "Session", keybind: "mod+shift+s", slash: "new", @@ -437,7 +436,7 @@ export default function Page() { { id: "terminal.toggle", title: "Toggle terminal", - description: "Show or hide the terminal", + description: "", category: "View", keybind: "ctrl+`", slash: "terminal", @@ -446,7 +445,7 @@ export default function Page() { { id: "review.toggle", title: "Toggle review", - description: "Show or hide the review panel", + description: "", category: "View", keybind: "mod+shift+r", onSelect: () => view().reviewPanel.toggle(), From e0c6459faa3461585ed7df91e60de85b2fa242c9 Mon Sep 17 00:00:00 2001 From: David Hill Date: Sat, 17 Jan 2026 00:37:56 +0000 Subject: [PATCH 034/192] fix: remove smooth scroll behavior from list component --- packages/ui/src/components/list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index fd618fbdd2c..5be7f95aeec 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -82,7 +82,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) return } const element = scrollRef()?.querySelector(`[data-key="${active()}"]`) - element?.scrollIntoView({ block: "center", behavior: "smooth" }) + element?.scrollIntoView({ block: "center" }) }) createEffect(() => { From 38847e13bb10d3cd951779836826b3b5272ec85b Mon Sep 17 00:00:00 2001 From: David Hill Date: Sat, 17 Jan 2026 00:41:47 +0000 Subject: [PATCH 035/192] fix: truncate long search queries in empty state --- packages/ui/src/components/list.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css index 03a77df0587..ee9be422c97 100644 --- a/packages/ui/src/components/list.css +++ b/packages/ui/src/components/list.css @@ -94,7 +94,7 @@ [data-slot="list-empty-state"] { display: flex; - padding: 32px 0px; + padding: 32px 48px; flex-direction: column; justify-content: center; align-items: center; @@ -106,8 +106,9 @@ justify-content: center; align-items: center; gap: 2px; + max-width: 100%; color: var(--text-weak); - text-align: center; + white-space: nowrap; /* text-14-regular */ font-family: var(--font-family-sans); @@ -120,6 +121,8 @@ [data-slot="list-filter"] { color: var(--text-strong); + overflow: hidden; + text-overflow: ellipsis; } } From 759ce8fb8ef28c6c880d2867a36cc5c8fe44809b Mon Sep 17 00:00:00 2001 From: David Hill Date: Sat, 17 Jan 2026 01:06:53 +0000 Subject: [PATCH 036/192] fix: prevent text clipping on search button descenders --- packages/app/src/components/session/session-header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index cf49734b7a5..7070f0c9337 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -60,7 +60,7 @@ export function SessionHeader() { >
- + Search {name()}
From 06c543e93814d58ab3440fe4b8fbd1eb740ac694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 17 Jan 2026 03:26:08 +0100 Subject: [PATCH 037/192] fix(nix): resolve hash race condition in parallel matrix jobs (#8995) --- .github/workflows/nix-desktop.yml | 4 +- .github/workflows/update-nix-hashes.yml | 202 ++++++++++++++++++++---- nix/scripts/update-hashes.sh | 119 -------------- 3 files changed, 173 insertions(+), 152 deletions(-) delete mode 100755 nix/scripts/update-hashes.sh diff --git a/.github/workflows/nix-desktop.yml b/.github/workflows/nix-desktop.yml index 01cfaed78b4..3d7c4803133 100644 --- a/.github/workflows/nix-desktop.yml +++ b/.github/workflows/nix-desktop.yml @@ -9,6 +9,7 @@ on: - "nix/**" - "packages/app/**" - "packages/desktop/**" + - ".github/workflows/nix-desktop.yml" pull_request: paths: - "flake.nix" @@ -16,6 +17,7 @@ on: - "nix/**" - "packages/app/**" - "packages/desktop/**" + - ".github/workflows/nix-desktop.yml" workflow_dispatch: jobs: @@ -26,7 +28,7 @@ jobs: os: - blacksmith-4vcpu-ubuntu-2404 - blacksmith-4vcpu-ubuntu-2404-arm - - macos-15 + - macos-15-intel - macos-latest runs-on: ${{ matrix.os }} timeout-minutes: 60 diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 19373f748f2..f80a57d25d8 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -10,11 +10,13 @@ on: - "bun.lock" - "package.json" - "packages/*/package.json" + - ".github/workflows/update-nix-hashes.yml" pull_request: paths: - "bun.lock" - "package.json" - "packages/*/package.json" + - ".github/workflows/update-nix-hashes.yml" jobs: update-flake: @@ -25,7 +27,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 @@ -43,9 +45,9 @@ jobs: - name: Update ${{ env.TITLE }} run: | set -euo pipefail - echo "📦 Updating $TITLE..." + echo "Updating $TITLE..." nix flake update - echo "✅ $TITLE updated successfully" + echo "$TITLE updated successfully" - name: Commit ${{ env.TITLE }} changes env: @@ -53,7 +55,7 @@ jobs: run: | set -euo pipefail - echo "🔍 Checking for changes in tracked files..." + echo "Checking for changes in tracked files..." summarize() { local status="$1" @@ -71,29 +73,29 @@ jobs: FILES=(flake.lock flake.nix) STATUS="$(git status --short -- "${FILES[@]}" || true)" if [ -z "$STATUS" ]; then - echo "✅ No changes detected." + echo "No changes detected." summarize "no changes" exit 0 fi - echo "📝 Changes detected:" + echo "Changes detected:" echo "$STATUS" - echo "🔗 Staging files..." + echo "Staging files..." git add "${FILES[@]}" - echo "💾 Committing changes..." + echo "Committing changes..." git commit -m "Update $TITLE" - echo "✅ Changes committed" + echo "Changes committed" BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" - echo "🌳 Pulling latest from branch: $BRANCH" - git pull --rebase origin "$BRANCH" - echo "🚀 Pushing changes to branch: $BRANCH" + echo "Pulling latest from branch: $BRANCH" + git pull --rebase --autostash origin "$BRANCH" + echo "Pushing changes to branch: $BRANCH" git push origin HEAD:"$BRANCH" - echo "✅ Changes pushed successfully" + echo "Changes pushed successfully" summarize "committed $(git rev-parse --short HEAD)" - update-node-modules-hash: + compute-node-modules-hash: needs: update-flake if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository strategy: @@ -111,11 +113,10 @@ jobs: runs-on: ${{ matrix.host }} env: SYSTEM: ${{ matrix.system }} - TITLE: node_modules hash (${{ matrix.system }}) steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 @@ -125,6 +126,104 @@ jobs: - name: Setup Nix uses: nixbuild/nix-quick-install-action@v34 + - name: Compute node_modules hash + run: | + set -euo pipefail + + DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + HASH_FILE="nix/hashes.json" + OUTPUT_FILE="hash-${SYSTEM}.txt" + + export NIX_KEEP_OUTPUTS=1 + export NIX_KEEP_DERIVATIONS=1 + + BUILD_LOG=$(mktemp) + TMP_JSON=$(mktemp) + trap 'rm -f "$BUILD_LOG" "$TMP_JSON"' EXIT + + if [ ! -f "$HASH_FILE" ]; then + mkdir -p "$(dirname "$HASH_FILE")" + echo '{"nodeModules":{}}' > "$HASH_FILE" + fi + + # Set dummy hash to force nix to rebuild and reveal correct hash + jq --arg system "$SYSTEM" --arg value "$DUMMY" \ + '.nodeModules = (.nodeModules // {}) | .nodeModules[$system] = $value' "$HASH_FILE" > "$TMP_JSON" + mv "$TMP_JSON" "$HASH_FILE" + + MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules" + DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")" + + echo "Building node_modules for ${SYSTEM} to discover correct hash..." + echo "Attempting to realize derivation: ${DRV_PATH}" + REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true) + + BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true) + CORRECT_HASH="" + + if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then + echo "Realized node_modules output: $BUILD_PATH" + CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true) + fi + + # Try to extract hash from build log + if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" + fi + + if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" + fi + + # Try to hash from kept failed build directory + if [ -z "$CORRECT_HASH" ]; then + KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1 || true) + if [ -z "$KEPT_DIR" ]; then + KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1 || true) + fi + + if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then + HASH_PATH="$KEPT_DIR" + [ -d "$KEPT_DIR/build" ] && HASH_PATH="$KEPT_DIR/build" + + if [ -d "$HASH_PATH/node_modules" ]; then + CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true) + fi + fi + fi + + if [ -z "$CORRECT_HASH" ]; then + echo "Failed to determine correct node_modules hash for ${SYSTEM}." + cat "$BUILD_LOG" + exit 1 + fi + + echo "$CORRECT_HASH" > "$OUTPUT_FILE" + echo "Hash for ${SYSTEM}: $CORRECT_HASH" + + - name: Upload hash artifact + uses: actions/upload-artifact@v6 + with: + name: hash-${{ matrix.system }} + path: hash-${{ matrix.system }}.txt + retention-days: 1 + + commit-node-modules-hashes: + needs: compute-node-modules-hash + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: blacksmith-4vcpu-ubuntu-2404 + env: + TITLE: node_modules hashes + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + ref: ${{ github.head_ref || github.ref_name }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + - name: Configure git run: | git config --global user.email "action@github.com" @@ -135,14 +234,57 @@ jobs: TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} run: | BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" - git pull origin "$BRANCH" + git pull --rebase --autostash origin "$BRANCH" - - name: Update ${{ env.TITLE }} + - name: Download all hash artifacts + uses: actions/download-artifact@v7 + with: + pattern: hash-* + merge-multiple: true + + - name: Merge hashes into hashes.json run: | set -euo pipefail - echo "🔄 Updating $TITLE..." - nix/scripts/update-hashes.sh - echo "✅ $TITLE updated successfully" + + HASH_FILE="nix/hashes.json" + + if [ ! -f "$HASH_FILE" ]; then + mkdir -p "$(dirname "$HASH_FILE")" + echo '{"nodeModules":{}}' > "$HASH_FILE" + fi + + echo "Merging hashes into ${HASH_FILE}..." + + shopt -s nullglob + files=(hash-*.txt) + if [ ${#files[@]} -eq 0 ]; then + echo "No hash files found, nothing to update" + exit 0 + fi + + EXPECTED_SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin" + for sys in $EXPECTED_SYSTEMS; do + if [ ! -f "hash-${sys}.txt" ]; then + echo "WARNING: Missing hash file for $sys" + fi + done + + for f in "${files[@]}"; do + system="${f#hash-}" + system="${system%.txt}" + hash=$(cat "$f") + if [ -z "$hash" ]; then + echo "WARNING: Empty hash for $system, skipping" + continue + fi + echo " $system: $hash" + jq --arg sys "$system" --arg h "$hash" \ + '.nodeModules = (.nodeModules // {}) | .nodeModules[$sys] = $h' "$HASH_FILE" > "${HASH_FILE}.tmp" + mv "${HASH_FILE}.tmp" "$HASH_FILE" + done + + echo "All hashes merged:" + cat "$HASH_FILE" - name: Commit ${{ env.TITLE }} changes env: @@ -150,7 +292,8 @@ jobs: run: | set -euo pipefail - echo "🔍 Checking for changes in tracked files..." + HASH_FILE="nix/hashes.json" + echo "Checking for changes..." summarize() { local status="$1" @@ -166,27 +309,22 @@ jobs: echo "" >> "$GITHUB_STEP_SUMMARY" } - FILES=(nix/hashes.json) + FILES=("$HASH_FILE") STATUS="$(git status --short -- "${FILES[@]}" || true)" if [ -z "$STATUS" ]; then - echo "✅ No changes detected." + echo "No changes detected." summarize "no changes" exit 0 fi - echo "📝 Changes detected:" + echo "Changes detected:" echo "$STATUS" - echo "🔗 Staging files..." git add "${FILES[@]}" - echo "💾 Committing changes..." git commit -m "Update $TITLE" - echo "✅ Changes committed" BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" - echo "🌳 Pulling latest from branch: $BRANCH" - git pull --rebase origin "$BRANCH" - echo "🚀 Pushing changes to branch: $BRANCH" + git pull --rebase --autostash origin "$BRANCH" git push origin HEAD:"$BRANCH" - echo "✅ Changes pushed successfully" + echo "Changes pushed successfully" summarize "committed $(git rev-parse --short HEAD)" diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh deleted file mode 100755 index 1e294fe4fb4..00000000000 --- a/nix/scripts/update-hashes.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" -SYSTEM=${SYSTEM:-x86_64-linux} -DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json} -HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE} - -if [ ! -f "$HASH_FILE" ]; then - cat >"$HASH_FILE" </dev/null 2>&1; then - if ! git ls-files --error-unmatch "$HASH_FILE" >/dev/null 2>&1; then - git add -N "$HASH_FILE" >/dev/null 2>&1 || true - fi -fi - -export DUMMY -export NIX_KEEP_OUTPUTS=1 -export NIX_KEEP_DERIVATIONS=1 - -cleanup() { - rm -f "${JSON_OUTPUT:-}" "${BUILD_LOG:-}" "${TMP_EXPR:-}" -} - -trap cleanup EXIT - -write_node_modules_hash() { - local value="$1" - local system="${2:-$SYSTEM}" - local temp - temp=$(mktemp) - - if jq -e '.nodeModules | type == "object"' "$HASH_FILE" >/dev/null 2>&1; then - jq --arg system "$system" --arg value "$value" '.nodeModules[$system] = $value' "$HASH_FILE" >"$temp" - else - jq --arg system "$system" --arg value "$value" '.nodeModules = {($system): $value}' "$HASH_FILE" >"$temp" - fi - - mv "$temp" "$HASH_FILE" -} - -TARGET="packages.${SYSTEM}.default" -MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules" -CORRECT_HASH="" - -DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")" - -echo "Setting dummy node_modules outputHash for ${SYSTEM}..." -write_node_modules_hash "$DUMMY" - -BUILD_LOG=$(mktemp) -JSON_OUTPUT=$(mktemp) - -echo "Building node_modules for ${SYSTEM} to discover correct outputHash..." -echo "Attempting to realize derivation: ${DRV_PATH}" -REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true) - -BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true) -if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then - echo "Realized node_modules output: $BUILD_PATH" - CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true) -fi - -if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" - - if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" - fi - - if [ -z "$CORRECT_HASH" ]; then - echo "Searching for kept failed build directory..." - KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1) - - if [ -z "$KEPT_DIR" ]; then - KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1) - fi - - if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then - echo "Found kept build directory: $KEPT_DIR" - if [ -d "$KEPT_DIR/build" ]; then - HASH_PATH="$KEPT_DIR/build" - else - HASH_PATH="$KEPT_DIR" - fi - - echo "Attempting to hash: $HASH_PATH" - ls -la "$HASH_PATH" || true - - if [ -d "$HASH_PATH/node_modules" ]; then - CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true) - echo "Computed hash from kept build: $CORRECT_HASH" - fi - fi - fi -fi - -if [ -z "$CORRECT_HASH" ]; then - echo "Failed to determine correct node_modules hash for ${SYSTEM}." - echo "Build log:" - cat "$BUILD_LOG" - exit 1 -fi - -write_node_modules_hash "$CORRECT_HASH" - -jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null - -echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH" - -rm -f "$BUILD_LOG" -unset BUILD_LOG From e8357a87b0687da5eff66a3cead3a49ade7e396a Mon Sep 17 00:00:00 2001 From: Github Action Date: Sat, 17 Jan 2026 02:51:01 +0000 Subject: [PATCH 038/192] Update node_modules hashes --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index 41fb0260278..16a1c1f398b 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,7 +1,7 @@ { "nodeModules": { "x86_64-linux": "sha256-4zchRpxzvHnPMcwumgL9yaX0deIXS5IGPp131eYsSvg=", - "aarch64-linux": "sha256-0Im52dLeZ0ZtaPJr/U4m7+IRtOfziHNJI/Bu/V6cPho=", + "aarch64-linux": "sha256-3/BSRsl5pI0Iz3qAFZxIkOehFLZ2Ox9UsbdDHYzqlVg=", "aarch64-darwin": "sha256-86d/G1q6xiHSSlm+/irXoKLb/yLQbV348uuSrBV70+Q=", "x86_64-darwin": "sha256-WYaP44PWRGtoG1DIuUJUH4DvuaCuFhlJZ9fPzGsiIfE=" } From 7c3eeeb0fa2ce8516a776e7b375d333b8120b698 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 16 Jan 2026 20:09:36 -0800 Subject: [PATCH 039/192] fix: gpt id stuff fr fr this time :/ (#9006) --- packages/opencode/src/provider/provider.ts | 18 ++++ packages/opencode/src/provider/transform.ts | 102 ++++++++---------- .../opencode/test/provider/transform.test.ts | 46 ++++---- 3 files changed, 87 insertions(+), 79 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 9e2dd0ba0b5..bcb115edf41 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -999,6 +999,24 @@ export namespace Provider { opts.signal = combined } + // Strip openai itemId metadata following what codex does + // Codex uses #[serde(skip_serializing)] on id fields for all item types: + // Message, Reasoning, FunctionCall, LocalShellCall, CustomToolCall, WebSearchCall + // IDs are only re-attached for Azure with store=true + if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") { + const body = JSON.parse(opts.body as string) + const isAzure = model.providerID.includes("azure") + const keepIds = isAzure && body.store === true + if (!keepIds && Array.isArray(body.input)) { + for (const item of body.input) { + if ("id" in item) { + delete item.id + } + } + opts.body = JSON.stringify(body) + } + } + return fetchFn(input, { ...opts, // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682 diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 023b00dd304..71295ba2b67 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -16,34 +16,33 @@ function mimeToModality(mime: string): Modality | undefined { } export namespace ProviderTransform { + // Maps npm package to the key the AI SDK expects for providerOptions + function sdkKey(npm: string): string | undefined { + switch (npm) { + case "@ai-sdk/github-copilot": + case "@ai-sdk/openai": + case "@ai-sdk/azure": + return "openai" + case "@ai-sdk/amazon-bedrock": + return "bedrock" + case "@ai-sdk/anthropic": + return "anthropic" + case "@ai-sdk/google-vertex": + case "@ai-sdk/google": + return "google" + case "@ai-sdk/gateway": + return "gateway" + case "@openrouter/ai-sdk-provider": + return "openrouter" + } + return undefined + } + function normalizeMessages( msgs: ModelMessage[], model: Provider.Model, options: Record, ): ModelMessage[] { - // Strip openai itemId metadata following what codex does - if (model.api.npm === "@ai-sdk/openai" || options.store === false) { - msgs = msgs.map((msg) => { - if (msg.providerOptions) { - for (const options of Object.values(msg.providerOptions)) { - delete options["itemId"] - } - } - if (!Array.isArray(msg.content)) { - return msg - } - const content = msg.content.map((part) => { - if (part.providerOptions) { - for (const options of Object.values(part.providerOptions)) { - delete options["itemId"] - } - } - return part - }) - return { ...msg, content } as typeof msg - }) - } - // Anthropic rejects messages with empty content - filter out empty string messages // and remove empty text/reasoning parts from array content if (model.api.npm === "@ai-sdk/anthropic") { @@ -257,6 +256,28 @@ export namespace ProviderTransform { msgs = applyCaching(msgs, model.providerID) } + // Remap providerOptions keys from stored providerID to expected SDK key + const key = sdkKey(model.api.npm) + if (key && key !== model.providerID) { + const remap = (opts: Record | undefined) => { + if (!opts) return opts + if (!(model.providerID in opts)) return opts + const result = { ...opts } + result[key] = result[model.providerID] + delete result[model.providerID] + return result + } + + msgs = msgs.map((msg) => { + if (!Array.isArray(msg.content)) return { ...msg, providerOptions: remap(msg.providerOptions) } + return { + ...msg, + providerOptions: remap(msg.providerOptions), + content: msg.content.map((part) => ({ ...part, providerOptions: remap(part.providerOptions) })), + } as typeof msg + }) + } + return msgs } @@ -574,39 +595,8 @@ export namespace ProviderTransform { } export function providerOptions(model: Provider.Model, options: { [x: string]: any }) { - switch (model.api.npm) { - case "@ai-sdk/github-copilot": - case "@ai-sdk/openai": - case "@ai-sdk/azure": - return { - ["openai" as string]: options, - } - case "@ai-sdk/amazon-bedrock": - return { - ["bedrock" as string]: options, - } - case "@ai-sdk/anthropic": - return { - ["anthropic" as string]: options, - } - case "@ai-sdk/google-vertex": - case "@ai-sdk/google": - return { - ["google" as string]: options, - } - case "@ai-sdk/gateway": - return { - ["gateway" as string]: options, - } - case "@openrouter/ai-sdk-provider": - return { - ["openrouter" as string]: options, - } - default: - return { - [model.providerID]: options, - } - } + const key = sdkKey(model.api.npm) ?? model.providerID + return { [key]: options } } export function maxOutputTokens( diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 33047b5bcb4..0f203b93022 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -649,7 +649,7 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( headers: {}, } as any - test("strips itemId and reasoningEncryptedContent when store=false", () => { + test("preserves itemId and reasoningEncryptedContent when store=false", () => { const msgs = [ { role: "assistant", @@ -680,11 +680,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[] expect(result).toHaveLength(1) - expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined() - expect(result[0].content[1].providerOptions?.openai?.itemId).toBeUndefined() + expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123") + expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456") }) - test("strips itemId and reasoningEncryptedContent when store=false even when not openai", () => { + test("preserves itemId and reasoningEncryptedContent when store=false even when not openai", () => { const zenModel = { ...openaiModel, providerID: "zen", @@ -719,11 +719,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( const result = ProviderTransform.message(msgs, zenModel, { store: false }) as any[] expect(result).toHaveLength(1) - expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined() - expect(result[0].content[1].providerOptions?.openai?.itemId).toBeUndefined() + expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123") + expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456") }) - test("preserves other openai options when stripping itemId", () => { + test("preserves other openai options including itemId", () => { const msgs = [ { role: "assistant", @@ -744,11 +744,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[] - expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined() + expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123") expect(result[0].content[0].providerOptions?.openai?.otherOption).toBe("value") }) - test("strips metadata for openai package even when store is true", () => { + test("preserves metadata for openai package when store is true", () => { const msgs = [ { role: "assistant", @@ -766,13 +766,13 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( }, ] as any[] - // openai package always strips itemId regardless of store value + // openai package preserves itemId regardless of store value const result = ProviderTransform.message(msgs, openaiModel, { store: true }) as any[] - expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined() + expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123") }) - test("strips metadata for non-openai packages when store is false", () => { + test("preserves metadata for non-openai packages when store is false", () => { const anthropicModel = { ...openaiModel, providerID: "anthropic", @@ -799,13 +799,13 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( }, ] as any[] - // store=false triggers stripping even for non-openai packages + // store=false preserves metadata for non-openai packages const result = ProviderTransform.message(msgs, anthropicModel, { store: false }) as any[] - expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined() + expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123") }) - test("strips metadata using providerID key when store is false", () => { + test("preserves metadata using providerID key when store is false", () => { const opencodeModel = { ...openaiModel, providerID: "opencode", @@ -835,11 +835,11 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[] - expect(result[0].content[0].providerOptions?.opencode?.itemId).toBeUndefined() + expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_123") expect(result[0].content[0].providerOptions?.opencode?.otherOption).toBe("value") }) - test("strips itemId across all providerOptions keys", () => { + test("preserves itemId across all providerOptions keys", () => { const opencodeModel = { ...openaiModel, providerID: "opencode", @@ -873,12 +873,12 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[] - expect(result[0].providerOptions?.openai?.itemId).toBeUndefined() - expect(result[0].providerOptions?.opencode?.itemId).toBeUndefined() - expect(result[0].providerOptions?.extra?.itemId).toBeUndefined() - expect(result[0].content[0].providerOptions?.openai?.itemId).toBeUndefined() - expect(result[0].content[0].providerOptions?.opencode?.itemId).toBeUndefined() - expect(result[0].content[0].providerOptions?.extra?.itemId).toBeUndefined() + expect(result[0].providerOptions?.openai?.itemId).toBe("msg_root") + expect(result[0].providerOptions?.opencode?.itemId).toBe("msg_opencode") + expect(result[0].providerOptions?.extra?.itemId).toBe("msg_extra") + expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_openai_part") + expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_opencode_part") + expect(result[0].content[0].providerOptions?.extra?.itemId).toBe("msg_extra_part") }) test("does not strip metadata for non-openai packages when store is not false", () => { From db78a59f03c2b8acbda506b9f3f5d6c92d3755de Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 16 Jan 2026 20:15:59 -0800 Subject: [PATCH 040/192] docs: Add OpenWork to ecosystem (#8741) --- packages/web/src/content/docs/ecosystem.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/src/content/docs/ecosystem.mdx b/packages/web/src/content/docs/ecosystem.mdx index 44a73de69e9..19e84b3ba66 100644 --- a/packages/web/src/content/docs/ecosystem.mdx +++ b/packages/web/src/content/docs/ecosystem.mdx @@ -57,6 +57,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw | [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Vercel AI SDK provider for using OpenCode via @opencode-ai/sdk | | [OpenChamber](https://github.com/btriapitsyn/openchamber) | Web / Desktop App and VS Code Extension for OpenCode | | [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Obsidian plugin that embedds OpenCode in Obsidian's UI | +| [OpenWork](https://github.com/different-ai/openwork) | An open-source alternative to Claude Cowork, powered by OpenCode| --- From 5d613a038d1e562197f1a22d44d4a5c51477b1cd Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 17 Jan 2026 04:16:35 +0000 Subject: [PATCH 041/192] chore: generate --- packages/web/src/content/docs/ecosystem.mdx | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/web/src/content/docs/ecosystem.mdx b/packages/web/src/content/docs/ecosystem.mdx index 19e84b3ba66..ce3e3deb86c 100644 --- a/packages/web/src/content/docs/ecosystem.mdx +++ b/packages/web/src/content/docs/ecosystem.mdx @@ -47,17 +47,17 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw ## Projects -| Name | Description | -| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | -| [kimaki](https://github.com/remorses/kimaki) | Discord bot to control OpenCode sessions, built on the SDK | -| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Neovim plugin for editor-aware prompts, built on the API | -| [portal](https://github.com/hosenur/portal) | Mobile-first web UI for OpenCode over Tailscale/VPN | -| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Template for building OpenCode plugins | -| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Neovim frontend for opencode - a terminal-based AI coding agent | -| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Vercel AI SDK provider for using OpenCode via @opencode-ai/sdk | -| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Web / Desktop App and VS Code Extension for OpenCode | -| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Obsidian plugin that embedds OpenCode in Obsidian's UI | -| [OpenWork](https://github.com/different-ai/openwork) | An open-source alternative to Claude Cowork, powered by OpenCode| +| Name | Description | +| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | +| [kimaki](https://github.com/remorses/kimaki) | Discord bot to control OpenCode sessions, built on the SDK | +| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Neovim plugin for editor-aware prompts, built on the API | +| [portal](https://github.com/hosenur/portal) | Mobile-first web UI for OpenCode over Tailscale/VPN | +| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Template for building OpenCode plugins | +| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Neovim frontend for opencode - a terminal-based AI coding agent | +| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | Vercel AI SDK provider for using OpenCode via @opencode-ai/sdk | +| [OpenChamber](https://github.com/btriapitsyn/openchamber) | Web / Desktop App and VS Code Extension for OpenCode | +| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Obsidian plugin that embedds OpenCode in Obsidian's UI | +| [OpenWork](https://github.com/different-ai/openwork) | An open-source alternative to Claude Cowork, powered by OpenCode | --- From 33290c54cdd68081236ede205133d255ed639455 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 16 Jan 2026 21:00:25 -0600 Subject: [PATCH 042/192] Revert "feat(mcp): add OAuth redirect URI configuration for MCP servers (#7379)" This reverts commit 40b275d7e6759d2209bbaec7d4d13f729198bda3. --- packages/opencode/src/cli/cmd/mcp.ts | 6 -- packages/opencode/src/config/config.ts | 4 - packages/opencode/src/mcp/index.ts | 27 +++---- packages/opencode/src/mcp/oauth-callback.ts | 34 +++------ packages/opencode/src/mcp/oauth-provider.ts | 24 ------ .../opencode/test/mcp/oauth-callback.test.ts | 75 ------------------- 6 files changed, 19 insertions(+), 151 deletions(-) delete mode 100644 packages/opencode/test/mcp/oauth-callback.test.ts diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index fedad92856f..95719215e32 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -6,7 +6,6 @@ import * as prompts from "@clack/prompts" import { UI } from "../ui" import { MCP } from "../../mcp" import { McpAuth } from "../../mcp/auth" -import { McpOAuthCallback } from "../../mcp/oauth-callback" import { McpOAuthProvider } from "../../mcp/oauth-provider" import { Config } from "../../config/config" import { Instance } from "../../project/instance" @@ -683,10 +682,6 @@ export const McpDebugCommand = cmd({ // Try to discover OAuth metadata const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined - - // Start callback server - await McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri) - const authProvider = new McpOAuthProvider( serverName, serverConfig.url, @@ -694,7 +689,6 @@ export const McpDebugCommand = cmd({ clientId: oauthConfig?.clientId, clientSecret: oauthConfig?.clientSecret, scope: oauthConfig?.scope, - redirectUri: oauthConfig?.redirectUri, }, { onRedirect: async () => {}, diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 355b3ba0017..1574c644d32 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -435,10 +435,6 @@ export namespace Config { .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."), clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"), scope: z.string().optional().describe("OAuth scopes to request during authorization"), - redirectUri: z - .string() - .optional() - .describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."), }) .strict() .meta({ diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 7b9a8c2076a..66843aedc11 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -308,8 +308,6 @@ export namespace MCP { let authProvider: McpOAuthProvider | undefined if (!oauthDisabled) { - await McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri) - authProvider = new McpOAuthProvider( key, mcp.url, @@ -317,7 +315,6 @@ export namespace MCP { clientId: oauthConfig?.clientId, clientSecret: oauthConfig?.clientSecret, scope: oauthConfig?.scope, - redirectUri: oauthConfig?.redirectUri, }, { onRedirect: async (url) => { @@ -347,7 +344,6 @@ export namespace MCP { let lastError: Error | undefined const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT - for (const { name, transport } of transports) { try { const client = new Client({ @@ -574,8 +570,7 @@ export namespace MCP { for (const [clientName, client] of Object.entries(clientsSnapshot)) { // Only include tools from connected MCPs (skip disabled ones) - const clientStatus = s.status[clientName]?.status - if (clientStatus !== "connected") { + if (s.status[clientName]?.status !== "connected") { continue } @@ -725,10 +720,8 @@ export namespace MCP { throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`) } - // OAuth config is optional - if not provided, we'll use auto-discovery - const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined - - await McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri) + // Start the callback server + await McpOAuthCallback.ensureRunning() // Generate and store a cryptographically secure state parameter BEFORE creating the provider // The SDK will call provider.state() to read this value @@ -738,6 +731,8 @@ export namespace MCP { await McpAuth.updateOAuthState(mcpName, oauthState) // Create a new auth provider for this flow + // OAuth config is optional - if not provided, we'll use auto-discovery + const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined let capturedUrl: URL | undefined const authProvider = new McpOAuthProvider( mcpName, @@ -746,7 +741,6 @@ export namespace MCP { clientId: oauthConfig?.clientId, clientSecret: oauthConfig?.clientSecret, scope: oauthConfig?.scope, - redirectUri: oauthConfig?.redirectUri, }, { onRedirect: async (url) => { @@ -775,7 +769,6 @@ export namespace MCP { pendingOAuthTransports.set(mcpName, transport) return { authorizationUrl: capturedUrl.toString() } } - throw error } } @@ -785,9 +778,9 @@ export namespace MCP { * Opens the browser and waits for callback. */ export async function authenticate(mcpName: string): Promise { - const result = await startAuth(mcpName) + const { authorizationUrl } = await startAuth(mcpName) - if (!result.authorizationUrl) { + if (!authorizationUrl) { // Already authenticated const s = await state() return s.status[mcpName] ?? { status: "connected" } @@ -801,9 +794,9 @@ export namespace MCP { // The SDK has already added the state parameter to the authorization URL // We just need to open the browser - log.info("opening browser for oauth", { mcpName, url: result.authorizationUrl, state: oauthState }) + log.info("opening browser for oauth", { mcpName, url: authorizationUrl, state: oauthState }) try { - const subprocess = await open(result.authorizationUrl) + const subprocess = await open(authorizationUrl) // The open package spawns a detached process and returns immediately. // We need to listen for errors which fire asynchronously: // - "error" event: command not found (ENOENT) @@ -826,7 +819,7 @@ export namespace MCP { // Browser opening failed (e.g., in remote/headless sessions like SSH, devcontainers) // Emit event so CLI can display the URL for manual opening log.warn("failed to open browser, user must open URL manually", { mcpName, error }) - Bus.publish(BrowserOpenFailed, { mcpName, url: result.authorizationUrl }) + Bus.publish(BrowserOpenFailed, { mcpName, url: authorizationUrl }) } // Wait for callback using the OAuth state parameter diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts index a690ab5e336..bb3b56f2e95 100644 --- a/packages/opencode/src/mcp/oauth-callback.ts +++ b/packages/opencode/src/mcp/oauth-callback.ts @@ -1,12 +1,8 @@ import { Log } from "../util/log" -import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider" +import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider" const log = Log.create({ service: "mcp.oauth-callback" }) -// Current callback server configuration (may differ from defaults if custom redirectUri is used) -let currentPort = OAUTH_CALLBACK_PORT -let currentPath = OAUTH_CALLBACK_PATH - const HTML_SUCCESS = ` @@ -60,33 +56,21 @@ export namespace McpOAuthCallback { const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes - export async function ensureRunning(redirectUri?: string): Promise { - // Parse the redirect URI to get port and path (uses defaults if not provided) - const { port, path } = parseRedirectUri(redirectUri) - - // If server is running on a different port/path, stop it first - if (server && (currentPort !== port || currentPath !== path)) { - log.info("stopping oauth callback server to reconfigure", { oldPort: currentPort, newPort: port }) - await stop() - } - + export async function ensureRunning(): Promise { if (server) return - const running = await isPortInUse(port) + const running = await isPortInUse() if (running) { - log.info("oauth callback server already running on another instance", { port }) + log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT }) return } - currentPort = port - currentPath = path - server = Bun.serve({ - port: currentPort, + port: OAUTH_CALLBACK_PORT, fetch(req) { const url = new URL(req.url) - if (url.pathname !== currentPath) { + if (url.pathname !== OAUTH_CALLBACK_PATH) { return new Response("Not found", { status: 404 }) } @@ -149,7 +133,7 @@ export namespace McpOAuthCallback { }, }) - log.info("oauth callback server started", { port: currentPort, path: currentPath }) + log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT }) } export function waitForCallback(oauthState: string): Promise { @@ -174,11 +158,11 @@ export namespace McpOAuthCallback { } } - export async function isPortInUse(port: number = OAUTH_CALLBACK_PORT): Promise { + export async function isPortInUse(): Promise { return new Promise((resolve) => { Bun.connect({ hostname: "127.0.0.1", - port, + port: OAUTH_CALLBACK_PORT, socket: { open(socket) { socket.end() diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts index 82bad60da33..35ead25e8be 100644 --- a/packages/opencode/src/mcp/oauth-provider.ts +++ b/packages/opencode/src/mcp/oauth-provider.ts @@ -17,7 +17,6 @@ export interface McpOAuthConfig { clientId?: string clientSecret?: string scope?: string - redirectUri?: string } export interface McpOAuthCallbacks { @@ -33,10 +32,6 @@ export class McpOAuthProvider implements OAuthClientProvider { ) {} get redirectUrl(): string { - // Use configured redirectUri if provided, otherwise use OpenCode defaults - if (this.config.redirectUri) { - return this.config.redirectUri - } return `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}` } @@ -157,22 +152,3 @@ export class McpOAuthProvider implements OAuthClientProvider { } export { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } - -/** - * Parse a redirect URI to extract port and path for the callback server. - * Returns defaults if the URI can't be parsed. - */ -export function parseRedirectUri(redirectUri?: string): { port: number; path: string } { - if (!redirectUri) { - return { port: OAUTH_CALLBACK_PORT, path: OAUTH_CALLBACK_PATH } - } - - try { - const url = new URL(redirectUri) - const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80 - const path = url.pathname || OAUTH_CALLBACK_PATH - return { port, path } - } catch { - return { port: OAUTH_CALLBACK_PORT, path: OAUTH_CALLBACK_PATH } - } -} diff --git a/packages/opencode/test/mcp/oauth-callback.test.ts b/packages/opencode/test/mcp/oauth-callback.test.ts deleted file mode 100644 index aa23f4dfb5d..00000000000 --- a/packages/opencode/test/mcp/oauth-callback.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { test, expect, describe, afterEach } from "bun:test" -import { McpOAuthCallback } from "../../src/mcp/oauth-callback" -import { parseRedirectUri } from "../../src/mcp/oauth-provider" - -describe("McpOAuthCallback.ensureRunning", () => { - afterEach(async () => { - await McpOAuthCallback.stop() - }) - - test("starts server with default config when no redirectUri provided", async () => { - await McpOAuthCallback.ensureRunning() - expect(McpOAuthCallback.isRunning()).toBe(true) - }) - - test("starts server with custom redirectUri", async () => { - await McpOAuthCallback.ensureRunning("http://127.0.0.1:18000/custom/callback") - expect(McpOAuthCallback.isRunning()).toBe(true) - }) - - test("is idempotent when called with same redirectUri", async () => { - await McpOAuthCallback.ensureRunning("http://127.0.0.1:18001/callback") - await McpOAuthCallback.ensureRunning("http://127.0.0.1:18001/callback") - expect(McpOAuthCallback.isRunning()).toBe(true) - }) - - test("restarts server when redirectUri changes", async () => { - await McpOAuthCallback.ensureRunning("http://127.0.0.1:18002/path1") - expect(McpOAuthCallback.isRunning()).toBe(true) - - await McpOAuthCallback.ensureRunning("http://127.0.0.1:18003/path2") - expect(McpOAuthCallback.isRunning()).toBe(true) - }) - - test("isRunning returns false when not started", async () => { - expect(McpOAuthCallback.isRunning()).toBe(false) - }) - - test("isRunning returns false after stop", async () => { - await McpOAuthCallback.ensureRunning() - await McpOAuthCallback.stop() - expect(McpOAuthCallback.isRunning()).toBe(false) - }) -}) - -describe("parseRedirectUri", () => { - test("returns defaults when no URI provided", () => { - const result = parseRedirectUri() - expect(result.port).toBe(19876) - expect(result.path).toBe("/mcp/oauth/callback") - }) - - test("parses port and path from URI", () => { - const result = parseRedirectUri("http://127.0.0.1:8080/oauth/callback") - expect(result.port).toBe(8080) - expect(result.path).toBe("/oauth/callback") - }) - - test("defaults to port 80 for http without explicit port", () => { - const result = parseRedirectUri("http://127.0.0.1/callback") - expect(result.port).toBe(80) - expect(result.path).toBe("/callback") - }) - - test("defaults to port 443 for https without explicit port", () => { - const result = parseRedirectUri("https://127.0.0.1/callback") - expect(result.port).toBe(443) - expect(result.path).toBe("/callback") - }) - - test("returns defaults for invalid URI", () => { - const result = parseRedirectUri("not-a-valid-url") - expect(result.port).toBe(19876) - expect(result.path).toBe("/mcp/oauth/callback") - }) -}) From 85ab9798c65dfb36b0f47de3b1fd786e7751f761 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 17 Jan 2026 04:18:35 +0000 Subject: [PATCH 043/192] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 4 ---- packages/sdk/openapi.json | 4 ---- 2 files changed, 8 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 32321a7dfd8..e47c4f5f7f1 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1530,10 +1530,6 @@ export type McpOAuthConfig = { * OAuth scopes to request during authorization */ scope?: string - /** - * OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback). - */ - redirectUri?: string } export type McpRemoteConfig = { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 904f3eeaefa..0dc174c1b0a 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -9122,10 +9122,6 @@ "scope": { "description": "OAuth scopes to request during authorization", "type": "string" - }, - "redirectUri": { - "description": "OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback).", - "type": "string" } }, "additionalProperties": false From ea13b6e8aa219f0c75eb13a6a58271d75c37521e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 17 Jan 2026 00:35:49 -0600 Subject: [PATCH 044/192] test: add azure test case --- packages/opencode/src/provider/transform.ts | 2 +- .../opencode/test/provider/transform.test.ts | 82 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 71295ba2b67..79892db4cca 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -258,7 +258,7 @@ export namespace ProviderTransform { // Remap providerOptions keys from stored providerID to expected SDK key const key = sdkKey(model.api.npm) - if (key && key !== model.providerID) { + if (key && key !== model.providerID && model.api.npm !== "@ai-sdk/azure") { const remap = (opts: Record | undefined) => { if (!opts) return opts if (!(model.providerID in opts)) return opts diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 0f203b93022..dcf16c65cbd 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -914,6 +914,88 @@ describe("ProviderTransform.message - strip openai metadata when store=false", ( }) }) +describe("ProviderTransform.message - providerOptions key remapping", () => { + const createModel = (providerID: string, npm: string) => + ({ + id: `${providerID}/test-model`, + providerID, + api: { + id: "test-model", + url: "https://api.test.com", + npm, + }, + name: "Test Model", + capabilities: { + temperature: true, + reasoning: false, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } }, + limit: { context: 128000, output: 8192 }, + status: "active", + options: {}, + headers: {}, + }) as any + + test("azure keeps 'azure' key and does not remap to 'openai'", () => { + const model = createModel("azure", "@ai-sdk/azure") + const msgs = [ + { + role: "user", + content: "Hello", + providerOptions: { + azure: { someOption: "value" }, + }, + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) + + expect(result[0].providerOptions?.azure).toEqual({ someOption: "value" }) + expect(result[0].providerOptions?.openai).toBeUndefined() + }) + + test("openai with github-copilot npm remaps providerID to 'openai'", () => { + const model = createModel("github-copilot", "@ai-sdk/github-copilot") + const msgs = [ + { + role: "user", + content: "Hello", + providerOptions: { + "github-copilot": { someOption: "value" }, + }, + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) + + expect(result[0].providerOptions?.openai).toEqual({ someOption: "value" }) + expect(result[0].providerOptions?.["github-copilot"]).toBeUndefined() + }) + + test("bedrock remaps providerID to 'bedrock' key", () => { + const model = createModel("my-bedrock", "@ai-sdk/amazon-bedrock") + const msgs = [ + { + role: "user", + content: "Hello", + providerOptions: { + "my-bedrock": { someOption: "value" }, + }, + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) + + expect(result[0].providerOptions?.bedrock).toEqual({ someOption: "value" }) + expect(result[0].providerOptions?.["my-bedrock"]).toBeUndefined() + }) +}) + describe("ProviderTransform.variants", () => { const createMockModel = (overrides: Partial = {}): any => ({ id: "test/test-model", From 2729705594b9429ce44cf371dbf7268ac3457d8b Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sat, 17 Jan 2026 05:23:11 -0600 Subject: [PATCH 045/192] fix(app): archive session sometimes flaky --- packages/app/src/context/global-sync.tsx | 2 ++ packages/app/src/pages/layout.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 74641a0a243..96f8c63eab2 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -379,6 +379,8 @@ function createGlobalSync() { }), ) } + if (event.properties.info.parentID) break + setStore("sessionTotal", (value) => Math.max(0, value - 1)) break } if (result.found) { diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index bc62c70232f..eb09b154b9c 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -501,7 +501,7 @@ export default function Layout(props: ParentProps) { const [dirStore] = globalSync.child(dir) const dirSessions = dirStore.session .filter((session) => session.directory === dirStore.path.directory) - .filter((session) => !session.parentID) + .filter((session) => !session.parentID && !session.time?.archived) .toSorted(sortSessions) result.push(...dirSessions) } @@ -510,7 +510,7 @@ export default function Layout(props: ParentProps) { const [projectStore] = globalSync.child(project.worktree) return projectStore.session .filter((session) => session.directory === projectStore.path.directory) - .filter((session) => !session.parentID) + .filter((session) => !session.parentID && !session.time?.archived) .toSorted(sortSessions) }) @@ -1203,7 +1203,7 @@ export default function Layout(props: ParentProps) { const sessions = createMemo(() => workspaceStore.session .filter((session) => session.directory === workspaceStore.path.directory) - .filter((session) => !session.parentID) + .filter((session) => !session.parentID && !session.time?.archived) .toSorted(sortSessions), ) const local = createMemo(() => props.directory === props.project.worktree) @@ -1349,7 +1349,7 @@ export default function Layout(props: ParentProps) { const [data] = globalSync.child(directory) return data.session .filter((session) => session.directory === data.path.directory) - .filter((session) => !session.parentID) + .filter((session) => !session.parentID && !session.time?.archived) .toSorted(sortSessions) .slice(0, 2) } @@ -1358,7 +1358,7 @@ export default function Layout(props: ParentProps) { const [data] = globalSync.child(props.project.worktree) return data.session .filter((session) => session.directory === data.path.directory) - .filter((session) => !session.parentID) + .filter((session) => !session.parentID && !session.time?.archived) .toSorted(sortSessions) .slice(0, 2) } @@ -1445,7 +1445,7 @@ export default function Layout(props: ParentProps) { const sessions = createMemo(() => workspaceStore.session .filter((session) => session.directory === workspaceStore.path.directory) - .filter((session) => !session.parentID) + .filter((session) => !session.parentID && !session.time?.archived) .toSorted(sortSessions), ) const loading = createMemo(() => workspaceStore.status !== "complete" && sessions().length === 0) From c4e4f2a0586df665988cd4afcecb810df4995627 Mon Sep 17 00:00:00 2001 From: Eric Guo Date: Sat, 17 Jan 2026 19:45:31 +0800 Subject: [PATCH 046/192] fix(desktop): Added a Windows-only guard that makes window.getComputedStyle fall back to document.documentElement (#9054) --- packages/desktop/src/index.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 7a46ba8cde0..8398f457766 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -26,6 +26,18 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { ) } +const isWindows = ostype() === "windows" +if (isWindows) { + const originalGetComputedStyle = window.getComputedStyle + window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => { + if (!(elt instanceof Element)) { + // WebView2 can call into Floating UI with non-elements; fall back to a safe element. + return originalGetComputedStyle(document.documentElement, pseudoElt ?? undefined) + } + return originalGetComputedStyle(elt, pseudoElt ?? undefined) + }) as typeof window.getComputedStyle +} + let update: Update | null = null const createPlatform = (password: Accessor): Platform => ({ From 7030f49a7485e7b8f2f553351019b778dca64af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Ace=C3=B1a?= <40375118+j0nl1@users.noreply.github.com> Date: Sat, 17 Jan 2026 12:47:19 +0100 Subject: [PATCH 047/192] fix: mdns discover hostname (#9039) --- packages/opencode/src/cli/cmd/web.ts | 2 +- packages/opencode/src/server/mdns.ts | 4 +++- packages/opencode/src/server/server.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 2c207ecc2f2..d799b7cc25e 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -60,7 +60,7 @@ export const WebCommand = cmd({ } if (opts.mdns) { - UI.println(UI.Style.TEXT_INFO_BOLD + " mDNS: ", UI.Style.TEXT_NORMAL, "opencode.local") + UI.println(UI.Style.TEXT_INFO_BOLD + " mDNS: ", UI.Style.TEXT_NORMAL, `opencode.local:${server.port}`) } // Open localhost in browser diff --git a/packages/opencode/src/server/mdns.ts b/packages/opencode/src/server/mdns.ts index 8bddb910503..953269de444 100644 --- a/packages/opencode/src/server/mdns.ts +++ b/packages/opencode/src/server/mdns.ts @@ -7,15 +7,17 @@ export namespace MDNS { let bonjour: Bonjour | undefined let currentPort: number | undefined - export function publish(port: number, name = "opencode") { + export function publish(port: number) { if (currentPort === port) return if (bonjour) unpublish() try { + const name = `opencode-${port}` bonjour = new Bonjour() const service = bonjour.publish({ name, type: "http", + host: "opencode.local", port, txt: { path: "/" }, }) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index f0c64b49f81..28dec7f4043 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -562,7 +562,7 @@ export namespace Server { opts.hostname !== "localhost" && opts.hostname !== "::1" if (shouldPublishMDNS) { - MDNS.publish(server.port!, `opencode-${server.port!}`) + MDNS.publish(server.port!) } else if (opts.mdns) { log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") } From d37724649182ef26fd158cd4d36c9e6f97a6ea5c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 17 Jan 2026 11:47:55 +0000 Subject: [PATCH 048/192] chore: generate --- packages/opencode/src/cli/cmd/web.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index d799b7cc25e..5fa2bb42640 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -60,7 +60,11 @@ export const WebCommand = cmd({ } if (opts.mdns) { - UI.println(UI.Style.TEXT_INFO_BOLD + " mDNS: ", UI.Style.TEXT_NORMAL, `opencode.local:${server.port}`) + UI.println( + UI.Style.TEXT_INFO_BOLD + " mDNS: ", + UI.Style.TEXT_NORMAL, + `opencode.local:${server.port}`, + ) } // Open localhost in browser From 07dc8d8ce48300e42c4cd5026fac609be1b2aaca Mon Sep 17 00:00:00 2001 From: Slone <50995948+Slone123c@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:48:38 +0800 Subject: [PATCH 049/192] fix: escape CSS selector keys to handle special characters (#9030) --- packages/ui/src/components/list.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 5be7f95aeec..631b3e33a29 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -69,7 +69,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) if (!props.current) return const key = props.key(props.current) requestAnimationFrame(() => { - const element = scrollRef()?.querySelector(`[data-key="${key}"]`) + const element = scrollRef()?.querySelector(`[data-key="${CSS.escape(key)}"]`) element?.scrollIntoView({ block: "center" }) }) }) @@ -81,7 +81,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) scrollRef()?.scrollTo(0, 0) return } - const element = scrollRef()?.querySelector(`[data-key="${active()}"]`) + const element = scrollRef()?.querySelector(`[data-key="${CSS.escape(active()!)}"]`) element?.scrollIntoView({ block: "center" }) }) From a58d1be8226c749f015047c15c47950735cd9370 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 17 Jan 2026 12:04:18 +0000 Subject: [PATCH 050/192] ignore: update download stats 2026-01-17 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index e09c57e8f41..9a665612b14 100644 --- a/STATS.md +++ b/STATS.md @@ -202,3 +202,4 @@ | 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) | | 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) | | 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) | +| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) | From a813fcb41cb43f463c2fd14dfe9d499c4d28c151 Mon Sep 17 00:00:00 2001 From: Colby Gilbert Date: Sat, 17 Jan 2026 11:04:43 -0800 Subject: [PATCH 051/192] docs: add firmware provider to providers docs (#8993) --- packages/web/src/content/docs/providers.mdx | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index e1d684de00a..6022d174a7d 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -558,6 +558,33 @@ Cloudflare AI Gateway lets you access models from OpenAI, Anthropic, Workers AI, --- +### Firmware + +1. Head over to the [Firmware dashboard](https://app.firmware.ai/signup), create an account, and generate an API key. + +2. Run the `/connect` command and search for **Firmware**. + + ```txt + /connect + ``` + +3. Enter your Firmware API key. + + ```txt + ┌ API key + │ + │ + └ enter + ``` + +4. Run the `/models` command to select a model. + + ```txt + /models + ``` + +--- + ### Fireworks AI 1. Head over to the [Fireworks AI console](https://app.fireworks.ai/), create an account, and click **Create API Key**. From eb968a6651e2af4b806ca8b466a82b11b04c56a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernat=20Peric=C3=A0s?= Date: Sat, 17 Jan 2026 20:07:03 +0100 Subject: [PATCH 052/192] docs(config): explain that `autoupdate` doesn't work when installed with a package manager (#9092) --- packages/web/src/content/docs/config.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 30edbbd2146..eeb41b943c9 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -425,6 +425,7 @@ OpenCode will automatically download any new updates when it starts up. You can ``` If you don't want updates but want to be notified when a new version is available, set `autoupdate` to `"notify"`. +Notice that this only works if it was not installed using a package manager such as Homebrew. --- From 5a199b04cbbc8a582d8130d4d8e37e052094cc50 Mon Sep 17 00:00:00 2001 From: Rahul Mishra Date: Sun, 18 Jan 2026 00:38:11 +0530 Subject: [PATCH 053/192] fix: don't try to open command palette if a dialog is already open (#9116) --- packages/app/src/context/command.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index a93ffc02454..d8dc13e2344 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -1,5 +1,6 @@ import { createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" +import { useDialog } from "@opencode-ai/ui/context/dialog" const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) @@ -122,6 +123,7 @@ export function formatKeybind(config: string): string { export const { use: useCommand, provider: CommandProvider } = createSimpleContext({ name: "Command", init: () => { + const dialog = useDialog() const [registrations, setRegistrations] = createSignal[]>([]) const [suspendCount, setSuspendCount] = createSignal(0) @@ -165,7 +167,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex } const handleKeyDown = (event: KeyboardEvent) => { - if (suspended()) return + if (suspended() || dialog.active) return const paletteKeybinds = parseKeybind("mod+shift+p") if (matchKeybind(paletteKeybinds, event)) { From 58f7da6e9f8fb08a9f42a0d2cc34a18b2475ddea Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 17 Jan 2026 13:09:27 -0600 Subject: [PATCH 054/192] docs: document the plural forms --- packages/web/src/content/docs/agents.mdx | 12 +++++----- packages/web/src/content/docs/commands.mdx | 24 +++++++++---------- packages/web/src/content/docs/config.mdx | 10 +++++--- .../web/src/content/docs/custom-tools.mdx | 16 ++++++------- packages/web/src/content/docs/modes.mdx | 12 +++++----- packages/web/src/content/docs/permissions.mdx | 2 +- packages/web/src/content/docs/plugins.mdx | 24 +++++++++---------- packages/web/src/content/docs/skills.mdx | 10 ++++---- 8 files changed, 57 insertions(+), 53 deletions(-) diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index b85bd2142fa..22bed7f16a4 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -157,10 +157,10 @@ Configure agents in your `opencode.json` config file: You can also define agents using markdown files. Place them in: -- Global: `~/.config/opencode/agent/` -- Per-project: `.opencode/agent/` +- Global: `~/.config/opencode/agents/` +- Per-project: `.opencode/agents/` -```markdown title="~/.config/opencode/agent/review.md" +```markdown title="~/.config/opencode/agents/review.md" --- description: Reviews code for quality and best practices mode: subagent @@ -419,7 +419,7 @@ You can override these permissions per agent. You can also set permissions in Markdown agents. -```markdown title="~/.config/opencode/agent/review.md" +```markdown title="~/.config/opencode/agents/review.md" --- description: Code review without edits mode: subagent @@ -637,7 +637,7 @@ Do you have an agent you'd like to share? [Submit a PR](https://github.com/anoma ### Documentation agent -```markdown title="~/.config/opencode/agent/docs-writer.md" +```markdown title="~/.config/opencode/agents/docs-writer.md" --- description: Writes and maintains project documentation mode: subagent @@ -659,7 +659,7 @@ Focus on: ### Security auditor -```markdown title="~/.config/opencode/agent/security-auditor.md" +```markdown title="~/.config/opencode/agents/security-auditor.md" --- description: Performs security audits and identifies vulnerabilities mode: subagent diff --git a/packages/web/src/content/docs/commands.mdx b/packages/web/src/content/docs/commands.mdx index 92ca08bd2e9..1d7e4f1c21a 100644 --- a/packages/web/src/content/docs/commands.mdx +++ b/packages/web/src/content/docs/commands.mdx @@ -15,11 +15,11 @@ Custom commands are in addition to the built-in commands like `/init`, `/undo`, ## Create command files -Create markdown files in the `command/` directory to define custom commands. +Create markdown files in the `commands/` directory to define custom commands. -Create `.opencode/command/test.md`: +Create `.opencode/commands/test.md`: -```md title=".opencode/command/test.md" +```md title=".opencode/commands/test.md" --- description: Run tests with coverage agent: build @@ -42,7 +42,7 @@ Use the command by typing `/` followed by the command name. ## Configure -You can add custom commands through the OpenCode config or by creating markdown files in the `command/` directory. +You can add custom commands through the OpenCode config or by creating markdown files in the `commands/` directory. --- @@ -79,10 +79,10 @@ Now you can run this command in the TUI: You can also define commands using markdown files. Place them in: -- Global: `~/.config/opencode/command/` -- Per-project: `.opencode/command/` +- Global: `~/.config/opencode/commands/` +- Per-project: `.opencode/commands/` -```markdown title="~/.config/opencode/command/test.md" +```markdown title="~/.config/opencode/commands/test.md" --- description: Run tests with coverage agent: build @@ -112,7 +112,7 @@ The prompts for the custom commands support several special placeholders and syn Pass arguments to commands using the `$ARGUMENTS` placeholder. -```md title=".opencode/command/component.md" +```md title=".opencode/commands/component.md" --- description: Create a new component --- @@ -138,7 +138,7 @@ You can also access individual arguments using positional parameters: For example: -```md title=".opencode/command/create-file.md" +```md title=".opencode/commands/create-file.md" --- description: Create a new file with content --- @@ -167,7 +167,7 @@ Use _!`command`_ to inject [bash command](/docs/tui#bash-commands) output into y For example, to create a custom command that analyzes test coverage: -```md title=".opencode/command/analyze-coverage.md" +```md title=".opencode/commands/analyze-coverage.md" --- description: Analyze test coverage --- @@ -180,7 +180,7 @@ Based on these results, suggest improvements to increase coverage. Or to review recent changes: -```md title=".opencode/command/review-changes.md" +```md title=".opencode/commands/review-changes.md" --- description: Review recent changes --- @@ -199,7 +199,7 @@ Commands run in your project's root directory and their output becomes part of t Include files in your command using `@` followed by the filename. -```md title=".opencode/command/review-component.md" +```md title=".opencode/commands/review-component.md" --- description: Review component --- diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index eeb41b943c9..1474cb91558 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -51,6 +51,10 @@ Config sources are loaded in this order (later sources override earlier ones): This means project configs can override global defaults, and global configs can override remote organizational defaults. +:::note +The `.opencode` and `~/.config/opencode` directories use **plural names** for subdirectories: `agents/`, `commands/`, `modes/`, `plugins/`, `skills/`, `tools/`, and `themes/`. Singular names (e.g., `agent/`) are also supported for backwards compatibility. +::: + --- ### Remote @@ -330,7 +334,7 @@ You can configure specialized agents for specific tasks through the `agent` opti } ``` -You can also define agents using markdown files in `~/.config/opencode/agent/` or `.opencode/agent/`. [Learn more here](/docs/agents). +You can also define agents using markdown files in `~/.config/opencode/agents/` or `.opencode/agents/`. [Learn more here](/docs/agents). --- @@ -394,7 +398,7 @@ You can configure custom commands for repetitive tasks through the `command` opt } ``` -You can also define commands using markdown files in `~/.config/opencode/command/` or `.opencode/command/`. [Learn more here](/docs/commands). +You can also define commands using markdown files in `~/.config/opencode/commands/` or `.opencode/commands/`. [Learn more here](/docs/commands). --- @@ -530,7 +534,7 @@ You can configure MCP servers you want to use through the `mcp` option. [Plugins](/docs/plugins) extend OpenCode with custom tools, hooks, and integrations. -Place plugin files in `.opencode/plugin/` or `~/.config/opencode/plugin/`. You can also load plugins from npm through the `plugin` option. +Place plugin files in `.opencode/plugins/` or `~/.config/opencode/plugins/`. You can also load plugins from npm through the `plugin` option. ```json title="opencode.json" { diff --git a/packages/web/src/content/docs/custom-tools.mdx b/packages/web/src/content/docs/custom-tools.mdx index 2701be65086..e089a035b4b 100644 --- a/packages/web/src/content/docs/custom-tools.mdx +++ b/packages/web/src/content/docs/custom-tools.mdx @@ -17,8 +17,8 @@ Tools are defined as **TypeScript** or **JavaScript** files. However, the tool d They can be defined: -- Locally by placing them in the `.opencode/tool/` directory of your project. -- Or globally, by placing them in `~/.config/opencode/tool/`. +- Locally by placing them in the `.opencode/tools/` directory of your project. +- Or globally, by placing them in `~/.config/opencode/tools/`. --- @@ -26,7 +26,7 @@ They can be defined: The easiest way to create tools is using the `tool()` helper which provides type-safety and validation. -```ts title=".opencode/tool/database.ts" {1} +```ts title=".opencode/tools/database.ts" {1} import { tool } from "@opencode-ai/plugin" export default tool({ @@ -49,7 +49,7 @@ The **filename** becomes the **tool name**. The above creates a `database` tool. You can also export multiple tools from a single file. Each export becomes **a separate tool** with the name **`_`**: -```ts title=".opencode/tool/math.ts" +```ts title=".opencode/tools/math.ts" import { tool } from "@opencode-ai/plugin" export const add = tool({ @@ -112,7 +112,7 @@ export default { Tools receive context about the current session: -```ts title=".opencode/tool/project.ts" {8} +```ts title=".opencode/tools/project.ts" {8} import { tool } from "@opencode-ai/plugin" export default tool({ @@ -136,7 +136,7 @@ You can write your tools in any language you want. Here's an example that adds t First, create the tool as a Python script: -```python title=".opencode/tool/add.py" +```python title=".opencode/tools/add.py" import sys a = int(sys.argv[1]) @@ -146,7 +146,7 @@ print(a + b) Then create the tool definition that invokes it: -```ts title=".opencode/tool/python-add.ts" {10} +```ts title=".opencode/tools/python-add.ts" {10} import { tool } from "@opencode-ai/plugin" export default tool({ @@ -156,7 +156,7 @@ export default tool({ b: tool.schema.number().describe("Second number"), }, async execute(args) { - const result = await Bun.$`python3 .opencode/tool/add.py ${args.a} ${args.b}`.text() + const result = await Bun.$`python3 .opencode/tools/add.py ${args.a} ${args.b}`.text() return result.trim() }, }) diff --git a/packages/web/src/content/docs/modes.mdx b/packages/web/src/content/docs/modes.mdx index a31a8223b07..57c1c54a956 100644 --- a/packages/web/src/content/docs/modes.mdx +++ b/packages/web/src/content/docs/modes.mdx @@ -87,10 +87,10 @@ Configure modes in your `opencode.json` config file: You can also define modes using markdown files. Place them in: -- Global: `~/.config/opencode/mode/` -- Project: `.opencode/mode/` +- Global: `~/.config/opencode/modes/` +- Project: `.opencode/modes/` -```markdown title="~/.config/opencode/mode/review.md" +```markdown title="~/.config/opencode/modes/review.md" --- model: anthropic/claude-sonnet-4-20250514 temperature: 0.1 @@ -268,9 +268,9 @@ You can create your own custom modes by adding them to the configuration. Here a ### Using markdown files -Create mode files in `.opencode/mode/` for project-specific modes or `~/.config/opencode/mode/` for global modes: +Create mode files in `.opencode/modes/` for project-specific modes or `~/.config/opencode/modes/` for global modes: -```markdown title=".opencode/mode/debug.md" +```markdown title=".opencode/modes/debug.md" --- temperature: 0.1 tools: @@ -294,7 +294,7 @@ Focus on: Do not make any changes to files. Only investigate and report. ``` -```markdown title="~/.config/opencode/mode/refactor.md" +```markdown title="~/.config/opencode/modes/refactor.md" --- model: anthropic/claude-sonnet-4-20250514 temperature: 0.2 diff --git a/packages/web/src/content/docs/permissions.mdx b/packages/web/src/content/docs/permissions.mdx index b4f0691ced7..4df3841e34a 100644 --- a/packages/web/src/content/docs/permissions.mdx +++ b/packages/web/src/content/docs/permissions.mdx @@ -174,7 +174,7 @@ Refer to the [Granular Rules (Object Syntax)](#granular-rules-object-syntax) sec You can also configure agent permissions in Markdown: -```markdown title="~/.config/opencode/agent/review.md" +```markdown title="~/.config/opencode/agents/review.md" --- description: Code review without edits mode: subagent diff --git a/packages/web/src/content/docs/plugins.mdx b/packages/web/src/content/docs/plugins.mdx index bf26744f6c4..66a1b3cad95 100644 --- a/packages/web/src/content/docs/plugins.mdx +++ b/packages/web/src/content/docs/plugins.mdx @@ -19,8 +19,8 @@ There are two ways to load plugins. Place JavaScript or TypeScript files in the plugin directory. -- `.opencode/plugin/` - Project-level plugins -- `~/.config/opencode/plugin/` - Global plugins +- `.opencode/plugins/` - Project-level plugins +- `~/.config/opencode/plugins/` - Global plugins Files in these directories are automatically loaded at startup. @@ -57,8 +57,8 @@ Plugins are loaded from all sources and all hooks run in sequence. The load orde 1. Global config (`~/.config/opencode/opencode.json`) 2. Project config (`opencode.json`) -3. Global plugin directory (`~/.config/opencode/plugin/`) -4. Project plugin directory (`.opencode/plugin/`) +3. Global plugin directory (`~/.config/opencode/plugins/`) +4. Project plugin directory (`.opencode/plugins/`) Duplicate npm packages with the same name and version are loaded once. However, a local plugin and an npm plugin with similar names are both loaded separately. @@ -85,7 +85,7 @@ Local plugins and custom tools can use external npm packages. Add a `package.jso OpenCode runs `bun install` at startup to install these. Your plugins and tools can then import them. -```ts title=".opencode/plugin/my-plugin.ts" +```ts title=".opencode/plugins/my-plugin.ts" import { escape } from "shescape" export const MyPlugin = async (ctx) => { @@ -103,7 +103,7 @@ export const MyPlugin = async (ctx) => { ### Basic structure -```js title=".opencode/plugin/example.js" +```js title=".opencode/plugins/example.js" export const MyPlugin = async ({ project, client, $, directory, worktree }) => { console.log("Plugin initialized!") @@ -215,7 +215,7 @@ Here are some examples of plugins you can use to extend opencode. Send notifications when certain events occur: -```js title=".opencode/plugin/notification.js" +```js title=".opencode/plugins/notification.js" export const NotificationPlugin = async ({ project, client, $, directory, worktree }) => { return { event: async ({ event }) => { @@ -240,7 +240,7 @@ If you’re using the OpenCode desktop app, it can send system notifications aut Prevent opencode from reading `.env` files: -```javascript title=".opencode/plugin/env-protection.js" +```javascript title=".opencode/plugins/env-protection.js" export const EnvProtection = async ({ project, client, $, directory, worktree }) => { return { "tool.execute.before": async (input, output) => { @@ -258,7 +258,7 @@ export const EnvProtection = async ({ project, client, $, directory, worktree }) Plugins can also add custom tools to opencode: -```ts title=".opencode/plugin/custom-tools.ts" +```ts title=".opencode/plugins/custom-tools.ts" import { type Plugin, tool } from "@opencode-ai/plugin" export const CustomToolsPlugin: Plugin = async (ctx) => { @@ -292,7 +292,7 @@ Your custom tools will be available to opencode alongside built-in tools. Use `client.app.log()` instead of `console.log` for structured logging: -```ts title=".opencode/plugin/my-plugin.ts" +```ts title=".opencode/plugins/my-plugin.ts" export const MyPlugin = async ({ client }) => { await client.app.log({ service: "my-plugin", @@ -311,7 +311,7 @@ Levels: `debug`, `info`, `warn`, `error`. See [SDK documentation](https://openco Customize the context included when a session is compacted: -```ts title=".opencode/plugin/compaction.ts" +```ts title=".opencode/plugins/compaction.ts" import type { Plugin } from "@opencode-ai/plugin" export const CompactionPlugin: Plugin = async (ctx) => { @@ -335,7 +335,7 @@ The `experimental.session.compacting` hook fires before the LLM generates a cont You can also replace the compaction prompt entirely by setting `output.prompt`: -```ts title=".opencode/plugin/custom-compaction.ts" +```ts title=".opencode/plugins/custom-compaction.ts" import type { Plugin } from "@opencode-ai/plugin" export const CustomCompactionPlugin: Plugin = async (ctx) => { diff --git a/packages/web/src/content/docs/skills.mdx b/packages/web/src/content/docs/skills.mdx index 54c2c9d06ef..553931eec49 100644 --- a/packages/web/src/content/docs/skills.mdx +++ b/packages/web/src/content/docs/skills.mdx @@ -13,8 +13,8 @@ Skills are loaded on-demand via the native `skill` tool—agents see available s Create one folder per skill name and put a `SKILL.md` inside it. OpenCode searches these locations: -- Project config: `.opencode/skill//SKILL.md` -- Global config: `~/.config/opencode/skill//SKILL.md` +- Project config: `.opencode/skills//SKILL.md` +- Global config: `~/.config/opencode/skills//SKILL.md` - Project Claude-compatible: `.claude/skills//SKILL.md` - Global Claude-compatible: `~/.claude/skills//SKILL.md` @@ -23,9 +23,9 @@ OpenCode searches these locations: ## Understand discovery For project-local paths, OpenCode walks up from your current working directory until it reaches the git worktree. -It loads any matching `skill/*/SKILL.md` in `.opencode/` and any matching `.claude/skills/*/SKILL.md` along the way. +It loads any matching `skills/*/SKILL.md` in `.opencode/` and any matching `.claude/skills/*/SKILL.md` along the way. -Global definitions are also loaded from `~/.config/opencode/skill/*/SKILL.md` and `~/.claude/skills/*/SKILL.md`. +Global definitions are also loaded from `~/.config/opencode/skills/*/SKILL.md` and `~/.claude/skills/*/SKILL.md`. --- @@ -71,7 +71,7 @@ Keep it specific enough for the agent to choose correctly. ## Use an example -Create `.opencode/skill/git-release/SKILL.md` like this: +Create `.opencode/skills/git-release/SKILL.md` like this: ```markdown --- From 3aff88c23d139f47af7f4db7bbc14e08a30f3b6e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:36:54 -0600 Subject: [PATCH 055/192] docs: add use_github_token to example (#9120) Co-authored-by: opencode-agent[bot] Co-authored-by: rekram1-node --- github/README.md | 2 ++ packages/web/src/content/docs/github.mdx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/github/README.md b/github/README.md index 8238bdc42aa..17b24ffb1d6 100644 --- a/github/README.md +++ b/github/README.md @@ -91,8 +91,10 @@ This will walk you through installing the GitHub app, creating the workflow, and uses: anomalyco/opencode/github@latest env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: model: anthropic/claude-sonnet-4-20250514 + use_github_token: true ``` 3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys. diff --git a/packages/web/src/content/docs/github.mdx b/packages/web/src/content/docs/github.mdx index 6e8b9de4d79..a31fe1e7be8 100644 --- a/packages/web/src/content/docs/github.mdx +++ b/packages/web/src/content/docs/github.mdx @@ -180,8 +180,10 @@ jobs: - uses: anomalyco/opencode/github@latest env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: model: anthropic/claude-sonnet-4-20250514 + use_github_token: true prompt: | Review this pull request: - Check for code quality issues From f3513bacffc83355369ccf5b7e8c264dc764d901 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 17 Jan 2026 14:41:42 -0600 Subject: [PATCH 056/192] tui: fix model state persistence when model store is not ready --- packages/opencode/src/cli/cmd/tui/context/local.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 63f1d9743bf..d058ce54fb3 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -113,8 +113,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const file = Bun.file(path.join(Global.Path.state, "model.json")) + const state = { + pending: false, + } function save() { + if (!modelStore.ready) { + state.pending = true + return + } + state.pending = false Bun.write( file, JSON.stringify({ @@ -135,6 +143,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ .catch(() => {}) .finally(() => { setModelStore("ready", true) + if (state.pending) save() }) const args = useArgs() From c3d33562c705445e258648bb0d34e94fea7fe68f Mon Sep 17 00:00:00 2001 From: David Hill Date: Sat, 17 Jan 2026 21:30:38 +0000 Subject: [PATCH 057/192] fix: align project avatar notification dot --- packages/app/src/pages/layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index eb09b154b9c..27535c01b0d 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1018,7 +1018,7 @@ export default function Layout(props: ParentProps) { const notifications = createMemo(() => notification.project.unseen(props.project.worktree)) const hasError = createMemo(() => notifications().some((n) => n.type === "error")) const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) - const mask = "radial-gradient(circle 6px at calc(100% - 3px) 3px, transparent 6px, black 6.5px)" + const mask = "radial-gradient(circle 6px at calc(100% - 5px) 5px, transparent 6px, black 6.5px)" const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" return ( @@ -1039,7 +1039,7 @@ export default function Layout(props: ParentProps) { 0 && props.notify}>
Date: Sat, 17 Jan 2026 21:41:01 +0000 Subject: [PATCH 058/192] fix: update desktop initializing splash logo --- packages/desktop/src/index.tsx | 5 ++--- packages/ui/src/components/logo.tsx | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 8398f457766..0d9e383790a 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -12,7 +12,7 @@ import { relaunch } from "@tauri-apps/plugin-process" import { AsyncStorage } from "@solid-primitives/storage" import { fetch as tauriFetch } from "@tauri-apps/plugin-http" import { Store } from "@tauri-apps/plugin-store" -import { Logo } from "@opencode-ai/ui/logo" +import { Splash } from "@opencode-ai/ui/logo" import { createSignal, Show, Accessor, JSX, createResource, onMount, onCleanup } from "solid-js" import { UPDATER_ENABLED } from "./updater" @@ -357,8 +357,7 @@ function ServerGate(props: { children: (data: Accessor) => JSX. when={serverData.state !== "pending" && serverData()} fallback={
- -
Initializing...
+
} > diff --git a/packages/ui/src/components/logo.tsx b/packages/ui/src/components/logo.tsx index 5ddf3fba329..26f312bda75 100644 --- a/packages/ui/src/components/logo.tsx +++ b/packages/ui/src/components/logo.tsx @@ -13,6 +13,21 @@ export const Mark = (props: { class?: string }) => { ) } +export const Splash = (props: { class?: string }) => { + return ( + + + + + ) +} + export const Logo = (props: { class?: string }) => { return ( Date: Sat, 17 Jan 2026 21:43:03 +0000 Subject: [PATCH 059/192] fix: shrink project notification dot and mask --- packages/app/src/pages/layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 27535c01b0d..0e46298e7ed 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1018,7 +1018,7 @@ export default function Layout(props: ParentProps) { const notifications = createMemo(() => notification.project.unseen(props.project.worktree)) const hasError = createMemo(() => notifications().some((n) => n.type === "error")) const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) - const mask = "radial-gradient(circle 6px at calc(100% - 5px) 5px, transparent 6px, black 6.5px)" + const mask = "radial-gradient(circle 5px at calc(100% - 4px) 4px, transparent 5px, black 5.5px)" const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" return ( @@ -1039,7 +1039,7 @@ export default function Layout(props: ParentProps) { 0 && props.notify}>
Date: Sat, 17 Jan 2026 21:46:21 +0000 Subject: [PATCH 060/192] fix: command pallete file list item spacing --- packages/app/src/components/dialog-select-file.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 432e531e192..0e8d69628bb 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -149,7 +149,7 @@ export function DialogSelectFile() { +
From b4075cd8565cc95c5417f8e578167515c71c4a01 Mon Sep 17 00:00:00 2001 From: David Hill Date: Sat, 17 Jan 2026 21:54:51 +0000 Subject: [PATCH 061/192] fix: remove loading text after splash --- packages/app/src/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index d0678dc5369..d03d10d0ea7 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -29,7 +29,7 @@ import { Suspense } from "solid-js" const Home = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) -const Loading = () =>
Loading...
+const Loading = () =>
declare global { interface Window { From 0cc9a22a428498fbbd8414d1b9fbc5263fa58383 Mon Sep 17 00:00:00 2001 From: David Hill Date: Sat, 17 Jan 2026 22:51:49 +0000 Subject: [PATCH 062/192] fix: show project name in avatar hover --- packages/app/src/pages/layout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 0e46298e7ed..25c94554069 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1383,7 +1383,8 @@ export default function Layout(props: ParentProps) {
-
Recent sessions
+
{displayName(props.project)}
+
Recent sessions
Date: Sat, 17 Jan 2026 22:54:30 +0000 Subject: [PATCH 063/192] fix: reduce prompt dock bottom spacing --- packages/app/src/pages/session.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index aad0b596cb9..dbdbbc7eb55 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1091,7 +1091,7 @@ export default function Page() { file.load(path) }} classes={{ - root: "pb-[calc(var(--prompt-height,8rem)+32px)]", + root: "pb-[calc(var(--prompt-height,8rem)+24px)]", header: "px-4", container: "px-4", }} @@ -1237,7 +1237,7 @@ export default function Page() { {/* Prompt input */}
(promptDock = el)} - class="absolute inset-x-0 bottom-0 pt-12 pb-4 md:pb-8 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none" + class="absolute inset-x-0 bottom-0 pt-12 pb-4 md:pb-6 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none" >
Date: Sat, 17 Jan 2026 23:21:34 +0000 Subject: [PATCH 064/192] fix: adjust recent sessions popover padding --- packages/app/src/pages/layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 25c94554069..101cc2eaad8 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1383,8 +1383,8 @@ export default function Layout(props: ParentProps) {
-
{displayName(props.project)}
-
Recent sessions
+
{displayName(props.project)}
+
Recent sessions
Date: Sat, 17 Jan 2026 23:40:06 +0000 Subject: [PATCH 065/192] fix: keep project avatar hover styles while popover open --- packages/app/src/pages/layout.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 101cc2eaad8..98f62dc11b4 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1338,6 +1338,8 @@ export default function Layout(props: ParentProps) { const workspaces = createMemo(() => workspaceIds(props.project).slice(0, 2)) const workspaceEnabled = createMemo(() => layout.sidebar.workspaces(props.project.worktree)()) + const [open, setOpen] = createSignal(false) + const label = (directory: string) => { const [data] = globalSync.child(directory) const kind = directory === props.project.worktree ? "local" : "sandbox" @@ -1370,7 +1372,8 @@ export default function Layout(props: ParentProps) { "flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true, "bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": selected(), "bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base": - !selected(), + !selected() && !open(), + "bg-surface-base-hover border border-border-weak-base": !selected() && open(), }} onClick={() => navigateToProject(props.project.worktree)} > @@ -1381,7 +1384,14 @@ export default function Layout(props: ParentProps) { return ( // @ts-ignore
- +
{displayName(props.project)}
Recent sessions
From ded9bd26bba4d9c23d614519088083f006114bb4 Mon Sep 17 00:00:00 2001 From: David Hill Date: Sun, 18 Jan 2026 00:07:21 +0000 Subject: [PATCH 066/192] fix: adjust session list tooltip trigger and delay --- packages/app/src/pages/layout.tsx | 80 +++++++++++++++++-------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 98f62dc11b4..f7182d26fea 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1089,33 +1089,39 @@ export default function Layout(props: ParentProps) { class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3 hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active" > - - prefetchSession(props.session, "high")} - onFocus={() => prefetchSession(props.session, "high")} - > -
-
- }> - - - - -
- - -
- - 0}> -
- - -
+
prefetchSession(props.session, "high")} + onFocus={() => prefetchSession(props.session, "high")} + > +
+
+ }> + + + + + +
From e36b3433fcabed407b3501f299e008173907ac9e Mon Sep 17 00:00:00 2001 From: David Hill Date: Sun, 18 Jan 2026 00:48:06 +0000 Subject: [PATCH 067/192] fix: remove max width on sidebar new buttons --- packages/app/src/pages/layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index f7182d26fea..26ca34f934e 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1669,7 +1669,7 @@ export default function Layout(props: ParentProps) {
From 389d97ece9a08907c2c101ba18ae5712dfe9b8fd Mon Sep 17 00:00:00 2001 From: David Hill Date: Sun, 18 Jan 2026 00:48:49 +0000 Subject: [PATCH 068/192] fix: adjust project path tooltip placement Move the desktop project path tooltip above the header and tune spacing/offset; add content style hooks to Tooltip for max-width and horizontal shift. --- packages/app/src/pages/layout.tsx | 11 ++++++++++- packages/ui/src/components/tooltip.tsx | 11 +++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 26ca34f934e..2a488ec4966 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1627,7 +1627,16 @@ export default function Layout(props: ParentProps) { stopPropagation /> - + {project()?.worktree.replace(homedir(), "~")} diff --git a/packages/ui/src/components/tooltip.tsx b/packages/ui/src/components/tooltip.tsx index c38ee5847db..711047030cc 100644 --- a/packages/ui/src/components/tooltip.tsx +++ b/packages/ui/src/components/tooltip.tsx @@ -5,6 +5,8 @@ import type { ComponentProps } from "solid-js" export interface TooltipProps extends ComponentProps { value: JSX.Element class?: string + contentClass?: string + contentStyle?: JSX.CSSProperties inactive?: boolean } @@ -30,7 +32,7 @@ export function TooltipKeybind(props: TooltipKeybindProps) { export function Tooltip(props: TooltipProps) { const [open, setOpen] = createSignal(false) - const [local, others] = splitProps(props, ["children", "class", "inactive"]) + const [local, others] = splitProps(props, ["children", "class", "contentClass", "contentStyle", "inactive"]) const c = children(() => local.children) @@ -58,7 +60,12 @@ export function Tooltip(props: TooltipProps) { {c()} - + {others.value} {/* */} From c3ab76c8ad90fb7ef99604e8ef0a1017c5ddda58 Mon Sep 17 00:00:00 2001 From: David Hill Date: Sun, 18 Jan 2026 00:51:35 +0000 Subject: [PATCH 069/192] fix: increase max-width of session name tooltip --- packages/app/src/pages/layout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2a488ec4966..6c860d98edc 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1119,7 +1119,8 @@ export default function Layout(props: ParentProps) { placement="top-start" value={props.session.title} gutter={0} - openDelay={2000} + openDelay={3000} + contentStyle={{ maxWidth: "640px" }} class="grow-1 min-w-0" > Date: Sun, 18 Jan 2026 00:57:21 +0000 Subject: [PATCH 070/192] Revert "fix: increase max-width of session name tooltip" This reverts commit c3ab76c8ad90fb7ef99604e8ef0a1017c5ddda58. --- packages/app/src/pages/layout.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 6c860d98edc..2a488ec4966 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1119,8 +1119,7 @@ export default function Layout(props: ParentProps) { placement="top-start" value={props.session.title} gutter={0} - openDelay={3000} - contentStyle={{ maxWidth: "640px" }} + openDelay={2000} class="grow-1 min-w-0" > Date: Sun, 18 Jan 2026 00:59:41 +0000 Subject: [PATCH 071/192] fix: remove max-width of session name tooltip --- packages/app/src/pages/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2a488ec4966..56d6bfbf8ca 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1119,7 +1119,7 @@ export default function Layout(props: ParentProps) { placement="top-start" value={props.session.title} gutter={0} - openDelay={2000} + openDelay={3000} class="grow-1 min-w-0" > Date: Sat, 17 Jan 2026 20:32:57 -0600 Subject: [PATCH 072/192] Hide variants hint when list empty (#9179) Co-authored-by: opencode-agent[bot] Co-authored-by: rekram1-node --- .../opencode/src/cli/cmd/tui/component/prompt/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 145fa9da0c3..d61fbcfdd86 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1065,9 +1065,11 @@ export function Prompt(props: PromptProps) { - - {keybind.print("variant_cycle")} variants - + 0}> + + {keybind.print("variant_cycle")} variants + + {keybind.print("agent_cycle")} agents From 759e68616e53f4c4d3a647606203bf46a9193733 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 17 Jan 2026 21:39:19 -0500 Subject: [PATCH 073/192] refactor(tui): unify command registry and derive slash commands (#9115) --- packages/opencode/src/cli/cmd/tui/app.tsx | 45 +++++- .../cli/cmd/tui/component/dialog-command.tsx | 80 ++++++---- .../cmd/tui/component/prompt/autocomplete.tsx | 149 ++---------------- .../cli/cmd/tui/component/prompt/index.tsx | 22 +-- .../src/cli/cmd/tui/routes/session/index.tsx | 118 +++++++++----- .../src/cli/cmd/tui/ui/dialog-select.tsx | 2 +- 6 files changed, 194 insertions(+), 222 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 2ec1fb703f9..4b177e292cf 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -288,6 +288,10 @@ function App() { keybind: "session_list", category: "Session", suggested: sync.data.session.length > 0, + slash: { + name: "sessions", + aliases: ["resume", "continue"], + }, onSelect: () => { dialog.replace(() => ) }, @@ -298,6 +302,10 @@ function App() { value: "session.new", keybind: "session_new", category: "Session", + slash: { + name: "new", + aliases: ["clear"], + }, onSelect: () => { const current = promptRef.current // Don't require focus - if there's any text, preserve it @@ -315,26 +323,29 @@ function App() { keybind: "model_list", suggested: true, category: "Agent", + slash: { + name: "models", + }, onSelect: () => { dialog.replace(() => ) }, }, { title: "Model cycle", - disabled: true, value: "model.cycle_recent", keybind: "model_cycle_recent", category: "Agent", + hidden: true, onSelect: () => { local.model.cycle(1) }, }, { title: "Model cycle reverse", - disabled: true, value: "model.cycle_recent_reverse", keybind: "model_cycle_recent_reverse", category: "Agent", + hidden: true, onSelect: () => { local.model.cycle(-1) }, @@ -344,6 +355,7 @@ function App() { value: "model.cycle_favorite", keybind: "model_cycle_favorite", category: "Agent", + hidden: true, onSelect: () => { local.model.cycleFavorite(1) }, @@ -353,6 +365,7 @@ function App() { value: "model.cycle_favorite_reverse", keybind: "model_cycle_favorite_reverse", category: "Agent", + hidden: true, onSelect: () => { local.model.cycleFavorite(-1) }, @@ -362,6 +375,9 @@ function App() { value: "agent.list", keybind: "agent_list", category: "Agent", + slash: { + name: "agents", + }, onSelect: () => { dialog.replace(() => ) }, @@ -370,6 +386,9 @@ function App() { title: "Toggle MCPs", value: "mcp.list", category: "Agent", + slash: { + name: "mcps", + }, onSelect: () => { dialog.replace(() => ) }, @@ -379,7 +398,7 @@ function App() { value: "agent.cycle", keybind: "agent_cycle", category: "Agent", - disabled: true, + hidden: true, onSelect: () => { local.agent.move(1) }, @@ -389,6 +408,7 @@ function App() { value: "variant.cycle", keybind: "variant_cycle", category: "Agent", + hidden: true, onSelect: () => { local.model.variant.cycle() }, @@ -398,7 +418,7 @@ function App() { value: "agent.cycle.reverse", keybind: "agent_cycle_reverse", category: "Agent", - disabled: true, + hidden: true, onSelect: () => { local.agent.move(-1) }, @@ -407,6 +427,9 @@ function App() { title: "Connect provider", value: "provider.connect", suggested: !connected(), + slash: { + name: "connect", + }, onSelect: () => { dialog.replace(() => ) }, @@ -416,6 +439,9 @@ function App() { title: "View status", keybind: "status_view", value: "opencode.status", + slash: { + name: "status", + }, onSelect: () => { dialog.replace(() => ) }, @@ -425,6 +451,9 @@ function App() { title: "Switch theme", value: "theme.switch", keybind: "theme_list", + slash: { + name: "themes", + }, onSelect: () => { dialog.replace(() => ) }, @@ -442,6 +471,9 @@ function App() { { title: "Help", value: "help.show", + slash: { + name: "help", + }, onSelect: () => { dialog.replace(() => ) }, @@ -468,6 +500,10 @@ function App() { { title: "Exit the app", value: "app.exit", + slash: { + name: "exit", + aliases: ["quit", "q"], + }, onSelect: () => exit(), category: "System", }, @@ -508,6 +544,7 @@ function App() { value: "terminal.suspend", keybind: "terminal_suspend", category: "System", + hidden: true, onSelect: () => { process.once("SIGCONT", () => { renderer.resume() 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 d19e93188b2..38dc402758b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx @@ -16,9 +16,17 @@ import type { KeybindsConfig } from "@opencode-ai/sdk/v2" type Context = ReturnType const ctx = createContext() -export type CommandOption = DialogSelectOption & { +export type Slash = { + name: string + aliases?: string[] +} + +export type CommandOption = DialogSelectOption & { keybind?: keyof KeybindsConfig suggested?: boolean + slash?: Slash + hidden?: boolean + enabled?: boolean } function init() { @@ -26,27 +34,35 @@ function init() { const [suspendCount, setSuspendCount] = createSignal(0) const dialog = useDialog() const keybind = useKeybind() - const options = createMemo(() => { + + const entries = createMemo(() => { const all = registrations().flatMap((x) => x()) - const suggested = all.filter((x) => x.suggested) - return [ - ...suggested.map((x) => ({ - ...x, - category: "Suggested", - value: "suggested." + x.value, - })), - ...all, - ].map((x) => ({ + return all.map((x) => ({ ...x, footer: x.keybind ? keybind.print(x.keybind) : undefined, })) }) + + const isEnabled = (option: CommandOption) => option.enabled !== false + const isVisible = (option: CommandOption) => isEnabled(option) && !option.hidden + + const visibleOptions = createMemo(() => entries().filter((option) => isVisible(option))) + const suggestedOptions = createMemo(() => + visibleOptions() + .filter((option) => option.suggested) + .map((option) => ({ + ...option, + value: `suggested:${option.value}`, + category: "Suggested", + })), + ) const suspended = () => suspendCount() > 0 useKeyboard((evt) => { if (suspended()) return if (dialog.stack.length > 0) return - for (const option of options()) { + for (const option of entries()) { + if (!isEnabled(option)) continue if (option.keybind && keybind.match(option.keybind, evt)) { evt.preventDefault() option.onSelect?.(dialog) @@ -56,20 +72,33 @@ function init() { }) const result = { - trigger(name: string, source?: "prompt") { - for (const option of options()) { + trigger(name: string) { + for (const option of entries()) { if (option.value === name) { - option.onSelect?.(dialog, source) + if (!isEnabled(option)) return + option.onSelect?.(dialog) return } } }, + slashes() { + return visibleOptions().flatMap((option) => { + const slash = option.slash + if (!slash) return [] + return { + display: "/" + slash.name, + description: option.description ?? option.title, + aliases: slash.aliases?.map((alias) => "/" + alias), + onSelect: () => result.trigger(option.value), + } + }) + }, keybinds(enabled: boolean) { setSuspendCount((count) => count + (enabled ? -1 : 1)) }, suspended, show() { - dialog.replace(() => ) + dialog.replace(() => ) }, register(cb: () => CommandOption[]) { const results = createMemo(cb) @@ -78,9 +107,6 @@ function init() { setRegistrations((arr) => arr.filter((x) => x !== results)) }) }, - get options() { - return options() - }, } return result } @@ -104,7 +130,7 @@ export function CommandProvider(props: ParentProps) { if (evt.defaultPrevented) return if (keybind.match("command_list", evt)) { evt.preventDefault() - dialog.replace(() => ) + value.show() return } }) @@ -112,13 +138,11 @@ export function CommandProvider(props: ParentProps) { return {props.children} } -function DialogCommand(props: { options: CommandOption[] }) { +function DialogCommand(props: { options: CommandOption[]; suggestedOptions: CommandOption[] }) { let ref: DialogSelectRef - return ( - (ref = r)} - title="Commands" - options={props.options.filter((x) => !ref?.filter || !x.value.startsWith("suggested."))} - /> - ) + const list = () => { + if (ref?.filter) return props.options + return [...props.suggestedOptions, ...props.options] + } + return (ref = r)} title="Commands" options={list()} /> } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 601eb82bc48..e27c32dfb2e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -332,16 +332,15 @@ export function Autocomplete(props: { ) }) - const session = createMemo(() => (props.sessionID ? sync.session.get(props.sessionID) : undefined)) const commands = createMemo((): AutocompleteOption[] => { - const results: AutocompleteOption[] = [] - const s = session() - for (const command of sync.data.command) { + const results: AutocompleteOption[] = [...command.slashes()] + + for (const serverCommand of sync.data.command) { results.push({ - display: "/" + command.name + (command.mcp ? " (MCP)" : ""), - description: command.description, + display: "/" + serverCommand.name + (serverCommand.mcp ? " (MCP)" : ""), + description: serverCommand.description, onSelect: () => { - const newText = "/" + command.name + " " + const newText = "/" + serverCommand.name + " " const cursor = props.input().logicalCursor props.input().deleteRange(0, 0, cursor.row, cursor.col) props.input().insertText(newText) @@ -349,138 +348,9 @@ export function Autocomplete(props: { }, }) } - if (s) { - results.push( - { - display: "/undo", - description: "undo the last message", - onSelect: () => { - command.trigger("session.undo") - }, - }, - { - display: "/redo", - description: "redo the last message", - onSelect: () => command.trigger("session.redo"), - }, - { - display: "/compact", - aliases: ["/summarize"], - description: "compact the session", - onSelect: () => command.trigger("session.compact"), - }, - { - display: "/unshare", - disabled: !s.share, - description: "unshare a session", - onSelect: () => command.trigger("session.unshare"), - }, - { - display: "/rename", - description: "rename session", - onSelect: () => command.trigger("session.rename"), - }, - { - display: "/copy", - description: "copy session transcript to clipboard", - onSelect: () => command.trigger("session.copy"), - }, - { - display: "/export", - description: "export session transcript to file", - onSelect: () => command.trigger("session.export"), - }, - { - display: "/timeline", - description: "jump to message", - onSelect: () => command.trigger("session.timeline"), - }, - { - display: "/fork", - description: "fork from message", - onSelect: () => command.trigger("session.fork"), - }, - { - display: "/thinking", - description: "toggle thinking visibility", - onSelect: () => command.trigger("session.toggle.thinking"), - }, - ) - if (sync.data.config.share !== "disabled") { - results.push({ - display: "/share", - disabled: !!s.share?.url, - description: "share a session", - onSelect: () => command.trigger("session.share"), - }) - } - } - results.push( - { - display: "/new", - aliases: ["/clear"], - description: "create a new session", - onSelect: () => command.trigger("session.new"), - }, - { - display: "/models", - description: "list models", - onSelect: () => command.trigger("model.list"), - }, - { - display: "/agents", - description: "list agents", - onSelect: () => command.trigger("agent.list"), - }, - { - display: "/session", - aliases: ["/resume", "/continue"], - description: "list sessions", - onSelect: () => command.trigger("session.list"), - }, - { - display: "/status", - description: "show status", - onSelect: () => command.trigger("opencode.status"), - }, - { - display: "/mcp", - description: "toggle MCPs", - onSelect: () => command.trigger("mcp.list"), - }, - { - display: "/theme", - description: "toggle theme", - onSelect: () => command.trigger("theme.switch"), - }, - { - display: "/editor", - description: "open editor", - onSelect: () => command.trigger("prompt.editor", "prompt"), - }, - { - display: "/connect", - description: "connect to a provider", - onSelect: () => command.trigger("provider.connect"), - }, - { - display: "/help", - description: "show help", - onSelect: () => command.trigger("help.show"), - }, - { - display: "/commands", - description: "show all commands", - onSelect: () => command.show(), - }, - { - display: "/exit", - aliases: ["/quit", "/q"], - description: "exit the app", - onSelect: () => command.trigger("app.exit"), - }, - ) + results.sort((a, b) => a.display.localeCompare(b.display)) + const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length if (!max) return results return results.map((item) => ({ @@ -494,9 +364,8 @@ export function Autocomplete(props: { const agentsValue = agents() const commandsValue = commands() - const mixed: AutocompleteOption[] = ( + const mixed: AutocompleteOption[] = store.visible === "@" ? [...agentsValue, ...(filesValue || []), ...mcpResources()] : [...commandsValue] - ).filter((x) => x.disabled !== true) const currentFilter = filter() diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index d61fbcfdd86..730da20c265 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -157,7 +157,7 @@ export function Prompt(props: PromptProps) { title: "Clear prompt", value: "prompt.clear", category: "Prompt", - disabled: true, + hidden: true, onSelect: (dialog) => { input.extmarks.clear() input.clear() @@ -167,9 +167,9 @@ export function Prompt(props: PromptProps) { { title: "Submit prompt", value: "prompt.submit", - disabled: true, keybind: "input_submit", category: "Prompt", + hidden: true, onSelect: (dialog) => { if (!input.focused) return submit() @@ -179,9 +179,9 @@ export function Prompt(props: PromptProps) { { title: "Paste", value: "prompt.paste", - disabled: true, keybind: "input_paste", category: "Prompt", + hidden: true, onSelect: async () => { const content = await Clipboard.read() if (content?.mime.startsWith("image/")) { @@ -197,8 +197,9 @@ export function Prompt(props: PromptProps) { title: "Interrupt session", value: "session.interrupt", keybind: "session_interrupt", - disabled: status().type === "idle", category: "Session", + hidden: true, + enabled: status().type !== "idle", onSelect: (dialog) => { if (autocomplete.visible) return if (!input.focused) return @@ -229,7 +230,10 @@ export function Prompt(props: PromptProps) { category: "Session", keybind: "editor_open", value: "prompt.editor", - onSelect: async (dialog, trigger) => { + slash: { + name: "editor", + }, + onSelect: async (dialog) => { dialog.clear() // replace summarized text parts with the actual text @@ -242,7 +246,7 @@ export function Prompt(props: PromptProps) { const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text") - const value = trigger === "prompt" ? "" : text + const value = text const content = await Editor.open({ value, renderer }) if (!content) return @@ -432,7 +436,7 @@ export function Prompt(props: PromptProps) { title: "Stash prompt", value: "prompt.stash", category: "Prompt", - disabled: !store.prompt.input, + enabled: !!store.prompt.input, onSelect: (dialog) => { if (!store.prompt.input) return stash.push({ @@ -450,7 +454,7 @@ export function Prompt(props: PromptProps) { title: "Stash pop", value: "prompt.stash.pop", category: "Prompt", - disabled: stash.list().length === 0, + enabled: stash.list().length > 0, onSelect: (dialog) => { const entry = stash.pop() if (entry) { @@ -466,7 +470,7 @@ export function Prompt(props: PromptProps) { title: "Stash list", value: "prompt.stash.list", category: "Prompt", - disabled: stash.list().length === 0, + enabled: stash.list().length > 0, onSelect: (dialog) => { dialog.replace(() => ( [ - ...(sync.data.config.share !== "disabled" - ? [ - { - title: "Share session", - value: "session.share", - suggested: route.type === "session", - keybind: "session_share" as const, - disabled: !!session()?.share?.url, - category: "Session", - onSelect: async (dialog: any) => { - await sdk.client.session - .share({ - sessionID: route.sessionID, - }) - .then((res) => - Clipboard.copy(res.data!.share!.url).catch(() => - toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }), - ), - ) - .then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" })) - .catch(() => toast.show({ message: "Failed to share session", variant: "error" })) - dialog.clear() - }, - }, - ] - : []), + { + title: "Share session", + value: "session.share", + suggested: route.type === "session", + keybind: "session_share", + category: "Session", + enabled: sync.data.config.share !== "disabled" && !session()?.share?.url, + slash: { + name: "share", + }, + onSelect: async (dialog) => { + await sdk.client.session + .share({ + sessionID: route.sessionID, + }) + .then((res) => + Clipboard.copy(res.data!.share!.url).catch(() => + toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }), + ), + ) + .then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" })) + .catch(() => toast.show({ message: "Failed to share session", variant: "error" })) + dialog.clear() + }, + }, { title: "Rename session", value: "session.rename", keybind: "session_rename", category: "Session", + slash: { + name: "rename", + }, onSelect: (dialog) => { dialog.replace(() => ) }, @@ -335,6 +337,9 @@ export function Session() { value: "session.timeline", keybind: "session_timeline", category: "Session", + slash: { + name: "timeline", + }, onSelect: (dialog) => { dialog.replace(() => ( { dialog.replace(() => ( { const selectedModel = local.model.current() if (!selectedModel) { @@ -396,8 +408,11 @@ export function Session() { title: "Unshare session", value: "session.unshare", keybind: "session_unshare", - disabled: !session()?.share?.url, category: "Session", + enabled: !!session()?.share?.url, + slash: { + name: "unshare", + }, onSelect: async (dialog) => { await sdk.client.session .unshare({ @@ -413,6 +428,9 @@ export function Session() { value: "session.undo", keybind: "messages_undo", category: "Session", + slash: { + name: "undo", + }, onSelect: async (dialog) => { const status = sync.data.session_status?.[route.sessionID] if (status?.type !== "idle") await sdk.client.session.abort({ sessionID: route.sessionID }).catch(() => {}) @@ -447,8 +465,11 @@ export function Session() { title: "Redo", value: "session.redo", keybind: "messages_redo", - disabled: !session()?.revert?.messageID, category: "Session", + enabled: !!session()?.revert?.messageID, + slash: { + name: "redo", + }, onSelect: (dialog) => { dialog.clear() const messageID = session()?.revert?.messageID @@ -495,6 +516,10 @@ export function Session() { title: showTimestamps() ? "Hide timestamps" : "Show timestamps", value: "session.toggle.timestamps", category: "Session", + slash: { + name: "timestamps", + aliases: ["toggle-timestamps"], + }, onSelect: (dialog) => { setTimestamps((prev) => (prev === "show" ? "hide" : "show")) dialog.clear() @@ -504,6 +529,10 @@ export function Session() { title: showThinking() ? "Hide thinking" : "Show thinking", value: "session.toggle.thinking", category: "Session", + slash: { + name: "thinking", + aliases: ["toggle-thinking"], + }, onSelect: (dialog) => { setShowThinking((prev) => !prev) dialog.clear() @@ -513,6 +542,9 @@ export function Session() { title: "Toggle diff wrapping", value: "session.toggle.diffwrap", category: "Session", + slash: { + name: "diffwrap", + }, onSelect: (dialog) => { setDiffWrapMode((prev) => (prev === "word" ? "none" : "word")) dialog.clear() @@ -552,7 +584,7 @@ export function Session() { value: "session.page.up", keybind: "messages_page_up", category: "Session", - disabled: true, + hidden: true, onSelect: (dialog) => { scroll.scrollBy(-scroll.height / 2) dialog.clear() @@ -563,7 +595,7 @@ export function Session() { value: "session.page.down", keybind: "messages_page_down", category: "Session", - disabled: true, + hidden: true, onSelect: (dialog) => { scroll.scrollBy(scroll.height / 2) dialog.clear() @@ -574,7 +606,7 @@ export function Session() { value: "session.half.page.up", keybind: "messages_half_page_up", category: "Session", - disabled: true, + hidden: true, onSelect: (dialog) => { scroll.scrollBy(-scroll.height / 4) dialog.clear() @@ -585,7 +617,7 @@ export function Session() { value: "session.half.page.down", keybind: "messages_half_page_down", category: "Session", - disabled: true, + hidden: true, onSelect: (dialog) => { scroll.scrollBy(scroll.height / 4) dialog.clear() @@ -596,7 +628,7 @@ export function Session() { value: "session.first", keybind: "messages_first", category: "Session", - disabled: true, + hidden: true, onSelect: (dialog) => { scroll.scrollTo(0) dialog.clear() @@ -607,7 +639,7 @@ export function Session() { value: "session.last", keybind: "messages_last", category: "Session", - disabled: true, + hidden: true, onSelect: (dialog) => { scroll.scrollTo(scroll.scrollHeight) dialog.clear() @@ -618,6 +650,7 @@ export function Session() { value: "session.messages_last_user", keybind: "messages_last_user", category: "Session", + hidden: true, onSelect: () => { const messages = sync.data.message[route.sessionID] if (!messages || !messages.length) return @@ -649,7 +682,7 @@ export function Session() { value: "session.message.next", keybind: "messages_next", category: "Session", - disabled: true, + hidden: true, onSelect: (dialog) => scrollToMessage("next", dialog), }, { @@ -657,7 +690,7 @@ export function Session() { value: "session.message.previous", keybind: "messages_previous", category: "Session", - disabled: true, + hidden: true, onSelect: (dialog) => scrollToMessage("prev", dialog), }, { @@ -706,8 +739,10 @@ export function Session() { { title: "Copy session transcript", value: "session.copy", - keybind: "session_copy", category: "Session", + slash: { + name: "copy", + }, onSelect: async (dialog) => { try { const sessionData = session() @@ -735,6 +770,9 @@ export function Session() { value: "session.export", keybind: "session_export", category: "Session", + slash: { + name: "export", + }, onSelect: async (dialog) => { try { const sessionData = session() @@ -793,7 +831,7 @@ export function Session() { value: "session.child.next", keybind: "session_child_cycle", category: "Session", - disabled: true, + hidden: true, onSelect: (dialog) => { moveChild(1) dialog.clear() @@ -804,7 +842,7 @@ export function Session() { value: "session.child.previous", keybind: "session_child_cycle_reverse", category: "Session", - disabled: true, + hidden: true, onSelect: (dialog) => { moveChild(-1) dialog.clear() @@ -815,7 +853,7 @@ export function Session() { value: "session.parent", keybind: "session_parent", category: "Session", - disabled: true, + hidden: true, onSelect: (dialog) => { const parentID = session()?.parentID if (parentID) { diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index f1cdaaa5292..5c37a493dfa 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -38,7 +38,7 @@ export interface DialogSelectOption { disabled?: boolean bg?: RGBA gutter?: JSX.Element - onSelect?: (ctx: DialogContext, trigger?: "prompt") => void + onSelect?: (ctx: DialogContext) => void } export type DialogSelectRef = { From 052f887a9a7aaf79d9f1a560f9b686d59faa8348 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 17 Jan 2026 20:59:42 -0600 Subject: [PATCH 074/192] core: prevent env variables in config from being replaced with actual values When opencode.json was missing a $schema, the config loader would add it and write the file back - but with env variables like {env:API_KEY} replaced with their actual secret values. This made it impossible to safely commit opencode.json to version control. Now the original config text is preserved when adding $schema, keeping variable placeholders intact. --- packages/opencode/src/config/config.ts | 5 ++- packages/opencode/test/config/config.test.ts | 38 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 1574c644d32..5a2e086bff5 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1115,6 +1115,7 @@ export namespace Config { } async function load(text: string, configFilepath: string) { + const original = text text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => { return process.env[varName] || "" }) @@ -1184,7 +1185,9 @@ export namespace Config { if (parsed.success) { if (!parsed.data.$schema) { parsed.data.$schema = "https://opencode.ai/config.json" - await Bun.write(configFilepath, JSON.stringify(parsed.data, null, 2)).catch(() => {}) + // Write the $schema to the original text to preserve variables like {env:VAR} + const updated = original.replace(/^\s*\{/, '{\n "$schema": "https://opencode.ai/config.json",') + await Bun.write(configFilepath, updated).catch(() => {}) } const data = parsed.data if (data.plugin) { diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 86cadca5d81..0463d29d7c5 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -127,6 +127,44 @@ test("handles environment variable substitution", async () => { } }) +test("preserves env variables when adding $schema to config", async () => { + const originalEnv = process.env["PRESERVE_VAR"] + process.env["PRESERVE_VAR"] = "secret_value" + + try { + await using tmp = await tmpdir({ + init: async (dir) => { + // Config without $schema - should trigger auto-add + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + theme: "{env:PRESERVE_VAR}", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.theme).toBe("secret_value") + + // Read the file to verify the env variable was preserved + const content = await Bun.file(path.join(tmp.path, "opencode.json")).text() + expect(content).toContain("{env:PRESERVE_VAR}") + expect(content).not.toContain("secret_value") + expect(content).toContain("$schema") + }, + }) + } finally { + if (originalEnv !== undefined) { + process.env["PRESERVE_VAR"] = originalEnv + } else { + delete process.env["PRESERVE_VAR"] + } + } +}) + test("handles file inclusion substitution", async () => { await using tmp = await tmpdir({ init: async (dir) => { From bfb8c531c22c0101d7c906c9d542b118c5a0aae0 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sat, 17 Jan 2026 19:54:26 -0800 Subject: [PATCH 075/192] feat: bind vim-style line-by-line scrolling (#8980) Co-authored-by: Aiden Cline --- packages/opencode/src/cli/cmd/tui/event.ts | 2 ++ .../src/cli/cmd/tui/routes/session/index.tsx | 22 +++++++++++++++++++ packages/opencode/src/config/config.ts | 10 +++++++-- packages/opencode/src/server/routes/tui.ts | 2 ++ packages/sdk/js/src/gen/types.gen.ts | 8 +++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 8 +++++++ packages/sdk/openapi.json | 14 ++++++++++-- packages/web/src/content/docs/keybinds.mdx | 6 +++-- 8 files changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/event.ts b/packages/opencode/src/cli/cmd/tui/event.ts index 7c75523c136..9466ae54f2d 100644 --- a/packages/opencode/src/cli/cmd/tui/event.ts +++ b/packages/opencode/src/cli/cmd/tui/event.ts @@ -16,6 +16,8 @@ export const TuiEvent = { "session.compact", "session.page.up", "session.page.down", + "session.line.up", + "session.line.down", "session.half.page.up", "session.half.page.down", "session.first", 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 196cd3ba1a5..1842a955d2a 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -601,6 +601,28 @@ export function Session() { dialog.clear() }, }, + { + title: "Line up", + value: "session.line.up", + keybind: "messages_line_up", + category: "Session", + disabled: true, + onSelect: (dialog) => { + scroll.scrollBy(-1) + dialog.clear() + }, + }, + { + title: "Line down", + value: "session.line.down", + keybind: "messages_line_down", + category: "Session", + disabled: true, + onSelect: (dialog) => { + scroll.scrollBy(1) + dialog.clear() + }, + }, { title: "Half page up", value: "session.half.page.up", diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 5a2e086bff5..ddb3af4b0a8 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -651,8 +651,14 @@ export namespace Config { session_unshare: z.string().optional().default("none").describe("Unshare current session"), session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"), session_compact: z.string().optional().default("c").describe("Compact the session"), - messages_page_up: z.string().optional().default("pageup").describe("Scroll messages up by one page"), - messages_page_down: z.string().optional().default("pagedown").describe("Scroll messages down by one page"), + messages_page_up: z.string().optional().default("pageup,ctrl+alt+b").describe("Scroll messages up by one page"), + messages_page_down: z + .string() + .optional() + .default("pagedown,ctrl+alt+f") + .describe("Scroll messages down by one page"), + messages_line_up: z.string().optional().default("ctrl+alt+y").describe("Scroll messages up by one line"), + messages_line_down: z.string().optional().default("ctrl+alt+e").describe("Scroll messages down by one line"), messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"), messages_half_page_down: z .string() diff --git a/packages/opencode/src/server/routes/tui.ts b/packages/opencode/src/server/routes/tui.ts index 0577429dd74..8650a0cccf7 100644 --- a/packages/opencode/src/server/routes/tui.ts +++ b/packages/opencode/src/server/routes/tui.ts @@ -275,6 +275,8 @@ export const TuiRoutes = lazy(() => session_compact: "session.compact", messages_page_up: "session.page.up", messages_page_down: "session.page.down", + messages_line_up: "session.line.up", + messages_line_down: "session.line.down", messages_half_page_up: "session.half.page.up", messages_half_page_down: "session.half.page.down", messages_first: "session.first", diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 32f33f66219..ca13e5e93cf 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -842,6 +842,14 @@ export type KeybindsConfig = { * 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 */ diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index e47c4f5f7f1..40bbf3feb6f 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1019,6 +1019,14 @@ export type KeybindsConfig = { * 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 */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 0dc174c1b0a..6a3f5b2f41e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8282,12 +8282,22 @@ }, "messages_page_up": { "description": "Scroll messages up by one page", - "default": "pageup", + "default": "pageup,ctrl+alt+b", "type": "string" }, "messages_page_down": { "description": "Scroll messages down by one page", - "default": "pagedown", + "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": { diff --git a/packages/web/src/content/docs/keybinds.mdx b/packages/web/src/content/docs/keybinds.mdx index 267d194c099..51508a4f864 100644 --- a/packages/web/src/content/docs/keybinds.mdx +++ b/packages/web/src/content/docs/keybinds.mdx @@ -31,8 +31,10 @@ OpenCode has a list of keybinds that you can customize through the OpenCode conf "session_child_cycle": "right", "session_child_cycle_reverse": "left", "session_parent": "up", - "messages_page_up": "pageup", - "messages_page_down": "pagedown", + "messages_page_up": "pageup,ctrl+alt+b", + "messages_page_down": "pagedown,ctrl+alt+f", + "messages_line_up": "ctrl+alt+y", + "messages_line_down": "ctrl+alt+e", "messages_half_page_up": "ctrl+alt+u", "messages_half_page_down": "ctrl+alt+d", "messages_first": "ctrl+g,home", From 073f9d99b58339e15e927ff4372d27d0f8ade5f2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 18 Jan 2026 03:55:03 +0000 Subject: [PATCH 076/192] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 2 ++ packages/sdk/openapi.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 40bbf3feb6f..912001c82cd 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -651,6 +651,8 @@ export type EventTuiCommandExecute = { | "session.compact" | "session.page.up" | "session.page.down" + | "session.line.up" + | "session.line.down" | "session.half.page.up" | "session.half.page.down" | "session.first" diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 6a3f5b2f41e..f0af6448cca 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7411,6 +7411,8 @@ "session.compact", "session.page.up", "session.page.down", + "session.line.up", + "session.line.down", "session.half.page.up", "session.half.page.down", "session.first", From 10433cb45b6ed932368fb147032d671eaed0d273 Mon Sep 17 00:00:00 2001 From: Patrick Schiel Date: Sun, 18 Jan 2026 07:30:45 +0100 Subject: [PATCH 077/192] fix(windows): fix jdtls download on Windows (#9195) --- packages/opencode/src/lsp/server.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 24da77edcfe..e7efd99dcbd 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1157,10 +1157,24 @@ export namespace LSPServer { await fs.mkdir(distPath, { recursive: true }) const releaseURL = "https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz" - const archivePath = path.join(distPath, "release.tar.gz") - await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow() - await $`tar -xzf ${archivePath}`.cwd(distPath).quiet().nothrow() - await fs.rm(archivePath, { force: true }) + const archiveName = "release.tar.gz" + + log.info("Downloading JDTLS archive", { url: releaseURL, dest: distPath }) + const curlResult = await $`curl -L -o ${archiveName} '${releaseURL}'`.cwd(distPath).quiet().nothrow() + if (curlResult.exitCode !== 0) { + log.error("Failed to download JDTLS", { exitCode: curlResult.exitCode, stderr: curlResult.stderr.toString() }) + return + } + + log.info("Extracting JDTLS archive") + const tarResult = await $`tar -xzf ${archiveName}`.cwd(distPath).quiet().nothrow() + if (tarResult.exitCode !== 0) { + log.error("Failed to extract JDTLS", { exitCode: tarResult.exitCode, stderr: tarResult.stderr.toString() }) + return + } + + await fs.rm(path.join(distPath, archiveName), { force: true }) + log.info("JDTLS download and extraction completed") } const jarFileName = await $`ls org.eclipse.equinox.launcher_*.jar` .cwd(launcherDir) From b7ad6bd83922e2259a467fe59f27806af8060629 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:35:09 -0800 Subject: [PATCH 078/192] feat: apply_patch tool for openai models (#9127) --- packages/opencode/src/cli/cmd/debug/agent.ts | 4 +- .../src/cli/cmd/tui/routes/session/index.tsx | 80 ++- packages/opencode/src/patch/index.ts | 81 ++- .../src/server/routes/experimental.ts | 4 +- packages/opencode/src/session/prompt.ts | 5 +- .../opencode/src/session/prompt/codex.txt | 1 + packages/opencode/src/tool/apply_patch.ts | 277 ++++++++++ packages/opencode/src/tool/apply_patch.txt | 1 + packages/opencode/src/tool/batch.ts | 2 +- packages/opencode/src/tool/patch.ts | 201 ------- packages/opencode/src/tool/patch.txt | 1 - packages/opencode/src/tool/registry.ts | 19 +- .../opencode/test/tool/apply_patch.test.ts | 515 ++++++++++++++++++ packages/opencode/test/tool/patch.test.ts | 261 --------- packages/ui/src/components/message-part.css | 72 +++ packages/ui/src/components/message-part.tsx | 94 ++++ 16 files changed, 1122 insertions(+), 496 deletions(-) create mode 100644 packages/opencode/src/tool/apply_patch.ts create mode 100644 packages/opencode/src/tool/apply_patch.txt delete mode 100644 packages/opencode/src/tool/patch.ts delete mode 100644 packages/opencode/src/tool/patch.txt create mode 100644 packages/opencode/test/tool/apply_patch.test.ts delete mode 100644 packages/opencode/test/tool/patch.test.ts diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts index ef6b0c4fc92..d1236ff40bc 100644 --- a/packages/opencode/src/cli/cmd/debug/agent.ts +++ b/packages/opencode/src/cli/cmd/debug/agent.ts @@ -70,8 +70,8 @@ export const AgentCommand = cmd({ }) async function getAvailableTools(agent: Agent.Info) { - const providerID = agent.model?.providerID ?? (await Provider.defaultModel()).providerID - return ToolRegistry.tools(providerID, agent) + const model = agent.model ?? (await Provider.defaultModel()) + return ToolRegistry.tools(model, agent) } async function resolveTools(agent: Agent.Info, availableTools: Awaited>) { 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 1842a955d2a..1294ab849e9 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -39,7 +39,7 @@ import { TodoWriteTool } from "@/tool/todo" import type { GrepTool } from "@/tool/grep" import type { ListTool } from "@/tool/ls" import type { EditTool } from "@/tool/edit" -import type { PatchTool } from "@/tool/patch" +import type { ApplyPatchTool } from "@/tool/apply_patch" import type { WebFetchTool } from "@/tool/webfetch" import type { TaskTool } from "@/tool/task" import type { QuestionTool } from "@/tool/question" @@ -1445,8 +1445,8 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess - - + + @@ -1895,20 +1895,74 @@ function Edit(props: ToolProps) { ) } -function Patch(props: ToolProps) { - const { theme } = useTheme() +function ApplyPatch(props: ToolProps) { + const ctx = use() + const { theme, syntax } = useTheme() + + const files = createMemo(() => props.metadata.files ?? []) + + const view = createMemo(() => { + const diffStyle = ctx.sync.data.config.tui?.diff_style + if (diffStyle === "stacked") return "unified" + return ctx.width > 120 ? "split" : "unified" + }) + + function Diff(p: { diff: string; filePath: string }) { + return ( + + + + ) + } + + function title(file: { type: string; relativePath: string; filePath: string; deletions: number }) { + if (file.type === "delete") return "# Deleted " + file.relativePath + if (file.type === "add") return "# Created " + file.relativePath + if (file.type === "move") return "# Moved " + normalizePath(file.filePath) + " → " + file.relativePath + return "← Patched " + file.relativePath + } + return ( - - - - {props.output?.trim()} - - + 0}> + + {(file) => ( + + + -{file.deletions} line{file.deletions !== 1 ? "s" : ""} + + } + > + + + + )} + - - Patch + + apply_patch diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts index 91d52065f6f..888a4d94b89 100644 --- a/packages/opencode/src/patch/index.ts +++ b/packages/opencode/src/patch/index.ts @@ -177,8 +177,18 @@ export namespace Patch { return { content, nextIdx: i } } + function stripHeredoc(input: string): string { + // Match heredoc patterns like: cat <<'EOF'\n...\nEOF or < 0 && pattern[pattern.length - 1] === "") { @@ -371,7 +381,7 @@ export namespace Patch { if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") { newSlice = newSlice.slice(0, -1) } - found = seekSequence(originalLines, pattern, lineIndex) + found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file) } if (found !== -1) { @@ -407,28 +417,75 @@ export namespace Patch { return result } - function seekSequence(lines: string[], pattern: string[], startIndex: number): number { - if (pattern.length === 0) return -1 + // Normalize Unicode punctuation to ASCII equivalents (like Rust's normalize_unicode) + function normalizeUnicode(str: string): string { + return str + .replace(/[\u2018\u2019\u201A\u201B]/g, "'") // single quotes + .replace(/[\u201C\u201D\u201E\u201F]/g, '"') // double quotes + .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015]/g, "-") // dashes + .replace(/\u2026/g, "...") // ellipsis + .replace(/\u00A0/g, " ") // non-breaking space + } + + type Comparator = (a: string, b: string) => boolean + + function tryMatch(lines: string[], pattern: string[], startIndex: number, compare: Comparator, eof: boolean): number { + // If EOF anchor, try matching from end of file first + if (eof) { + const fromEnd = lines.length - pattern.length + if (fromEnd >= startIndex) { + let matches = true + for (let j = 0; j < pattern.length; j++) { + if (!compare(lines[fromEnd + j], pattern[j])) { + matches = false + break + } + } + if (matches) return fromEnd + } + } - // Simple substring search implementation + // Forward search from startIndex for (let i = startIndex; i <= lines.length - pattern.length; i++) { let matches = true - for (let j = 0; j < pattern.length; j++) { - if (lines[i + j] !== pattern[j]) { + if (!compare(lines[i + j], pattern[j])) { matches = false break } } - - if (matches) { - return i - } + if (matches) return i } return -1 } + function seekSequence(lines: string[], pattern: string[], startIndex: number, eof = false): number { + if (pattern.length === 0) return -1 + + // Pass 1: exact match + const exact = tryMatch(lines, pattern, startIndex, (a, b) => a === b, eof) + if (exact !== -1) return exact + + // Pass 2: rstrip (trim trailing whitespace) + const rstrip = tryMatch(lines, pattern, startIndex, (a, b) => a.trimEnd() === b.trimEnd(), eof) + if (rstrip !== -1) return rstrip + + // Pass 3: trim (both ends) + const trim = tryMatch(lines, pattern, startIndex, (a, b) => a.trim() === b.trim(), eof) + if (trim !== -1) return trim + + // Pass 4: normalized (Unicode punctuation to ASCII) + const normalized = tryMatch( + lines, + pattern, + startIndex, + (a, b) => normalizeUnicode(a.trim()) === normalizeUnicode(b.trim()), + eof, + ) + return normalized + } + function generateUnifiedDiff(oldContent: string, newContent: string): string { const oldLines = oldContent.split("\n") const newLines = newContent.split("\n") diff --git a/packages/opencode/src/server/routes/experimental.ts b/packages/opencode/src/server/routes/experimental.ts index c6b1d42e8e5..0fb2a5e9d2e 100644 --- a/packages/opencode/src/server/routes/experimental.ts +++ b/packages/opencode/src/server/routes/experimental.ts @@ -74,8 +74,8 @@ export const ExperimentalRoutes = lazy(() => }), ), async (c) => { - const { provider } = c.req.valid("query") - const tools = await ToolRegistry.tools(provider) + const { provider, model } = c.req.valid("query") + const tools = await ToolRegistry.tools({ providerID: provider, modelID: model }) return c.json( tools.map((t) => ({ id: t.id, diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 8327698fd5f..0d3d25feb8d 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -685,7 +685,10 @@ export namespace SessionPrompt { }, }) - for (const item of await ToolRegistry.tools(input.model.providerID, input.agent)) { + for (const item of await ToolRegistry.tools( + { modelID: input.model.api.id, providerID: input.model.providerID }, + input.agent, + )) { const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters)) tools[item.id] = tool({ id: item.id as any, diff --git a/packages/opencode/src/session/prompt/codex.txt b/packages/opencode/src/session/prompt/codex.txt index d26e2e01aa7..daad8237758 100644 --- a/packages/opencode/src/session/prompt/codex.txt +++ b/packages/opencode/src/session/prompt/codex.txt @@ -5,6 +5,7 @@ You are an interactive CLI tool that helps users with software engineering tasks ## Editing constraints - Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them. - Only add comments if they are necessary to make a non-obvious block easier to understand. +- Try to use apply_patch for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use apply_patch for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase). ## Tool usage - Prefer specialized tools over shell for file operations: diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts new file mode 100644 index 00000000000..d070eaefa97 --- /dev/null +++ b/packages/opencode/src/tool/apply_patch.ts @@ -0,0 +1,277 @@ +import z from "zod" +import * as path from "path" +import * as fs from "fs/promises" +import { Tool } from "./tool" +import { FileTime } from "../file/time" +import { Bus } from "../bus" +import { FileWatcher } from "../file/watcher" +import { Instance } from "../project/instance" +import { Patch } from "../patch" +import { createTwoFilesPatch, diffLines } from "diff" +import { assertExternalDirectory } from "./external-directory" +import { trimDiff } from "./edit" +import { LSP } from "../lsp" +import { Filesystem } from "../util/filesystem" + +const PatchParams = z.object({ + patchText: z.string().describe("The full patch text that describes all changes to be made"), +}) + +export const ApplyPatchTool = Tool.define("apply_patch", { + description: "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.", + parameters: PatchParams, + async execute(params, ctx) { + if (!params.patchText) { + throw new Error("patchText is required") + } + + // Parse the patch to get hunks + let hunks: Patch.Hunk[] + try { + const parseResult = Patch.parsePatch(params.patchText) + hunks = parseResult.hunks + } catch (error) { + throw new Error(`apply_patch verification failed: ${error}`) + } + + if (hunks.length === 0) { + const normalized = params.patchText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim() + if (normalized === "*** Begin Patch\n*** End Patch") { + throw new Error("patch rejected: empty patch") + } + throw new Error("apply_patch verification failed: no hunks found") + } + + // Validate file paths and check permissions + const fileChanges: Array<{ + filePath: string + oldContent: string + newContent: string + type: "add" | "update" | "delete" | "move" + movePath?: string + diff: string + additions: number + deletions: number + }> = [] + + let totalDiff = "" + + for (const hunk of hunks) { + const filePath = path.resolve(Instance.directory, hunk.path) + await assertExternalDirectory(ctx, filePath) + + switch (hunk.type) { + case "add": { + const oldContent = "" + const newContent = + hunk.contents.length === 0 || hunk.contents.endsWith("\n") ? hunk.contents : `${hunk.contents}\n` + const diff = trimDiff(createTwoFilesPatch(filePath, filePath, oldContent, newContent)) + + let additions = 0 + let deletions = 0 + for (const change of diffLines(oldContent, newContent)) { + if (change.added) additions += change.count || 0 + if (change.removed) deletions += change.count || 0 + } + + fileChanges.push({ + filePath, + oldContent, + newContent, + type: "add", + diff, + additions, + deletions, + }) + + totalDiff += diff + "\n" + break + } + + case "update": { + // Check if file exists for update + const stats = await fs.stat(filePath).catch(() => null) + if (!stats || stats.isDirectory()) { + throw new Error(`apply_patch verification failed: Failed to read file to update: ${filePath}`) + } + + // Read file and update time tracking (like edit tool does) + await FileTime.assert(ctx.sessionID, filePath) + const oldContent = await fs.readFile(filePath, "utf-8") + let newContent = oldContent + + // Apply the update chunks to get new content + try { + const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks) + newContent = fileUpdate.content + } catch (error) { + throw new Error(`apply_patch verification failed: ${error}`) + } + + const diff = trimDiff(createTwoFilesPatch(filePath, filePath, oldContent, newContent)) + + let additions = 0 + let deletions = 0 + for (const change of diffLines(oldContent, newContent)) { + if (change.added) additions += change.count || 0 + if (change.removed) deletions += change.count || 0 + } + + const movePath = hunk.move_path ? path.resolve(Instance.directory, hunk.move_path) : undefined + await assertExternalDirectory(ctx, movePath) + + fileChanges.push({ + filePath, + oldContent, + newContent, + type: hunk.move_path ? "move" : "update", + movePath, + diff, + additions, + deletions, + }) + + totalDiff += diff + "\n" + break + } + + case "delete": { + const contentToDelete = await fs.readFile(filePath, "utf-8").catch((error) => { + throw new Error(`apply_patch verification failed: ${error}`) + }) + const deleteDiff = trimDiff(createTwoFilesPatch(filePath, filePath, contentToDelete, "")) + + const deletions = contentToDelete.split("\n").length + + fileChanges.push({ + filePath, + oldContent: contentToDelete, + newContent: "", + type: "delete", + diff: deleteDiff, + additions: 0, + deletions, + }) + + totalDiff += deleteDiff + "\n" + break + } + } + } + + // Check permissions if needed + await ctx.ask({ + permission: "edit", + patterns: fileChanges.map((c) => path.relative(Instance.worktree, c.filePath)), + always: ["*"], + metadata: { + diff: totalDiff, + }, + }) + + // Apply the changes + const changedFiles: string[] = [] + + for (const change of fileChanges) { + switch (change.type) { + case "add": + // Create parent directories (recursive: true is safe on existing/root dirs) + await fs.mkdir(path.dirname(change.filePath), { recursive: true }) + await fs.writeFile(change.filePath, change.newContent, "utf-8") + changedFiles.push(change.filePath) + break + + case "update": + await fs.writeFile(change.filePath, change.newContent, "utf-8") + changedFiles.push(change.filePath) + break + + case "move": + if (change.movePath) { + // Create parent directories (recursive: true is safe on existing/root dirs) + await fs.mkdir(path.dirname(change.movePath), { recursive: true }) + await fs.writeFile(change.movePath, change.newContent, "utf-8") + await fs.unlink(change.filePath) + changedFiles.push(change.movePath) + } + break + + case "delete": + await fs.unlink(change.filePath) + changedFiles.push(change.filePath) + break + } + + // Update file time tracking + FileTime.read(ctx.sessionID, change.filePath) + if (change.movePath) { + FileTime.read(ctx.sessionID, change.movePath) + } + } + + // Publish file change events + for (const filePath of changedFiles) { + await Bus.publish(FileWatcher.Event.Updated, { file: filePath, event: "change" }) + } + + // Notify LSP of file changes and collect diagnostics + for (const change of fileChanges) { + if (change.type === "delete") continue + const target = change.movePath ?? change.filePath + await LSP.touchFile(target, true) + } + const diagnostics = await LSP.diagnostics() + + // Generate output summary + const summaryLines = fileChanges.map((change) => { + if (change.type === "add") { + return `A ${path.relative(Instance.worktree, change.filePath)}` + } + if (change.type === "delete") { + return `D ${path.relative(Instance.worktree, change.filePath)}` + } + const target = change.movePath ?? change.filePath + return `M ${path.relative(Instance.worktree, target)}` + }) + let output = `Success. Updated the following files:\n${summaryLines.join("\n")}` + + // Report LSP errors for changed files + const MAX_DIAGNOSTICS_PER_FILE = 20 + for (const change of fileChanges) { + if (change.type === "delete") continue + const target = change.movePath ?? change.filePath + const normalized = Filesystem.normalizePath(target) + const issues = diagnostics[normalized] ?? [] + const errors = issues.filter((item) => item.severity === 1) + if (errors.length > 0) { + const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE) + const suffix = + errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : "" + output += `\n\nLSP errors detected in ${path.relative(Instance.worktree, target)}, please fix:\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n` + } + } + + // Build per-file metadata for UI rendering + const files = fileChanges.map((change) => ({ + filePath: change.filePath, + relativePath: path.relative(Instance.worktree, change.movePath ?? change.filePath), + type: change.type, + diff: change.diff, + before: change.oldContent, + after: change.newContent, + additions: change.additions, + deletions: change.deletions, + movePath: change.movePath, + })) + + return { + title: output, + metadata: { + diff: totalDiff, + files, + diagnostics, + }, + output, + } + }, +}) diff --git a/packages/opencode/src/tool/apply_patch.txt b/packages/opencode/src/tool/apply_patch.txt new file mode 100644 index 00000000000..1af0606109f --- /dev/null +++ b/packages/opencode/src/tool/apply_patch.txt @@ -0,0 +1 @@ +Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON. diff --git a/packages/opencode/src/tool/batch.ts b/packages/opencode/src/tool/batch.ts index ba1b94a3e60..8bffbd54a28 100644 --- a/packages/opencode/src/tool/batch.ts +++ b/packages/opencode/src/tool/batch.ts @@ -37,7 +37,7 @@ export const BatchTool = Tool.define("batch", async () => { const discardedCalls = params.tool_calls.slice(10) const { ToolRegistry } = await import("./registry") - const availableTools = await ToolRegistry.tools("") + const availableTools = await ToolRegistry.tools({ modelID: "", providerID: "" }) const toolMap = new Map(availableTools.map((t) => [t.id, t])) const executeCall = async (call: (typeof toolCalls)[0]) => { diff --git a/packages/opencode/src/tool/patch.ts b/packages/opencode/src/tool/patch.ts deleted file mode 100644 index 08a58bfea9c..00000000000 --- a/packages/opencode/src/tool/patch.ts +++ /dev/null @@ -1,201 +0,0 @@ -import z from "zod" -import * as path from "path" -import * as fs from "fs/promises" -import { Tool } from "./tool" -import { FileTime } from "../file/time" -import { Bus } from "../bus" -import { FileWatcher } from "../file/watcher" -import { Instance } from "../project/instance" -import { Patch } from "../patch" -import { createTwoFilesPatch } from "diff" -import { assertExternalDirectory } from "./external-directory" - -const PatchParams = z.object({ - patchText: z.string().describe("The full patch text that describes all changes to be made"), -}) - -export const PatchTool = Tool.define("patch", { - description: - "Apply a patch to modify multiple files. Supports adding, updating, and deleting files with context-aware changes.", - parameters: PatchParams, - async execute(params, ctx) { - if (!params.patchText) { - throw new Error("patchText is required") - } - - // Parse the patch to get hunks - let hunks: Patch.Hunk[] - try { - const parseResult = Patch.parsePatch(params.patchText) - hunks = parseResult.hunks - } catch (error) { - throw new Error(`Failed to parse patch: ${error}`) - } - - if (hunks.length === 0) { - throw new Error("No file changes found in patch") - } - - // Validate file paths and check permissions - const fileChanges: Array<{ - filePath: string - oldContent: string - newContent: string - type: "add" | "update" | "delete" | "move" - movePath?: string - }> = [] - - let totalDiff = "" - - for (const hunk of hunks) { - const filePath = path.resolve(Instance.directory, hunk.path) - await assertExternalDirectory(ctx, filePath) - - switch (hunk.type) { - case "add": - if (hunk.type === "add") { - const oldContent = "" - const newContent = hunk.contents - const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent) - - fileChanges.push({ - filePath, - oldContent, - newContent, - type: "add", - }) - - totalDiff += diff + "\n" - } - break - - case "update": - // Check if file exists for update - const stats = await fs.stat(filePath).catch(() => null) - if (!stats || stats.isDirectory()) { - throw new Error(`File not found or is directory: ${filePath}`) - } - - // Read file and update time tracking (like edit tool does) - await FileTime.assert(ctx.sessionID, filePath) - const oldContent = await fs.readFile(filePath, "utf-8") - let newContent = oldContent - - // Apply the update chunks to get new content - try { - const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks) - newContent = fileUpdate.content - } catch (error) { - throw new Error(`Failed to apply update to ${filePath}: ${error}`) - } - - const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent) - - const movePath = hunk.move_path ? path.resolve(Instance.directory, hunk.move_path) : undefined - await assertExternalDirectory(ctx, movePath) - - fileChanges.push({ - filePath, - oldContent, - newContent, - type: hunk.move_path ? "move" : "update", - movePath, - }) - - totalDiff += diff + "\n" - break - - case "delete": - // Check if file exists for deletion - await FileTime.assert(ctx.sessionID, filePath) - const contentToDelete = await fs.readFile(filePath, "utf-8") - const deleteDiff = createTwoFilesPatch(filePath, filePath, contentToDelete, "") - - fileChanges.push({ - filePath, - oldContent: contentToDelete, - newContent: "", - type: "delete", - }) - - totalDiff += deleteDiff + "\n" - break - } - } - - // Check permissions if needed - await ctx.ask({ - permission: "edit", - patterns: fileChanges.map((c) => path.relative(Instance.worktree, c.filePath)), - always: ["*"], - metadata: { - diff: totalDiff, - }, - }) - - // Apply the changes - const changedFiles: string[] = [] - - for (const change of fileChanges) { - switch (change.type) { - case "add": - // Create parent directories - const addDir = path.dirname(change.filePath) - if (addDir !== "." && addDir !== "/") { - await fs.mkdir(addDir, { recursive: true }) - } - await fs.writeFile(change.filePath, change.newContent, "utf-8") - changedFiles.push(change.filePath) - break - - case "update": - await fs.writeFile(change.filePath, change.newContent, "utf-8") - changedFiles.push(change.filePath) - break - - case "move": - if (change.movePath) { - // Create parent directories for destination - const moveDir = path.dirname(change.movePath) - if (moveDir !== "." && moveDir !== "/") { - await fs.mkdir(moveDir, { recursive: true }) - } - // Write to new location - await fs.writeFile(change.movePath, change.newContent, "utf-8") - // Remove original - await fs.unlink(change.filePath) - changedFiles.push(change.movePath) - } - break - - case "delete": - await fs.unlink(change.filePath) - changedFiles.push(change.filePath) - break - } - - // Update file time tracking - FileTime.read(ctx.sessionID, change.filePath) - if (change.movePath) { - FileTime.read(ctx.sessionID, change.movePath) - } - } - - // Publish file change events - for (const filePath of changedFiles) { - await Bus.publish(FileWatcher.Event.Updated, { file: filePath, event: "change" }) - } - - // Generate output summary - const relativePaths = changedFiles.map((filePath) => path.relative(Instance.worktree, filePath)) - const summary = `${fileChanges.length} files changed` - - return { - title: summary, - metadata: { - diff: totalDiff, - }, - output: `Patch applied successfully. ${summary}:\n${relativePaths.map((p) => ` ${p}`).join("\n")}`, - } - }, -}) diff --git a/packages/opencode/src/tool/patch.txt b/packages/opencode/src/tool/patch.txt deleted file mode 100644 index 88a50f6347a..00000000000 --- a/packages/opencode/src/tool/patch.txt +++ /dev/null @@ -1 +0,0 @@ -do not use diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 35e378f080b..faa5f72bcce 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -26,6 +26,7 @@ import { Log } from "@/util/log" import { LspTool } from "./lsp" import { Truncate } from "./truncation" import { PlanExitTool, PlanEnterTool } from "./plan" +import { ApplyPatchTool } from "./apply_patch" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) @@ -108,6 +109,7 @@ export namespace ToolRegistry { WebSearchTool, CodeSearchTool, SkillTool, + ApplyPatchTool, ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), ...(config.experimental?.batch_tool === true ? [BatchTool] : []), ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []), @@ -119,15 +121,28 @@ export namespace ToolRegistry { return all().then((x) => x.map((t) => t.id)) } - export async function tools(providerID: string, agent?: Agent.Info) { + export async function tools( + model: { + providerID: string + modelID: string + }, + agent?: Agent.Info, + ) { const tools = await all() const result = await Promise.all( tools .filter((t) => { // Enable websearch/codesearch for zen users OR via enable flag if (t.id === "codesearch" || t.id === "websearch") { - return providerID === "opencode" || Flag.OPENCODE_ENABLE_EXA + return model.providerID === "opencode" || Flag.OPENCODE_ENABLE_EXA } + + // use apply tool in same format as codex + const usePatch = + model.modelID.includes("gpt-") && !model.modelID.includes("oss") && !model.modelID.includes("gpt-4") + if (t.id === "apply_patch") return usePatch + if (t.id === "edit" || t.id === "write") return !usePatch + return true }) .map(async (t) => { diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts new file mode 100644 index 00000000000..d8f05a9d911 --- /dev/null +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -0,0 +1,515 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import * as fs from "fs/promises" +import { ApplyPatchTool } from "../../src/tool/apply_patch" +import { Instance } from "../../src/project/instance" +import { FileTime } from "../../src/file/time" +import { tmpdir } from "../fixture/fixture" + +const baseCtx = { + sessionID: "test", + messageID: "", + callID: "", + agent: "build", + abort: AbortSignal.any([]), + metadata: () => {}, +} + +type AskInput = { + permission: string + patterns: string[] + always: string[] + metadata: { diff: string } +} + +type ToolCtx = typeof baseCtx & { + ask: (input: AskInput) => Promise +} + +const execute = async (params: { patchText: string }, ctx: ToolCtx) => { + const tool = await ApplyPatchTool.init() + return tool.execute(params, ctx) +} + +const makeCtx = () => { + const calls: AskInput[] = [] + const ctx: ToolCtx = { + ...baseCtx, + ask: async (input) => { + calls.push(input) + }, + } + + return { ctx, calls } +} + +describe("tool.apply_patch freeform", () => { + test("requires patchText", async () => { + const { ctx } = makeCtx() + await expect(execute({ patchText: "" }, ctx)).rejects.toThrow("patchText is required") + }) + + test("rejects invalid patch format", async () => { + const { ctx } = makeCtx() + await expect(execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow("apply_patch verification failed") + }) + + test("rejects empty patch", async () => { + const { ctx } = makeCtx() + const emptyPatch = "*** Begin Patch\n*** End Patch" + await expect(execute({ patchText: emptyPatch }, ctx)).rejects.toThrow("patch rejected: empty patch") + }) + + test("applies add/update/delete in one patch", async () => { + await using fixture = await tmpdir() + const { ctx, calls } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const modifyPath = path.join(fixture.path, "modify.txt") + const deletePath = path.join(fixture.path, "delete.txt") + await fs.writeFile(modifyPath, "line1\nline2\n", "utf-8") + await fs.writeFile(deletePath, "obsolete\n", "utf-8") + FileTime.read(ctx.sessionID, modifyPath) + FileTime.read(ctx.sessionID, deletePath) + + const patchText = + "*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Delete File: delete.txt\n*** Update File: modify.txt\n@@\n-line2\n+changed\n*** End Patch" + + const result = await execute({ patchText }, ctx) + + expect(result.title).toContain("Success. Updated the following files") + expect(result.output).toContain("Success. Updated the following files") + expect(result.metadata.diff).toContain("Index:") + expect(calls.length).toBe(1) + + const added = await fs.readFile(path.join(fixture.path, "nested", "new.txt"), "utf-8") + expect(added).toBe("created\n") + expect(await fs.readFile(modifyPath, "utf-8")).toBe("line1\nchanged\n") + await expect(fs.readFile(deletePath, "utf-8")).rejects.toThrow() + }, + }) + }) + + test("applies multiple hunks to one file", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "multi.txt") + await fs.writeFile(target, "line1\nline2\nline3\nline4\n", "utf-8") + FileTime.read(ctx.sessionID, target) + + const patchText = + "*** Begin Patch\n*** Update File: multi.txt\n@@\n-line2\n+changed2\n@@\n-line4\n+changed4\n*** End Patch" + + await execute({ patchText }, ctx) + + expect(await fs.readFile(target, "utf-8")).toBe("line1\nchanged2\nline3\nchanged4\n") + }, + }) + }) + + test("inserts lines with insert-only hunk", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "insert_only.txt") + await fs.writeFile(target, "alpha\nomega\n", "utf-8") + FileTime.read(ctx.sessionID, target) + + const patchText = "*** Begin Patch\n*** Update File: insert_only.txt\n@@\n alpha\n+beta\n omega\n*** End Patch" + + await execute({ patchText }, ctx) + + expect(await fs.readFile(target, "utf-8")).toBe("alpha\nbeta\nomega\n") + }, + }) + }) + + test("appends trailing newline on update", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "no_newline.txt") + await fs.writeFile(target, "no newline at end", "utf-8") + FileTime.read(ctx.sessionID, target) + + const patchText = + "*** Begin Patch\n*** Update File: no_newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch" + + await execute({ patchText }, ctx) + + const contents = await fs.readFile(target, "utf-8") + expect(contents.endsWith("\n")).toBe(true) + expect(contents).toBe("first line\nsecond line\n") + }, + }) + }) + + test("moves file to a new directory", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const original = path.join(fixture.path, "old", "name.txt") + await fs.mkdir(path.dirname(original), { recursive: true }) + await fs.writeFile(original, "old content\n", "utf-8") + FileTime.read(ctx.sessionID, original) + + const patchText = + "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-old content\n+new content\n*** End Patch" + + await execute({ patchText }, ctx) + + const moved = path.join(fixture.path, "renamed", "dir", "name.txt") + await expect(fs.readFile(original, "utf-8")).rejects.toThrow() + expect(await fs.readFile(moved, "utf-8")).toBe("new content\n") + }, + }) + }) + + test("moves file overwriting existing destination", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const original = path.join(fixture.path, "old", "name.txt") + const destination = path.join(fixture.path, "renamed", "dir", "name.txt") + await fs.mkdir(path.dirname(original), { recursive: true }) + await fs.mkdir(path.dirname(destination), { recursive: true }) + await fs.writeFile(original, "from\n", "utf-8") + await fs.writeFile(destination, "existing\n", "utf-8") + FileTime.read(ctx.sessionID, original) + + const patchText = + "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-from\n+new\n*** End Patch" + + await execute({ patchText }, ctx) + + await expect(fs.readFile(original, "utf-8")).rejects.toThrow() + expect(await fs.readFile(destination, "utf-8")).toBe("new\n") + }, + }) + }) + + test("adds file overwriting existing file", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "duplicate.txt") + await fs.writeFile(target, "old content\n", "utf-8") + + const patchText = "*** Begin Patch\n*** Add File: duplicate.txt\n+new content\n*** End Patch" + + await execute({ patchText }, ctx) + expect(await fs.readFile(target, "utf-8")).toBe("new content\n") + }, + }) + }) + + test("rejects update when target file is missing", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const patchText = "*** Begin Patch\n*** Update File: missing.txt\n@@\n-nope\n+better\n*** End Patch" + + await expect(execute({ patchText }, ctx)).rejects.toThrow( + "apply_patch verification failed: Failed to read file to update", + ) + }, + }) + }) + + test("rejects delete when file is missing", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const patchText = "*** Begin Patch\n*** Delete File: missing.txt\n*** End Patch" + + await expect(execute({ patchText }, ctx)).rejects.toThrow() + }, + }) + }) + + test("rejects delete when target is a directory", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const dirPath = path.join(fixture.path, "dir") + await fs.mkdir(dirPath) + + const patchText = "*** Begin Patch\n*** Delete File: dir\n*** End Patch" + + await expect(execute({ patchText }, ctx)).rejects.toThrow() + }, + }) + }) + + test("rejects invalid hunk header", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const patchText = "*** Begin Patch\n*** Frobnicate File: foo\n*** End Patch" + + await expect(execute({ patchText }, ctx)).rejects.toThrow("apply_patch verification failed") + }, + }) + }) + + test("rejects update with missing context", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "modify.txt") + await fs.writeFile(target, "line1\nline2\n", "utf-8") + FileTime.read(ctx.sessionID, target) + + const patchText = "*** Begin Patch\n*** Update File: modify.txt\n@@\n-missing\n+changed\n*** End Patch" + + await expect(execute({ patchText }, ctx)).rejects.toThrow("apply_patch verification failed") + expect(await fs.readFile(target, "utf-8")).toBe("line1\nline2\n") + }, + }) + }) + + test("verification failure leaves no side effects", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const patchText = + "*** Begin Patch\n*** Add File: created.txt\n+hello\n*** Update File: missing.txt\n@@\n-old\n+new\n*** End Patch" + + await expect(execute({ patchText }, ctx)).rejects.toThrow() + + const createdPath = path.join(fixture.path, "created.txt") + await expect(fs.readFile(createdPath, "utf-8")).rejects.toThrow() + }, + }) + }) + + test("supports end of file anchor", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "tail.txt") + await fs.writeFile(target, "alpha\nlast\n", "utf-8") + FileTime.read(ctx.sessionID, target) + + const patchText = "*** Begin Patch\n*** Update File: tail.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch" + + await execute({ patchText }, ctx) + expect(await fs.readFile(target, "utf-8")).toBe("alpha\nend\n") + }, + }) + }) + + test("rejects missing second chunk context", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "two_chunks.txt") + await fs.writeFile(target, "a\nb\nc\nd\n", "utf-8") + FileTime.read(ctx.sessionID, target) + + const patchText = "*** Begin Patch\n*** Update File: two_chunks.txt\n@@\n-b\n+B\n\n-d\n+D\n*** End Patch" + + await expect(execute({ patchText }, ctx)).rejects.toThrow() + expect(await fs.readFile(target, "utf-8")).toBe("a\nb\nc\nd\n") + }, + }) + }) + + test("disambiguates change context with @@ header", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "multi_ctx.txt") + await fs.writeFile(target, "fn a\nx=10\ny=2\nfn b\nx=10\ny=20\n", "utf-8") + FileTime.read(ctx.sessionID, target) + + const patchText = "*** Begin Patch\n*** Update File: multi_ctx.txt\n@@ fn b\n-x=10\n+x=11\n*** End Patch" + + await execute({ patchText }, ctx) + expect(await fs.readFile(target, "utf-8")).toBe("fn a\nx=10\ny=2\nfn b\nx=11\ny=20\n") + }, + }) + }) + + test("EOF anchor matches from end of file first", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "eof_anchor.txt") + // File has duplicate "marker" lines - one in middle, one at end + await fs.writeFile(target, "start\nmarker\nmiddle\nmarker\nend\n", "utf-8") + FileTime.read(ctx.sessionID, target) + + // With EOF anchor, should match the LAST "marker" line, not the first + const patchText = + "*** Begin Patch\n*** Update File: eof_anchor.txt\n@@\n-marker\n-end\n+marker-changed\n+end\n*** End of File\n*** End Patch" + + await execute({ patchText }, ctx) + // First marker unchanged, second marker changed + expect(await fs.readFile(target, "utf-8")).toBe("start\nmarker\nmiddle\nmarker-changed\nend\n") + }, + }) + }) + + test("parses heredoc-wrapped patch", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const patchText = `cat <<'EOF' +*** Begin Patch +*** Add File: heredoc_test.txt ++heredoc content +*** End Patch +EOF` + + await execute({ patchText }, ctx) + const content = await fs.readFile(path.join(fixture.path, "heredoc_test.txt"), "utf-8") + expect(content).toBe("heredoc content\n") + }, + }) + }) + + test("parses heredoc-wrapped patch without cat", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const patchText = `< { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "trailing_ws.txt") + // File has trailing spaces on some lines + await fs.writeFile(target, "line1 \nline2\nline3 \n", "utf-8") + FileTime.read(ctx.sessionID, target) + + // Patch doesn't have trailing spaces - should still match via rstrip pass + const patchText = "*** Begin Patch\n*** Update File: trailing_ws.txt\n@@\n-line2\n+changed\n*** End Patch" + + await execute({ patchText }, ctx) + expect(await fs.readFile(target, "utf-8")).toBe("line1 \nchanged\nline3 \n") + }, + }) + }) + + test("matches with leading whitespace differences", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "leading_ws.txt") + // File has leading spaces + await fs.writeFile(target, " line1\nline2\n line3\n", "utf-8") + FileTime.read(ctx.sessionID, target) + + // Patch without leading spaces - should match via trim pass + const patchText = "*** Begin Patch\n*** Update File: leading_ws.txt\n@@\n-line2\n+changed\n*** End Patch" + + await execute({ patchText }, ctx) + expect(await fs.readFile(target, "utf-8")).toBe(" line1\nchanged\n line3\n") + }, + }) + }) + + test("matches with Unicode punctuation differences", async () => { + await using fixture = await tmpdir() + const { ctx } = makeCtx() + + await Instance.provide({ + directory: fixture.path, + fn: async () => { + const target = path.join(fixture.path, "unicode.txt") + // File has fancy Unicode quotes (U+201C, U+201D) and em-dash (U+2014) + const leftQuote = "\u201C" + const rightQuote = "\u201D" + const emDash = "\u2014" + await fs.writeFile(target, `He said ${leftQuote}hello${rightQuote}\nsome${emDash}dash\nend\n`, "utf-8") + FileTime.read(ctx.sessionID, target) + + // Patch uses ASCII equivalents - should match via normalized pass + // The replacement uses ASCII quotes from the patch (not preserving Unicode) + const patchText = + '*** Begin Patch\n*** Update File: unicode.txt\n@@\n-He said "hello"\n+He said "hi"\n*** End Patch' + + await execute({ patchText }, ctx) + // Result has ASCII quotes because that's what the patch specifies + expect(await fs.readFile(target, "utf-8")).toBe(`He said "hi"\nsome${emDash}dash\nend\n`) + }, + }) + }) +}) diff --git a/packages/opencode/test/tool/patch.test.ts b/packages/opencode/test/tool/patch.test.ts deleted file mode 100644 index 3d3ec574e60..00000000000 --- a/packages/opencode/test/tool/patch.test.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { describe, expect, test } from "bun:test" -import path from "path" -import { PatchTool } from "../../src/tool/patch" -import { Instance } from "../../src/project/instance" -import { tmpdir } from "../fixture/fixture" -import { PermissionNext } from "../../src/permission/next" -import * as fs from "fs/promises" - -const ctx = { - sessionID: "test", - messageID: "", - callID: "", - agent: "build", - abort: AbortSignal.any([]), - metadata: () => {}, - ask: async () => {}, -} - -const patchTool = await PatchTool.init() - -describe("tool.patch", () => { - test("should validate required parameters", async () => { - await Instance.provide({ - directory: "/tmp", - fn: async () => { - expect(patchTool.execute({ patchText: "" }, ctx)).rejects.toThrow("patchText is required") - }, - }) - }) - - test("should validate patch format", async () => { - await Instance.provide({ - directory: "/tmp", - fn: async () => { - expect(patchTool.execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow("Failed to parse patch") - }, - }) - }) - - test("should handle empty patch", async () => { - await Instance.provide({ - directory: "/tmp", - fn: async () => { - const emptyPatch = `*** Begin Patch -*** End Patch` - - expect(patchTool.execute({ patchText: emptyPatch }, ctx)).rejects.toThrow("No file changes found in patch") - }, - }) - }) - - test.skip("should ask permission for files outside working directory", async () => { - await Instance.provide({ - directory: "/tmp", - fn: async () => { - const maliciousPatch = `*** Begin Patch -*** Add File: /etc/passwd -+malicious content -*** End Patch` - patchTool.execute({ patchText: maliciousPatch }, ctx) - // TODO: this sucks - await new Promise((resolve) => setTimeout(resolve, 1000)) - const pending = await PermissionNext.list() - expect(pending.find((p) => p.sessionID === ctx.sessionID)).toBeDefined() - }, - }) - }) - - test("should handle simple add file operation", async () => { - await using fixture = await tmpdir() - - await Instance.provide({ - directory: fixture.path, - fn: async () => { - const patchText = `*** Begin Patch -*** Add File: test-file.txt -+Hello World -+This is a test file -*** End Patch` - - const result = await patchTool.execute({ patchText }, ctx) - - expect(result.title).toContain("files changed") - expect(result.metadata.diff).toBeDefined() - expect(result.output).toContain("Patch applied successfully") - - // Verify file was created - const filePath = path.join(fixture.path, "test-file.txt") - const content = await fs.readFile(filePath, "utf-8") - expect(content).toBe("Hello World\nThis is a test file") - }, - }) - }) - - test("should handle file with context update", async () => { - await using fixture = await tmpdir() - - await Instance.provide({ - directory: fixture.path, - fn: async () => { - const patchText = `*** Begin Patch -*** Add File: config.js -+const API_KEY = "test-key" -+const DEBUG = false -+const VERSION = "1.0" -*** End Patch` - - const result = await patchTool.execute({ patchText }, ctx) - - expect(result.title).toContain("files changed") - expect(result.metadata.diff).toBeDefined() - expect(result.output).toContain("Patch applied successfully") - - // Verify file was created with correct content - const filePath = path.join(fixture.path, "config.js") - const content = await fs.readFile(filePath, "utf-8") - expect(content).toBe('const API_KEY = "test-key"\nconst DEBUG = false\nconst VERSION = "1.0"') - }, - }) - }) - - test("should handle multiple file operations", async () => { - await using fixture = await tmpdir() - - await Instance.provide({ - directory: fixture.path, - fn: async () => { - const patchText = `*** Begin Patch -*** Add File: file1.txt -+Content of file 1 -*** Add File: file2.txt -+Content of file 2 -*** Add File: file3.txt -+Content of file 3 -*** End Patch` - - const result = await patchTool.execute({ patchText }, ctx) - - expect(result.title).toContain("3 files changed") - expect(result.metadata.diff).toBeDefined() - expect(result.output).toContain("Patch applied successfully") - - // Verify all files were created - for (let i = 1; i <= 3; i++) { - const filePath = path.join(fixture.path, `file${i}.txt`) - const content = await fs.readFile(filePath, "utf-8") - expect(content).toBe(`Content of file ${i}`) - } - }, - }) - }) - - test("should create parent directories when adding nested files", async () => { - await using fixture = await tmpdir() - - await Instance.provide({ - directory: fixture.path, - fn: async () => { - const patchText = `*** Begin Patch -*** Add File: deep/nested/file.txt -+Deep nested content -*** End Patch` - - const result = await patchTool.execute({ patchText }, ctx) - - expect(result.title).toContain("files changed") - expect(result.output).toContain("Patch applied successfully") - - // Verify nested file was created - const nestedPath = path.join(fixture.path, "deep", "nested", "file.txt") - const exists = await fs - .access(nestedPath) - .then(() => true) - .catch(() => false) - expect(exists).toBe(true) - - const content = await fs.readFile(nestedPath, "utf-8") - expect(content).toBe("Deep nested content") - }, - }) - }) - - test("should generate proper unified diff in metadata", async () => { - await using fixture = await tmpdir() - - await Instance.provide({ - directory: fixture.path, - fn: async () => { - // First create a file with simple content - const patchText1 = `*** Begin Patch -*** Add File: test.txt -+line 1 -+line 2 -+line 3 -*** End Patch` - - await patchTool.execute({ patchText: patchText1 }, ctx) - - // Now create an update patch - const patchText2 = `*** Begin Patch -*** Update File: test.txt -@@ - line 1 --line 2 -+line 2 updated - line 3 -*** End Patch` - - const result = await patchTool.execute({ patchText: patchText2 }, ctx) - - expect(result.metadata.diff).toBeDefined() - expect(result.metadata.diff).toContain("@@") - expect(result.metadata.diff).toContain("-line 2") - expect(result.metadata.diff).toContain("+line 2 updated") - }, - }) - }) - - test("should handle complex patch with multiple operations", async () => { - await using fixture = await tmpdir() - - await Instance.provide({ - directory: fixture.path, - fn: async () => { - const patchText = `*** Begin Patch -*** Add File: new.txt -+This is a new file -+with multiple lines -*** Add File: existing.txt -+old content -+new line -+more content -*** Add File: config.json -+{ -+ "version": "1.0", -+ "debug": true -+} -*** End Patch` - - const result = await patchTool.execute({ patchText }, ctx) - - expect(result.title).toContain("3 files changed") - expect(result.metadata.diff).toBeDefined() - expect(result.output).toContain("Patch applied successfully") - - // Verify all files were created - const newPath = path.join(fixture.path, "new.txt") - const newContent = await fs.readFile(newPath, "utf-8") - expect(newContent).toBe("This is a new file\nwith multiple lines") - - const existingPath = path.join(fixture.path, "existing.txt") - const existingContent = await fs.readFile(existingPath, "utf-8") - expect(existingContent).toBe("old content\nnew line\nmore content") - - const configPath = path.join(fixture.path, "config.json") - const configContent = await fs.readFile(configPath, "utf-8") - expect(configContent).toBe('{\n "version": "1.0",\n "debug": true\n}') - }, - }) - }) -}) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 4a249ec4f42..184565e9cb3 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -689,3 +689,75 @@ } } } + +[data-component="apply-patch-files"] { + display: flex; + flex-direction: column; +} + +[data-component="apply-patch-file"] { + display: flex; + flex-direction: column; + border-top: 1px solid var(--border-weaker-base); + + &:first-child { + border-top: 1px solid var(--border-weaker-base); + } + + [data-slot="apply-patch-file-header"] { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background-color: var(--surface-inset-base); + } + + [data-slot="apply-patch-file-action"] { + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + color: var(--text-base); + flex-shrink: 0; + + &[data-type="delete"] { + color: var(--text-critical-base); + } + + &[data-type="add"] { + color: var(--text-success-base); + } + + &[data-type="move"] { + color: var(--text-warning-base); + } + } + + [data-slot="apply-patch-file-path"] { + font-family: var(--font-family-mono); + font-size: var(--font-size-small); + color: var(--text-weak); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-grow: 1; + } + + [data-slot="apply-patch-deletion-count"] { + font-family: var(--font-family-mono); + font-size: var(--font-size-small); + color: var(--text-critical-base); + flex-shrink: 0; + } +} + +[data-component="apply-patch-file-diff"] { + max-height: 420px; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } +} diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 165f46f6c50..47403786b22 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -233,6 +233,12 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo { title: "Write", subtitle: input.filePath ? getFilename(input.filePath) : undefined, } + case "apply_patch": + return { + icon: "code-lines", + title: "Patch", + subtitle: input.files?.length ? `${input.files.length} file${input.files.length > 1 ? "s" : ""}` : undefined, + } case "todowrite": return { icon: "checklist", @@ -1027,6 +1033,94 @@ ToolRegistry.register({ }, }) +interface ApplyPatchFile { + filePath: string + relativePath: string + type: "add" | "update" | "delete" | "move" + diff: string + before: string + after: string + additions: number + deletions: number + movePath?: string +} + +ToolRegistry.register({ + name: "apply_patch", + render(props) { + const diffComponent = useDiffComponent() + const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[]) + + const subtitle = createMemo(() => { + const count = files().length + if (count === 0) return "" + return `${count} file${count > 1 ? "s" : ""}` + }) + + return ( + + 0}> +
+ + {(file) => ( +
+
+ + + + Deleted + + + + + Created + + + + + Moved + + + + + Patched + + + + {file.relativePath} + + + + + -{file.deletions} + +
+ +
+ +
+
+
+ )} +
+
+
+
+ ) + }, +}) + ToolRegistry.register({ name: "todowrite", render(props) { From 90f848fbc697c6ec5c52edc84bbde33c0fa6a560 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 18 Jan 2026 06:35:48 +0000 Subject: [PATCH 079/192] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 18 +++--- packages/sdk/openapi.json | 76 ++++++++++++------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 912001c82cd..060a00e6087 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -633,6 +633,14 @@ export type EventTodoUpdated = { } } +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + export type EventTuiPromptAppend = { type: "tui.prompt.append" properties: { @@ -791,14 +799,6 @@ export type EventSessionError = { } } -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - export type EventVcsBranchUpdated = { type: "vcs.branch.updated" properties: { @@ -880,6 +880,7 @@ export type Event = | EventQuestionRejected | EventSessionCompacted | EventTodoUpdated + | EventFileWatcherUpdated | EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow @@ -892,7 +893,6 @@ export type Event = | EventSessionDeleted | EventSessionDiff | EventSessionError - | EventFileWatcherUpdated | EventVcsBranchUpdated | EventPtyCreated | EventPtyUpdated diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index f0af6448cca..a307a666edc 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7370,6 +7370,41 @@ }, "required": ["type", "properties"] }, + "Event.file.watcher.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "file.watcher.updated" + }, + "properties": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "event": { + "anyOf": [ + { + "type": "string", + "const": "add" + }, + { + "type": "string", + "const": "change" + }, + { + "type": "string", + "const": "unlink" + } + ] + } + }, + "required": ["file", "event"] + } + }, + "required": ["type", "properties"] + }, "Event.tui.prompt.append": { "type": "object", "properties": { @@ -7798,41 +7833,6 @@ }, "required": ["type", "properties"] }, - "Event.file.watcher.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "file.watcher.updated" - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "anyOf": [ - { - "type": "string", - "const": "add" - }, - { - "type": "string", - "const": "change" - }, - { - "type": "string", - "const": "unlink" - } - ] - } - }, - "required": ["file", "event"] - } - }, - "required": ["type", "properties"] - }, "Event.vcs.branch.updated": { "type": "object", "properties": { @@ -8054,6 +8054,9 @@ { "$ref": "#/components/schemas/Event.todo.updated" }, + { + "$ref": "#/components/schemas/Event.file.watcher.updated" + }, { "$ref": "#/components/schemas/Event.tui.prompt.append" }, @@ -8090,9 +8093,6 @@ { "$ref": "#/components/schemas/Event.session.error" }, - { - "$ref": "#/components/schemas/Event.file.watcher.updated" - }, { "$ref": "#/components/schemas/Event.vcs.branch.updated" }, From 3591372c45a8cfb3114d0f221f4ea8d51c527103 Mon Sep 17 00:00:00 2001 From: Bowen Dwelle Date: Sat, 17 Jan 2026 23:41:36 -0700 Subject: [PATCH 080/192] feat(tool): increase question header and label limits (#9201) --- packages/opencode/src/question/index.ts | 4 +- packages/opencode/test/tool/question.test.ts | 107 +++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 packages/opencode/test/tool/question.test.ts diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts index d18098a9c4f..41029ecbbdb 100644 --- a/packages/opencode/src/question/index.ts +++ b/packages/opencode/src/question/index.ts @@ -10,7 +10,7 @@ export namespace Question { export const Option = z .object({ - label: z.string().describe("Display text (1-5 words, concise)"), + label: z.string().max(30).describe("Display text (1-5 words, concise)"), description: z.string().describe("Explanation of choice"), }) .meta({ @@ -21,7 +21,7 @@ export namespace Question { export const Info = z .object({ question: z.string().describe("Complete question"), - header: z.string().max(12).describe("Very short label (max 12 chars)"), + header: z.string().max(30).describe("Very short label (max 30 chars)"), options: z.array(Option).describe("Available choices"), multiple: z.boolean().optional().describe("Allow selecting multiple choices"), custom: z.boolean().optional().describe("Allow typing a custom answer (default: true)"), diff --git a/packages/opencode/test/tool/question.test.ts b/packages/opencode/test/tool/question.test.ts new file mode 100644 index 00000000000..9e3f4e25cb2 --- /dev/null +++ b/packages/opencode/test/tool/question.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test" +import { z } from "zod" +import { QuestionTool } from "../../src/tool/question" +import * as QuestionModule from "../../src/question" + +const ctx = { + sessionID: "test-session", + messageID: "test-message", + callID: "test-call", + agent: "test-agent", + abort: AbortSignal.any([]), + metadata: () => {}, + ask: async () => {}, +} + +describe("tool.question", () => { + let askSpy: any; + + beforeEach(() => { + askSpy = spyOn(QuestionModule.Question, "ask").mockImplementation(async () => { + return [] + }) + }) + + afterEach(() => { + askSpy.mockRestore() + }) + + test("should successfully execute with valid question parameters", async () => { + const tool = await QuestionTool.init() + const questions = [ + { + question: "What is your favorite color?", + header: "Color", + options: [ + { label: "Red", description: "The color of passion" }, + { label: "Blue", description: "The color of sky" }, + ], + multiple: false, + }, + ] + + askSpy.mockResolvedValueOnce([["Red"]]) + + const result = await tool.execute( + { questions }, + ctx, + ) + expect(askSpy).toHaveBeenCalledTimes(1) + expect(result.title).toBe("Asked 1 question") + }) + + test("should now pass with a header longer than 12 but less than 30 chars", async () => { + const tool = await QuestionTool.init() + const questions = [ + { + question: "What is your favorite animal?", + header: "This Header is Over 12", + options: [{ label: "Dog", description: "Man's best friend" }], + }, + ] + + askSpy.mockResolvedValueOnce([["Dog"]]) + + const result = await tool.execute({ questions }, ctx) + expect(result.output).toContain(`"What is your favorite animal?"="Dog"`) + }) + + test("should throw an Error for header exceeding 30 characters", async () => { + const tool = await QuestionTool.init() + const questions = [ + { + question: "What is your favorite animal?", + header: "This Header is Definitely More Than Thirty Characters Long", + options: [{ label: "Dog", description: "Man's best friend" }], + }, + ] + try { + await tool.execute({ questions }, ctx) + // If it reaches here, the test should fail + expect(true).toBe(false) + } catch (e: any) { + expect(e).toBeInstanceOf(Error) + expect(e.cause).toBeInstanceOf(z.ZodError) + } + }) + + test("should throw an Error for label exceeding 30 characters", async () => { + const tool = await QuestionTool.init() + const questions = [ + { + question: "A question with a very long label", + header: "Long Label", + options: [{ label: "This is a very, very, very long label that will exceed the limit", description: "A description" }], + }, + ] + try { + await tool.execute({ questions }, ctx) + // If it reaches here, the test should fail + expect(true).toBe(false) + } catch (e: any) { + expect(e).toBeInstanceOf(Error) + expect(e.cause).toBeInstanceOf(z.ZodError) + } + }) +}) + From d13c0ea915dfceeda0003247f5a513df9787429d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 18 Jan 2026 06:42:13 +0000 Subject: [PATCH 081/192] chore: generate --- packages/opencode/test/tool/question.test.ts | 16 +++++++--------- packages/sdk/js/src/v2/gen/types.gen.ts | 2 +- packages/sdk/openapi.json | 7 ++++--- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/opencode/test/tool/question.test.ts b/packages/opencode/test/tool/question.test.ts index 9e3f4e25cb2..fa95e9612b6 100644 --- a/packages/opencode/test/tool/question.test.ts +++ b/packages/opencode/test/tool/question.test.ts @@ -14,11 +14,11 @@ const ctx = { } describe("tool.question", () => { - let askSpy: any; + let askSpy: any beforeEach(() => { askSpy = spyOn(QuestionModule.Question, "ask").mockImplementation(async () => { - return [] + return [] }) }) @@ -42,10 +42,7 @@ describe("tool.question", () => { askSpy.mockResolvedValueOnce([["Red"]]) - const result = await tool.execute( - { questions }, - ctx, - ) + const result = await tool.execute({ questions }, ctx) expect(askSpy).toHaveBeenCalledTimes(1) expect(result.title).toBe("Asked 1 question") }) @@ -59,7 +56,7 @@ describe("tool.question", () => { options: [{ label: "Dog", description: "Man's best friend" }], }, ] - + askSpy.mockResolvedValueOnce([["Dog"]]) const result = await tool.execute({ questions }, ctx) @@ -91,7 +88,9 @@ describe("tool.question", () => { { question: "A question with a very long label", header: "Long Label", - options: [{ label: "This is a very, very, very long label that will exceed the limit", description: "A description" }], + options: [ + { label: "This is a very, very, very long label that will exceed the limit", description: "A description" }, + ], }, ] try { @@ -104,4 +103,3 @@ describe("tool.question", () => { } }) }) - diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 060a00e6087..04e7144eb72 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -545,7 +545,7 @@ export type QuestionInfo = { */ question: string /** - * Very short label (max 12 chars) + * Very short label (max 30 chars) */ header: string /** diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index a307a666edc..104cedce1e5 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7163,7 +7163,8 @@ "properties": { "label": { "description": "Display text (1-5 words, concise)", - "type": "string" + "type": "string", + "maxLength": 30 }, "description": { "description": "Explanation of choice", @@ -7180,9 +7181,9 @@ "type": "string" }, "header": { - "description": "Very short label (max 12 chars)", + "description": "Very short label (max 30 chars)", "type": "string", - "maxLength": 12 + "maxLength": 30 }, "options": { "description": "Available choices", From bef1f6628118359f98d6c41e195b16e1f68794fa Mon Sep 17 00:00:00 2001 From: Noam Bressler Date: Sun, 18 Jan 2026 09:29:42 +0200 Subject: [PATCH 082/192] fix(acp): use single global event subscription and route by sessionID (#5628) Co-authored-by: noamzbr Co-authored-by: noam-v --- packages/opencode/src/acp/agent.ts | 582 ++++++++++-------- packages/opencode/src/acp/session.ts | 4 + .../test/acp/event-subscription.test.ts | 436 +++++++++++++ 3 files changed, 754 insertions(+), 268 deletions(-) create mode 100644 packages/opencode/test/acp/event-subscription.test.ts diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index f8792393c60..a077bb9fbd3 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -20,7 +20,7 @@ import { } from "@agentclientprotocol/sdk" import { Log } from "../util/log" import { ACPSessionManager } from "./session" -import type { ACPConfig, ACPSessionState } from "./types" +import type { ACPConfig } from "./types" import { Provider } from "../provider/provider" import { Agent as AgentModule } from "../agent/agent" import { Installation } from "@/installation" @@ -29,7 +29,7 @@ import { Config } from "@/config/config" import { Todo } from "@/session/todo" import { z } from "zod" import { LoadAPIKeyError } from "ai" -import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" +import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" import { applyPatch } from "diff" export namespace ACP { @@ -47,304 +47,354 @@ export namespace ACP { private connection: AgentSideConnection private config: ACPConfig private sdk: OpencodeClient - private sessionManager + private sessionManager: ACPSessionManager + private eventAbort = new AbortController() + private eventStarted = false + private permissionQueues = new Map>() + private permissionOptions: PermissionOption[] = [ + { optionId: "once", kind: "allow_once", name: "Allow once" }, + { optionId: "always", kind: "allow_always", name: "Always allow" }, + { optionId: "reject", kind: "reject_once", name: "Reject" }, + ] constructor(connection: AgentSideConnection, config: ACPConfig) { this.connection = connection this.config = config this.sdk = config.sdk this.sessionManager = new ACPSessionManager(this.sdk) + this.startEventSubscription() } - private setupEventSubscriptions(session: ACPSessionState) { - const sessionId = session.id - const directory = session.cwd + private startEventSubscription() { + if (this.eventStarted) return + this.eventStarted = true + this.runEventSubscription().catch((error) => { + if (this.eventAbort.signal.aborted) return + log.error("event subscription failed", { error }) + }) + } - const options: PermissionOption[] = [ - { optionId: "once", kind: "allow_once", name: "Allow once" }, - { optionId: "always", kind: "allow_always", name: "Always allow" }, - { optionId: "reject", kind: "reject_once", name: "Reject" }, - ] - this.config.sdk.event.subscribe({ directory }).then(async (events) => { + private async runEventSubscription() { + while (true) { + if (this.eventAbort.signal.aborted) return + const events = await this.sdk.global.event({ + signal: this.eventAbort.signal, + }) for await (const event of events.stream) { - switch (event.type) { - case "permission.asked": - try { - const permission = event.properties - const res = await this.connection - .requestPermission({ - sessionId, - toolCall: { - toolCallId: permission.tool?.callID ?? permission.id, - status: "pending", - title: permission.permission, - rawInput: permission.metadata, - kind: toToolKind(permission.permission), - locations: toLocations(permission.permission, permission.metadata), - }, - options, - }) - .catch(async (error) => { - log.error("failed to request permission from ACP", { - error, - permissionID: permission.id, - sessionID: permission.sessionID, - }) - await this.config.sdk.permission.reply({ - requestID: permission.id, - reply: "reject", - directory, - }) - return + if (this.eventAbort.signal.aborted) return + const payload = (event as any)?.payload + if (!payload) continue + await this.handleEvent(payload as Event).catch((error) => { + log.error("failed to handle event", { error, type: payload.type }) + }) + } + } + } + + private async handleEvent(event: Event) { + switch (event.type) { + case "permission.asked": { + const permission = event.properties + const session = this.sessionManager.tryGet(permission.sessionID) + if (!session) return + + const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve() + const next = prev + .then(async () => { + const directory = session.cwd + + const res = await this.connection + .requestPermission({ + sessionId: permission.sessionID, + toolCall: { + toolCallId: permission.tool?.callID ?? permission.id, + status: "pending", + title: permission.permission, + rawInput: permission.metadata, + kind: toToolKind(permission.permission), + locations: toLocations(permission.permission, permission.metadata), + }, + options: this.permissionOptions, + }) + .catch(async (error) => { + log.error("failed to request permission from ACP", { + error, + permissionID: permission.id, + sessionID: permission.sessionID, }) - if (!res) return - if (res.outcome.outcome !== "selected") { - await this.config.sdk.permission.reply({ + await this.sdk.permission.reply({ requestID: permission.id, reply: "reject", directory, }) - return - } - if (res.outcome.optionId !== "reject" && permission.permission == "edit") { - const metadata = permission.metadata || {} - const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : "" - const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : "" - - const content = await Bun.file(filepath).text() - const newContent = getNewContent(content, diff) - - if (newContent) { - this.connection.writeTextFile({ - sessionId: sessionId, - path: filepath, - content: newContent, - }) - } - } - await this.config.sdk.permission.reply({ + return undefined + }) + + if (!res) return + if (res.outcome.outcome !== "selected") { + await this.sdk.permission.reply({ requestID: permission.id, - reply: res.outcome.optionId as "once" | "always" | "reject", + reply: "reject", directory, }) - } catch (err) { - log.error("unexpected error when handling permission", { error: err }) - } finally { - break + return } - case "message.part.updated": - log.info("message part updated", { event: event.properties }) - try { - const props = event.properties - const { part } = props - - const message = await this.config.sdk.session - .message( - { - sessionID: part.sessionID, - messageID: part.messageID, - directory, + if (res.outcome.optionId !== "reject" && permission.permission == "edit") { + const metadata = permission.metadata || {} + const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : "" + const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : "" + + const content = await Bun.file(filepath).text() + const newContent = getNewContent(content, diff) + + if (newContent) { + this.connection.writeTextFile({ + sessionId: session.id, + path: filepath, + content: newContent, + }) + } + } + + await this.sdk.permission.reply({ + requestID: permission.id, + reply: res.outcome.optionId as "once" | "always" | "reject", + directory, + }) + }) + .catch((error) => { + log.error("failed to handle permission", { error, permissionID: permission.id }) + }) + .finally(() => { + if (this.permissionQueues.get(permission.sessionID) === next) { + this.permissionQueues.delete(permission.sessionID) + } + }) + this.permissionQueues.set(permission.sessionID, next) + return + } + + case "message.part.updated": { + log.info("message part updated", { event: event.properties }) + const props = event.properties + const part = props.part + 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") { + 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: {}, }, - { throwOnError: true }, - ) - .then((x) => x.data) - .catch((err) => { - log.error("unexpected error when fetching message", { error: err }) - return undefined }) + .catch((error) => { + log.error("failed to send tool pending to ACP", { error }) + }) + return - if (!message || message.info.role !== "assistant") return - - if (part.type === "tool") { - 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((err) => { - log.error("failed to send tool pending to ACP", { error: err }) - }) - break - case "running": - 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((err) => { - log.error("failed to send tool in_progress to ACP", { error: err }) - }) - break - case "completed": - const kind = toToolKind(part.tool) - const content: ToolCallContent[] = [ - { - type: "content", - content: { - type: "text", - text: part.state.output, - }, - }, - ] - - if (kind === "edit") { - const input = part.state.input - const filePath = typeof input["filePath"] === "string" ? input["filePath"] : "" - const oldText = typeof input["oldString"] === "string" ? input["oldString"] : "" - const newText = - typeof input["newString"] === "string" - ? input["newString"] - : typeof input["content"] === "string" - ? input["content"] - : "" - content.push({ - type: "diff", - path: filePath, - oldText, - newText, - }) - } - - if (part.tool === "todowrite") { - const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output)) - if (parsedTodos.success) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "plan", - entries: parsedTodos.data.map((todo) => { - const status: PlanEntry["status"] = - todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) - return { - priority: "medium", - status, - content: todo.content, - } - }), - }, - }) - .catch((err) => { - log.error("failed to send session update for todo", { error: err }) - }) - } else { - log.error("failed to parse todo output", { error: parsedTodos.error }) - } - } - - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "completed", - kind, - content, - title: part.state.title, - rawInput: part.state.input, - rawOutput: { - output: part.state.output, - metadata: part.state.metadata, - }, - }, - }) - .catch((err) => { - log.error("failed to send tool completed to ACP", { error: err }) - }) - break - case "error": - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "failed", - kind: toToolKind(part.tool), - title: part.tool, - rawInput: part.state.input, - content: [ - { - type: "content", - content: { - type: "text", - text: part.state.error, - }, - }, - ], - rawOutput: { - error: part.state.error, - }, - }, - }) - .catch((err) => { - log.error("failed to send tool error to ACP", { error: err }) - }) - break - } - } else if (part.type === "text") { - const delta = props.delta - if (delta && part.synthetic !== true) { + case "running": + 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 + + case "completed": { + const kind = toToolKind(part.tool) + const content: ToolCallContent[] = [ + { + type: "content", + content: { + type: "text", + text: part.state.output, + }, + }, + ] + + if (kind === "edit") { + const input = part.state.input + const filePath = typeof input["filePath"] === "string" ? input["filePath"] : "" + const oldText = typeof input["oldString"] === "string" ? input["oldString"] : "" + const newText = + typeof input["newString"] === "string" + ? input["newString"] + : typeof input["content"] === "string" + ? input["content"] + : "" + content.push({ + type: "diff", + path: filePath, + oldText, + newText, + }) + } + + if (part.tool === "todowrite") { + const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output)) + if (parsedTodos.success) { await this.connection .sessionUpdate({ sessionId, update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: delta, - }, + sessionUpdate: "plan", + entries: parsedTodos.data.map((todo) => { + const status: PlanEntry["status"] = + todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) + return { + priority: "medium", + status, + content: todo.content, + } + }), }, }) - .catch((err) => { - log.error("failed to send text to ACP", { error: err }) + .catch((error) => { + log.error("failed to send session update for todo", { error }) }) + } else { + log.error("failed to parse todo output", { error: parsedTodos.error }) } - } else if (part.type === "reasoning") { - const delta = props.delta - if (delta) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "agent_thought_chunk", + } + + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "completed", + kind, + content, + title: part.state.title, + rawInput: part.state.input, + rawOutput: { + output: part.state.output, + metadata: part.state.metadata, + }, + }, + }) + .catch((error) => { + log.error("failed to send tool completed to ACP", { error }) + }) + return + } + case "error": + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "failed", + kind: toToolKind(part.tool), + title: part.tool, + rawInput: part.state.input, + content: [ + { + type: "content", content: { type: "text", - text: delta, + text: part.state.error, }, }, - }) - .catch((err) => { - log.error("failed to send reasoning to ACP", { error: err }) - }) - } - } - } finally { - break - } + ], + rawOutput: { + error: part.state.error, + }, + }, + }) + .catch((error) => { + log.error("failed to send tool error to ACP", { error }) + }) + return + } + } + + if (part.type === "text") { + const delta = props.delta + if (delta && part.synthetic !== true) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_message_chunk", + content: { + type: "text", + text: delta, + }, + }, + }) + .catch((error) => { + log.error("failed to send text to ACP", { error }) + }) + } + return } + + if (part.type === "reasoning") { + const delta = props.delta + if (delta) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_thought_chunk", + content: { + type: "text", + text: delta, + }, + }, + }) + .catch((error) => { + log.error("failed to send reasoning to ACP", { error }) + }) + } + } + return } - }) + } } async initialize(params: InitializeRequest): Promise { @@ -409,8 +459,6 @@ export namespace ACP { sessionId, }) - this.setupEventSubscriptions(state) - return { sessionId, models: load.models, @@ -436,7 +484,7 @@ export namespace ACP { const model = await defaultModel(this.config, directory) // Store ACP session state - const state = await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model) + await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model) log.info("load_session", { sessionId, mcpServers: params.mcpServers.length }) @@ -446,8 +494,6 @@ export namespace ACP { sessionId, }) - this.setupEventSubscriptions(state) - // Replay session history const messages = await this.sdk.session .messages( diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index 70b65834705..151fa5646ba 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -13,6 +13,10 @@ export class ACPSessionManager { this.sdk = sdk } + tryGet(sessionId: string): ACPSessionState | undefined { + return this.sessions.get(sessionId) + } + async create(cwd: string, mcpServers: McpServer[], model?: ACPSessionState["model"]): Promise { const session = await this.sdk.session .create( diff --git a/packages/opencode/test/acp/event-subscription.test.ts b/packages/opencode/test/acp/event-subscription.test.ts new file mode 100644 index 00000000000..8e139ff5973 --- /dev/null +++ b/packages/opencode/test/acp/event-subscription.test.ts @@ -0,0 +1,436 @@ +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 { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" + +type SessionUpdateParams = Parameters[0] +type RequestPermissionParams = Parameters[0] +type RequestPermissionResult = Awaited> + +type GlobalEventEnvelope = { + directory?: string + payload?: Event +} + +type EventController = { + push: (event: GlobalEventEnvelope) => void + close: () => void +} + +function createEventStream() { + const queue: GlobalEventEnvelope[] = [] + const waiters: Array<(value: GlobalEventEnvelope | undefined) => void> = [] + const state = { closed: false } + + const push = (event: GlobalEventEnvelope) => { + const waiter = waiters.shift() + if (waiter) { + waiter(event) + return + } + queue.push(event) + } + + const close = () => { + state.closed = true + for (const waiter of waiters.splice(0)) { + waiter(undefined) + } + } + + const stream = async function* (signal?: AbortSignal) { + while (true) { + if (signal?.aborted) return + const next = queue.shift() + if (next) { + yield next + continue + } + if (state.closed) return + const value = await new Promise((resolve) => { + waiters.push(resolve) + if (!signal) return + signal.addEventListener("abort", () => resolve(undefined), { once: true }) + }) + if (!value) return + yield value + } + } + + return { controller: { push, close } satisfies EventController, stream } +} + +function createFakeAgent() { + const updates = new Map() + const chunks = new Map() + const record = (sessionId: string, type: string) => { + const list = updates.get(sessionId) ?? [] + list.push(type) + updates.set(sessionId, list) + } + + const connection = { + async sessionUpdate(params: SessionUpdateParams) { + const update = params.update + const type = update?.sessionUpdate ?? "unknown" + record(params.sessionId, type) + if (update?.sessionUpdate === "agent_message_chunk") { + const content = update.content + if (content?.type !== "text") return + if (typeof content.text !== "string") return + chunks.set(params.sessionId, (chunks.get(params.sessionId) ?? "") + content.text) + } + }, + async requestPermission(_params: RequestPermissionParams): Promise { + return { outcome: { outcome: "selected", optionId: "once" } } as RequestPermissionResult + }, + } as unknown as AgentSideConnection + + const { controller, stream } = createEventStream() + const calls = { + eventSubscribe: 0, + sessionCreate: 0, + } + + const sdk = { + global: { + event: async (opts?: { signal?: AbortSignal }) => { + calls.eventSubscribe++ + return { stream: stream(opts?.signal) } + }, + }, + session: { + create: async (_params?: any) => { + calls.sessionCreate++ + return { + data: { + id: `ses_${calls.sessionCreate}`, + time: { created: new Date().toISOString() }, + }, + } + }, + get: async (_params?: any) => { + return { + data: { + id: "ses_1", + time: { created: new Date().toISOString() }, + }, + } + }, + messages: async () => { + return { data: [] } + }, + message: async () => { + return { + data: { + info: { + role: "assistant", + }, + }, + } + }, + }, + permission: { + respond: async () => { + return { data: true } + }, + }, + config: { + providers: async () => { + return { + data: { + providers: [ + { + id: "opencode", + name: "opencode", + models: { + "big-pickle": { id: "big-pickle", name: "big-pickle" }, + }, + }, + ], + }, + } + }, + }, + app: { + agents: async () => { + return { + data: [ + { + name: "build", + description: "build", + mode: "agent", + }, + ], + } + }, + }, + command: { + list: async () => { + return { data: [] } + }, + }, + mcp: { + add: async () => { + return { data: true } + }, + }, + } as any + + const agent = new ACP.Agent(connection, { + sdk, + defaultModel: { providerID: "opencode", modelID: "big-pickle" }, + } as any) + + const stop = () => { + controller.close() + ;(agent as any).eventAbort.abort() + } + + return { agent, controller, calls, updates, chunks, stop, sdk, connection } +} + +describe("acp.agent event subscription", () => { + test("routes message.part.updated by the event sessionID (no cross-session pollution)", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const { agent, controller, updates, stop } = createFakeAgent() + const cwd = "/tmp/opencode-acp-test" + + const sessionA = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + const sessionB = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + + controller.push({ + directory: cwd, + payload: { + type: "message.part.updated", + properties: { + part: { + sessionID: sessionB, + messageID: "msg_1", + type: "text", + synthetic: false, + }, + delta: "hello", + }, + }, + } as any) + + await new Promise((r) => setTimeout(r, 10)) + + expect((updates.get(sessionA) ?? []).includes("agent_message_chunk")).toBe(false) + expect((updates.get(sessionB) ?? []).includes("agent_message_chunk")).toBe(true) + + stop() + }, + }) + }) + + test("keeps concurrent sessions isolated when message.part.updated events are interleaved", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const { agent, controller, chunks, stop } = createFakeAgent() + const cwd = "/tmp/opencode-acp-test" + + const sessionA = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + const sessionB = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + + const tokenA = ["ALPHA_", "111", "_X"] + const tokenB = ["BETA_", "222", "_Y"] + + const push = (sessionId: string, messageID: string, delta: string) => { + controller.push({ + directory: cwd, + payload: { + type: "message.part.updated", + properties: { + part: { + sessionID: sessionId, + messageID, + type: "text", + synthetic: false, + }, + delta, + }, + }, + } as any) + } + + push(sessionA, "msg_a", tokenA[0]) + push(sessionB, "msg_b", tokenB[0]) + push(sessionA, "msg_a", tokenA[1]) + push(sessionB, "msg_b", tokenB[1]) + push(sessionA, "msg_a", tokenA[2]) + push(sessionB, "msg_b", tokenB[2]) + + await new Promise((r) => setTimeout(r, 20)) + + const a = chunks.get(sessionA) ?? "" + const b = chunks.get(sessionB) ?? "" + + expect(a).toContain(tokenA.join("")) + expect(b).toContain(tokenB.join("")) + for (const part of tokenB) expect(a).not.toContain(part) + for (const part of tokenA) expect(b).not.toContain(part) + + stop() + }, + }) + }) + + test("does not create additional event subscriptions on repeated loadSession()", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const { agent, calls, stop } = createFakeAgent() + const cwd = "/tmp/opencode-acp-test" + + const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + + await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any) + await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any) + await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any) + await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any) + + expect(calls.eventSubscribe).toBe(1) + + stop() + }, + }) + }) + + test("permission.asked events are handled and replied", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const permissionReplies: string[] = [] + const { agent, controller, stop, sdk } = createFakeAgent() + sdk.permission.reply = async (params: any) => { + permissionReplies.push(params.requestID) + return { data: true } + } + const cwd = "/tmp/opencode-acp-test" + + const sessionA = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + + controller.push({ + directory: cwd, + payload: { + type: "permission.asked", + properties: { + id: "perm_1", + sessionID: sessionA, + permission: "bash", + patterns: ["*"], + metadata: {}, + always: [], + }, + }, + } as any) + + await new Promise((r) => setTimeout(r, 20)) + + expect(permissionReplies).toContain("perm_1") + + stop() + }, + }) + }) + + test("permission prompt on session A does not block message updates for session B", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const permissionReplies: string[] = [] + let resolvePermissionA: (() => void) | undefined + const permissionABlocking = new Promise((r) => { + resolvePermissionA = r + }) + + const { agent, controller, chunks, stop, sdk, connection } = createFakeAgent() + + // Make permission request for session A block until we release it + const originalRequestPermission = connection.requestPermission.bind(connection) + let permissionCalls = 0 + connection.requestPermission = async (params: RequestPermissionParams) => { + permissionCalls++ + if (params.sessionId.endsWith("1")) { + await permissionABlocking + } + return originalRequestPermission(params) + } + + sdk.permission.reply = async (params: any) => { + permissionReplies.push(params.requestID) + return { data: true } + } + + const cwd = "/tmp/opencode-acp-test" + + const sessionA = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + const sessionB = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + + // Push permission.asked for session A (will block) + controller.push({ + directory: cwd, + payload: { + type: "permission.asked", + properties: { + id: "perm_a", + sessionID: sessionA, + permission: "bash", + patterns: ["*"], + metadata: {}, + always: [], + }, + }, + } as any) + + // Give time for permission handling to start + await new Promise((r) => setTimeout(r, 10)) + + // Push message for session B while A's permission is pending + controller.push({ + directory: cwd, + payload: { + type: "message.part.updated", + properties: { + part: { + sessionID: sessionB, + messageID: "msg_b", + type: "text", + synthetic: false, + }, + delta: "session_b_message", + }, + }, + } as any) + + // Wait for session B's message to be processed + await new Promise((r) => setTimeout(r, 20)) + + // Session B should have received message even though A's permission is still pending + expect(chunks.get(sessionB) ?? "").toContain("session_b_message") + expect(permissionReplies).not.toContain("perm_a") + + // Release session A's permission + resolvePermissionA!() + await new Promise((r) => setTimeout(r, 20)) + + // Now session A's permission should be replied + expect(permissionReplies).toContain("perm_a") + + stop() + }, + }) + }) +}) From ee4ea653116803fcb2af731ca0a520e5fee2c68d Mon Sep 17 00:00:00 2001 From: Noam Bressler Date: Sun, 18 Jan 2026 09:29:57 +0200 Subject: [PATCH 083/192] fix: restore persisted model/agent when loading ACP session (#7809) Co-authored-by: noam-v --- packages/opencode/src/acp/agent.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index a077bb9fbd3..5fca2725587 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -488,7 +488,7 @@ export namespace ACP { log.info("load_session", { sessionId, mcpServers: params.mcpServers.length }) - const mode = await this.loadSessionMode({ + const result = await this.loadSessionMode({ cwd: directory, mcpServers: params.mcpServers, sessionId, @@ -509,12 +509,20 @@ export namespace ACP { return undefined }) + const lastUser = messages?.findLast((m) => m.info.role === "user")?.info + if (lastUser?.role === "user") { + result.models.currentModelId = `${lastUser.model.providerID}/${lastUser.model.modelID}` + if (result.modes.availableModes.some((m) => m.id === lastUser.agent)) { + result.modes.currentModeId = lastUser.agent + } + } + for (const msg of messages ?? []) { log.debug("replay message", msg) await this.processMessage(msg) } - return mode + return result } catch (e) { const error = MessageV2.fromError(e, { providerID: this.config.defaultModel?.providerID ?? "unknown", From 0ccf9bd9acf5d861c89c39878f50fc9cd7f48735 Mon Sep 17 00:00:00 2001 From: Mani Sundararajan <10191300+itsrainingmani@users.noreply.github.com> Date: Sun, 18 Jan 2026 03:40:01 -0500 Subject: [PATCH 084/192] feat(cli): uninstall opencode installed via windows package managers (#8571) --- packages/opencode/src/cli/cmd/uninstall.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts index 62210d57586..704d3572bbb 100644 --- a/packages/opencode/src/cli/cmd/uninstall.ts +++ b/packages/opencode/src/cli/cmd/uninstall.ts @@ -133,6 +133,8 @@ async function showRemovalSummary(targets: RemovalTargets, method: Installation. bun: "bun remove -g opencode-ai", yarn: "yarn global remove opencode-ai", brew: "brew uninstall opencode", + choco: "choco uninstall opencode", + scoop: "scoop uninstall opencode", } prompts.log.info(` ✓ Package: ${cmds[method] || method}`) } @@ -182,16 +184,27 @@ async function executeUninstall(method: Installation.Method, targets: RemovalTar bun: ["bun", "remove", "-g", "opencode-ai"], yarn: ["yarn", "global", "remove", "opencode-ai"], brew: ["brew", "uninstall", "opencode"], + choco: ["choco", "uninstall", "opencode"], + scoop: ["scoop", "uninstall", "opencode"], } const cmd = cmds[method] if (cmd) { spinner.start(`Running ${cmd.join(" ")}...`) - const result = await $`${cmd}`.quiet().nothrow() + const result = + method === "choco" + ? await $`echo Y | choco uninstall opencode -y -r`.quiet().nothrow() + : await $`${cmd}`.quiet().nothrow() if (result.exitCode !== 0) { - spinner.stop(`Package manager uninstall failed`, 1) - prompts.log.warn(`You may need to run manually: ${cmd.join(" ")}`) - errors.push(`Package manager: exit code ${result.exitCode}`) + spinner.stop(`Package manager uninstall failed: exit code ${result.exitCode}`, 1) + if ( + method === "choco" && + result.stdout.toString("utf8").includes("not running from an elevated command shell") + ) { + prompts.log.warn(`You may need to run '${cmd.join(" ")}' from an elevated command shell`) + } else { + prompts.log.warn(`You may need to run manually: ${cmd.join(" ")}`) + } } else { spinner.stop("Package removed") } From 06bc4dcb06a814b501dd1190cb1a424eafb91f13 Mon Sep 17 00:00:00 2001 From: Mani Sundararajan <10191300+itsrainingmani@users.noreply.github.com> Date: Sun, 18 Jan 2026 06:12:07 -0500 Subject: [PATCH 085/192] feat(desktop): implement session unshare button (#8660) --- .../src/components/session/session-header.tsx | 180 ++++++++++++++---- packages/app/src/pages/session.tsx | 66 +++++++ 2 files changed, 212 insertions(+), 34 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 7070f0c9337..4c709feefec 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -1,15 +1,17 @@ -import { createMemo, createResource, Show } from "solid-js" +import { createEffect, createMemo, 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 { useServer } from "@/context/server" // import { useDialog } from "@opencode-ai/ui/context/dialog" +import { usePlatform } from "@/context/platform" import { useSync } from "@/context/sync" import { useGlobalSDK } from "@/context/global-sdk" import { getFilename } from "@opencode-ai/util/path" import { base64Decode } from "@opencode-ai/util/encode" -import { iife } from "@opencode-ai/util/iife" + import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Button } from "@opencode-ai/ui/button" @@ -26,6 +28,7 @@ export function SessionHeader() { // const server = useServer() // const dialog = useDialog() const sync = useSync() + const platform = usePlatform() const projectDirectory = createMemo(() => base64Decode(params.dir ?? "")) const project = createMemo(() => { @@ -45,6 +48,78 @@ export function SessionHeader() { const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const view = createMemo(() => layout.view(sessionKey())) + const [state, setState] = createStore({ + share: false, + unshare: false, + copied: false, + timer: undefined as number | undefined, + }) + const shareUrl = createMemo(() => currentSession()?.share?.url) + + createEffect(() => { + const url = shareUrl() + if (url) return + if (state.timer) window.clearTimeout(state.timer) + setState({ copied: false, timer: undefined }) + }) + + onCleanup(() => { + if (state.timer) window.clearTimeout(state.timer) + }) + + function shareSession() { + const session = currentSession() + if (!session || state.share) return + setState("share", true) + globalSDK.client.session + .share({ sessionID: session.id, directory: projectDirectory() }) + .catch((error) => { + console.error("Failed to share session", error) + }) + .finally(() => { + setState("share", false) + }) + } + + function unshareSession() { + const session = currentSession() + if (!session || state.unshare) return + setState("unshare", true) + globalSDK.client.session + .unshare({ sessionID: session.id, directory: projectDirectory() }) + .catch((error) => { + console.error("Failed to unshare session", error) + }) + .finally(() => { + setState("unshare", false) + }) + } + + function copyLink() { + const url = shareUrl() + if (!url) return + navigator.clipboard + .writeText(url) + .then(() => { + if (state.timer) window.clearTimeout(state.timer) + setState("copied", true) + const timer = window.setTimeout(() => { + setState("copied", false) + setState("timer", undefined) + }, 3000) + setState("timer", timer) + }) + .catch((error) => { + console.error("Failed to copy share link", error) + }) + } + + function viewShare() { + const url = shareUrl() + if (!url) return + platform.openLink(url) + } + const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center")) const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right")) @@ -159,40 +234,77 @@ export function SessionHeader() {
- - - - } - > - {iife(() => { - const [url] = createResource( - () => currentSession(), - async (session) => { - if (!session) return - let shareURL = session.share?.url - if (!shareURL) { - shareURL = await globalSDK.client.session - .share({ sessionID: session.id, directory: projectDirectory() }) - .then((r) => r.data?.share?.url) - .catch((e) => { - console.error("Failed to share session", e) - return undefined - }) +
+ + + + } + > +
+ + +
} - return shareURL - }, - { initialValue: "" }, - ) - return ( - - {(shareUrl) => } + > +
+ +
+ + +
+
- ) - })} -
+
+
+ + + + + +
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index dbdbbc7eb55..d76ff99b340 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -654,6 +654,72 @@ export default function Page() { disabled: !params.id || visibleUserMessages().length === 0, onSelect: () => dialog.show(() => ), }, + ...(sync.data.config.share !== "disabled" + ? [ + { + id: "session.share", + title: "Share session", + description: "Share this session and copy the URL to clipboard", + category: "Session", + slash: "share", + disabled: !params.id || !!info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .share({ sessionID: params.id }) + .then((res) => { + navigator.clipboard.writeText(res.data!.share!.url).catch(() => + showToast({ + title: "Failed to copy URL to clipboard", + variant: "error", + }), + ) + }) + .then(() => + showToast({ + title: "Session shared", + description: "Share URL copied to clipboard!", + variant: "success", + }), + ) + .catch(() => + showToast({ + title: "Failed to share session", + description: "An error occurred while sharing the session", + variant: "error", + }), + ) + }, + }, + { + id: "session.unshare", + title: "Unshare session", + description: "Stop sharing this session", + category: "Session", + slash: "unshare", + disabled: !params.id || !info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .unshare({ sessionID: params.id }) + .then(() => + showToast({ + title: "Session unshared", + description: "Session unshared successfully!", + variant: "success", + }), + ) + .catch(() => + showToast({ + title: "Failed to unshare session", + description: "An error occurred while unsharing the session", + variant: "error", + }), + ) + }, + }, + ] + : []), ]) const handleKeyDown = (event: KeyboardEvent) => { From 5c9cc9c7488f5217080d67bd981d4170b445107a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 18 Jan 2026 12:05:11 +0000 Subject: [PATCH 086/192] ignore: update download stats 2026-01-18 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 9a665612b14..a2041d49ac6 100644 --- a/STATS.md +++ b/STATS.md @@ -203,3 +203,4 @@ | 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) | | 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) | | 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) | +| 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) | From 6c0991d1623b620e8c95d9ea8a184861efe57f23 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 18 Jan 2026 09:00:49 -0500 Subject: [PATCH 087/192] fix(app): remove redundant toast for thinking effort changes (#9181) --- packages/app/src/pages/session.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index d76ff99b340..f063ce35b40 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -533,10 +533,6 @@ export default function Page() { keybind: "shift+mod+t", onSelect: () => { local.model.variant.cycle() - showToast({ - title: "Thinking effort changed", - description: "The thinking effort has been changed to " + (local.model.variant.current() ?? "Default"), - }) }, }, { From ad2e03284bad0c828aaed8c41cecd67672f4fe4a Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Sun, 18 Jan 2026 15:10:38 +0100 Subject: [PATCH 088/192] refactor(desktop): improve layout and styling of session search button (#9251) --- packages/app/src/components/session/session-header.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 4c709feefec..96ed762c448 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -133,14 +133,14 @@ export function SessionHeader() { class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active" onClick={() => command.trigger("file.open")} > -
- - +
+ + Search {name()}
- {(keybind) => {keybind()}} + {(keybind) => {keybind()}} )} From 2dcca4755d644b2ec66c7a284484101609db210b Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 18 Jan 2026 09:47:18 -0600 Subject: [PATCH 089/192] fix: import issue in patch module --- packages/opencode/src/patch/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts index 888a4d94b89..0efeff544f6 100644 --- a/packages/opencode/src/patch/index.ts +++ b/packages/opencode/src/patch/index.ts @@ -1,6 +1,7 @@ import z from "zod" import * as path from "path" import * as fs from "fs/promises" +import { readFileSync } from "fs" import { Log } from "../util/log" export namespace Patch { @@ -311,7 +312,7 @@ export namespace Patch { // Read original file content let originalContent: string try { - originalContent = require("fs").readFileSync(filePath, "utf-8") + originalContent = readFileSync(filePath, "utf-8") } catch (error) { throw new Error(`Failed to read file ${filePath}: ${error}`) } From f7fef99ddddb5e8fffa10f392f193e263552d7d0 Mon Sep 17 00:00:00 2001 From: Chawye Hsu Date: Sun, 18 Jan 2026 23:58:34 +0800 Subject: [PATCH 090/192] refactor(installation): update scoop installation method (#9243) Signed-off-by: Chawye Hsu --- README.md | 4 +++- README.zh-CN.md | 4 +++- README.zh-TW.md | 4 +++- packages/opencode/src/installation/index.ts | 4 ++-- packages/web/src/content/docs/index.mdx | 3 +-- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d0ba487402f..64ca1ef7a6f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ curl -fsSL https://opencode.ai/install | bash # Package managers npm i -g opencode-ai@latest # or bun/pnpm/yarn -scoop bucket add extras; scoop install extras/opencode # Windows +scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date) brew install opencode # macOS and Linux (official brew formula, updated less) @@ -52,6 +52,8 @@ OpenCode is also available as a desktop application. Download directly from the ```bash # macOS (Homebrew) brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop ``` #### Installation Directory diff --git a/README.zh-CN.md b/README.zh-CN.md index 30757f5fe9d..4b56e0fb0b0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -26,7 +26,7 @@ curl -fsSL https://opencode.ai/install | bash # 软件包管理器 npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn -scoop bucket add extras; scoop install extras/opencode # Windows +scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS 和 Linux(推荐,始终保持最新) brew install opencode # macOS 和 Linux(官方 brew formula,更新频率较低) @@ -52,6 +52,8 @@ OpenCode 也提供桌面版应用。可直接从 [发布页 (releases page)](htt ```bash # macOS (Homebrew Cask) brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop ``` #### 安装目录 diff --git a/README.zh-TW.md b/README.zh-TW.md index 9e27c48f27e..66664a70305 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -26,7 +26,7 @@ curl -fsSL https://opencode.ai/install | bash # 套件管理員 npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn -scoop bucket add extras; scoop install extras/opencode # Windows +scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS 與 Linux(推薦,始終保持最新) brew install opencode # macOS 與 Linux(官方 brew formula,更新頻率較低) @@ -52,6 +52,8 @@ OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (rele ```bash # macOS (Homebrew Cask) brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop ``` #### 安裝目錄 diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index dea312adb0c..d18c9e31a13 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -158,7 +158,7 @@ export namespace Installation { cmd = $`echo Y | choco upgrade opencode --version=${target}` break case "scoop": - cmd = $`scoop install extras/opencode@${target}` + cmd = $`scoop install opencode@${target}` break default: throw new Error(`Unknown method: ${method}`) @@ -226,7 +226,7 @@ export namespace Installation { } if (detectedMethod === "scoop") { - return fetch("https://raw.githubusercontent.com/ScoopInstaller/Extras/master/bucket/opencode.json", { + return fetch("https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json", { headers: { Accept: "application/json" }, }) .then((res) => { diff --git a/packages/web/src/content/docs/index.mdx b/packages/web/src/content/docs/index.mdx index bee5bd3a38e..8b3d3a9c824 100644 --- a/packages/web/src/content/docs/index.mdx +++ b/packages/web/src/content/docs/index.mdx @@ -98,8 +98,7 @@ You can also install it with the following commands: - **Using Scoop** ```bash - scoop bucket add extras - scoop install extras/opencode + scoop install opencode ``` - **Using NPM** From 095a64291d8713f7a9b6b2931d28911dc5df9059 Mon Sep 17 00:00:00 2001 From: Lior Date: Sun, 18 Jan 2026 18:45:25 +0200 Subject: [PATCH 091/192] fix(acp): preserve file attachment metadata during session replay (#6342) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/acp/agent.ts | 105 +++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 5fca2725587..469b33b025a 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -354,7 +354,7 @@ export namespace ACP { if (part.type === "text") { const delta = props.delta - if (delta && part.synthetic !== true) { + if (delta && part.ignored !== true) { await this.connection .sessionUpdate({ sessionId, @@ -687,7 +687,7 @@ export namespace ACP { break } } else if (part.type === "text") { - if (part.text) { + if (part.text && !part.ignored) { await this.connection .sessionUpdate({ sessionId, @@ -703,6 +703,79 @@ export namespace ACP { log.error("failed to send text to ACP", { error: err }) }) } + } else if (part.type === "file") { + // Replay file attachments as appropriate ACP content blocks. + // OpenCode stores files internally as { type: "file", url, filename, mime }. + // We convert these back to ACP blocks based on the URL scheme and MIME type: + // - file:// URLs → resource_link + // - data: URLs with image/* → image block + // - data: URLs with text/* or application/json → resource with text + // - data: URLs with other types → resource with blob + const url = part.url + const filename = part.filename ?? "file" + const mime = part.mime || "application/octet-stream" + const messageChunk = message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk" + + if (url.startsWith("file://")) { + // Local file reference - send as resource_link + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: messageChunk, + content: { type: "resource_link", uri: url, name: filename, mimeType: mime }, + }, + }) + .catch((err) => { + log.error("failed to send resource_link to ACP", { error: err }) + }) + } else if (url.startsWith("data:")) { + // Embedded content - parse data URL and send as appropriate block type + const base64Match = url.match(/^data:([^;]+);base64,(.*)$/) + const dataMime = base64Match?.[1] + const base64Data = base64Match?.[2] ?? "" + + const effectiveMime = dataMime || mime + + if (effectiveMime.startsWith("image/")) { + // Image - send as image block + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: messageChunk, + content: { + type: "image", + mimeType: effectiveMime, + data: base64Data, + uri: `file://${filename}`, + }, + }, + }) + .catch((err) => { + log.error("failed to send image to ACP", { error: err }) + }) + } else { + // Non-image: text types get decoded, binary types stay as blob + const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json" + const resource = isText + ? { uri: `file://${filename}`, mimeType: effectiveMime, text: Buffer.from(base64Data, "base64").toString("utf-8") } + : { uri: `file://${filename}`, mimeType: effectiveMime, blob: base64Data } + + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: messageChunk, + content: { type: "resource", resource }, + }, + }) + .catch((err) => { + log.error("failed to send resource to ACP", { error: err }) + }) + } + } + // URLs that don't match file:// or data: are skipped (unsupported) } else if (part.type === "reasoning") { if (part.text) { await this.connection @@ -901,39 +974,57 @@ export namespace ACP { text: part.text, }) break - case "image": + case "image": { + const parsed = parseUri(part.uri ?? "") + const filename = parsed.type === "file" ? parsed.filename : "image" if (part.data) { parts.push({ type: "file", url: `data:${part.mimeType};base64,${part.data}`, - filename: "image", + filename, mime: part.mimeType, }) } else if (part.uri && part.uri.startsWith("http:")) { parts.push({ type: "file", url: part.uri, - filename: "image", + filename, mime: part.mimeType, }) } break + } case "resource_link": const parsed = parseUri(part.uri) + // Use the name from resource_link if available + if (part.name && parsed.type === "file") { + parsed.filename = part.name + } parts.push(parsed) break - case "resource": + case "resource": { const resource = part.resource - if ("text" in resource) { + if ("text" in resource && resource.text) { parts.push({ type: "text", text: resource.text, }) + } else if ("blob" in resource && resource.blob && resource.mimeType) { + // Binary resource (PDFs, etc.): store as file part with data URL + const parsed = parseUri(resource.uri ?? "") + const filename = parsed.type === "file" ? parsed.filename : "file" + parts.push({ + type: "file", + url: `data:${resource.mimeType};base64,${resource.blob}`, + filename, + mime: resource.mimeType, + }) } break + } default: break From 5009f10406c15c4b69c04fa626756ee7bf81b300 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 18 Jan 2026 16:46:02 +0000 Subject: [PATCH 092/192] chore: generate --- packages/opencode/src/acp/agent.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 469b33b025a..6330fae97a2 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -707,7 +707,7 @@ export namespace ACP { // Replay file attachments as appropriate ACP content blocks. // OpenCode stores files internally as { type: "file", url, filename, mime }. // We convert these back to ACP blocks based on the URL scheme and MIME type: - // - file:// URLs → resource_link + // - file:// URLs → resource_link // - data: URLs with image/* → image block // - data: URLs with text/* or application/json → resource with text // - data: URLs with other types → resource with blob @@ -759,7 +759,11 @@ export namespace ACP { // Non-image: text types get decoded, binary types stay as blob const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json" const resource = isText - ? { uri: `file://${filename}`, mimeType: effectiveMime, text: Buffer.from(base64Data, "base64").toString("utf-8") } + ? { + uri: `file://${filename}`, + mimeType: effectiveMime, + text: Buffer.from(base64Data, "base64").toString("utf-8"), + } : { uri: `file://${filename}`, mimeType: effectiveMime, blob: base64Data } await this.connection From dac099a4892689d11abedb0fcc1098b50e0958c8 Mon Sep 17 00:00:00 2001 From: Caleb Norton Date: Sun, 18 Jan 2026 11:14:13 -0600 Subject: [PATCH 093/192] feat(nix): overhaul nix flake and packages (#9032) --- .github/workflows/update-nix-hashes.yml | 77 --------- flake.lock | 6 +- flake.nix | 110 +++--------- nix/bundle.ts | 40 ----- nix/desktop.nix | 191 +++++++-------------- nix/node-modules.nix | 62 ------- nix/opencode.nix | 211 ++++++++++++++---------- nix/scripts/bun-build.ts | 120 -------------- nix/scripts/patch-wasm.ts | 43 ----- packages/opencode/script/build.ts | 5 + 10 files changed, 212 insertions(+), 653 deletions(-) delete mode 100644 nix/bundle.ts delete mode 100644 nix/node-modules.nix delete mode 100644 nix/scripts/bun-build.ts delete mode 100644 nix/scripts/patch-wasm.ts diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index f80a57d25d8..f9817fe1eac 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -19,84 +19,7 @@ on: - ".github/workflows/update-nix-hashes.yml" jobs: - update-flake: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - runs-on: blacksmith-4vcpu-ubuntu-2404 - env: - TITLE: flake.lock - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - ref: ${{ github.head_ref || github.ref_name }} - repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - - - name: Setup Nix - uses: nixbuild/nix-quick-install-action@v34 - - - name: Configure git - run: | - git config --global user.email "action@github.com" - git config --global user.name "Github Action" - - - name: Update ${{ env.TITLE }} - run: | - set -euo pipefail - echo "Updating $TITLE..." - nix flake update - echo "$TITLE updated successfully" - - - name: Commit ${{ env.TITLE }} changes - env: - TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} - run: | - set -euo pipefail - - echo "Checking for changes in tracked files..." - - summarize() { - local status="$1" - { - echo "### Nix $TITLE" - echo "" - echo "- ref: ${GITHUB_REF_NAME}" - echo "- status: ${status}" - } >> "$GITHUB_STEP_SUMMARY" - if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then - echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY" - fi - echo "" >> "$GITHUB_STEP_SUMMARY" - } - FILES=(flake.lock flake.nix) - STATUS="$(git status --short -- "${FILES[@]}" || true)" - if [ -z "$STATUS" ]; then - echo "No changes detected." - summarize "no changes" - exit 0 - fi - - echo "Changes detected:" - echo "$STATUS" - echo "Staging files..." - git add "${FILES[@]}" - echo "Committing changes..." - git commit -m "Update $TITLE" - echo "Changes committed" - - BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" - echo "Pulling latest from branch: $BRANCH" - git pull --rebase --autostash origin "$BRANCH" - echo "Pushing changes to branch: $BRANCH" - git push origin HEAD:"$BRANCH" - echo "Changes pushed successfully" - - summarize "committed $(git rev-parse --short HEAD)" - compute-node-modules-hash: - needs: update-flake if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository strategy: fail-fast: false diff --git a/flake.lock b/flake.lock index 2bfad510e7b..5ef276f0a08 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1768456270, - "narHash": "sha256-NgaL2CCiUR6nsqUIY4yxkzz07iQUlUCany44CFv+OxY=", + "lastModified": 1768302833, + "narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f4606b01b39e09065df37905a2133905246db9ed", + "rev": "61db79b0c6b838d9894923920b612048e1201926", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 32614640ad3..20833fc49ed 100644 --- a/flake.nix +++ b/flake.nix @@ -6,11 +6,7 @@ }; outputs = - { - self, - nixpkgs, - ... - }: + { self, nixpkgs, ... }: let systems = [ "aarch64-linux" @@ -18,99 +14,35 @@ "aarch64-darwin" "x86_64-darwin" ]; - inherit (nixpkgs) lib; - forEachSystem = lib.genAttrs systems; - pkgsFor = system: nixpkgs.legacyPackages.${system}; - packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json); - bunTarget = { - "aarch64-linux" = "bun-linux-arm64"; - "x86_64-linux" = "bun-linux-x64"; - "aarch64-darwin" = "bun-darwin-arm64"; - "x86_64-darwin" = "bun-darwin-x64"; - }; - - # Parse "bun-{os}-{cpu}" to {os, cpu} - parseBunTarget = - target: - let - parts = lib.splitString "-" target; - in - { - os = builtins.elemAt parts 1; - cpu = builtins.elemAt parts 2; - }; - - hashesFile = "${./nix}/hashes.json"; - hashesData = - if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { }; - # Lookup hash: supports per-system ({system: hash}) or legacy single hash - nodeModulesHashFor = - system: - if builtins.isAttrs hashesData.nodeModules then - hashesData.nodeModules.${system} - else - hashesData.nodeModules; - modelsDev = forEachSystem ( - system: - let - pkgs = pkgsFor system; - in - pkgs."models-dev" - ); + forEachSystem = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); + rev = self.shortRev or self.dirtyShortRev or "dirty"; in { - devShells = forEachSystem ( - system: - let - pkgs = pkgsFor system; - in - { - default = pkgs.mkShell { - packages = with pkgs; [ - bun - nodejs_20 - pkg-config - openssl - git - ]; - }; - } - ); + devShells = forEachSystem (pkgs: { + default = pkgs.mkShell { + packages = with pkgs; [ + bun + nodejs_20 + pkg-config + openssl + git + ]; + }; + }); packages = forEachSystem ( - system: + pkgs: let - pkgs = pkgsFor system; - bunPlatform = parseBunTarget bunTarget.${system}; - mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { - hash = nodeModulesHashFor system; - bunCpu = bunPlatform.cpu; - bunOs = bunPlatform.os; + opencode = pkgs.callPackage ./nix/opencode.nix { + inherit rev; }; - mkOpencode = pkgs.callPackage ./nix/opencode.nix { }; - mkDesktop = pkgs.callPackage ./nix/desktop.nix { }; - - opencodePkg = mkOpencode { - inherit (packageJson) version; - src = ./.; - scripts = ./nix/scripts; - target = bunTarget.${system}; - modelsDev = "${modelsDev.${system}}/dist/_api.json"; - inherit mkNodeModules; - }; - - desktopPkg = mkDesktop { - inherit (packageJson) version; - src = ./.; - scripts = ./nix/scripts; - mkNodeModules = mkNodeModules; - opencode = opencodePkg; + desktop = pkgs.callPackage ./nix/desktop.nix { + inherit opencode; }; in { - default = self.packages.${system}.opencode; - opencode = opencodePkg; - desktop = desktopPkg; + default = opencode; + inherit opencode desktop; } ); }; diff --git a/nix/bundle.ts b/nix/bundle.ts deleted file mode 100644 index effb1dff7cc..00000000000 --- a/nix/bundle.ts +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bun - -import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin" -import path from "path" -import fs from "fs" - -const dir = process.cwd() -const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js")) -const worker = "./src/cli/cmd/tui/worker.ts" -const version = process.env.OPENCODE_VERSION ?? "local" -const channel = process.env.OPENCODE_CHANNEL ?? "local" - -fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true }) - -const result = await Bun.build({ - entrypoints: ["./src/index.ts", worker, parser], - outdir: "./dist", - target: "bun", - sourcemap: "none", - tsconfig: "./tsconfig.json", - plugins: [solidPlugin], - external: ["@opentui/core"], - define: { - OPENCODE_VERSION: `'${version}'`, - OPENCODE_CHANNEL: `'${channel}'`, - // Leave undefined so runtime picks bundled/dist worker or fallback in code. - OPENCODE_WORKER_PATH: "undefined", - OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href', - }, -}) - -if (!result.success) { - console.error("bundle failed") - for (const log of result.logs) console.error(log) - process.exit(1) -} - -const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js") -fs.mkdirSync(path.dirname(parserOut), { recursive: true }) -await Bun.write(parserOut, Bun.file(parser)) diff --git a/nix/desktop.nix b/nix/desktop.nix index 9fb73b56316..9625f75c271 100644 --- a/nix/desktop.nix +++ b/nix/desktop.nix @@ -2,166 +2,99 @@ lib, stdenv, rustPlatform, - bun, pkg-config, - dbus ? null, - openssl, - glib ? null, - gtk3 ? null, - libsoup_3 ? null, - webkitgtk_4_1 ? null, - librsvg ? null, - libappindicator-gtk3 ? null, + cargo-tauri, + bun, + nodejs, cargo, rustc, - makeBinaryWrapper, - copyDesktopItems, - makeDesktopItem, - nodejs, jq, + wrapGAppsHook4, + makeWrapper, + dbus, + glib, + gtk4, + libsoup_3, + librsvg, + libappindicator, + glib-networking, + openssl, + webkitgtk_4_1, + gst_all_1, + opencode, }: -args: -let - scripts = args.scripts; - mkModules = - attrs: - args.mkNodeModules ( - attrs - // { - canonicalizeScript = scripts + "/canonicalize-node-modules.ts"; - normalizeBinsScript = scripts + "/normalize-bun-binaries.ts"; - } - ); -in -rustPlatform.buildRustPackage rec { +rustPlatform.buildRustPackage (finalAttrs: { pname = "opencode-desktop"; - version = args.version; + inherit (opencode) + version + src + node_modules + patches + ; - src = args.src; - - # We need to set the root for cargo, but we also need access to the whole repo. - postUnpack = '' - # Update sourceRoot to point to the tauri app - sourceRoot+=/packages/desktop/src-tauri - ''; - - cargoLock = { - lockFile = ../packages/desktop/src-tauri/Cargo.lock; - allowBuiltinFetchGit = true; - }; - - node_modules = mkModules { - version = version; - src = src; - }; + cargoRoot = "packages/desktop/src-tauri"; + cargoLock.lockFile = ../packages/desktop/src-tauri/Cargo.lock; + buildAndTestSubdir = finalAttrs.cargoRoot; nativeBuildInputs = [ pkg-config + cargo-tauri.hook bun - makeBinaryWrapper - copyDesktopItems + nodejs # for patchShebangs node_modules cargo rustc - nodejs jq - ]; - - # based on packages/desktop/src-tauri/release/appstream.metainfo.xml - desktopItems = lib.optionals stdenv.isLinux [ - (makeDesktopItem { - name = "ai.opencode.opencode"; - desktopName = "OpenCode"; - comment = "Open source AI coding agent"; - exec = "opencode-desktop"; - icon = "opencode"; - terminal = false; - type = "Application"; - categories = [ "Development" "IDE" ]; - startupWMClass = "opencode"; - }) - ]; - - buildInputs = [ - openssl + makeWrapper ] - ++ lib.optionals stdenv.isLinux [ + ++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ]; + + buildInputs = lib.optionals stdenv.isLinux [ dbus glib - gtk3 + gtk4 libsoup_3 - webkitgtk_4_1 librsvg - libappindicator-gtk3 + libappindicator + glib-networking + openssl + webkitgtk_4_1 + gst_all_1.gstreamer + gst_all_1.gst-plugins-base + gst_all_1.gst-plugins-good ]; - preBuild = '' - # Restore node_modules - pushd ../../.. - - # Copy node_modules from the fixed-output derivation - # We use cp -r --no-preserve=mode to ensure we can write to them if needed, - # though we usually just read. - cp -r ${node_modules}/node_modules . - cp -r ${node_modules}/packages . + strictDeps = true; - # Ensure node_modules is writable so patchShebangs can update script headers - chmod -R u+w node_modules - # Ensure workspace packages are writable for tsgo incremental outputs (.tsbuildinfo) - chmod -R u+w packages - # Patch shebangs so scripts can run + preBuild = '' + cp -a ${finalAttrs.node_modules}/{node_modules,packages} . + chmod -R u+w node_modules packages patchShebangs node_modules + patchShebangs packages/desktop/node_modules - # Copy sidecar mkdir -p packages/desktop/src-tauri/sidecars - targetTriple=${stdenv.hostPlatform.rust.rustcTarget} - cp ${args.opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-$targetTriple - - # Merge prod config into tauri.conf.json - if ! jq -s '.[0] * .[1]' \ - packages/desktop/src-tauri/tauri.conf.json \ - packages/desktop/src-tauri/tauri.prod.conf.json \ - > packages/desktop/src-tauri/tauri.conf.json.tmp; then - echo "Error: failed to merge tauri.conf.json with tauri.prod.conf.json" >&2 - exit 1 - fi - mv packages/desktop/src-tauri/tauri.conf.json.tmp packages/desktop/src-tauri/tauri.conf.json - - # Build the frontend - cd packages/desktop - - # The 'build' script runs 'bun run typecheck && vite build'. - bun run build - - popd + cp ${opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-${stdenv.hostPlatform.rust.rustcTarget} ''; - # Tauri bundles the assets during the rust build phase (which happens after preBuild). - # It looks for them in the location specified in tauri.conf.json. - - postInstall = lib.optionalString stdenv.isLinux '' - # Install icon - mkdir -p $out/share/icons/hicolor/128x128/apps - cp ../../../packages/desktop/src-tauri/icons/prod/128x128.png $out/share/icons/hicolor/128x128/apps/opencode.png + # see publish-tauri job in .github/workflows/publish.yml + tauriBuildFlags = [ + "--config" + "tauri.prod.conf.json" + "--no-sign" # no code signing or auto updates + ]; - # Wrap the binary to ensure it finds the libraries - wrapProgram $out/bin/opencode-desktop \ - --prefix LD_LIBRARY_PATH : ${ - lib.makeLibraryPath [ - gtk3 - webkitgtk_4_1 - librsvg - glib - libsoup_3 - ] - } + # FIXME: workaround for concerns about case insensitive filesystems + # should be removed once binary is renamed or decided otherwise + # darwin output is a .app bundle so no conflict + postFixup = lib.optionalString stdenv.hostPlatform.isLinux '' + mv $out/bin/OpenCode $out/bin/opencode-desktop + sed -i 's|^Exec=OpenCode$|Exec=opencode-desktop|' $out/share/applications/OpenCode.desktop ''; - meta = with lib; { + meta = { description = "OpenCode Desktop App"; homepage = "https://opencode.ai"; - license = licenses.mit; - maintainers = with maintainers; [ ]; + license = lib.licenses.mit; mainProgram = "opencode-desktop"; - platforms = platforms.linux ++ platforms.darwin; + inherit (opencode.meta) platforms; }; -} +}) \ No newline at end of file diff --git a/nix/node-modules.nix b/nix/node-modules.nix deleted file mode 100644 index 2a8f0a47cb0..00000000000 --- a/nix/node-modules.nix +++ /dev/null @@ -1,62 +0,0 @@ -{ - hash, - lib, - stdenvNoCC, - bun, - cacert, - curl, - bunCpu, - bunOs, -}: -args: -stdenvNoCC.mkDerivation { - pname = "opencode-node_modules"; - inherit (args) version src; - - impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [ - "GIT_PROXY_COMMAND" - "SOCKS_SERVER" - ]; - - nativeBuildInputs = [ - bun - cacert - curl - ]; - - dontConfigure = true; - - buildPhase = '' - runHook preBuild - export HOME=$(mktemp -d) - export BUN_INSTALL_CACHE_DIR=$(mktemp -d) - bun install \ - --cpu="${bunCpu}" \ - --os="${bunOs}" \ - --frozen-lockfile \ - --ignore-scripts \ - --no-progress \ - --linker=isolated - bun --bun ${args.canonicalizeScript} - bun --bun ${args.normalizeBinsScript} - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - mkdir -p $out - while IFS= read -r dir; do - rel="''${dir#./}" - dest="$out/$rel" - mkdir -p "$(dirname "$dest")" - cp -R "$dir" "$dest" - done < <(find . -type d -name node_modules -prune | sort) - runHook postInstall - ''; - - dontFixup = true; - - outputHashAlgo = "sha256"; - outputHashMode = "recursive"; - outputHash = hash; -} diff --git a/nix/opencode.nix b/nix/opencode.nix index 714aabe094f..4d6f8e9b423 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -2,60 +2,115 @@ lib, stdenvNoCC, bun, - ripgrep, + sysctl, makeBinaryWrapper, + models-dev, + ripgrep, + installShellFiles, + versionCheckHook, + writableTmpDirAsHomeHook, + rev ? "dirty", }: -args: let - inherit (args) scripts; - mkModules = - attrs: - args.mkNodeModules ( - attrs - // { - canonicalizeScript = scripts + "/canonicalize-node-modules.ts"; - normalizeBinsScript = scripts + "/normalize-bun-binaries.ts"; - } - ); + packageJson = lib.pipe ../packages/opencode/package.json [ + builtins.readFile + builtins.fromJSON + ]; in stdenvNoCC.mkDerivation (finalAttrs: { pname = "opencode"; - inherit (args) version src; + version = "${packageJson.version}-${rev}"; + + src = lib.fileset.toSource { + root = ../.; + fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) ( + lib.fileset.unions [ + ../packages + ../bun.lock + ../package.json + ../patches + ../install + ] + ); + }; - node_modules = mkModules { + node_modules = stdenvNoCC.mkDerivation { + pname = "${finalAttrs.pname}-node_modules"; inherit (finalAttrs) version src; + + impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [ + "GIT_PROXY_COMMAND" + "SOCKS_SERVER" + ]; + + nativeBuildInputs = [ + bun + ]; + + dontConfigure = true; + + buildPhase = '' + runHook preBuild + export HOME=$(mktemp -d) + export BUN_INSTALL_CACHE_DIR=$(mktemp -d) + bun install \ + --cpu="${if stdenvNoCC.hostPlatform.isAarch64 then "arm64" else "x64"}" \ + --os="${if stdenvNoCC.hostPlatform.isLinux then "linux" else "darwin"}" \ + --frozen-lockfile \ + --ignore-scripts \ + --no-progress \ + --linker=isolated + bun --bun ${./scripts/canonicalize-node-modules.ts} + bun --bun ${./scripts/normalize-bun-binaries.ts} + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out + find . -type d -name node_modules -exec cp -R --parents {} $out \; + + runHook postInstall + ''; + + dontFixup = true; + + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + outputHash = + (lib.pipe ./hashes.json [ + builtins.readFile + builtins.fromJSON + ]).nodeModules.${stdenvNoCC.hostPlatform.system}; }; nativeBuildInputs = [ bun + installShellFiles makeBinaryWrapper + models-dev + writableTmpDirAsHomeHook ]; - env.MODELS_DEV_API_JSON = args.modelsDev; - env.OPENCODE_VERSION = args.version; - env.OPENCODE_CHANNEL = "stable"; - dontConfigure = true; + configurePhase = '' + runHook preConfigure - buildPhase = '' - runHook preBuild + cp -R ${finalAttrs.node_modules}/. . - cp -r ${finalAttrs.node_modules}/node_modules . - cp -r ${finalAttrs.node_modules}/packages . + runHook postConfigure + ''; - ( - cd packages/opencode + env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json"; + env.OPENCODE_VERSION = finalAttrs.version; + env.OPENCODE_CHANNEL = "local"; - chmod -R u+w ./node_modules - mkdir -p ./node_modules/@opencode-ai - rm -f ./node_modules/@opencode-ai/{script,sdk,plugin} - ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script - ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk - ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin + buildPhase = '' + runHook preBuild - cp ${./bundle.ts} ./bundle.ts - chmod +x ./bundle.ts - bun run ./bundle.ts - ) + cd ./packages/opencode + bun --bun ./script/build.ts --single --skip-install + bun --bun ./script/schema.ts schema.json runHook postBuild ''; @@ -63,76 +118,52 @@ stdenvNoCC.mkDerivation (finalAttrs: { installPhase = '' runHook preInstall - cd packages/opencode - if [ ! -d dist ]; then - echo "ERROR: dist directory missing after bundle step" - exit 1 - fi - - mkdir -p $out/lib/opencode - cp -r dist $out/lib/opencode/ - chmod -R u+w $out/lib/opencode/dist - - # Select bundled worker assets deterministically (sorted find output) - worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1) - parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1) - if [ -z "$worker_file" ]; then - echo "ERROR: bundled worker not found" - exit 1 - fi - - main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1) - wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print) - for patch_file in "$worker_file" "$parser_worker_file"; do - [ -z "$patch_file" ] && continue - [ ! -f "$patch_file" ] && continue - if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then - # Rewrite wasm references to absolute store paths to avoid runtime resolve failures. - bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list - fi - done - - mkdir -p $out/lib/opencode/node_modules - cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/ - mkdir -p $out/lib/opencode/node_modules/@opentui - - mkdir -p $out/bin - makeWrapper ${bun}/bin/bun $out/bin/opencode \ - --add-flags "run" \ - --add-flags "$out/lib/opencode/dist/src/index.js" \ - --prefix PATH : ${lib.makeBinPath [ ripgrep ]} \ - --argv0 opencode + install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode + install -Dm644 schema.json $out/share/opencode/schema.json + + wrapProgram $out/bin/opencode \ + --prefix PATH : ${ + lib.makeBinPath ( + [ + ripgrep + ] + # bun runs sysctl to detect if dunning on rosetta2 + ++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl + ) + } runHook postInstall ''; - postInstall = '' - for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-* $out/lib/opencode/node_modules/.bun/@opentui+solid-* $out/lib/opencode/node_modules/.bun/@opentui+core@* $out/lib/opencode/node_modules/.bun/@opentui+solid@*; do - if [ -d "$pkg" ]; then - pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/') - ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \ - $out/lib/opencode/node_modules/@opentui/$pkgName - fi - done + postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) '' + # trick yargs into also generating zsh completions + installShellCompletion --cmd opencode \ + --bash <($out/bin/opencode completion) \ + --zsh <(SHELL=/bin/zsh $out/bin/opencode completion) ''; - dontFixup = true; + nativeInstallCheckInputs = [ + versionCheckHook + writableTmpDirAsHomeHook + ]; + doInstallCheck = true; + versionCheckKeepEnvironment = [ "HOME" ]; + versionCheckProgramArg = "--version"; + + passthru = { + jsonschema = "${placeholder "out"}/share/opencode/schema.json"; + }; meta = { - description = "AI coding agent built for the terminal"; - longDescription = '' - OpenCode is a terminal-based agent that can build anything. - It combines a TypeScript/JavaScript core with a Go-based TUI - to provide an interactive AI coding experience. - ''; - homepage = "https://github.com/anomalyco/opencode"; + description = "The open source coding agent"; + homepage = "https://opencode.ai/"; license = lib.licenses.mit; + mainProgram = "opencode"; platforms = [ "aarch64-linux" "x86_64-linux" "aarch64-darwin" "x86_64-darwin" ]; - mainProgram = "opencode"; }; }) diff --git a/nix/scripts/bun-build.ts b/nix/scripts/bun-build.ts deleted file mode 100644 index e607676cb11..00000000000 --- a/nix/scripts/bun-build.ts +++ /dev/null @@ -1,120 +0,0 @@ -import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin" -import path from "path" -import fs from "fs" - -const version = "@VERSION@" -const pkg = path.join(process.cwd(), "packages/opencode") -const parser = fs.realpathSync(path.join(pkg, "./node_modules/@opentui/core/parser.worker.js")) -const worker = "./src/cli/cmd/tui/worker.ts" -const target = process.env["BUN_COMPILE_TARGET"] - -if (!target) { - throw new Error("BUN_COMPILE_TARGET not set") -} - -process.chdir(pkg) - -const manifestName = "opencode-assets.manifest" -const manifestPath = path.join(pkg, manifestName) - -const readTrackedAssets = () => { - if (!fs.existsSync(manifestPath)) return [] - return fs - .readFileSync(manifestPath, "utf8") - .split("\n") - .map((line) => line.trim()) - .filter((line) => line.length > 0) -} - -const removeTrackedAssets = () => { - for (const file of readTrackedAssets()) { - const filePath = path.join(pkg, file) - if (fs.existsSync(filePath)) { - fs.rmSync(filePath, { force: true }) - } - } -} - -const assets = new Set() - -const addAsset = async (p: string) => { - const file = path.basename(p) - const dest = path.join(pkg, file) - await Bun.write(dest, Bun.file(p)) - assets.add(file) -} - -removeTrackedAssets() - -const result = await Bun.build({ - conditions: ["browser"], - tsconfig: "./tsconfig.json", - plugins: [solidPlugin], - sourcemap: "external", - entrypoints: ["./src/index.ts", parser, worker], - define: { - OPENCODE_VERSION: `'@VERSION@'`, - OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(pkg, parser).replace(/\\/g, "/"), - OPENCODE_CHANNEL: "'latest'", - }, - compile: { - target, - outfile: "opencode", - autoloadBunfig: false, - autoloadDotenv: false, - //@ts-ignore (bun types aren't up to date) - autoloadTsconfig: true, - autoloadPackageJson: true, - execArgv: ["--user-agent=opencode/" + version, "--use-system-ca", "--"], - windows: {}, - }, -}) - -if (!result.success) { - console.error("Build failed!") - for (const log of result.logs) { - console.error(log) - } - throw new Error("Compilation failed") -} - -const assetOutputs = result.outputs?.filter((x) => x.kind === "asset") ?? [] -for (const x of assetOutputs) { - await addAsset(x.path) -} - -const bundle = await Bun.build({ - entrypoints: [worker], - tsconfig: "./tsconfig.json", - plugins: [solidPlugin], - target: "bun", - outdir: "./.opencode-worker", - sourcemap: "none", -}) - -if (!bundle.success) { - console.error("Worker build failed!") - for (const log of bundle.logs) { - console.error(log) - } - throw new Error("Worker compilation failed") -} - -const workerAssets = bundle.outputs?.filter((x) => x.kind === "asset") ?? [] -for (const x of workerAssets) { - await addAsset(x.path) -} - -const output = bundle.outputs.find((x) => x.kind === "entry-point") -if (!output) { - throw new Error("Worker build produced no entry-point output") -} - -const dest = path.join(pkg, "opencode-worker.js") -await Bun.write(dest, Bun.file(output.path)) -fs.rmSync(path.dirname(output.path), { recursive: true, force: true }) - -const list = Array.from(assets) -await Bun.write(manifestPath, list.length > 0 ? list.join("\n") + "\n" : "") - -console.log("Build successful!") diff --git a/nix/scripts/patch-wasm.ts b/nix/scripts/patch-wasm.ts deleted file mode 100644 index 88a06c2bd2b..00000000000 --- a/nix/scripts/patch-wasm.ts +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bun - -import fs from "fs" -import path from "path" - -/** - * Rewrite tree-sitter wasm references inside a JS file to absolute paths. - * argv: [node, script, file, mainWasm, ...wasmPaths] - */ -const [, , file, mainWasm, ...wasmPaths] = process.argv - -if (!file || !mainWasm) { - console.error("usage: patch-wasm [wasmPaths...]") - process.exit(1) -} - -const content = fs.readFileSync(file, "utf8") -const byName = new Map() - -for (const wasm of wasmPaths) { - const name = path.basename(wasm) - byName.set(name, wasm) -} - -let next = content - -for (const [name, wasmPath] of byName) { - next = next.replaceAll(name, wasmPath) -} - -next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm) - -// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...") -const nixStorePrefix = process.env.NIX_STORE || "/nix/store" -next = next.replace(/(\.\/)+/g, "./") -next = next.replace( - new RegExp(`(\\.\\.\\/)+\\/{1,2}(${nixStorePrefix.replace(/^\//, "").replace(/\//g, "\\/")}[^"']+)`, "g"), - "/$2", -) -next = next.replace(new RegExp(`(["'])\\/{2,}(\\/${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3") -next = next.replace(new RegExp(`(["'])\\/\\/(${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3") - -if (next !== content) fs.writeFileSync(file, next) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 61a665312f0..cb88db2c478 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -90,6 +90,11 @@ const targets = singleFlag return baselineFlag } + // also skip abi-specific builds for the same reason + if (item.abi !== undefined) { + return false + } + return true }) : allTargets From bfd2f91d5b4b7ee28346bfcfd1481a8c0370574c Mon Sep 17 00:00:00 2001 From: Spoon <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:11:22 +0100 Subject: [PATCH 094/192] feat(hook): command execute before hook (#9267) --- packages/opencode/src/session/prompt.ts | 10 ++++++++++ packages/plugin/src/index.ts | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 0d3d25feb8d..f4793d1a798 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1702,6 +1702,16 @@ NOTE: At any point in time through this workflow you should feel free to ask the : await lastModel(input.sessionID) : taskModel + await Plugin.trigger( + "command.execute.before", + { + command: input.command, + sessionID: input.sessionID, + arguments: input.arguments, + }, + { parts }, + ) + const result = (await prompt({ sessionID: input.sessionID, messageID: input.messageID, diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index e57eff579e6..36a4657d74c 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -173,6 +173,10 @@ export interface Hooks { output: { temperature: number; topP: number; topK: number; options: Record }, ) => Promise "permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise + "command.execute.before"?: ( + input: { command: string; sessionID: string; arguments: string }, + output: { parts: Part[] }, + ) => Promise "tool.execute.before"?: ( input: { tool: string; sessionID: string; callID: string }, output: { args: any }, From 501ef2d989afde09b54299d309442a7b1a39a680 Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Sun, 18 Jan 2026 20:11:34 +0100 Subject: [PATCH 095/192] fix: update gitlab-ai-provider to 1.3.2 (#9279) --- bun.lock | 4 ++-- packages/opencode/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 9cda088153c..a9cabb31114 100644 --- a/bun.lock +++ b/bun.lock @@ -281,7 +281,7 @@ "@ai-sdk/vercel": "1.0.31", "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", - "@gitlab/gitlab-ai-provider": "3.1.1", + "@gitlab/gitlab-ai-provider": "3.1.2", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", "@modelcontextprotocol/sdk": "1.25.2", @@ -917,7 +917,7 @@ "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], - "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-7AtFrCflq2NzC99bj7YaqbQDCZyaScM1+L4ujllV5syiRTFE239Uhnd/yEkPXa7sUAnNRfN3CWusCkQ2zK/q9g=="], + "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-p0NZhZJSavWDX9r/Px/mOK2YIC803GZa8iRzcg3f1C6S0qfea/HBTe4/NWvT2+2kWIwhCePGuI4FN2UFiUWXUg=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 757e6efde90..e1918193470 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -70,7 +70,7 @@ "@ai-sdk/vercel": "1.0.31", "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", - "@gitlab/gitlab-ai-provider": "3.1.1", + "@gitlab/gitlab-ai-provider": "3.1.2", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", "@modelcontextprotocol/sdk": "1.25.2", From 38c641a2fc6d45c504d419609359f64710a4e732 Mon Sep 17 00:00:00 2001 From: zerone0x Date: Mon, 19 Jan 2026 03:17:49 +0800 Subject: [PATCH 096/192] fix(tool): treat .fbs files as text instead of images (#9276) Co-authored-by: Claude --- packages/opencode/src/tool/read.ts | 4 +++- packages/opencode/test/tool/read.test.ts | 29 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index ce4ab28619d..3b1484cbc0f 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -59,7 +59,9 @@ export const ReadTool = Tool.define("read", { throw new Error(`File not found: ${filepath}`) } - const isImage = file.type.startsWith("image/") && file.type !== "image/svg+xml" + // Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files) + const isImage = + file.type.startsWith("image/") && file.type !== "image/svg+xml" && file.type !== "image/vnd.fastbidsheet" const isPdf = file.type === "application/pdf" if (isImage || isPdf) { const mime = file.type diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 04ffc80ea67..7250bd2fd1e 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -300,4 +300,33 @@ describe("tool.read truncation", () => { }, }) }) + + test(".fbs files (FlatBuffers schema) are read as text, not images", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + // FlatBuffers schema content + const fbsContent = `namespace MyGame; + +table Monster { + pos:Vec3; + name:string; + inventory:[ubyte]; +} + +root_type Monster;` + await Bun.write(path.join(dir, "schema.fbs"), fbsContent) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const read = await ReadTool.init() + const result = await read.execute({ filePath: path.join(tmp.path, "schema.fbs") }, ctx) + // Should be read as text, not as image + expect(result.attachments).toBeUndefined() + expect(result.output).toContain("namespace MyGame") + expect(result.output).toContain("table Monster") + }, + }) + }) }) From c29d44fcef12b393f82407d6fbd26b0ce8aa979a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 18 Jan 2026 13:22:39 -0600 Subject: [PATCH 097/192] docs: note untracked files in review --- packages/opencode/src/command/template/review.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/command/template/review.txt b/packages/opencode/src/command/template/review.txt index 1ffa0fca0b4..9f6fbfcc3a8 100644 --- a/packages/opencode/src/command/template/review.txt +++ b/packages/opencode/src/command/template/review.txt @@ -13,6 +13,7 @@ Based on the input provided, determine which type of review to perform: 1. **No arguments (default)**: Review all uncommitted changes - Run: `git diff` for unstaged changes - Run: `git diff --cached` for staged changes + - Run: `git status --short` to identify untracked (net new) files 2. **Commit hash** (40-char SHA or short hash): Review that specific commit - Run: `git show $ARGUMENTS` @@ -33,6 +34,7 @@ Use best judgement when processing input. **Diffs alone are not enough.** After getting the diff, read the entire file(s) being modified to understand the full context. Code that looks wrong in isolation may be correct given surrounding logic—and vice versa. - Use the diff to identify which files changed +- Use `git status --short` to identify untracked files, then read their full contents - Read the full file to understand existing patterns, control flow, and error handling - Check for existing style guide or conventions files (CONVENTIONS.md, AGENTS.md, .editorconfig, etc.) From 19cf9344e12891f92662498ee2c9f132ac78480b Mon Sep 17 00:00:00 2001 From: Github Action Date: Sun, 18 Jan 2026 19:24:21 +0000 Subject: [PATCH 098/192] Update 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 16a1c1f398b..5bbdf921bbd 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-4zchRpxzvHnPMcwumgL9yaX0deIXS5IGPp131eYsSvg=", - "aarch64-linux": "sha256-3/BSRsl5pI0Iz3qAFZxIkOehFLZ2Ox9UsbdDHYzqlVg=", - "aarch64-darwin": "sha256-86d/G1q6xiHSSlm+/irXoKLb/yLQbV348uuSrBV70+Q=", - "x86_64-darwin": "sha256-WYaP44PWRGtoG1DIuUJUH4DvuaCuFhlJZ9fPzGsiIfE=" + "x86_64-linux": "sha256-D1VXuKJagfq3mxh8Xs8naHoYNJUJzAM9JLJqpHcItDk=", + "aarch64-linux": "sha256-9wXcg50Sv56Wb2x5NWe15olNGE/uMiDkmGRmqPoeW1U=", + "aarch64-darwin": "sha256-i5eTTjsNAARwcw69sd6wuse2BKTUi/Vfgo4M28l+RoY=", + "x86_64-darwin": "sha256-oFtQnIzgTS2zcjkhBTnXxYqr20KXdA2I+b908piLs+c=" } } From d841e70d2646d84c31f839e8cf7f94bc9bda66a8 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 18 Jan 2026 14:21:08 -0600 Subject: [PATCH 099/192] fix: bad variants for grok models --- packages/opencode/src/provider/transform.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 79892db4cca..b803bd66ce1 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -325,9 +325,24 @@ export namespace ProviderTransform { const id = model.id.toLowerCase() if (id.includes("deepseek") || id.includes("minimax") || id.includes("glm") || id.includes("mistral")) return {} + // see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks + if (id.includes("grok") && id.includes("grok-3-mini")) { + if (model.api.npm === "@openrouter/ai-sdk-provider") { + return { + low: { reasoning: { effort: "low" } }, + high: { reasoning: { effort: "high" } }, + } + } + return { + low: { reasoningEffort: "low" }, + high: { reasoningEffort: "high" }, + } + } + if (id.includes("grok")) return {} + switch (model.api.npm) { case "@openrouter/ai-sdk-provider": - if (!model.id.includes("gpt") && !model.id.includes("gemini-3") && !model.id.includes("grok-4")) return {} + if (!model.id.includes("gpt") && !model.id.includes("gemini-3")) return {} return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }])) // TODO: YOU CANNOT SET max_tokens if this is set!!! From 0d8e706facd193610572f1d5b8ddeba80de0b63a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 18 Jan 2026 14:44:39 -0600 Subject: [PATCH 100/192] test: fix transfomr test --- .../opencode/test/provider/transform.test.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index dcf16c65cbd..2b8f1872f56 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1140,7 +1140,7 @@ describe("ProviderTransform.variants", () => { expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) }) - test("grok-4 returns OPENAI_EFFORTS with reasoning", () => { + test("grok-4 returns empty object", () => { const model = createMockModel({ id: "openrouter/grok-4", providerID: "openrouter", @@ -1151,7 +1151,23 @@ describe("ProviderTransform.variants", () => { }, }) const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) + expect(result).toEqual({}) + }) + + test("grok-3-mini returns low and high with reasoning", () => { + const model = createMockModel({ + id: "openrouter/grok-3-mini", + providerID: "openrouter", + api: { + id: "grok-3-mini", + url: "https://openrouter.ai", + npm: "@openrouter/ai-sdk-provider", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "high"]) + expect(result.low).toEqual({ reasoning: { effort: "low" } }) + expect(result.high).toEqual({ reasoning: { effort: "high" } }) }) }) @@ -1210,7 +1226,7 @@ describe("ProviderTransform.variants", () => { }) describe("@ai-sdk/xai", () => { - test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => { + test("grok-3 returns empty object", () => { const model = createMockModel({ id: "xai/grok-3", providerID: "xai", @@ -1221,7 +1237,21 @@ describe("ProviderTransform.variants", () => { }, }) const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high"]) + expect(result).toEqual({}) + }) + + test("grok-3-mini returns low and high with reasoningEffort", () => { + const model = createMockModel({ + id: "xai/grok-3-mini", + providerID: "xai", + api: { + id: "grok-3-mini", + url: "https://api.x.ai", + npm: "@ai-sdk/xai", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "high"]) expect(result.low).toEqual({ reasoningEffort: "low" }) expect(result.high).toEqual({ reasoningEffort: "high" }) }) From b4d4a1ea7d2e590e3963b36580989404377e4ce4 Mon Sep 17 00:00:00 2001 From: Alan Pogrebinschi Date: Sun, 18 Jan 2026 14:46:04 -0800 Subject: [PATCH 101/192] docs: clarify agent tool access and explore vs general distinction (#9300) --- packages/web/src/content/docs/agents.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index 22bed7f16a4..ea1f779cd37 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -21,7 +21,7 @@ There are two types of agents in OpenCode; primary agents and subagents. ### Primary agents -Primary agents are the main assistants you interact with directly. You can cycle through them using the **Tab** key, or your configured `switch_agent` keybind. These agents handle your main conversation and can access all configured tools. +Primary agents are the main assistants you interact with directly. You can cycle through them using the **Tab** key, or your configured `switch_agent` keybind. These agents handle your main conversation. Tool access is configured via permissions — for example, Build has all tools enabled while Plan is restricted. :::tip You can use the **Tab** key to switch between primary agents during a session. @@ -72,7 +72,7 @@ This agent is useful when you want the LLM to analyze code, suggest changes, or _Mode_: `subagent` -A general-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. Use when searching for keywords or files and you're not confident you'll find the right match in the first few tries. +A general-purpose agent for researching complex questions and executing multi-step tasks. Has full tool access (except todo), so it can make file changes when needed. Use this to run multiple units of work in parallel. --- @@ -80,7 +80,7 @@ A general-purpose agent for researching complex questions, searching for code, a _Mode_: `subagent` -A fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns, search code for keywords, or answer questions about the codebase. +A fast, read-only agent for exploring codebases. Cannot modify files. Use this when you need to quickly find files by patterns, search code for keywords, or answer questions about the codebase. --- From e81bb86795c062dae736568c9c4a4426e8fe9474 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 19 Jan 2026 09:27:30 +1000 Subject: [PATCH 102/192] fix: Windows evaluating text on copy (#9293) --- .../src/cli/cmd/tui/util/clipboard.ts | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 2526f41714c..0e287fbc41a 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -125,9 +125,25 @@ export namespace Clipboard { if (os === "win32") { console.log("clipboard: using powershell") return async (text: string) => { - // need to escape backticks because powershell uses them as escape code - const escaped = text.replace(/"/g, '""').replace(/`/g, "``") - await $`powershell -NonInteractive -NoProfile -Command "Set-Clipboard -Value \"${escaped}\""`.nothrow().quiet() + // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.) + const proc = Bun.spawn( + [ + "powershell.exe", + "-NonInteractive", + "-NoProfile", + "-Command", + "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())", + ], + { + stdin: "pipe", + stdout: "ignore", + stderr: "ignore", + }, + ) + + proc.stdin.write(text) + proc.stdin.end() + await proc.exited.catch(() => {}) } } From bee2f654090f92f607fbf4f7d1ff669ae76ede39 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 18 Jan 2026 19:18:58 -0500 Subject: [PATCH 103/192] zen: fix checkout link for black users --- packages/console/core/src/billing.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index f052e6fc6fe..36e8a76b79d 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -218,6 +218,7 @@ export namespace Billing { customer: customer.customerID, customer_update: { name: "auto", + address: "auto", }, } : { From d939a3ad547f1794ab39a5455517bedfc310f286 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:42:10 +1000 Subject: [PATCH 104/192] feat(tui): use mouse for permission buttons (#9305) --- .../opencode/src/cli/cmd/tui/routes/session/permission.tsx | 6 ++++++ 1 file changed, 6 insertions(+) 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 eab2adb100c..c4ff4c04b0c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -280,6 +280,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { reply: "reject", requestID: props.request.id, }) + return } sdk.client.permission.reply({ reply: "once", @@ -456,6 +457,11 @@ function Prompt>(props: { paddingLeft={1} paddingRight={1} backgroundColor={option === store.selected ? theme.warning : theme.backgroundMenu} + onMouseOver={() => setStore("selected", option)} + onMouseUp={() => { + setStore("selected", option) + props.onSelect(option) + }} > {props.options[option]} From 2fc4ab9687219aae4cef5fba042264f7638c5ebc Mon Sep 17 00:00:00 2001 From: Caleb Norton Date: Sun, 18 Jan 2026 21:46:00 -0600 Subject: [PATCH 105/192] ci: simplify nix hash updates (#9309) --- .github/workflows/update-nix-hashes.yml | 171 ++++-------------------- flake.nix | 24 +++- nix/node_modules.nix | 85 ++++++++++++ nix/opencode.nix | 83 +----------- 4 files changed, 140 insertions(+), 223 deletions(-) create mode 100644 nix/node_modules.nix diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index f9817fe1eac..6e937da5270 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -10,32 +10,22 @@ on: - "bun.lock" - "package.json" - "packages/*/package.json" + - "flake.lock" - ".github/workflows/update-nix-hashes.yml" pull_request: paths: - "bun.lock" - "package.json" - "packages/*/package.json" + - "flake.lock" - ".github/workflows/update-nix-hashes.yml" jobs: - compute-node-modules-hash: + update-node-modules-hashes: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - strategy: - fail-fast: false - matrix: - include: - - system: x86_64-linux - host: blacksmith-4vcpu-ubuntu-2404 - - system: aarch64-linux - host: blacksmith-4vcpu-ubuntu-2404-arm - - system: x86_64-darwin - host: macos-15-intel - - system: aarch64-darwin - host: macos-latest - runs-on: ${{ matrix.host }} + runs-on: blacksmith-4vcpu-ubuntu-2404 env: - SYSTEM: ${{ matrix.system }} + TITLE: node_modules hashes steps: - name: Checkout repository @@ -49,104 +39,6 @@ jobs: - name: Setup Nix uses: nixbuild/nix-quick-install-action@v34 - - name: Compute node_modules hash - run: | - set -euo pipefail - - DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - HASH_FILE="nix/hashes.json" - OUTPUT_FILE="hash-${SYSTEM}.txt" - - export NIX_KEEP_OUTPUTS=1 - export NIX_KEEP_DERIVATIONS=1 - - BUILD_LOG=$(mktemp) - TMP_JSON=$(mktemp) - trap 'rm -f "$BUILD_LOG" "$TMP_JSON"' EXIT - - if [ ! -f "$HASH_FILE" ]; then - mkdir -p "$(dirname "$HASH_FILE")" - echo '{"nodeModules":{}}' > "$HASH_FILE" - fi - - # Set dummy hash to force nix to rebuild and reveal correct hash - jq --arg system "$SYSTEM" --arg value "$DUMMY" \ - '.nodeModules = (.nodeModules // {}) | .nodeModules[$system] = $value' "$HASH_FILE" > "$TMP_JSON" - mv "$TMP_JSON" "$HASH_FILE" - - MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules" - DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")" - - echo "Building node_modules for ${SYSTEM} to discover correct hash..." - echo "Attempting to realize derivation: ${DRV_PATH}" - REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true) - - BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true) - CORRECT_HASH="" - - if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then - echo "Realized node_modules output: $BUILD_PATH" - CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true) - fi - - # Try to extract hash from build log - if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" - fi - - if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" - fi - - # Try to hash from kept failed build directory - if [ -z "$CORRECT_HASH" ]; then - KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1 || true) - if [ -z "$KEPT_DIR" ]; then - KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1 || true) - fi - - if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then - HASH_PATH="$KEPT_DIR" - [ -d "$KEPT_DIR/build" ] && HASH_PATH="$KEPT_DIR/build" - - if [ -d "$HASH_PATH/node_modules" ]; then - CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true) - fi - fi - fi - - if [ -z "$CORRECT_HASH" ]; then - echo "Failed to determine correct node_modules hash for ${SYSTEM}." - cat "$BUILD_LOG" - exit 1 - fi - - echo "$CORRECT_HASH" > "$OUTPUT_FILE" - echo "Hash for ${SYSTEM}: $CORRECT_HASH" - - - name: Upload hash artifact - uses: actions/upload-artifact@v6 - with: - name: hash-${{ matrix.system }} - path: hash-${{ matrix.system }}.txt - retention-days: 1 - - commit-node-modules-hashes: - needs: compute-node-modules-hash - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - runs-on: blacksmith-4vcpu-ubuntu-2404 - env: - TITLE: node_modules hashes - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - ref: ${{ github.head_ref || github.ref_name }} - repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - - name: Configure git run: | git config --global user.email "action@github.com" @@ -159,54 +51,47 @@ jobs: BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" git pull --rebase --autostash origin "$BRANCH" - - name: Download all hash artifacts - uses: actions/download-artifact@v7 - with: - pattern: hash-* - merge-multiple: true - - - name: Merge hashes into hashes.json + - name: Compute all node_modules hashes run: | set -euo pipefail HASH_FILE="nix/hashes.json" + SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin" if [ ! -f "$HASH_FILE" ]; then mkdir -p "$(dirname "$HASH_FILE")" echo '{"nodeModules":{}}' > "$HASH_FILE" fi - echo "Merging hashes into ${HASH_FILE}..." + for SYSTEM in $SYSTEMS; do + echo "Computing hash for ${SYSTEM}..." + BUILD_LOG=$(mktemp) + trap 'rm -f "$BUILD_LOG"' EXIT - shopt -s nullglob - files=(hash-*.txt) - if [ ${#files[@]} -eq 0 ]; then - echo "No hash files found, nothing to update" - exit 0 - fi + # The updater derivations use fakeHash, so they will fail and reveal the correct hash + UPDATER_ATTR=".#packages.x86_64-linux.${SYSTEM}_node_modules" - EXPECTED_SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin" - for sys in $EXPECTED_SYSTEMS; do - if [ ! -f "hash-${sys}.txt" ]; then - echo "WARNING: Missing hash file for $sys" + nix build "$UPDATER_ATTR" --no-link 2>&1 | tee "$BUILD_LOG" || true + + CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" + + if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" fi - done - for f in "${files[@]}"; do - system="${f#hash-}" - system="${system%.txt}" - hash=$(cat "$f") - if [ -z "$hash" ]; then - echo "WARNING: Empty hash for $system, skipping" - continue + if [ -z "$CORRECT_HASH" ]; then + echo "Failed to determine correct node_modules hash for ${SYSTEM}." + cat "$BUILD_LOG" + exit 1 fi - echo " $system: $hash" - jq --arg sys "$system" --arg h "$hash" \ - '.nodeModules = (.nodeModules // {}) | .nodeModules[$sys] = $h' "$HASH_FILE" > "${HASH_FILE}.tmp" + + echo " ${SYSTEM}: ${CORRECT_HASH}" + jq --arg sys "$SYSTEM" --arg h "$CORRECT_HASH" \ + '.nodeModules[$sys] = $h' "$HASH_FILE" > "${HASH_FILE}.tmp" mv "${HASH_FILE}.tmp" "$HASH_FILE" done - echo "All hashes merged:" + echo "All hashes computed:" cat "$HASH_FILE" - name: Commit ${{ env.TITLE }} changes diff --git a/flake.nix b/flake.nix index 20833fc49ed..0f425093741 100644 --- a/flake.nix +++ b/flake.nix @@ -33,17 +33,37 @@ packages = forEachSystem ( pkgs: let - opencode = pkgs.callPackage ./nix/opencode.nix { + node_modules = pkgs.callPackage ./nix/node_modules.nix { inherit rev; }; + opencode = pkgs.callPackage ./nix/opencode.nix { + inherit node_modules; + }; desktop = pkgs.callPackage ./nix/desktop.nix { inherit opencode; }; + # nixpkgs cpu naming to bun cpu naming + cpuMap = { x86_64 = "x64"; aarch64 = "arm64"; }; + # matrix of node_modules builds - these will always fail due to fakeHash usage + # but allow computation of the correct hash from any build machine for any cpu/os + # see the update-nix-hashes workflow for usage + moduleUpdaters = pkgs.lib.listToAttrs ( + pkgs.lib.concatMap (cpu: + map (os: { + name = "${cpu}_${os}_node_modules"; + value = node_modules.override { + bunCpu = cpuMap.${cpu}; + bunOs = os; + hash = pkgs.lib.fakeHash; + }; + }) [ "linux" "darwin" ] + ) [ "x86_64" "aarch64" ] + ); in { default = opencode; inherit opencode desktop; - } + } // moduleUpdaters ); }; } diff --git a/nix/node_modules.nix b/nix/node_modules.nix new file mode 100644 index 00000000000..981a60ef9ba --- /dev/null +++ b/nix/node_modules.nix @@ -0,0 +1,85 @@ +{ + lib, + stdenvNoCC, + bun, + bunCpu ? if stdenvNoCC.hostPlatform.isAarch64 then "arm64" else "x64", + bunOs ? if stdenvNoCC.hostPlatform.isLinux then "linux" else "darwin", + rev ? "dirty", + hash ? + (lib.pipe ./hashes.json [ + builtins.readFile + builtins.fromJSON + ]).nodeModules.${stdenvNoCC.hostPlatform.system}, +}: +let + packageJson = lib.pipe ../packages/opencode/package.json [ + builtins.readFile + builtins.fromJSON + ]; +in +stdenvNoCC.mkDerivation { + pname = "opencode-node_modules"; + version = "${packageJson.version}-${rev}"; + + src = lib.fileset.toSource { + root = ../.; + fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) ( + lib.fileset.unions [ + ../packages + ../bun.lock + ../package.json + ../patches + ../install + ] + ); + }; + + impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [ + "GIT_PROXY_COMMAND" + "SOCKS_SERVER" + ]; + + nativeBuildInputs = [ + bun + ]; + + dontConfigure = true; + + buildPhase = '' + runHook preBuild + export HOME=$(mktemp -d) + export BUN_INSTALL_CACHE_DIR=$(mktemp -d) + bun install \ + --cpu="${bunCpu}" \ + --os="${bunOs}" \ + --frozen-lockfile \ + --ignore-scripts \ + --no-progress \ + --linker=isolated + bun --bun ${./scripts/canonicalize-node-modules.ts} + bun --bun ${./scripts/normalize-bun-binaries.ts} + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out + find . -type d -name node_modules -exec cp -R --parents {} $out \; + + runHook postInstall + ''; + + dontFixup = true; + + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + outputHash = hash; + + meta.platforms = [ + "aarch64-linux" + "x86_64-linux" + "aarch64-darwin" + "x86_64-darwin" + ]; +} diff --git a/nix/opencode.nix b/nix/opencode.nix index 4d6f8e9b423..23d9fbe34e0 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -1,6 +1,7 @@ { lib, stdenvNoCC, + callPackage, bun, sysctl, makeBinaryWrapper, @@ -9,81 +10,12 @@ installShellFiles, versionCheckHook, writableTmpDirAsHomeHook, - rev ? "dirty", + node_modules ? callPackage ./node-modules.nix { }, }: -let - packageJson = lib.pipe ../packages/opencode/package.json [ - builtins.readFile - builtins.fromJSON - ]; -in stdenvNoCC.mkDerivation (finalAttrs: { pname = "opencode"; - version = "${packageJson.version}-${rev}"; - - src = lib.fileset.toSource { - root = ../.; - fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) ( - lib.fileset.unions [ - ../packages - ../bun.lock - ../package.json - ../patches - ../install - ] - ); - }; - - node_modules = stdenvNoCC.mkDerivation { - pname = "${finalAttrs.pname}-node_modules"; - inherit (finalAttrs) version src; - - impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [ - "GIT_PROXY_COMMAND" - "SOCKS_SERVER" - ]; - - nativeBuildInputs = [ - bun - ]; - - dontConfigure = true; - - buildPhase = '' - runHook preBuild - export HOME=$(mktemp -d) - export BUN_INSTALL_CACHE_DIR=$(mktemp -d) - bun install \ - --cpu="${if stdenvNoCC.hostPlatform.isAarch64 then "arm64" else "x64"}" \ - --os="${if stdenvNoCC.hostPlatform.isLinux then "linux" else "darwin"}" \ - --frozen-lockfile \ - --ignore-scripts \ - --no-progress \ - --linker=isolated - bun --bun ${./scripts/canonicalize-node-modules.ts} - bun --bun ${./scripts/normalize-bun-binaries.ts} - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - - mkdir -p $out - find . -type d -name node_modules -exec cp -R --parents {} $out \; - - runHook postInstall - ''; - - dontFixup = true; - - outputHashAlgo = "sha256"; - outputHashMode = "recursive"; - outputHash = - (lib.pipe ./hashes.json [ - builtins.readFile - builtins.fromJSON - ]).nodeModules.${stdenvNoCC.hostPlatform.system}; - }; + inherit (node_modules) version src; + inherit node_modules; nativeBuildInputs = [ bun @@ -159,11 +91,6 @@ stdenvNoCC.mkDerivation (finalAttrs: { homepage = "https://opencode.ai/"; license = lib.licenses.mit; mainProgram = "opencode"; - platforms = [ - "aarch64-linux" - "x86_64-linux" - "aarch64-darwin" - "x86_64-darwin" - ]; + inherit (node_modules.meta) platforms; }; }) From 6b481b5fb07134cac6f3df4fa2195aa56476295c Mon Sep 17 00:00:00 2001 From: Thiago Malek <14800002+thmalek@users.noreply.github.com> Date: Mon, 19 Jan 2026 02:22:31 -0300 Subject: [PATCH 106/192] fix(opencode): use streamObject when using openai oauth in agent generation (#9231) --- packages/opencode/src/agent/agent.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 0725933d731..2b44308f130 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -1,10 +1,12 @@ import { Config } from "../config/config" import z from "zod" import { Provider } from "../provider/provider" -import { generateObject, type ModelMessage } from "ai" +import { generateObject, streamObject, type ModelMessage } from "ai" import { SystemPrompt } from "../session/system" import { Instance } from "../project/instance" import { Truncate } from "../tool/truncation" +import { Auth } from "../auth" +import { ProviderTransform } from "../provider/transform" import PROMPT_GENERATE from "./generate.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" @@ -276,10 +278,12 @@ export namespace Agent { const defaultModel = input.model ?? (await Provider.defaultModel()) const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID) const language = await Provider.getLanguage(model) + const system = SystemPrompt.header(defaultModel.providerID) system.push(PROMPT_GENERATE) const existing = await list() - const result = await generateObject({ + + const params = { experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry, metadata: { @@ -305,7 +309,24 @@ export namespace Agent { whenToUse: z.string(), systemPrompt: z.string(), }), - }) + } satisfies Parameters[0] + + if (defaultModel.providerID === "openai" && (await Auth.get(defaultModel.providerID))?.type === "oauth") { + const result = streamObject({ + ...params, + providerOptions: ProviderTransform.providerOptions(model, { + instructions: SystemPrompt.instructions(), + store: false, + }), + onError: () => {}, + }) + for await (const part of result.fullStream) { + if (part.type === "error") throw part.error + } + return result.object + } + + const result = await generateObject(params) return result.object } } From fc6c9cbbd262daa0f98338ed3c79270fbfa086ad Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Mon, 19 Jan 2026 16:30:28 +1100 Subject: [PATCH 107/192] fix(github-copilot): auto-route GPT-5+ models to Responses API (#5877) Co-authored-by: Claude --- packages/opencode/src/provider/provider.ts | 22 +++++++++++++-------- packages/opencode/src/provider/transform.ts | 6 +++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index bcb115edf41..d4d4b3e2680 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -41,6 +41,18 @@ import { ProviderTransform } from "./transform" export namespace Provider { const log = Log.create({ service: "provider" }) + function isGpt5OrLater(modelID: string): boolean { + const match = /^gpt-(\d+)/.exec(modelID) + if (!match) { + return false + } + return Number(match[1]) >= 5 + } + + function shouldUseCopilotResponsesApi(modelID: string): boolean { + return isGpt5OrLater(modelID) && !modelID.startsWith("gpt-5-mini") + } + const BUNDLED_PROVIDERS: Record SDK> = { "@ai-sdk/amazon-bedrock": createAmazonBedrock, "@ai-sdk/anthropic": createAnthropic, @@ -120,10 +132,7 @@ export namespace Provider { return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { - if (modelID.includes("codex")) { - return sdk.responses(modelID) - } - return sdk.chat(modelID) + return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID) }, options: {}, } @@ -132,10 +141,7 @@ export namespace Provider { return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { - if (modelID.includes("codex")) { - return sdk.responses(modelID) - } - return sdk.chat(modelID) + return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID) }, options: {}, } diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index b803bd66ce1..2cacb61aaf4 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -524,7 +524,11 @@ export namespace ProviderTransform { const result: Record = {} // openai and providers using openai package should set store to false by default. - if (input.model.providerID === "openai" || input.model.api.npm === "@ai-sdk/openai") { + if ( + input.model.providerID === "openai" || + input.model.api.npm === "@ai-sdk/openai" || + input.model.api.npm === "@ai-sdk/github-copilot" + ) { result["store"] = false } From e2f1f4d81e152f19f6f9d2f8ed873f310296eba4 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:33:23 -0800 Subject: [PATCH 108/192] add scheduler, cleanup module (#9346) --- packages/opencode/src/project/bootstrap.ts | 4 ++ packages/opencode/src/scheduler/index.ts | 61 ++++++++++++++++++ packages/opencode/src/snapshot/index.ts | 37 +++++++++++ packages/opencode/src/tool/truncation.ts | 15 +++-- packages/opencode/test/scheduler.test.ts | 73 ++++++++++++++++++++++ 5 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 packages/opencode/src/scheduler/index.ts create mode 100644 packages/opencode/test/scheduler.test.ts diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 56fe4d13e66..efdcaba9909 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -11,6 +11,8 @@ import { Instance } from "./instance" import { Vcs } from "./vcs" import { Log } from "@/util/log" import { ShareNext } from "@/share/share-next" +import { Snapshot } from "../snapshot" +import { Truncate } from "../tool/truncation" export async function InstanceBootstrap() { Log.Default.info("bootstrapping", { directory: Instance.directory }) @@ -22,6 +24,8 @@ export async function InstanceBootstrap() { FileWatcher.init() File.init() Vcs.init() + Snapshot.init() + Truncate.init() Bus.subscribe(Command.Event.Executed, async (payload) => { if (payload.properties.name === Command.Default.INIT) { diff --git a/packages/opencode/src/scheduler/index.ts b/packages/opencode/src/scheduler/index.ts new file mode 100644 index 00000000000..cfafa7b9ced --- /dev/null +++ b/packages/opencode/src/scheduler/index.ts @@ -0,0 +1,61 @@ +import { Instance } from "../project/instance" +import { Log } from "../util/log" + +export namespace Scheduler { + const log = Log.create({ service: "scheduler" }) + + export type Task = { + id: string + interval: number + run: () => Promise + scope?: "instance" | "global" + } + + type Timer = ReturnType + type Entry = { + tasks: Map + timers: Map + } + + const create = (): Entry => { + const tasks = new Map() + const timers = new Map() + return { tasks, timers } + } + + const shared = create() + + const state = Instance.state( + () => create(), + async (entry) => { + for (const timer of entry.timers.values()) { + clearInterval(timer) + } + entry.tasks.clear() + entry.timers.clear() + }, + ) + + export function register(task: Task) { + const scope = task.scope ?? "instance" + const entry = scope === "global" ? shared : state() + const current = entry.timers.get(task.id) + if (current && scope === "global") return + if (current) clearInterval(current) + + entry.tasks.set(task.id, task) + void run(task) + const timer = setInterval(() => { + void run(task) + }, task.interval) + timer.unref() + entry.timers.set(task.id, timer) + } + + async function run(task: Task) { + log.info("run", { id: task.id }) + await task.run().catch((error) => { + log.error("run failed", { id: task.id, error }) + }) + } +} diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 69f2abc7903..46c97cf8dfd 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -6,9 +6,46 @@ import { Global } from "../global" import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" +import { Scheduler } from "../scheduler" export namespace Snapshot { const log = Log.create({ service: "snapshot" }) + const hour = 60 * 60 * 1000 + const prune = "7.days" + + export function init() { + Scheduler.register({ + id: "snapshot.cleanup", + interval: hour, + run: cleanup, + scope: "instance", + }) + } + + export async function cleanup() { + if (Instance.project.vcs !== "git") return + const cfg = await Config.get() + if (cfg.snapshot === false) return + const git = gitdir() + const exists = await fs + .stat(git) + .then(() => true) + .catch(() => false) + if (!exists) return + const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} gc --prune=${prune}` + .quiet() + .cwd(Instance.directory) + .nothrow() + if (result.exitCode !== 0) { + log.warn("cleanup failed", { + exitCode: result.exitCode, + stderr: result.stderr.toString(), + stdout: result.stdout.toString(), + }) + return + } + log.info("cleanup", { prune }) + } export async function track() { if (Instance.project.vcs !== "git") return diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts index 4172b6447e6..84e799c1310 100644 --- a/packages/opencode/src/tool/truncation.ts +++ b/packages/opencode/src/tool/truncation.ts @@ -2,9 +2,9 @@ import fs from "fs/promises" import path from "path" import { Global } from "../global" import { Identifier } from "../id/id" -import { lazy } from "../util/lazy" import { PermissionNext } from "../permission/next" import type { Agent } from "../agent/agent" +import { Scheduler } from "../scheduler" export namespace Truncate { export const MAX_LINES = 2000 @@ -12,6 +12,7 @@ export namespace Truncate { export const DIR = path.join(Global.Path.data, "tool-output") export const GLOB = path.join(DIR, "*") const RETENTION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days + const HOUR_MS = 60 * 60 * 1000 export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string } @@ -21,6 +22,15 @@ export namespace Truncate { direction?: "head" | "tail" } + export function init() { + Scheduler.register({ + id: "tool.truncation.cleanup", + interval: HOUR_MS, + run: cleanup, + scope: "global", + }) + } + export async function cleanup() { const cutoff = Identifier.timestamp(Identifier.create("tool", false, Date.now() - RETENTION_MS)) const glob = new Bun.Glob("tool_*") @@ -31,8 +41,6 @@ export namespace Truncate { } } - const init = lazy(cleanup) - function hasTaskTool(agent?: Agent.Info): boolean { if (!agent?.permission) return false const rule = PermissionNext.evaluate("task", "*", agent.permission) @@ -81,7 +89,6 @@ export namespace Truncate { const unit = hitBytes ? "bytes" : "lines" const preview = out.join("\n") - await init() const id = Identifier.ascending("tool") const filepath = path.join(DIR, id) await Bun.write(Bun.file(filepath), text) diff --git a/packages/opencode/test/scheduler.test.ts b/packages/opencode/test/scheduler.test.ts new file mode 100644 index 00000000000..328daad9b83 --- /dev/null +++ b/packages/opencode/test/scheduler.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, test } from "bun:test" +import { Scheduler } from "../src/scheduler" +import { Instance } from "../src/project/instance" +import { tmpdir } from "./fixture/fixture" + +describe("Scheduler.register", () => { + const hour = 60 * 60 * 1000 + + test("defaults to instance scope per directory", async () => { + await using one = await tmpdir({ git: true }) + await using two = await tmpdir({ git: true }) + const runs = { count: 0 } + const id = "scheduler.instance." + Math.random().toString(36).slice(2) + const task = { + id, + interval: hour, + run: async () => { + runs.count += 1 + }, + } + + await Instance.provide({ + directory: one.path, + fn: async () => { + Scheduler.register(task) + await Instance.dispose() + }, + }) + expect(runs.count).toBe(1) + + await Instance.provide({ + directory: two.path, + fn: async () => { + Scheduler.register(task) + await Instance.dispose() + }, + }) + expect(runs.count).toBe(2) + }) + + test("global scope runs once across instances", async () => { + await using one = await tmpdir({ git: true }) + await using two = await tmpdir({ git: true }) + const runs = { count: 0 } + const id = "scheduler.global." + Math.random().toString(36).slice(2) + const task = { + id, + interval: hour, + run: async () => { + runs.count += 1 + }, + scope: "global" as const, + } + + await Instance.provide({ + directory: one.path, + fn: async () => { + Scheduler.register(task) + await Instance.dispose() + }, + }) + expect(runs.count).toBe(1) + + await Instance.provide({ + directory: two.path, + fn: async () => { + Scheduler.register(task) + await Instance.dispose() + }, + }) + expect(runs.count).toBe(1) + }) +}) From 260ab60c0b9ba1667a326c1b19ea46473156df0c Mon Sep 17 00:00:00 2001 From: NateSmyth Date: Mon, 19 Jan 2026 01:11:54 -0500 Subject: [PATCH 109/192] fix: track reasoning by output_index for copilot compatibility (#9124) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/provider/provider.ts | 14 ++--- .../openai-responses-language-model.ts | 59 ++++++++++++------- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index d4d4b3e2680..ad57867df47 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -615,13 +615,13 @@ export namespace Provider { }, experimentalOver200K: model.cost?.context_over_200k ? { - cache: { - read: model.cost.context_over_200k.cache_read ?? 0, - write: model.cost.context_over_200k.cache_write ?? 0, - }, - input: model.cost.context_over_200k.input, - output: model.cost.context_over_200k.output, - } + cache: { + read: model.cost.context_over_200k.cache_read ?? 0, + write: model.cost.context_over_200k.cache_write ?? 0, + }, + input: model.cost.context_over_200k.input, + output: model.cost.context_over_200k.output, + } : undefined, }, limit: { diff --git a/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts b/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts index 94b0edaf3f4..0990b7e0077 100644 --- a/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +++ b/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts @@ -815,14 +815,20 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { // flag that checks if there have been client-side tool calls (not executed by openai) let hasFunctionCall = false + // Track reasoning by output_index instead of item_id + // GitHub Copilot rotates encrypted item IDs on every event const activeReasoning: Record< - string, + number, { + canonicalId: string // the item.id from output_item.added encryptedContent?: string | null summaryParts: number[] } > = {} + // Track current active reasoning output_index for correlating summary events + let currentReasoningOutputIndex: number | null = null + // Track a stable text part id for the current assistant message. // Copilot may change item_id across text deltas; normalize to one id. let currentTextId: string | null = null @@ -933,10 +939,12 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { }, }) } else if (isResponseOutputItemAddedReasoningChunk(value)) { - activeReasoning[value.item.id] = { + activeReasoning[value.output_index] = { + canonicalId: value.item.id, encryptedContent: value.item.encrypted_content, summaryParts: [0], } + currentReasoningOutputIndex = value.output_index controller.enqueue({ type: "reasoning-start", @@ -1091,22 +1099,25 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { currentTextId = null } } else if (isResponseOutputItemDoneReasoningChunk(value)) { - const activeReasoningPart = activeReasoning[value.item.id] + const activeReasoningPart = activeReasoning[value.output_index] if (activeReasoningPart) { for (const summaryIndex of activeReasoningPart.summaryParts) { controller.enqueue({ type: "reasoning-end", - id: `${value.item.id}:${summaryIndex}`, + id: `${activeReasoningPart.canonicalId}:${summaryIndex}`, providerMetadata: { openai: { - itemId: value.item.id, + itemId: activeReasoningPart.canonicalId, reasoningEncryptedContent: value.item.encrypted_content ?? null, }, }, }) } + delete activeReasoning[value.output_index] + if (currentReasoningOutputIndex === value.output_index) { + currentReasoningOutputIndex = null + } } - delete activeReasoning[value.item.id] } } else if (isResponseFunctionCallArgumentsDeltaChunk(value)) { const toolCall = ongoingToolCalls[value.output_index] @@ -1198,32 +1209,40 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { logprobs.push(value.logprobs) } } else if (isResponseReasoningSummaryPartAddedChunk(value)) { + const activeItem = + currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null + // the first reasoning start is pushed in isResponseOutputItemAddedReasoningChunk. - if (value.summary_index > 0) { - activeReasoning[value.item_id]?.summaryParts.push(value.summary_index) + if (activeItem && value.summary_index > 0) { + activeItem.summaryParts.push(value.summary_index) controller.enqueue({ type: "reasoning-start", - id: `${value.item_id}:${value.summary_index}`, + id: `${activeItem.canonicalId}:${value.summary_index}`, providerMetadata: { openai: { - itemId: value.item_id, - reasoningEncryptedContent: activeReasoning[value.item_id]?.encryptedContent ?? null, + itemId: activeItem.canonicalId, + reasoningEncryptedContent: activeItem.encryptedContent ?? null, }, }, }) } } else if (isResponseReasoningSummaryTextDeltaChunk(value)) { - controller.enqueue({ - type: "reasoning-delta", - id: `${value.item_id}:${value.summary_index}`, - delta: value.delta, - providerMetadata: { - openai: { - itemId: value.item_id, + const activeItem = + currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null + + if (activeItem) { + controller.enqueue({ + type: "reasoning-delta", + id: `${activeItem.canonicalId}:${value.summary_index}`, + delta: value.delta, + providerMetadata: { + openai: { + itemId: activeItem.canonicalId, + }, }, - }, - }) + }) + } } else if (isResponseFinishedChunk(value)) { finishReason = mapOpenAIResponseFinishReason({ finishReason: value.response.incomplete_details?.reason, From 6f847a794b919bab586172b2848464da33f1e452 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Jan 2026 06:12:36 +0000 Subject: [PATCH 110/192] chore: generate --- packages/opencode/src/provider/provider.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index ad57867df47..d4d4b3e2680 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -615,13 +615,13 @@ export namespace Provider { }, experimentalOver200K: model.cost?.context_over_200k ? { - cache: { - read: model.cost.context_over_200k.cache_read ?? 0, - write: model.cost.context_over_200k.cache_write ?? 0, - }, - input: model.cost.context_over_200k.input, - output: model.cost.context_over_200k.output, - } + cache: { + read: model.cost.context_over_200k.cache_read ?? 0, + write: model.cost.context_over_200k.cache_write ?? 0, + }, + input: model.cost.context_over_200k.input, + output: model.cost.context_over_200k.output, + } : undefined, }, limit: { From 86df915df02b4d25332de4837574cbe0a89bc9b3 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 00:07:07 -0600 Subject: [PATCH 111/192] chore: cleanup provider code to assign copilot sdk earlier in flow --- packages/opencode/src/provider/provider.ts | 23 ++++++++-------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index d4d4b3e2680..513c8524de9 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -598,11 +598,14 @@ export namespace Provider { providerID: provider.id, name: model.name, family: model.family, - api: { - id: model.id, - url: provider.api!, - npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", - }, + api: { + id: model.id, + url: provider.api!, + npm: iife(() => { + if (provider.id.startsWith("github-copilot")) return "@ai-sdk/github-copilot" + return model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible" + }), + }, status: model.status ?? "active", headers: model.headers ?? {}, options: model.options ?? {}, @@ -908,16 +911,6 @@ export namespace Provider { continue } - if (providerID === "github-copilot" || providerID === "github-copilot-enterprise") { - provider.models = mapValues(provider.models, (model) => ({ - ...model, - api: { - ...model.api, - npm: "@ai-sdk/github-copilot", - }, - })) - } - const configProvider = config.provider?.[providerID] for (const [modelID, model] of Object.entries(provider.models)) { From 91787ceb3e023507ec643eac43db211d8f68a52d Mon Sep 17 00:00:00 2001 From: Caleb Norton Date: Mon, 19 Jan 2026 00:14:14 -0600 Subject: [PATCH 112/192] fix: nix ci - swapped dash/underscore (#9352) --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 0f425093741..e4d214a0b93 100644 --- a/flake.nix +++ b/flake.nix @@ -50,7 +50,7 @@ moduleUpdaters = pkgs.lib.listToAttrs ( pkgs.lib.concatMap (cpu: map (os: { - name = "${cpu}_${os}_node_modules"; + name = "${cpu}-${os}_node_modules"; value = node_modules.override { bunCpu = cpuMap.${cpu}; bunOs = os; From 9d1803d00080b4ce88705862e367fc1961dfb00e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Jan 2026 06:14:40 +0000 Subject: [PATCH 113/192] chore: generate --- packages/opencode/src/provider/provider.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 513c8524de9..fdd4ccdfb61 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -598,14 +598,14 @@ export namespace Provider { providerID: provider.id, name: model.name, family: model.family, - api: { - id: model.id, - url: provider.api!, - npm: iife(() => { - if (provider.id.startsWith("github-copilot")) return "@ai-sdk/github-copilot" - return model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible" - }), - }, + api: { + id: model.id, + url: provider.api!, + npm: iife(() => { + if (provider.id.startsWith("github-copilot")) return "@ai-sdk/github-copilot" + return model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible" + }), + }, status: model.status ?? "active", headers: model.headers ?? {}, options: model.options ?? {}, From 4a7809f600f30a08d4ac3afd3ec4fc39f41983f7 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 00:18:31 -0600 Subject: [PATCH 114/192] add proper variant support to copilot --- packages/opencode/src/provider/transform.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 2cacb61aaf4..f6b7ec8cbcc 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -349,6 +349,18 @@ export namespace ProviderTransform { case "@ai-sdk/gateway": return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) + case "@ai-sdk/github-copilot": + return Object.fromEntries( + WIDELY_SUPPORTED_EFFORTS.map((effort) => [ + effort, + { + reasoningEffort: effort, + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + }, + ]), + ) + case "@ai-sdk/cerebras": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cerebras case "@ai-sdk/togetherai": From 3515b4ff7d21da9f5783df1705ad8fd382a5b7e0 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 01:06:26 -0600 Subject: [PATCH 115/192] omit todo tools for openai models --- packages/opencode/src/tool/registry.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index faa5f72bcce..dad9914a289 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -143,6 +143,11 @@ export namespace ToolRegistry { if (t.id === "apply_patch") return usePatch if (t.id === "edit" || t.id === "write") return !usePatch + // omit todo tools for openai models + if (t.id === "todoread" || t.id === "todowrite") { + if (model.modelID.includes("gpt-")) return false + } + return true }) .map(async (t) => { From 4299450d7d474d350bd06b9e810a5d1250957a00 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 01:31:30 -0600 Subject: [PATCH 116/192] tweak apply_patch tool description --- packages/opencode/src/tool/apply_patch.ts | 3 +- packages/opencode/src/tool/apply_patch.txt | 34 +++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index d070eaefa97..7b0ba6150ce 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -12,13 +12,14 @@ import { assertExternalDirectory } from "./external-directory" import { trimDiff } from "./edit" import { LSP } from "../lsp" import { Filesystem } from "../util/filesystem" +import DESCRIPTION from "./apply_patch.txt" const PatchParams = z.object({ patchText: z.string().describe("The full patch text that describes all changes to be made"), }) export const ApplyPatchTool = Tool.define("apply_patch", { - description: "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.", + description: DESCRIPTION, parameters: PatchParams, async execute(params, ctx) { if (!params.patchText) { diff --git a/packages/opencode/src/tool/apply_patch.txt b/packages/opencode/src/tool/apply_patch.txt index 1af0606109f..e195cd9cb18 100644 --- a/packages/opencode/src/tool/apply_patch.txt +++ b/packages/opencode/src/tool/apply_patch.txt @@ -1 +1,33 @@ -Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON. +Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON. Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope: + +*** Begin Patch +[ one or more file sections ] +*** End Patch + +Within that envelope, you get a sequence of file operations. +You MUST include a header to specify the action you are taking. +Each operation starts with one of three headers: + +*** Add File: - create a new file. Every following line is a + line (the initial contents). +*** Delete File: - remove an existing file. Nothing follows. +*** Update File: - patch an existing file in place (optionally with a rename). + +Example patch: + +``` +*** Begin Patch +*** Add File: hello.txt ++Hello world +*** Update File: src/app.py +*** Move to: src/main.py +@@ def greet(): +-print("Hi") ++print("Hello, world!") +*** Delete File: obsolete.txt +*** End Patch +``` + +It is important to remember: + +- You must include a header with your intended action (Add/Delete/Update) +- You must prefix new lines with `+` even when creating a new file From 13276aee8255ea809a975dc70808af08273773f2 Mon Sep 17 00:00:00 2001 From: Slone <50995948+Slone123c@users.noreply.github.com> Date: Mon, 19 Jan 2026 18:32:41 +0800 Subject: [PATCH 117/192] fix(desktop): apply getComputedStyle polyfill on all platforms (#9369) --- packages/desktop/src/index.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 0d9e383790a..6cd77d7d557 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -26,17 +26,16 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { ) } -const isWindows = ostype() === "windows" -if (isWindows) { - const originalGetComputedStyle = window.getComputedStyle - window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => { - if (!(elt instanceof Element)) { - // WebView2 can call into Floating UI with non-elements; fall back to a safe element. - return originalGetComputedStyle(document.documentElement, pseudoElt ?? undefined) - } - return originalGetComputedStyle(elt, pseudoElt ?? undefined) - }) as typeof window.getComputedStyle -} +// Floating UI can call getComputedStyle with non-elements (e.g., null refs, virtual elements). +// This happens on all platforms (WebView2 on Windows, WKWebView on macOS), not just Windows. +const originalGetComputedStyle = window.getComputedStyle +window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => { + if (!(elt instanceof Element)) { + // Fall back to a safe element when a non-element is passed. + return originalGetComputedStyle(document.documentElement, pseudoElt ?? undefined) + } + return originalGetComputedStyle(elt, pseudoElt ?? undefined) +}) as typeof window.getComputedStyle let update: Update | null = null From 08005d755b240dac3ec208aee504a76af7052de7 Mon Sep 17 00:00:00 2001 From: Mani Sundararajan <10191300+itsrainingmani@users.noreply.github.com> Date: Mon, 19 Jan 2026 05:34:40 -0500 Subject: [PATCH 118/192] refactor(desktop): tweak share button to prevent layout shift (#9322) --- packages/app/src/components/session/session-header.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 96ed762c448..7cded4bce29 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -244,7 +244,11 @@ export function SessionHeader() { } trigger={ - @@ -293,12 +297,12 @@ export function SessionHeader() {
- +
+ ) + } + createEffect( on( () => ({ ready: pageReady(), dir: params.dir, id: params.id }), @@ -1391,6 +1475,12 @@ export default function Layout(props: ParentProps) { navigate(`/${slug()}/session`)}> New session + dialog.show(() => )} + > + Reset workspace + dialog.show(() => )} diff --git a/packages/opencode/src/server/routes/experimental.ts b/packages/opencode/src/server/routes/experimental.ts index d300d3bf863..dc5f4f7ab76 100644 --- a/packages/opencode/src/server/routes/experimental.ts +++ b/packages/opencode/src/server/routes/experimental.ts @@ -159,6 +159,31 @@ export const ExperimentalRoutes = lazy(() => return c.json(true) }, ) + .post( + "/worktree/reset", + describeRoute({ + summary: "Reset worktree", + description: "Reset a worktree branch to the primary default branch.", + operationId: "worktree.reset", + responses: { + 200: { + description: "Worktree reset", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", Worktree.reset.schema), + async (c) => { + const body = c.req.valid("json") + await Worktree.reset(body) + return c.json(true) + }, + ) .get( "/resource", describeRoute({ diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index be10fd83d40..aa55355e098 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -43,6 +43,16 @@ export namespace Worktree { export type RemoveInput = z.infer + export const ResetInput = z + .object({ + directory: z.string(), + }) + .meta({ + ref: "WorktreeResetInput", + }) + + export type ResetInput = z.infer + export const NotGitError = NamedError.create( "WorktreeNotGitError", z.object({ @@ -78,6 +88,13 @@ export namespace Worktree { }), ) + export const ResetFailedError = NamedError.create( + "WorktreeResetFailedError", + z.object({ + message: z.string(), + }), + ) + const ADJECTIVES = [ "brave", "calm", @@ -280,4 +297,114 @@ export namespace Worktree { return true }) + + export const reset = fn(ResetInput, async (input) => { + if (Instance.project.vcs !== "git") { + throw new NotGitError({ message: "Worktrees are only supported for git projects" }) + } + + const directory = path.resolve(input.directory) + if (directory === path.resolve(Instance.worktree)) { + throw new ResetFailedError({ message: "Cannot reset the primary workspace" }) + } + + const list = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree) + if (list.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(list) || "Failed to read git worktrees" }) + } + + const lines = outputText(list.stdout) + .split("\n") + .map((line) => line.trim()) + const entries = lines.reduce<{ path?: string; branch?: string }[]>((acc, line) => { + if (!line) return acc + if (line.startsWith("worktree ")) { + acc.push({ path: line.slice("worktree ".length).trim() }) + return acc + } + const current = acc[acc.length - 1] + if (!current) return acc + if (line.startsWith("branch ")) { + current.branch = line.slice("branch ".length).trim() + } + return acc + }, []) + + const entry = entries.find((item) => item.path && path.resolve(item.path) === directory) + if (!entry?.path) { + throw new ResetFailedError({ message: "Worktree not found" }) + } + + const remoteList = await $`git remote`.quiet().nothrow().cwd(Instance.worktree) + if (remoteList.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(remoteList) || "Failed to list git remotes" }) + } + + const remotes = outputText(remoteList.stdout) + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + + const remote = remotes.includes("origin") + ? "origin" + : remotes.length === 1 + ? remotes[0] + : remotes.includes("upstream") + ? "upstream" + : "" + + const remoteHead = remote + ? await $`git symbolic-ref refs/remotes/${remote}/HEAD`.quiet().nothrow().cwd(Instance.worktree) + : { exitCode: 1, stdout: undefined, stderr: undefined } + + const remoteRef = remoteHead.exitCode === 0 ? outputText(remoteHead.stdout) : "" + const remoteTarget = remoteRef ? remoteRef.replace(/^refs\/remotes\//, "") : "" + const remoteBranch = remote && remoteTarget.startsWith(`${remote}/`) ? remoteTarget.slice(`${remote}/`.length) : "" + + const mainCheck = await $`git show-ref --verify --quiet refs/heads/main`.quiet().nothrow().cwd(Instance.worktree) + const masterCheck = await $`git show-ref --verify --quiet refs/heads/master` + .quiet() + .nothrow() + .cwd(Instance.worktree) + const localBranch = mainCheck.exitCode === 0 ? "main" : masterCheck.exitCode === 0 ? "master" : "" + + const target = remoteBranch ? `${remote}/${remoteBranch}` : localBranch + if (!target) { + throw new ResetFailedError({ message: "Default branch not found" }) + } + + if (remoteBranch) { + const fetch = await $`git fetch ${remote} ${remoteBranch}`.quiet().nothrow().cwd(Instance.worktree) + if (fetch.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(fetch) || `Failed to fetch ${target}` }) + } + } + + const checkout = await $`git checkout ${target}`.quiet().nothrow().cwd(entry.path) + if (checkout.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(checkout) || `Failed to checkout ${target}` }) + } + + const worktreeBranch = entry.branch?.replace(/^refs\/heads\//, "") + if (!worktreeBranch) { + throw new ResetFailedError({ message: "Worktree branch not found" }) + } + + const reset = await $`git reset --hard ${target}`.quiet().nothrow().cwd(entry.path) + if (reset.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(reset) || "Failed to reset worktree" }) + } + + const branchReset = await $`git branch -f ${worktreeBranch} ${target}`.quiet().nothrow().cwd(entry.path) + if (branchReset.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(branchReset) || "Failed to update worktree branch" }) + } + + const checkoutBranch = await $`git checkout ${worktreeBranch}`.quiet().nothrow().cwd(entry.path) + if (checkoutBranch.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(checkoutBranch) || "Failed to checkout worktree branch" }) + } + + return true + }) } diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index ba299f81fe6..59b7f06963b 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -165,6 +165,9 @@ import type { WorktreeRemoveErrors, WorktreeRemoveInput, WorktreeRemoveResponses, + WorktreeResetErrors, + WorktreeResetInput, + WorktreeResetResponses, } from "./types.gen.js" export type Options = Options2< @@ -745,6 +748,41 @@ export class Worktree extends HeyApiClient { }, }) } + + /** + * Reset worktree + * + * Reset a worktree branch to the primary default branch. + */ + public reset( + parameters?: { + directory?: string + worktreeResetInput?: WorktreeResetInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { key: "worktreeResetInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/worktree/reset", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } } export class Resource extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 58d3c3ae216..75540f90724 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1912,6 +1912,10 @@ export type WorktreeRemoveInput = { directory: string } +export type WorktreeResetInput = { + directory: string +} + export type McpResource = { name: string uri: string @@ -2630,6 +2634,33 @@ export type WorktreeCreateResponses = { export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses] +export type WorktreeResetData = { + body?: WorktreeResetInput + path?: never + query?: { + directory?: string + } + url: "/experimental/worktree/reset" +} + +export type WorktreeResetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type WorktreeResetError = WorktreeResetErrors[keyof WorktreeResetErrors] + +export type WorktreeResetResponses = { + /** + * Worktree reset + */ + 200: boolean +} + +export type WorktreeResetResponse = WorktreeResetResponses[keyof WorktreeResetResponses] + export type ExperimentalResourceListData = { body?: never path?: never From fcfe6d3d261edaa139d35cf463c05d7124045142 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:55:07 -0600 Subject: [PATCH 122/192] chore: cleanup --- packages/app/src/pages/layout.tsx | 56 +++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index e7acd5f89a5..bf44efa1c13 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -942,11 +942,26 @@ export default function Layout(props: ParentProps) { } } - const resetWorkspace = async (directory: string) => { + const resetWorkspace = async (directory: string, sessions: Session[]) => { const current = currentProject() if (!current) return if (directory === current.worktree) return + const pending = sessions.filter((session) => session.time.archived === undefined) + if (pending.length > 0) { + await Promise.all( + pending.map((session) => + globalSDK.client.session + .update({ + sessionID: session.id, + directory: session.directory, + time: { archived: Date.now() }, + }) + .catch(() => undefined), + ), + ) + } + const result = await globalSDK.client.worktree .reset({ directory: current.worktree, worktreeResetInput: { directory } }) .then((x) => x.data) @@ -1026,15 +1041,24 @@ export default function Layout(props: ParentProps) { function DialogResetWorkspace(props: { directory: string }) { const name = createMemo(() => getFilename(props.directory)) - const [data, setData] = createStore({ + const [state, setState] = createStore({ status: "loading" as "loading" | "ready" | "error", dirty: false, + sessions: [] as Session[], }) + const refreshSessions = () => { + const [workspace] = globalSync.child(props.directory) + const sessions = workspace.session + .filter((session) => session.directory === workspace.path.directory) + .filter((session) => session.time.archived === undefined) + setState({ sessions }) + } + onMount(() => { const current = currentProject() if (!current) { - setData({ status: "error", dirty: false }) + setState({ status: "error", dirty: false }) return } @@ -1043,39 +1067,49 @@ export default function Layout(props: ParentProps) { .then((x) => { const files = x.data ?? [] const dirty = files.length > 0 - setData({ status: "ready", dirty }) + setState({ status: "ready", dirty }) + refreshSessions() }) .catch(() => { - setData({ status: "error", dirty: false }) + setState({ status: "error", dirty: false }) }) }) const handleReset = async () => { - await resetWorkspace(props.directory) + await resetWorkspace(props.directory, state.sessions) dialog.close() } + const archivedCount = () => state.sessions.length + const description = () => { - if (data.status === "loading") return "Checking for unmerged changes..." - if (data.status === "error") return "Unable to verify git status." - if (!data.dirty) return "No unmerged changes detected." + if (state.status === "loading") return "Checking for unmerged changes..." + if (state.status === "error") return "Unable to verify git status." + if (!state.dirty) return "No unmerged changes detected." return "Unmerged changes detected in this workspace." } + const archivedLabel = () => { + const count = archivedCount() + if (count === 0) return "No active sessions will be archived." + const label = count === 1 ? "1 session" : `${count} sessions` + return `${label} will be archived.` + } + return (
Reset workspace "{name()}"? - {description()} This will reset the workspace to match the default branch. + {description()} {archivedLabel()} This will reset the workspace to match the default branch.
-
From dca2540ca786d818da4387ddff19eba865630a37 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:48:21 -0600 Subject: [PATCH 123/192] chore: cleanup --- packages/app/src/pages/layout.tsx | 4 ++-- packages/ui/src/components/dialog.css | 10 ++++++++-- packages/ui/src/components/dialog.tsx | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index bf44efa1c13..3b8c76e0412 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1020,7 +1020,7 @@ export default function Layout(props: ParentProps) { } return ( - +
Delete workspace "{name()}"? @@ -1097,7 +1097,7 @@ export default function Layout(props: ParentProps) { } return ( - +
Reset workspace "{name()}"? diff --git a/packages/ui/src/components/dialog.css b/packages/ui/src/components/dialog.css index 5c7cdc233bf..177dc0a7974 100644 --- a/packages/ui/src/components/dialog.css +++ b/packages/ui/src/components/dialog.css @@ -118,9 +118,15 @@ outline: none; } } + } + + &[data-fit] { + [data-slot="dialog-container"] { + height: auto; - &:focus-visible { - outline: none; + [data-slot="dialog-content"] { + min-height: 0; + } } } } diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index 40a6ac83d6b..797fbcbfc4c 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -8,11 +8,12 @@ export interface DialogProps extends ParentProps { action?: JSXElement class?: ComponentProps<"div">["class"] classList?: ComponentProps<"div">["classList"] + fit?: boolean } export function Dialog(props: DialogProps) { return ( -
+
Date: Thu, 15 Jan 2026 17:34:38 -0600 Subject: [PATCH 124/192] chore: cleanup --- packages/app/src/pages/layout.tsx | 24 +++++++++++++++--------- packages/opencode/src/worktree/index.ts | 10 ++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 3b8c76e0412..b152c4725d2 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -942,11 +942,16 @@ export default function Layout(props: ParentProps) { } } - const resetWorkspace = async (directory: string, sessions: Session[]) => { + const resetWorkspace = async (directory: string) => { const current = currentProject() if (!current) return if (directory === current.worktree) return + const sessions = await globalSDK.client.session + .list({ directory }) + .then((x) => x.data ?? []) + .catch(() => []) + const pending = sessions.filter((session) => session.time.archived === undefined) if (pending.length > 0) { await Promise.all( @@ -1047,12 +1052,13 @@ export default function Layout(props: ParentProps) { sessions: [] as Session[], }) - const refreshSessions = () => { - const [workspace] = globalSync.child(props.directory) - const sessions = workspace.session - .filter((session) => session.directory === workspace.path.directory) - .filter((session) => session.time.archived === undefined) - setState({ sessions }) + const refresh = async () => { + const sessions = await globalSDK.client.session + .list({ directory: props.directory }) + .then((x) => x.data ?? []) + .catch(() => []) + const active = sessions.filter((session) => session.time.archived === undefined) + setState({ sessions: active }) } onMount(() => { @@ -1068,7 +1074,7 @@ export default function Layout(props: ParentProps) { const files = x.data ?? [] const dirty = files.length > 0 setState({ status: "ready", dirty }) - refreshSessions() + void refresh() }) .catch(() => { setState({ status: "error", dirty: false }) @@ -1076,7 +1082,7 @@ export default function Layout(props: ParentProps) { }) const handleReset = async () => { - await resetWorkspace(props.directory, state.sessions) + await resetWorkspace(props.directory) dialog.close() } diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index aa55355e098..365dfd443c2 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -385,6 +385,11 @@ export namespace Worktree { throw new ResetFailedError({ message: errorText(checkout) || `Failed to checkout ${target}` }) } + const clean = await $`git clean -fd`.quiet().nothrow().cwd(entry.path) + if (clean.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(clean) || "Failed to clean worktree" }) + } + const worktreeBranch = entry.branch?.replace(/^refs\/heads\//, "") if (!worktreeBranch) { throw new ResetFailedError({ message: "Worktree branch not found" }) @@ -395,6 +400,11 @@ export namespace Worktree { throw new ResetFailedError({ message: errorText(reset) || "Failed to reset worktree" }) } + const cleanAfter = await $`git clean -fd`.quiet().nothrow().cwd(entry.path) + if (cleanAfter.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(cleanAfter) || "Failed to clean worktree" }) + } + const branchReset = await $`git branch -f ${worktreeBranch} ${target}`.quiet().nothrow().cwd(entry.path) if (branchReset.exitCode !== 0) { throw new ResetFailedError({ message: errorText(branchReset) || "Failed to update worktree branch" }) From 295f290efd52059f4684fa39e8fcd860cdafca8d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sun, 18 Jan 2026 05:21:03 -0600 Subject: [PATCH 125/192] chore: cleanup --- packages/app/src/pages/layout.tsx | 8 ++--- packages/opencode/src/worktree/index.ts | 46 ++++++++++++++----------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b152c4725d2..5347445a23c 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -952,15 +952,15 @@ export default function Layout(props: ParentProps) { .then((x) => x.data ?? []) .catch(() => []) - const pending = sessions.filter((session) => session.time.archived === undefined) - if (pending.length > 0) { + if (sessions.length > 0) { + const archivedAt = Date.now() await Promise.all( - pending.map((session) => + sessions.map((session) => globalSDK.client.session .update({ sessionID: session.id, directory: session.directory, - time: { archived: Date.now() }, + time: { archived: archivedAt }, }) .catch(() => undefined), ), diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 365dfd443c2..911383c069b 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -380,41 +380,45 @@ export namespace Worktree { } } - const checkout = await $`git checkout ${target}`.quiet().nothrow().cwd(entry.path) - if (checkout.exitCode !== 0) { - throw new ResetFailedError({ message: errorText(checkout) || `Failed to checkout ${target}` }) + if (!entry.path) { + throw new ResetFailedError({ message: "Worktree path not found" }) } - const clean = await $`git clean -fd`.quiet().nothrow().cwd(entry.path) + const worktreePath = entry.path + + const resetToTarget = await $`git reset --hard ${target}`.quiet().nothrow().cwd(worktreePath) + if (resetToTarget.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(resetToTarget) || "Failed to reset worktree to target" }) + } + + const clean = await $`git clean -fdx`.quiet().nothrow().cwd(worktreePath) if (clean.exitCode !== 0) { throw new ResetFailedError({ message: errorText(clean) || "Failed to clean worktree" }) } - const worktreeBranch = entry.branch?.replace(/^refs\/heads\//, "") - if (!worktreeBranch) { - throw new ResetFailedError({ message: "Worktree branch not found" }) + const update = await $`git submodule update --init --recursive --force`.quiet().nothrow().cwd(worktreePath) + if (update.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(update) || "Failed to update submodules" }) } - const reset = await $`git reset --hard ${target}`.quiet().nothrow().cwd(entry.path) - if (reset.exitCode !== 0) { - throw new ResetFailedError({ message: errorText(reset) || "Failed to reset worktree" }) + const subReset = await $`git submodule foreach --recursive git reset --hard`.quiet().nothrow().cwd(worktreePath) + if (subReset.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(subReset) || "Failed to reset submodules" }) } - const cleanAfter = await $`git clean -fd`.quiet().nothrow().cwd(entry.path) - if (cleanAfter.exitCode !== 0) { - throw new ResetFailedError({ message: errorText(cleanAfter) || "Failed to clean worktree" }) + const subClean = await $`git submodule foreach --recursive git clean -fdx`.quiet().nothrow().cwd(worktreePath) + if (subClean.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(subClean) || "Failed to clean submodules" }) } - const branchReset = await $`git branch -f ${worktreeBranch} ${target}`.quiet().nothrow().cwd(entry.path) - if (branchReset.exitCode !== 0) { - throw new ResetFailedError({ message: errorText(branchReset) || "Failed to update worktree branch" }) + const status = await $`git status --porcelain=v1`.quiet().nothrow().cwd(worktreePath) + if (status.exitCode !== 0) { + throw new ResetFailedError({ message: errorText(status) || "Failed to read git status" }) } - const checkoutBranch = await $`git checkout ${worktreeBranch}`.quiet().nothrow().cwd(entry.path) - if (checkoutBranch.exitCode !== 0) { - throw new ResetFailedError({ message: errorText(checkoutBranch) || "Failed to checkout worktree branch" }) - } + const dirty = outputText(status.stdout) + if (!dirty) return true - return true + throw new ResetFailedError({ message: `Worktree reset left local changes:\n${dirty}` }) }) } From 55739b7aa144291e335bb98b800be36898971545 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 05:09:40 -0600 Subject: [PATCH 126/192] chore: cleanup --- packages/app/src/pages/layout.tsx | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 5347445a23c..52574b5487e 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -400,7 +400,24 @@ export default function Layout(props: ParentProps) { const currentProject = createMemo(() => { const directory = params.dir ? base64Decode(params.dir) : undefined if (!directory) return - return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) + + const projects = layout.projects.list() + + const sandbox = projects.find((p) => p.sandboxes?.includes(directory)) + if (sandbox) return sandbox + + const direct = projects.find((p) => p.worktree === directory) + if (direct) return direct + + const [child] = globalSync.child(directory) + const id = child.project + if (!id) return + + const meta = globalSync.data.project.find((p) => p.id === id) + const root = meta?.worktree + if (!root) return + + return projects.find((p) => p.worktree === root) }) createEffect( @@ -1193,11 +1210,15 @@ export default function Layout(props: ParentProps) { function workspaceIds(project: LocalProject | undefined) { if (!project) return [] const dirs = [project.worktree, ...(project.sandboxes ?? [])] + const active = currentProject() + const directory = active?.worktree === project.worktree && params.dir ? base64Decode(params.dir) : undefined + const next = directory && directory !== project.worktree && !dirs.includes(directory) ? [...dirs, directory] : dirs + const existing = store.workspaceOrder[project.worktree] - if (!existing) return dirs + if (!existing) return next - const keep = existing.filter((d) => dirs.includes(d)) - const missing = dirs.filter((d) => !existing.includes(d)) + const keep = existing.filter((d) => next.includes(d)) + const missing = next.filter((d) => !existing.includes(d)) return [...keep, ...missing] } From 51804a47e92c3a8939a97417ac05dfc831a3d170 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 07:31:24 -0600 Subject: [PATCH 127/192] chore: cleanup --- packages/app/src/pages/layout.tsx | 45 ++++++++++++++++++------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 52574b5487e..e803681801d 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -964,12 +964,29 @@ export default function Layout(props: ParentProps) { if (!current) return if (directory === current.worktree) return - const sessions = await globalSDK.client.session - .list({ directory }) - .then((x) => x.data ?? []) - .catch(() => []) + const reset = globalSDK.client.worktree + .reset({ directory: current.worktree, worktreeResetInput: { directory } }) + .then((x) => x.data) + .catch((err) => { + showToast({ + title: "Failed to reset workspace", + description: errorMessage(err), + }) + return false + }) + + const href = `/${base64Encode(directory)}/session` + navigate(href) + layout.mobileSidebar.hide() + + void (async () => { + const sessions = await globalSDK.client.session + .list({ directory }) + .then((x) => x.data ?? []) + .catch(() => []) + + if (sessions.length === 0) return - if (sessions.length > 0) { const archivedAt = Date.now() await Promise.all( sessions.map((session) => @@ -982,19 +999,9 @@ export default function Layout(props: ParentProps) { .catch(() => undefined), ), ) - } - - const result = await globalSDK.client.worktree - .reset({ directory: current.worktree, worktreeResetInput: { directory } }) - .then((x) => x.data) - .catch((err) => { - showToast({ - title: "Failed to reset workspace", - description: errorMessage(err), - }) - return false - }) + })() + const result = await reset if (!result) return showToast({ @@ -1098,9 +1105,9 @@ export default function Layout(props: ParentProps) { }) }) - const handleReset = async () => { - await resetWorkspace(props.directory) + const handleReset = () => { dialog.close() + void resetWorkspace(props.directory) } const archivedCount = () => state.sessions.length From 23e9c02a7fd80063dd49e3b9cbd2a0c6519034bc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Jan 2026 13:37:19 +0000 Subject: [PATCH 128/192] chore: generate --- packages/sdk/openapi.json | 122 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 104cedce1e5..08dd98fd9bc 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -920,6 +920,110 @@ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.list({\n ...\n})" } ] + }, + "delete": { + "operationId": "worktree.remove", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Remove worktree", + "description": "Remove a git worktree and delete its branch.", + "responses": { + "200": { + "description": "Worktree removed", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorktreeRemoveInput" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.remove({\n ...\n})" + } + ] + } + }, + "/experimental/worktree/reset": { + "post": { + "operationId": "worktree.reset", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Reset worktree", + "description": "Reset a worktree branch to the primary default branch.", + "responses": { + "200": { + "description": "Worktree reset", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorktreeResetInput" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.reset({\n ...\n})" + } + ] } }, "/experimental/resource": { @@ -9988,6 +10092,24 @@ } } }, + "WorktreeRemoveInput": { + "type": "object", + "properties": { + "directory": { + "type": "string" + } + }, + "required": ["directory"] + }, + "WorktreeResetInput": { + "type": "object", + "properties": { + "directory": { + "type": "string" + } + }, + "required": ["directory"] + }, "McpResource": { "type": "object", "properties": { From 03d7467ea268f2f0f8d99f48ea1522741014b4bf Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:52:21 -0600 Subject: [PATCH 129/192] test(app): initial e2e test setup --- .github/workflows/test.yml | 63 +++++++++++++++++++++++ bun.lock | 10 ++++ package.json | 1 + packages/app/.gitignore | 2 + packages/app/README.md | 15 ++++++ packages/app/e2e/home.spec.ts | 6 +++ packages/app/package.json | 7 ++- packages/app/playwright.config.ts | 43 ++++++++++++++++ packages/opencode/script/seed-e2e.ts | 50 ++++++++++++++++++ packages/opencode/src/share/share-next.ts | 6 +++ packages/opencode/src/share/share.ts | 5 ++ 11 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 packages/app/e2e/home.spec.ts create mode 100644 packages/app/playwright.config.ts create mode 100644 packages/opencode/script/seed-e2e.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c39710bee8f..98eac1dab2f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,52 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun + - name: Install Playwright browsers + run: bun --cwd packages/app x playwright install --with-deps + + - name: Seed opencode data + run: bun --cwd packages/opencode script/seed-e2e.ts + env: + MODELS_DEV_API_JSON: ${{ github.workspace }}/packages/opencode/test/tool/fixtures/models-api.json + OPENCODE_DISABLE_MODELS_FETCH: "true" + OPENCODE_DISABLE_SHARE: "true" + OPENCODE_DISABLE_LSP_DOWNLOAD: "true" + OPENCODE_DISABLE_DEFAULT_PLUGINS: "true" + OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" + OPENCODE_TEST_HOME: ${{ runner.temp }}/opencode-e2e/home + XDG_DATA_HOME: ${{ runner.temp }}/opencode-e2e/share + XDG_CACHE_HOME: ${{ runner.temp }}/opencode-e2e/cache + XDG_CONFIG_HOME: ${{ runner.temp }}/opencode-e2e/config + XDG_STATE_HOME: ${{ runner.temp }}/opencode-e2e/state + OPENCODE_E2E_PROJECT_DIR: ${{ github.workspace }} + OPENCODE_E2E_SESSION_TITLE: "E2E Session" + OPENCODE_E2E_MESSAGE: "Seeded for UI e2e" + OPENCODE_E2E_MODEL: "opencode/gpt-5-nano" + + - name: Run opencode server + run: bun --cwd packages/opencode run dev -- --print-logs --log-level WARN serve --port 4096 --hostname 0.0.0.0 & + env: + MODELS_DEV_API_JSON: ${{ github.workspace }}/packages/opencode/test/tool/fixtures/models-api.json + OPENCODE_DISABLE_MODELS_FETCH: "true" + OPENCODE_DISABLE_SHARE: "true" + OPENCODE_DISABLE_LSP_DOWNLOAD: "true" + OPENCODE_DISABLE_DEFAULT_PLUGINS: "true" + OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" + OPENCODE_TEST_HOME: ${{ runner.temp }}/opencode-e2e/home + XDG_DATA_HOME: ${{ runner.temp }}/opencode-e2e/share + XDG_CACHE_HOME: ${{ runner.temp }}/opencode-e2e/cache + XDG_CONFIG_HOME: ${{ runner.temp }}/opencode-e2e/config + XDG_STATE_HOME: ${{ runner.temp }}/opencode-e2e/state + OPENCODE_CLIENT: "app" + + - name: Wait for opencode server + run: | + for i in {1..60}; do + curl -fsS "http://localhost:4096/global/health" > /dev/null && exit 0 + sleep 1 + done + exit 1 + - name: run run: | git config --global user.email "bot@opencode.ai" @@ -26,3 +72,20 @@ jobs: bun turbo test env: CI: true + MODELS_DEV_API_JSON: ${{ github.workspace }}/packages/opencode/test/tool/fixtures/models-api.json + OPENCODE_DISABLE_MODELS_FETCH: "true" + OPENCODE_DISABLE_SHARE: "true" + OPENCODE_DISABLE_LSP_DOWNLOAD: "true" + OPENCODE_DISABLE_DEFAULT_PLUGINS: "true" + OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" + OPENCODE_TEST_HOME: ${{ runner.temp }}/opencode-e2e/home + XDG_DATA_HOME: ${{ runner.temp }}/opencode-e2e/share + XDG_CACHE_HOME: ${{ runner.temp }}/opencode-e2e/cache + XDG_CONFIG_HOME: ${{ runner.temp }}/opencode-e2e/config + XDG_STATE_HOME: ${{ runner.temp }}/opencode-e2e/state + PLAYWRIGHT_SERVER_HOST: "localhost" + PLAYWRIGHT_SERVER_PORT: "4096" + VITE_OPENCODE_SERVER_HOST: "localhost" + VITE_OPENCODE_SERVER_PORT: "4096" + OPENCODE_CLIENT: "app" + timeout-minutes: 30 diff --git a/bun.lock b/bun.lock index a9cabb31114..e5892a7745d 100644 --- a/bun.lock +++ b/bun.lock @@ -56,6 +56,7 @@ }, "devDependencies": { "@happy-dom/global-registrator": "20.0.11", + "@playwright/test": "1.57.0", "@tailwindcss/vite": "catalog:", "@tsconfig/bun": "1.0.9", "@types/bun": "catalog:", @@ -502,6 +503,7 @@ "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", "@pierre/diffs": "1.0.2", + "@playwright/test": "1.51.0", "@solid-primitives/storage": "4.3.3", "@solidjs/meta": "0.29.4", "@solidjs/router": "0.15.4", @@ -1355,6 +1357,8 @@ "@planetscale/database": ["@planetscale/database@1.19.0", "", {}, "sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA=="], + "@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="], + "@poppinss/colors": ["@poppinss/colors@4.1.5", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw=="], "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], @@ -3291,6 +3295,10 @@ "planck": ["planck@1.4.2", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-mNbhnV3g8X2rwGxzcesjmN8BDA6qfXgQxXVMkWau9MCRlQY0RLNEkyHlVp6yFy/X6qrzAXyNONCnZ1cGDLrNew=="], + "playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="], + + "playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="], + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -4427,6 +4435,8 @@ "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], diff --git a/package.json b/package.json index f1d6c4fead1..ca9602174a2 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "luxon": "3.6.1", "marked": "17.0.1", "marked-shiki": "1.2.1", + "@playwright/test": "1.51.0", "typescript": "5.8.2", "@typescript/native-preview": "7.0.0-dev.20251207.1", "zod": "4.1.8", diff --git a/packages/app/.gitignore b/packages/app/.gitignore index 4a20d55a70d..d699efb38d2 100644 --- a/packages/app/.gitignore +++ b/packages/app/.gitignore @@ -1 +1,3 @@ src/assets/theme.css +e2e/test-results +e2e/playwright-report diff --git a/packages/app/README.md b/packages/app/README.md index bd10e6c8ddf..42a68815090 100644 --- a/packages/app/README.md +++ b/packages/app/README.md @@ -29,6 +29,21 @@ It correctly bundles Solid in production mode and optimizes the build for the be The build is minified and the filenames include the hashes.
Your app is ready to be deployed! +## E2E Testing + +The Playwright runner expects the app already running at `http://localhost:3000`. + +```bash +bun add -D @playwright/test +bunx playwright install +bun run test:e2e +``` + +Environment options: + +- `PLAYWRIGHT_BASE_URL` (default: `http://localhost:3000`) +- `PLAYWRIGHT_PORT` (default: `3000`) + ## Deployment You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.) diff --git a/packages/app/e2e/home.spec.ts b/packages/app/e2e/home.spec.ts new file mode 100644 index 00000000000..ff57923d58c --- /dev/null +++ b/packages/app/e2e/home.spec.ts @@ -0,0 +1,6 @@ +import { test, expect } from "@playwright/test" + +test("home shows recent projects header", async ({ page }) => { + await page.goto("/") + await expect(page.getByText("Recent projects")).toBeVisible() +}) diff --git a/packages/app/package.json b/packages/app/package.json index 38d9a25f50e..2a754c96735 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -12,11 +12,16 @@ "start": "vite", "dev": "vite", "build": "vite build", - "serve": "vite preview" + "serve": "vite preview", + "test": "playwright test", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:report": "playwright show-report e2e/playwright-report" }, "license": "MIT", "devDependencies": { "@happy-dom/global-registrator": "20.0.11", + "@playwright/test": "1.57.0", "@tailwindcss/vite": "catalog:", "@tsconfig/bun": "1.0.9", "@types/bun": "catalog:", diff --git a/packages/app/playwright.config.ts b/packages/app/playwright.config.ts new file mode 100644 index 00000000000..10819e69ffe --- /dev/null +++ b/packages/app/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from "@playwright/test" + +const port = Number(process.env.PLAYWRIGHT_PORT ?? 3000) +const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${port}` +const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" +const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" +const command = `bun run dev -- --host 0.0.0.0 --port ${port}` +const reuse = !process.env.CI + +export default defineConfig({ + testDir: "./e2e", + outputDir: "./e2e/test-results", + timeout: 60_000, + expect: { + timeout: 10_000, + }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]], + webServer: { + command, + url: baseURL, + reuseExistingServer: reuse, + timeout: 120_000, + env: { + VITE_OPENCODE_SERVER_HOST: serverHost, + VITE_OPENCODE_SERVER_PORT: serverPort, + }, + }, + use: { + baseURL, + trace: "on-first-retry", + screenshot: "only-on-failure", + video: "retain-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}) diff --git a/packages/opencode/script/seed-e2e.ts b/packages/opencode/script/seed-e2e.ts new file mode 100644 index 00000000000..ba2155cb692 --- /dev/null +++ b/packages/opencode/script/seed-e2e.ts @@ -0,0 +1,50 @@ +const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd() +const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session" +const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e" +const model = process.env.OPENCODE_E2E_MODEL ?? "opencode/gpt-5-nano" +const parts = model.split("/") +const providerID = parts[0] ?? "opencode" +const modelID = parts[1] ?? "gpt-5-nano" +const now = Date.now() + +const seed = async () => { + const { Instance } = await import("../src/project/instance") + const { InstanceBootstrap } = await import("../src/project/bootstrap") + const { Session } = await import("../src/session") + const { Identifier } = await import("../src/id/id") + const { Project } = await import("../src/project/project") + + await Instance.provide({ + directory: dir, + init: InstanceBootstrap, + fn: async () => { + const session = await Session.create({ title }) + const messageID = Identifier.descending("message") + const partID = Identifier.descending("part") + const message = { + id: messageID, + sessionID: session.id, + role: "user" as const, + time: { created: now }, + agent: "build", + model: { + providerID, + modelID, + }, + } + const part = { + id: partID, + sessionID: session.id, + messageID, + type: "text" as const, + text, + time: { start: now }, + } + await Session.updateMessage(message) + await Session.updatePart(part) + await Project.update({ projectID: Instance.project.id, name: "E2E Project" }) + }, + }) +} + +await seed() diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 95271f8c827..dddce95cb4f 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -15,7 +15,10 @@ export namespace ShareNext { return Config.get().then((x) => x.enterprise?.url ?? "https://opncd.ai") } + const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1" + export async function init() { + if (disabled) return Bus.subscribe(Session.Event.Updated, async (evt) => { await sync(evt.properties.info.id, [ { @@ -63,6 +66,7 @@ export namespace ShareNext { } export async function create(sessionID: string) { + if (disabled) return { id: "", url: "", secret: "" } log.info("creating share", { sessionID }) const result = await fetch(`${await url()}/api/share`, { method: "POST", @@ -110,6 +114,7 @@ export namespace ShareNext { const queue = new Map }>() async function sync(sessionID: string, data: Data[]) { + if (disabled) return const existing = queue.get(sessionID) if (existing) { for (const item of data) { @@ -145,6 +150,7 @@ export namespace ShareNext { } export async function remove(sessionID: string) { + if (disabled) return log.info("removing share", { sessionID }) const share = await get(sessionID) if (!share) return diff --git a/packages/opencode/src/share/share.ts b/packages/opencode/src/share/share.ts index 1006b23d556..f7bf4b3fa52 100644 --- a/packages/opencode/src/share/share.ts +++ b/packages/opencode/src/share/share.ts @@ -11,6 +11,7 @@ export namespace Share { const pending = new Map() export async function sync(key: string, content: any) { + if (disabled) return const [root, ...splits] = key.split("/") if (root !== "session") return const [sub, sessionID] = splits @@ -69,7 +70,10 @@ export namespace Share { process.env["OPENCODE_API"] ?? (Installation.isPreview() || Installation.isLocal() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai") + const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1" + export async function create(sessionID: string) { + if (disabled) return { url: "", secret: "" } return fetch(`${URL}/share_create`, { method: "POST", body: JSON.stringify({ sessionID: sessionID }), @@ -79,6 +83,7 @@ export namespace Share { } export async function remove(sessionID: string, secret: string) { + if (disabled) return {} return fetch(`${URL}/share_delete`, { method: "POST", body: JSON.stringify({ sessionID, secret }), From 19d15ca4dfb4eaad34e3b9e0a3b8f9e206d094c3 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sun, 18 Jan 2026 05:15:34 -0600 Subject: [PATCH 130/192] test(app): more e2e tests --- packages/app/e2e/home.spec.ts | 22 ++++++++++++++++-- packages/app/e2e/session.spec.ts | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 packages/app/e2e/session.spec.ts diff --git a/packages/app/e2e/home.spec.ts b/packages/app/e2e/home.spec.ts index ff57923d58c..29a07060932 100644 --- a/packages/app/e2e/home.spec.ts +++ b/packages/app/e2e/home.spec.ts @@ -1,6 +1,24 @@ import { test, expect } from "@playwright/test" -test("home shows recent projects header", async ({ page }) => { +test("home renders and shows an open project entrypoint", async ({ page }) => { await page.goto("/") - await expect(page.getByText("Recent projects")).toBeVisible() + + await expect(page.getByText("Recent projects").or(page.getByText("No recent projects"))).toBeVisible() + await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible() +}) + +test("server picker dialog opens from home", async ({ page }) => { + const host = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" + const port = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" + const name = `${host}:${port}` + + await page.goto("/") + + const trigger = page.getByRole("button", { name }) + await expect(trigger).toBeVisible() + await trigger.click() + + const dialog = page.getByRole("dialog", { name: "Servers" }) + await expect(dialog).toBeVisible() + await expect(dialog.getByPlaceholder("Search servers")).toBeVisible() }) diff --git a/packages/app/e2e/session.spec.ts b/packages/app/e2e/session.spec.ts new file mode 100644 index 00000000000..e1ca12449bb --- /dev/null +++ b/packages/app/e2e/session.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from "@playwright/test" +import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { base64Encode } from "@opencode-ai/util/encode" + +const host = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" +const port = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" +const url = `http://${host}:${port}` + +async function getWorktree() { + const sdk = createOpencodeClient({ baseUrl: url, throwOnError: true }) + const result = await sdk.path.get() + const data = result.data + if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${url}/path`) + return data.worktree +} + +test("can open an existing session and type into the prompt", async ({ page }) => { + const directory = await getWorktree() + const sdk = createOpencodeClient({ baseUrl: url, directory, throwOnError: true }) + const title = `e2e smoke ${Date.now()}` + const created = await sdk.session.create({ title }).then((r) => r.data) + + if (!created?.id) throw new Error("Session create did not return an id") + const sessionID = created.id + + try { + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + + const prompt = page.locator('[data-component="prompt-input"]') + await expect(prompt).toBeVisible() + + await prompt.click() + await page.keyboard.type("hello from e2e") + await expect(prompt).toContainText("hello from e2e") + } finally { + await sdk.session.delete({ sessionID }).catch(() => undefined) + } +}) From 91a708b12e3dc0219517aa93dbd30ef2a4aea6c8 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sun, 18 Jan 2026 05:36:02 -0600 Subject: [PATCH 131/192] test(app): more e2e tests --- packages/app/e2e/navigation.spec.ts | 24 ++++++++++++++++++++ packages/app/e2e/palette.spec.ts | 34 +++++++++++++++++++++++++++++ packages/app/e2e/terminal.spec.ts | 33 ++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 packages/app/e2e/navigation.spec.ts create mode 100644 packages/app/e2e/palette.spec.ts create mode 100644 packages/app/e2e/terminal.spec.ts diff --git a/packages/app/e2e/navigation.spec.ts b/packages/app/e2e/navigation.spec.ts new file mode 100644 index 00000000000..2783c5222cc --- /dev/null +++ b/packages/app/e2e/navigation.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from "@playwright/test" +import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { base64Encode } from "@opencode-ai/util/encode" + +const host = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" +const port = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" +const url = `http://${host}:${port}` + +async function getWorktree() { + const sdk = createOpencodeClient({ baseUrl: url, throwOnError: true }) + const result = await sdk.path.get() + const data = result.data + if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${url}/path`) + return data.worktree +} + +test("project route redirects to /session", async ({ page }) => { + const directory = await getWorktree() + const slug = base64Encode(directory) + + await page.goto(`/${slug}`) + await expect(page).toHaveURL(new RegExp(`/${slug}/session`)) + await expect(page.locator('[data-component="prompt-input"]')).toBeVisible() +}) diff --git a/packages/app/e2e/palette.spec.ts b/packages/app/e2e/palette.spec.ts new file mode 100644 index 00000000000..06294562945 --- /dev/null +++ b/packages/app/e2e/palette.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from "@playwright/test" +import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { base64Encode } from "@opencode-ai/util/encode" + +const host = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" +const port = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" +const url = `http://${host}:${port}` + +async function getWorktree() { + const sdk = createOpencodeClient({ baseUrl: url, throwOnError: true }) + const result = await sdk.path.get() + const data = result.data + if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${url}/path`) + return data.worktree +} + +const mod = process.platform === "darwin" ? "Meta" : "Control" + +test("search palette opens and closes", async ({ page }) => { + const directory = await getWorktree() + const slug = base64Encode(directory) + + await page.goto(`/${slug}/session`) + await expect(page.locator('[data-component="prompt-input"]')).toBeVisible() + + await page.keyboard.press(`${mod}+P`) + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + await expect(dialog.getByRole("textbox").first()).toBeVisible() + + await page.keyboard.press("Escape") + await expect(dialog).toHaveCount(0) +}) diff --git a/packages/app/e2e/terminal.spec.ts b/packages/app/e2e/terminal.spec.ts new file mode 100644 index 00000000000..9bb9947fe31 --- /dev/null +++ b/packages/app/e2e/terminal.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from "@playwright/test" +import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { base64Encode } from "@opencode-ai/util/encode" + +const host = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" +const port = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" +const url = `http://${host}:${port}` + +async function getWorktree() { + const sdk = createOpencodeClient({ baseUrl: url, throwOnError: true }) + const result = await sdk.path.get() + const data = result.data + if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${url}/path`) + return data.worktree +} + +test("terminal panel can be toggled", async ({ page }) => { + const directory = await getWorktree() + const slug = base64Encode(directory) + + await page.goto(`/${slug}/session`) + await expect(page.locator('[data-component="prompt-input"]')).toBeVisible() + + const terminal = page.locator('[data-component="terminal"]') + const initiallyOpen = await terminal.isVisible() + if (initiallyOpen) { + await page.keyboard.press("Control+Backquote") + await expect(terminal).toHaveCount(0) + } + + await page.keyboard.press("Control+Backquote") + await expect(terminal).toBeVisible() +}) From 7621c5cafbc5f853806bf8a9c95ebaa727bda15f Mon Sep 17 00:00:00 2001 From: Github Action Date: Sun, 18 Jan 2026 11:36:58 +0000 Subject: [PATCH 132/192] Update flake.lock --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 5ef276f0a08..87f95fb3eb7 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1768302833, - "narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=", + "lastModified": 1768569498, + "narHash": "sha256-bB6Nt99Cj8Nu5nIUq0GLmpiErIT5KFshMQJGMZwgqUo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "61db79b0c6b838d9894923920b612048e1201926", + "rev": "be5afa0fcb31f0a96bf9ecba05a516c66fcd8114", "type": "github" }, "original": { From 6bc823bd40935117b0f6cd4e0e6d00fd9ea64bfe Mon Sep 17 00:00:00 2001 From: Github Action Date: Sun, 18 Jan 2026 11:37:49 +0000 Subject: [PATCH 133/192] Update node_modules hash (x86_64-linux) --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 5bbdf921bbd..a7a7957e24d 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-D1VXuKJagfq3mxh8Xs8naHoYNJUJzAM9JLJqpHcItDk=", - "aarch64-linux": "sha256-9wXcg50Sv56Wb2x5NWe15olNGE/uMiDkmGRmqPoeW1U=", - "aarch64-darwin": "sha256-i5eTTjsNAARwcw69sd6wuse2BKTUi/Vfgo4M28l+RoY=", - "x86_64-darwin": "sha256-oFtQnIzgTS2zcjkhBTnXxYqr20KXdA2I+b908piLs+c=" + "x86_64-linux": "sha256-DFsg3Dyt+cm8Z7CU6lpFd4gTAzydAewfiQW1T3qvDxA=", + "aarch64-linux": "sha256-0Im52dLeZ0ZtaPJr/U4m7+IRtOfziHNJI/Bu/V6cPho=", + "aarch64-darwin": "sha256-U2UvE70nM0OI0VhIku8qnX+ptPbA+Q/y1BGXbFMcyt4=", + "x86_64-darwin": "sha256-CpZFHBMPJSib2Vqs6oC8HQjQtviPUMa/qezHAe22N/A=" } } From f5eb90514aca136e0aa2f48954f71d51b52e2b16 Mon Sep 17 00:00:00 2001 From: Github Action Date: Sun, 18 Jan 2026 11:43:58 +0000 Subject: [PATCH 134/192] Update node_modules hash (aarch64-darwin) --- nix/hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/hashes.json b/nix/hashes.json index a7a7957e24d..e51ed90463a 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -2,7 +2,7 @@ "nodeModules": { "x86_64-linux": "sha256-DFsg3Dyt+cm8Z7CU6lpFd4gTAzydAewfiQW1T3qvDxA=", "aarch64-linux": "sha256-0Im52dLeZ0ZtaPJr/U4m7+IRtOfziHNJI/Bu/V6cPho=", - "aarch64-darwin": "sha256-U2UvE70nM0OI0VhIku8qnX+ptPbA+Q/y1BGXbFMcyt4=", + "aarch64-darwin": "sha256-Rfd05h/BRLoM0SmKn+ui2RdXBNhkaVKkgBeT+uBs4J8=", "x86_64-darwin": "sha256-CpZFHBMPJSib2Vqs6oC8HQjQtviPUMa/qezHAe22N/A=" } } From dd19c3d8f2cfbb7d1b157bbe6f5a9a969cb6c239 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sun, 18 Jan 2026 05:43:34 -0600 Subject: [PATCH 135/192] test(app): e2e utilities --- packages/app/e2e/home.spec.ts | 15 ++++------ packages/app/e2e/navigation.spec.ts | 21 +++----------- packages/app/e2e/palette.spec.ts | 26 +++-------------- packages/app/e2e/session.spec.ts | 21 +++----------- packages/app/e2e/terminal.spec.ts | 28 ++++-------------- packages/app/e2e/utils.ts | 45 +++++++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 87 deletions(-) create mode 100644 packages/app/e2e/utils.ts diff --git a/packages/app/e2e/home.spec.ts b/packages/app/e2e/home.spec.ts index 29a07060932..5bb701076e9 100644 --- a/packages/app/e2e/home.spec.ts +++ b/packages/app/e2e/home.spec.ts @@ -1,24 +1,21 @@ import { test, expect } from "@playwright/test" +import { serverName } from "./utils" -test("home renders and shows an open project entrypoint", async ({ page }) => { +test("home renders and shows core entrypoints", async ({ page }) => { await page.goto("/") - await expect(page.getByText("Recent projects").or(page.getByText("No recent projects"))).toBeVisible() await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible() + await expect(page.getByRole("button", { name: serverName })).toBeVisible() }) test("server picker dialog opens from home", async ({ page }) => { - const host = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" - const port = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" - const name = `${host}:${port}` - await page.goto("/") - const trigger = page.getByRole("button", { name }) + const trigger = page.getByRole("button", { name: serverName }) await expect(trigger).toBeVisible() await trigger.click() - const dialog = page.getByRole("dialog", { name: "Servers" }) + const dialog = page.getByRole("dialog") await expect(dialog).toBeVisible() - await expect(dialog.getByPlaceholder("Search servers")).toBeVisible() + await expect(dialog.getByRole("textbox").first()).toBeVisible() }) diff --git a/packages/app/e2e/navigation.spec.ts b/packages/app/e2e/navigation.spec.ts index 2783c5222cc..4d0d3b2b9d4 100644 --- a/packages/app/e2e/navigation.spec.ts +++ b/packages/app/e2e/navigation.spec.ts @@ -1,24 +1,11 @@ import { test, expect } from "@playwright/test" -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/util/encode" - -const host = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" -const port = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" -const url = `http://${host}:${port}` - -async function getWorktree() { - const sdk = createOpencodeClient({ baseUrl: url, throwOnError: true }) - const result = await sdk.path.get() - const data = result.data - if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${url}/path`) - return data.worktree -} +import { dirPath, dirSlug, getWorktree, promptSelector } from "./utils" test("project route redirects to /session", async ({ page }) => { const directory = await getWorktree() - const slug = base64Encode(directory) + const slug = dirSlug(directory) - await page.goto(`/${slug}`) + await page.goto(dirPath(directory)) await expect(page).toHaveURL(new RegExp(`/${slug}/session`)) - await expect(page.locator('[data-component="prompt-input"]')).toBeVisible() + await expect(page.locator(promptSelector)).toBeVisible() }) diff --git a/packages/app/e2e/palette.spec.ts b/packages/app/e2e/palette.spec.ts index 06294562945..bad09aab96b 100644 --- a/packages/app/e2e/palette.spec.ts +++ b/packages/app/e2e/palette.spec.ts @@ -1,29 +1,11 @@ import { test, expect } from "@playwright/test" -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/util/encode" - -const host = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" -const port = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" -const url = `http://${host}:${port}` - -async function getWorktree() { - const sdk = createOpencodeClient({ baseUrl: url, throwOnError: true }) - const result = await sdk.path.get() - const data = result.data - if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${url}/path`) - return data.worktree -} - -const mod = process.platform === "darwin" ? "Meta" : "Control" +import { gotoSession, modKey, promptSelector } from "./utils" test("search palette opens and closes", async ({ page }) => { - const directory = await getWorktree() - const slug = base64Encode(directory) - - await page.goto(`/${slug}/session`) - await expect(page.locator('[data-component="prompt-input"]')).toBeVisible() + await gotoSession(page) + await expect(page.locator(promptSelector)).toBeVisible() - await page.keyboard.press(`${mod}+P`) + await page.keyboard.press(`${modKey}+P`) const dialog = page.getByRole("dialog") await expect(dialog).toBeVisible() diff --git a/packages/app/e2e/session.spec.ts b/packages/app/e2e/session.spec.ts index e1ca12449bb..d44736a4f56 100644 --- a/packages/app/e2e/session.spec.ts +++ b/packages/app/e2e/session.spec.ts @@ -1,22 +1,9 @@ import { test, expect } from "@playwright/test" -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/util/encode" - -const host = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" -const port = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" -const url = `http://${host}:${port}` - -async function getWorktree() { - const sdk = createOpencodeClient({ baseUrl: url, throwOnError: true }) - const result = await sdk.path.get() - const data = result.data - if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${url}/path`) - return data.worktree -} +import { createSdk, getWorktree, promptSelector, sessionPath } from "./utils" test("can open an existing session and type into the prompt", async ({ page }) => { const directory = await getWorktree() - const sdk = createOpencodeClient({ baseUrl: url, directory, throwOnError: true }) + const sdk = createSdk(directory) const title = `e2e smoke ${Date.now()}` const created = await sdk.session.create({ title }).then((r) => r.data) @@ -24,9 +11,9 @@ test("can open an existing session and type into the prompt", async ({ page }) = const sessionID = created.id try { - await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await page.goto(sessionPath(directory, sessionID)) - const prompt = page.locator('[data-component="prompt-input"]') + const prompt = page.locator(promptSelector) await expect(prompt).toBeVisible() await prompt.click() diff --git a/packages/app/e2e/terminal.spec.ts b/packages/app/e2e/terminal.spec.ts index 9bb9947fe31..8f90a3c0c1b 100644 --- a/packages/app/e2e/terminal.spec.ts +++ b/packages/app/e2e/terminal.spec.ts @@ -1,33 +1,17 @@ import { test, expect } from "@playwright/test" -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/util/encode" - -const host = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" -const port = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" -const url = `http://${host}:${port}` - -async function getWorktree() { - const sdk = createOpencodeClient({ baseUrl: url, throwOnError: true }) - const result = await sdk.path.get() - const data = result.data - if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${url}/path`) - return data.worktree -} +import { gotoSession, promptSelector, terminalSelector, terminalToggleKey } from "./utils" test("terminal panel can be toggled", async ({ page }) => { - const directory = await getWorktree() - const slug = base64Encode(directory) - - await page.goto(`/${slug}/session`) - await expect(page.locator('[data-component="prompt-input"]')).toBeVisible() + await gotoSession(page) + await expect(page.locator(promptSelector)).toBeVisible() - const terminal = page.locator('[data-component="terminal"]') + const terminal = page.locator(terminalSelector) const initiallyOpen = await terminal.isVisible() if (initiallyOpen) { - await page.keyboard.press("Control+Backquote") + await page.keyboard.press(terminalToggleKey) await expect(terminal).toHaveCount(0) } - await page.keyboard.press("Control+Backquote") + await page.keyboard.press(terminalToggleKey) await expect(terminal).toBeVisible() }) diff --git a/packages/app/e2e/utils.ts b/packages/app/e2e/utils.ts new file mode 100644 index 00000000000..c18b368029a --- /dev/null +++ b/packages/app/e2e/utils.ts @@ -0,0 +1,45 @@ +import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { base64Encode } from "@opencode-ai/util/encode" +import type { Page } from "@playwright/test" + +export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" +export const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" + +export const serverUrl = `http://${serverHost}:${serverPort}` +export const serverName = `${serverHost}:${serverPort}` + +export const modKey = process.platform === "darwin" ? "Meta" : "Control" +export const terminalToggleKey = "Control+Backquote" + +export const promptSelector = '[data-component="prompt-input"]' +export const terminalSelector = '[data-component="terminal"]' + +export function createSdk(directory?: string) { + return createOpencodeClient({ baseUrl: serverUrl, directory, throwOnError: true }) +} + +export async function getWorktree() { + const sdk = createSdk() + const result = await sdk.path.get() + const data = result.data + if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${serverUrl}/path`) + return data.worktree +} + +export function dirSlug(directory: string) { + return base64Encode(directory) +} + +export function dirPath(directory: string) { + return `/${dirSlug(directory)}` +} + +export function sessionPath(directory: string, sessionID?: string) { + return `${dirPath(directory)}/session${sessionID ? `/${sessionID}` : ""}` +} + +export async function gotoSession(page: Page, sessionID?: string) { + const directory = await getWorktree() + await page.goto(sessionPath(directory, sessionID)) + return { directory, slug: dirSlug(directory) } +} From f1daf3b4308cf85a3f61fb2f1d4b3f55316487b1 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sun, 18 Jan 2026 05:45:24 -0600 Subject: [PATCH 136/192] fix(app): tests in ci --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 98eac1dab2f..fa93b61cf66 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: uses: ./.github/actions/setup-bun - name: Install Playwright browsers - run: bun --cwd packages/app x playwright install --with-deps + run: bunx --cwd packages/app playwright install --with-deps - name: Seed opencode data run: bun --cwd packages/opencode script/seed-e2e.ts From 182c43a78f55d4c22440c7256cbf4ff464e01c6e Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 05:22:35 -0600 Subject: [PATCH 137/192] chore: cleanup --- packages/app/e2e/fixtures.ts | 40 +++++++++++++++++++++++++++++ packages/app/e2e/home.spec.ts | 2 +- packages/app/e2e/navigation.spec.ts | 10 +++----- packages/app/e2e/palette.spec.ts | 9 +++---- packages/app/e2e/session.spec.ts | 12 +++------ packages/app/e2e/terminal.spec.ts | 9 +++---- packages/app/e2e/utils.ts | 7 ----- 7 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 packages/app/e2e/fixtures.ts diff --git a/packages/app/e2e/fixtures.ts b/packages/app/e2e/fixtures.ts new file mode 100644 index 00000000000..721d60049ce --- /dev/null +++ b/packages/app/e2e/fixtures.ts @@ -0,0 +1,40 @@ +import { test as base, expect } from "@playwright/test" +import { createSdk, dirSlug, getWorktree, promptSelector, sessionPath } from "./utils" + +type TestFixtures = { + sdk: ReturnType + gotoSession: (sessionID?: string) => Promise +} + +type WorkerFixtures = { + directory: string + slug: string +} + +export const test = base.extend({ + directory: [ + async ({}, use) => { + const directory = await getWorktree() + await use(directory) + }, + { scope: "worker" }, + ], + slug: [ + async ({ directory }, use) => { + await use(dirSlug(directory)) + }, + { scope: "worker" }, + ], + sdk: async ({ directory }, use) => { + await use(createSdk(directory)) + }, + gotoSession: async ({ page, directory }, use) => { + const gotoSession = async (sessionID?: string) => { + await page.goto(sessionPath(directory, sessionID)) + await expect(page.locator(promptSelector)).toBeVisible() + } + await use(gotoSession) + }, +}) + +export { expect } diff --git a/packages/app/e2e/home.spec.ts b/packages/app/e2e/home.spec.ts index 5bb701076e9..c6fb0e3b074 100644 --- a/packages/app/e2e/home.spec.ts +++ b/packages/app/e2e/home.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test" +import { test, expect } from "./fixtures" import { serverName } from "./utils" test("home renders and shows core entrypoints", async ({ page }) => { diff --git a/packages/app/e2e/navigation.spec.ts b/packages/app/e2e/navigation.spec.ts index 4d0d3b2b9d4..76923af6ede 100644 --- a/packages/app/e2e/navigation.spec.ts +++ b/packages/app/e2e/navigation.spec.ts @@ -1,11 +1,9 @@ -import { test, expect } from "@playwright/test" -import { dirPath, dirSlug, getWorktree, promptSelector } from "./utils" - -test("project route redirects to /session", async ({ page }) => { - const directory = await getWorktree() - const slug = dirSlug(directory) +import { test, expect } from "./fixtures" +import { dirPath, promptSelector } from "./utils" +test("project route redirects to /session", async ({ page, directory, slug }) => { await page.goto(dirPath(directory)) + await expect(page).toHaveURL(new RegExp(`/${slug}/session`)) await expect(page.locator(promptSelector)).toBeVisible() }) diff --git a/packages/app/e2e/palette.spec.ts b/packages/app/e2e/palette.spec.ts index bad09aab96b..617c55ac167 100644 --- a/packages/app/e2e/palette.spec.ts +++ b/packages/app/e2e/palette.spec.ts @@ -1,9 +1,8 @@ -import { test, expect } from "@playwright/test" -import { gotoSession, modKey, promptSelector } from "./utils" +import { test, expect } from "./fixtures" +import { modKey } from "./utils" -test("search palette opens and closes", async ({ page }) => { - await gotoSession(page) - await expect(page.locator(promptSelector)).toBeVisible() +test("search palette opens and closes", async ({ page, gotoSession }) => { + await gotoSession() await page.keyboard.press(`${modKey}+P`) diff --git a/packages/app/e2e/session.spec.ts b/packages/app/e2e/session.spec.ts index d44736a4f56..19e25a42131 100644 --- a/packages/app/e2e/session.spec.ts +++ b/packages/app/e2e/session.spec.ts @@ -1,9 +1,7 @@ -import { test, expect } from "@playwright/test" -import { createSdk, getWorktree, promptSelector, sessionPath } from "./utils" +import { test, expect } from "./fixtures" +import { promptSelector } from "./utils" -test("can open an existing session and type into the prompt", async ({ page }) => { - const directory = await getWorktree() - const sdk = createSdk(directory) +test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => { const title = `e2e smoke ${Date.now()}` const created = await sdk.session.create({ title }).then((r) => r.data) @@ -11,11 +9,9 @@ test("can open an existing session and type into the prompt", async ({ page }) = const sessionID = created.id try { - await page.goto(sessionPath(directory, sessionID)) + await gotoSession(sessionID) const prompt = page.locator(promptSelector) - await expect(prompt).toBeVisible() - await prompt.click() await page.keyboard.type("hello from e2e") await expect(prompt).toContainText("hello from e2e") diff --git a/packages/app/e2e/terminal.spec.ts b/packages/app/e2e/terminal.spec.ts index 8f90a3c0c1b..fc558b63259 100644 --- a/packages/app/e2e/terminal.spec.ts +++ b/packages/app/e2e/terminal.spec.ts @@ -1,9 +1,8 @@ -import { test, expect } from "@playwright/test" -import { gotoSession, promptSelector, terminalSelector, terminalToggleKey } from "./utils" +import { test, expect } from "./fixtures" +import { terminalSelector, terminalToggleKey } from "./utils" -test("terminal panel can be toggled", async ({ page }) => { - await gotoSession(page) - await expect(page.locator(promptSelector)).toBeVisible() +test("terminal panel can be toggled", async ({ page, gotoSession }) => { + await gotoSession() const terminal = page.locator(terminalSelector) const initiallyOpen = await terminal.isVisible() diff --git a/packages/app/e2e/utils.ts b/packages/app/e2e/utils.ts index c18b368029a..eb0395950ae 100644 --- a/packages/app/e2e/utils.ts +++ b/packages/app/e2e/utils.ts @@ -1,6 +1,5 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { base64Encode } from "@opencode-ai/util/encode" -import type { Page } from "@playwright/test" export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" export const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" @@ -37,9 +36,3 @@ export function dirPath(directory: string) { export function sessionPath(directory: string, sessionID?: string) { return `${dirPath(directory)}/session${sessionID ? `/${sessionID}` : ""}` } - -export async function gotoSession(page: Page, sessionID?: string) { - const directory = await getWorktree() - await page.goto(sessionPath(directory, sessionID)) - return { directory, slug: dirSlug(directory) } -} From b90315bc7ee745f9afb81fb3cfab69b42980b79d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 05:28:15 -0600 Subject: [PATCH 138/192] chore: cleanup --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fa93b61cf66..fda848762f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,8 @@ jobs: uses: ./.github/actions/setup-bun - name: Install Playwright browsers - run: bunx --cwd packages/app playwright install --with-deps + working-directory: packages/app + run: bunx playwright install --with-deps - name: Seed opencode data run: bun --cwd packages/opencode script/seed-e2e.ts From 2b086f0584ad803f27bbf386b00b04ecdee204fe Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 06:54:47 -0600 Subject: [PATCH 139/192] test(app): more e2e tests --- packages/app/e2e/context.spec.ts | 28 ++++++++++++++++++++++++++++ packages/app/e2e/file-open.spec.ts | 24 ++++++++++++++++++++++++ packages/app/e2e/sidebar.spec.ts | 20 ++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 packages/app/e2e/context.spec.ts create mode 100644 packages/app/e2e/file-open.spec.ts create mode 100644 packages/app/e2e/sidebar.spec.ts diff --git a/packages/app/e2e/context.spec.ts b/packages/app/e2e/context.spec.ts new file mode 100644 index 00000000000..dbfeda8d8c5 --- /dev/null +++ b/packages/app/e2e/context.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from "./fixtures" +import { promptSelector } from "./utils" + +test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => { + const title = `e2e smoke context ${Date.now()}` + const created = await sdk.session.create({ title }).then((r) => r.data) + + if (!created?.id) throw new Error("Session create did not return an id") + const sessionID = created.id + + try { + await gotoSession(sessionID) + + const promptForm = page.locator("form").filter({ has: page.locator(promptSelector) }).first() + const contextButton = promptForm + .locator("button") + .filter({ has: promptForm.locator('[data-component="progress-circle"]').first() }) + .first() + + await expect(contextButton).toBeVisible() + await contextButton.click() + + const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') + await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible() + } finally { + await sdk.session.delete({ sessionID }).catch(() => undefined) + } +}) diff --git a/packages/app/e2e/file-open.spec.ts b/packages/app/e2e/file-open.spec.ts new file mode 100644 index 00000000000..673caf9dc34 --- /dev/null +++ b/packages/app/e2e/file-open.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from "./fixtures" +import { modKey } from "./utils" + +test("can open a file tab from the search palette", async ({ page, gotoSession }) => { + await gotoSession() + + await page.keyboard.press(`${modKey}+P`) + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + + const input = dialog.getByRole("textbox").first() + await input.fill("package.json") + + const firstItem = dialog.locator('[data-slot="list-item"]').first() + await expect(firstItem).toBeVisible() + await firstItem.click() + + await expect(dialog).toHaveCount(0) + + const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') + await expect(tabs).toBeVisible() + await expect(tabs.getByRole("tab").first()).toBeVisible() +}) diff --git a/packages/app/e2e/sidebar.spec.ts b/packages/app/e2e/sidebar.spec.ts new file mode 100644 index 00000000000..964b0a56c03 --- /dev/null +++ b/packages/app/e2e/sidebar.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from "./fixtures" +import { modKey } from "./utils" + +test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => { + await gotoSession() + + const createButton = page.getByRole("button", { name: /New (session|workspace)/ }).first() + const opened = (await createButton.count()) > 0 + + if (!opened) { + await page.keyboard.press(`${modKey}+B`) + await expect(createButton).toBeVisible() + } + + await page.keyboard.press(`${modKey}+B`) + await expect(createButton).toHaveCount(0) + + await page.keyboard.press(`${modKey}+B`) + await expect(createButton).toBeVisible() +}) From e9ede70793dae3f1f12b3e6144c0229b9ce96610 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 07:00:00 -0600 Subject: [PATCH 140/192] chore: cleanup --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fda848762f5..9cf83ca8df0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,8 @@ jobs: run: bunx playwright install --with-deps - name: Seed opencode data - run: bun --cwd packages/opencode script/seed-e2e.ts + working-directory: packages/opencode + run: bun script/seed-e2e.ts env: MODELS_DEV_API_JSON: ${{ github.workspace }}/packages/opencode/test/tool/fixtures/models-api.json OPENCODE_DISABLE_MODELS_FETCH: "true" @@ -42,7 +43,7 @@ jobs: OPENCODE_E2E_MODEL: "opencode/gpt-5-nano" - name: Run opencode server - run: bun --cwd packages/opencode run dev -- --print-logs --log-level WARN serve --port 4096 --hostname 0.0.0.0 & + run: bun run dev -- --print-logs --log-level WARN serve --port 4096 --hostname 0.0.0.0 & env: MODELS_DEV_API_JSON: ${{ github.workspace }}/packages/opencode/test/tool/fixtures/models-api.json OPENCODE_DISABLE_MODELS_FETCH: "true" From f00f18b926e21c5d48b8fdf5cb216888f1c52f82 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 07:15:57 -0600 Subject: [PATCH 141/192] chore: cleanup --- turbo.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/turbo.json b/turbo.json index 6b1c9b32425..5de1b8d7517 100644 --- a/turbo.json +++ b/turbo.json @@ -9,6 +9,10 @@ "opencode#test": { "dependsOn": ["^build"], "outputs": [] + }, + "@opencode-ai/app#test": { + "dependsOn": ["^build"], + "outputs": [] } } } From 1ba7c606e679bdcd1212c23a46146aed5646c504 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 08:38:42 -0600 Subject: [PATCH 142/192] chore: cleanup --- packages/app/e2e/context.spec.ts | 25 +++++++++++++++++++++---- packages/app/e2e/file-open.spec.ts | 9 ++++----- packages/app/e2e/sidebar.spec.ts | 13 +++++++------ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/app/e2e/context.spec.ts b/packages/app/e2e/context.spec.ts index dbfeda8d8c5..beabd2eb7dd 100644 --- a/packages/app/e2e/context.spec.ts +++ b/packages/app/e2e/context.spec.ts @@ -9,12 +9,29 @@ test("context panel can be opened from the prompt", async ({ page, sdk, gotoSess const sessionID = created.id try { + await sdk.session.promptAsync({ + sessionID, + noReply: true, + parts: [ + { + type: "text", + text: "seed context", + }, + ], + }) + + await expect + .poll(async () => { + const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? []) + return messages.length + }) + .toBeGreaterThan(0) + await gotoSession(sessionID) - const promptForm = page.locator("form").filter({ has: page.locator(promptSelector) }).first() - const contextButton = promptForm - .locator("button") - .filter({ has: promptForm.locator('[data-component="progress-circle"]').first() }) + const contextButton = page + .locator('[data-component="button"]') + .filter({ has: page.locator('[data-component="progress-circle"]').first() }) .first() await expect(contextButton).toBeVisible() diff --git a/packages/app/e2e/file-open.spec.ts b/packages/app/e2e/file-open.spec.ts index 673caf9dc34..fb7104b6b05 100644 --- a/packages/app/e2e/file-open.spec.ts +++ b/packages/app/e2e/file-open.spec.ts @@ -12,13 +12,12 @@ test("can open a file tab from the search palette", async ({ page, gotoSession } const input = dialog.getByRole("textbox").first() await input.fill("package.json") - const firstItem = dialog.locator('[data-slot="list-item"]').first() - await expect(firstItem).toBeVisible() - await firstItem.click() + const fileItem = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first() + await expect(fileItem).toBeVisible() + await fileItem.click() await expect(dialog).toHaveCount(0) const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') - await expect(tabs).toBeVisible() - await expect(tabs.getByRole("tab").first()).toBeVisible() + await expect(tabs.locator('[data-slot="tabs-trigger"]').first()).toBeVisible() }) diff --git a/packages/app/e2e/sidebar.spec.ts b/packages/app/e2e/sidebar.spec.ts index 964b0a56c03..925590f5106 100644 --- a/packages/app/e2e/sidebar.spec.ts +++ b/packages/app/e2e/sidebar.spec.ts @@ -4,17 +4,18 @@ import { modKey } from "./utils" test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => { await gotoSession() - const createButton = page.getByRole("button", { name: /New (session|workspace)/ }).first() - const opened = (await createButton.count()) > 0 + const main = page.locator("main") + const closedClass = /xl:border-l/ + const isClosed = await main.evaluate((node) => node.className.includes("xl:border-l")) - if (!opened) { + if (isClosed) { await page.keyboard.press(`${modKey}+B`) - await expect(createButton).toBeVisible() + await expect(main).not.toHaveClass(closedClass) } await page.keyboard.press(`${modKey}+B`) - await expect(createButton).toHaveCount(0) + await expect(main).toHaveClass(closedClass) await page.keyboard.press(`${modKey}+B`) - await expect(createButton).toBeVisible() + await expect(main).not.toHaveClass(closedClass) }) From 3186e7ec7c059bba75fe498f47328ad757e7406c Mon Sep 17 00:00:00 2001 From: Github Action Date: Mon, 19 Jan 2026 14:59:54 +0000 Subject: [PATCH 143/192] Update 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 e51ed90463a..fa91b3b3102 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-DFsg3Dyt+cm8Z7CU6lpFd4gTAzydAewfiQW1T3qvDxA=", - "aarch64-linux": "sha256-0Im52dLeZ0ZtaPJr/U4m7+IRtOfziHNJI/Bu/V6cPho=", - "aarch64-darwin": "sha256-Rfd05h/BRLoM0SmKn+ui2RdXBNhkaVKkgBeT+uBs4J8=", - "x86_64-darwin": "sha256-CpZFHBMPJSib2Vqs6oC8HQjQtviPUMa/qezHAe22N/A=" + "x86_64-linux": "sha256-80+b7FwUy4mRWTzEjPrBWuR5Um67I1Rn4U/n/s/lBjs=", + "aarch64-linux": "sha256-xH/Grwh3b+HWsUkKN8LMcyMaMcmnIJYlgp38WJCat5E=", + "aarch64-darwin": "sha256-Izv6PE9gNaeYYfcqDwjTU/WYtD1y+j65annwvLzkMD8=", + "x86_64-darwin": "sha256-EG1Z0uAeyFiOeVsv0Sz1sa8/mdXuw/uvbYYrkFR3EAg=" } } From 843d76191e3d463cdf157cdcc6393000d44c7dfd Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 19 Jan 2026 10:12:46 -0500 Subject: [PATCH 144/192] zen: fix black reset date --- packages/console/core/src/util/date.test.ts | 20 ++++++++++++++++++++ packages/console/core/src/util/date.ts | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 packages/console/core/src/util/date.test.ts diff --git a/packages/console/core/src/util/date.test.ts b/packages/console/core/src/util/date.test.ts new file mode 100644 index 00000000000..074df8a2fad --- /dev/null +++ b/packages/console/core/src/util/date.test.ts @@ -0,0 +1,20 @@ +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 7f34c9bb5eb..9c1ab12d2c9 100644 --- a/packages/console/core/src/util/date.ts +++ b/packages/console/core/src/util/date.ts @@ -1,7 +1,7 @@ export function getWeekBounds(date: Date) { - const dayOfWeek = date.getUTCDay() + const offset = (date.getUTCDay() + 6) % 7 const start = new Date(date) - start.setUTCDate(date.getUTCDate() - dayOfWeek + 1) + start.setUTCDate(date.getUTCDate() - offset) start.setUTCHours(0, 0, 0, 0) const end = new Date(start) end.setUTCDate(start.getUTCDate() + 7) From 31864cadb49fb356e98ec56b1021ec469972e82c Mon Sep 17 00:00:00 2001 From: Evgenii Kosenko <39831696+kedMertens@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:50:41 +0200 Subject: [PATCH 145/192] docs: update codecompanion.nvim acp doc (#9411) --- packages/web/src/content/docs/acp.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/content/docs/acp.mdx b/packages/web/src/content/docs/acp.mdx index 9129db13597..43d89eae186 100644 --- a/packages/web/src/content/docs/acp.mdx +++ b/packages/web/src/content/docs/acp.mdx @@ -125,7 +125,7 @@ To use OpenCode as an ACP agent in [CodeCompanion.nvim](https://github.com/olimo ```lua require("codecompanion").setup({ - strategies = { + interactions = { chat = { adapter = { name = "opencode", @@ -138,7 +138,7 @@ require("codecompanion").setup({ This config sets up CodeCompanion to use OpenCode as the ACP agent for chat. -If you need to pass environment variables (like `OPENCODE_API_KEY`), refer to [Configuring Adapters: Environment Variables](https://codecompanion.olimorris.dev/configuration/adapters#environment-variables-setting-an-api-key) in the CodeCompanion.nvim documentation for full details. +If you need to pass environment variables (like `OPENCODE_API_KEY`), refer to [Configuring Adapters: Environment Variables](https://codecompanion.olimorris.dev/getting-started#setting-an-api-key) in the CodeCompanion.nvim documentation for full details. ## Support From 29e206b6c6df09efd369218017c0da2569793899 Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Mon, 19 Jan 2026 16:51:27 +0100 Subject: [PATCH 146/192] docs: Improve Gitlab self-hosted instances documentation (#9391) --- packages/web/src/content/docs/providers.mdx | 62 ++++++++++++++++++++- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 6022d174a7d..2a7d2ffb424 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -654,21 +654,77 @@ GitLab Duo provides AI-powered agentic chat with native tool calling capabilitie - **duo-chat-sonnet-4-5** - Balanced performance for most workflows - **duo-chat-opus-4-5** - Most capable for complex analysis +:::note +You can also specify 'GITLAB_TOKEN' environment variable if you don't want +to store token in opencode auth storage. +::: + ##### Self-Hosted GitLab +:::note[compliance note] +OpenCode uses a small model for some AI tasks like generating the session title. +It is configured to use gpt-5-nano by default, hosted by Zen. To lock OpenCode +to only use your own GitLab-hosted instance, add the following to your +`opencode.json` file. It is also recommended to disable session sharing. + +```json +{ + "$schema": "https://opencode.ai/config.json", + "small_model": "gitlab/duo-chat-haiku-4-5", + "share": "disabled" +} +``` + +::: + For self-hosted GitLab instances: ```bash -GITLAB_INSTANCE_URL=https://gitlab.company.com GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx opencode +export GITLAB_INSTANCE_URL=https://gitlab.company.com +export GITLAB_TOKEN=glpat-... +``` + +If your instance runs a custom AI Gateway: + +```bash +GITLAB_AI_GATEWAY_URL=https://ai-gateway.company.com ``` Or add to your bash profile: ```bash title="~/.bash_profile" export GITLAB_INSTANCE_URL=https://gitlab.company.com -export GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx +export GITLAB_AI_GATEWAY_URL=https://ai-gateway.company.com +export GITLAB_TOKEN=glpat-... ``` +:::note +Your GitLab administrator must enable the following: + +1. [Duo Agent Platform](https://docs.gitlab.com/user/gitlab_duo/turn_on_off/) for the user, group, or instance +2. Feature flags (via Rails console): + - `agent_platform_claude_code` + - `third_party_agents_enabled` + ::: + +##### OAuth for Self-Hosted instances + +In order to make Oauth working for your self-hosted instance, you need to create +a new application (Settings → Applications) with the +callback URL `http://127.0.0.1:8080/callback` and following scopes: + +- api (Access the API on your behalf) +- read_user (Read your personal information) +- read_repository (Allows read-only access to the repository) + +Then expose application ID as environment variable: + +```bash +export GITLAB_OAUTH_CLIENT_ID=your_application_id_here +``` + +More documentation on [opencode-gitlab-auth](https://www.npmjs.com/package/@gitlab/opencode-gitlab-auth) homepage. + ##### Configuration Customize through `opencode.json`: @@ -690,7 +746,7 @@ Customize through `opencode.json`: } ``` -##### GitLab API Tools (Optional) +##### GitLab API Tools (Optional, but highly recommended) To access GitLab tools (merge requests, issues, pipelines, CI/CD, etc.): From b1684f3d12eb64f3b4f1912c6e7151f1638822e8 Mon Sep 17 00:00:00 2001 From: paulclou <117023154+paulclou@users.noreply.github.com> Date: Mon, 19 Jan 2026 09:59:51 -0600 Subject: [PATCH 147/192] fix(config): rename uv formatter from 'uv format' to 'uv' for config consistency (#9409) Co-authored-by: Paul C. Lou --- packages/opencode/src/format/formatter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 667a954c034..1939769d212 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -226,7 +226,7 @@ export const rlang: Info = { } export const uvformat: Info = { - name: "uv format", + name: "uv", command: ["uv", "format", "--", "$FILE"], extensions: [".py", ".pyi"], async enabled() { From 5b867246321ca0376a92641a748f22f82faa9bd3 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 10:15:24 -0600 Subject: [PATCH 148/192] fix: cargo fmt actually does not support formatting single files --- packages/opencode/src/format/formatter.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 1939769d212..1c58d5b8d15 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -347,13 +347,14 @@ export const rustfmt: Info = { }, } -export const cargofmt: Info = { - name: "cargofmt", - command: ["cargo", "fmt", "--", "$FILE"], - extensions: [".rs"], - async enabled() { - if (!Bun.which("cargo")) return false - const found = await Filesystem.findUp("Cargo.toml", Instance.directory, Instance.worktree) - return found.length > 0 - }, -} +// cargo fmt actually does not support formatting single files +// export const cargofmt: Info = { +// name: "cargofmt", +// command: ["cargo", "fmt", "--", "$FILE"], +// extensions: [".rs"], +// async enabled() { +// if (!Bun.which("cargo")) return false +// const found = await Filesystem.findUp("Cargo.toml", Instance.directory, Instance.worktree) +// return found.length > 0 +// }, +// } From 4ee540309f41d6eaa65d0d4dd7c70fea71c973d1 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:26:10 -0600 Subject: [PATCH 149/192] fix(app): hide settings button --- packages/app/src/pages/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index e803681801d..39ca39b676b 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1857,7 +1857,7 @@ export default function Layout(props: ParentProps) {
- + From 72cb7ccc00438625e58900efa780a004eda5a826 Mon Sep 17 00:00:00 2001 From: Joseph Campuzano Date: Mon, 19 Jan 2026 10:43:27 -0600 Subject: [PATCH 150/192] fix(app): list component jumping when mouse happens to be under the list and keyboard navigating. (#9435) --- packages/ui/src/components/list.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 631b3e33a29..bfe3440ea7f 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -206,6 +206,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) setActive(props.key(item)) }} onMouseLeave={() => { + if (!store.mouseActive) return; setActive(null) }} > From 453417ed47774fc3077ef67c49c831b4014494f4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Jan 2026 16:46:09 +0000 Subject: [PATCH 151/192] chore: generate --- packages/ui/src/components/list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index bfe3440ea7f..b8a8f746072 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -206,7 +206,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) setActive(props.key(item)) }} onMouseLeave={() => { - if (!store.mouseActive) return; + if (!store.mouseActive) return setActive(null) }} > From d5ae8e0bef991f2b2ad9766b9ae2f1c903badab3 Mon Sep 17 00:00:00 2001 From: Goni Zahavy Date: Mon, 19 Jan 2026 18:48:59 +0200 Subject: [PATCH 152/192] fix(opencode): `cargo fmt` is formatting whole workspace instead of edited file (#9436) --- packages/opencode/src/format/formatter.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 1c58d5b8d15..1a3aa1bb15a 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -337,24 +337,6 @@ export const rustfmt: Info = { command: ["rustfmt", "$FILE"], extensions: [".rs"], async enabled() { - if (!Bun.which("rustfmt")) return false - const configs = ["rustfmt.toml", ".rustfmt.toml"] - for (const config of configs) { - const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree) - if (found.length > 0) return true - } - return false + return Bun.which("rustfmt") !== null }, } - -// cargo fmt actually does not support formatting single files -// export const cargofmt: Info = { -// name: "cargofmt", -// command: ["cargo", "fmt", "--", "$FILE"], -// extensions: [".rs"], -// async enabled() { -// if (!Bun.which("cargo")) return false -// const found = await Filesystem.findUp("Cargo.toml", Instance.directory, Instance.worktree) -// return found.length > 0 -// }, -// } From 1f11a8a6ea46867e2ad199c987bf14696a1b91d8 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:32:15 -0600 Subject: [PATCH 153/192] feat(app): improved session layout --- packages/app/src/pages/layout.tsx | 140 ++++++++++++------ packages/app/src/pages/session.tsx | 20 +-- .../enterprise/src/routes/share/[shareID].tsx | 21 +-- packages/ui/src/components/hover-card.css | 5 +- packages/ui/src/components/message-nav.css | 4 + packages/ui/src/components/message-nav.tsx | 24 ++- .../src/components/session-message-rail.css | 44 ------ .../src/components/session-message-rail.tsx | 46 ------ 8 files changed, 121 insertions(+), 183 deletions(-) delete mode 100644 packages/ui/src/components/session-message-rail.css delete mode 100644 packages/ui/src/components/session-message-rail.tsx diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 39ca39b676b..2f71570f472 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -28,13 +28,14 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { InlineInput } from "@opencode-ai/ui/inline-input" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { HoverCard } from "@opencode-ai/ui/hover-card" +import { MessageNav } from "@opencode-ai/ui/message-nav" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { Spinner } from "@opencode-ai/ui/spinner" import { Dialog } from "@opencode-ai/ui/dialog" import { getFilename } from "@opencode-ai/util/path" -import { Session } from "@opencode-ai/sdk/v2/client" +import { Session, type Message, type TextPart } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" import { createStore, produce, reconcile } from "solid-js/store" import { @@ -1329,63 +1330,104 @@ export default function Layout(props: ParentProps) { return agent?.color }) + const hoverMessages = createMemo(() => + sessionStore.message[props.session.id]?.filter((message) => message.role === "user"), + ) + const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined) + const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened()) + const isActive = createMemo(() => props.session.id === params.id) + + const messageLabel = (message: Message) => { + const parts = sessionStore.part[message.id] ?? [] + const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored) + return text?.text + } + + const item = ( + prefetchSession(props.session, "high")} + onFocus={() => prefetchSession(props.session, "high")} + > +
+
+ }> + + + + +
+ + +
+ + 0}> +
+ + +
+ + props.session.title} + onSave={(next) => renameSession(props.session, next)} + class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" + displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" + stopPropagation + /> + + + {(summary) => ( +
+ +
+ )} +
+
+
+ )) + return (
- prefetchSession(props.session, "high")} - onFocus={() => prefetchSession(props.session, "high")} + -
-
- }> - - - - -
- - -
- - 0}> -
- - -
- - props.session.title} - onSave={(next) => renameSession(props.session, next)} - class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - stopPropagation + + Loading messages…
}> + { + if (!isActive()) { + navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`) + return + } + window.location.hash = `message-${message.id}` + window.dispatchEvent(new HashChangeEvent("hashchange")) + }} + size="normal" + class="w-60" /> - - - {(summary) => ( -
- -
- )}
-
-
+ +
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index f063ce35b40..b1c844f0c63 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -18,7 +18,6 @@ import { useCodeComponent } from "@opencode-ai/ui/context/code" import { SessionTurn } from "@opencode-ai/ui/session-turn" import { createAutoScroll } from "@opencode-ai/ui/hooks" import { SessionReview } from "@opencode-ai/ui/session-review" -import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail" import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" @@ -1163,17 +1162,6 @@ export default function Page() { } >
- -
- -
-
{ @@ -1255,13 +1243,7 @@ export default function Page() { root: "min-w-0 w-full relative", content: "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]", - container: - "px-4 md:px-6 " + - (!showTabs() - ? "md:max-w-200 md:mx-auto" - : visibleUserMessages().length > 1 - ? "md:pr-6 md:pl-18" - : ""), + container: "w-full px-4 md:px-6", }} />
diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index 776b4226451..d657ddc1229 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -16,7 +16,6 @@ import { iife } from "@opencode-ai/util/iife" import { Binary } from "@opencode-ai/util/binary" import { NamedError } from "@opencode-ai/util/error" import { DateTime } from "luxon" -import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail" import { createStore } from "solid-js/store" import z from "zod" import NotFound from "../[...404]" @@ -353,26 +352,16 @@ export default function () {
1, - "px-6": !wide() && messages().length === 1, + "w-full flex justify-start items-start min-w-0 px-6": true, }} > {title()}
- 1 - ? "pr-6 pl-18" - : "px-6"), + container: "w-full pb-20 px-6", }} >
void + getLabel?: (message: UserMessage) => string | undefined }, ) { - const [local, others] = splitProps(props, ["messages", "current", "size", "onMessageSelect"]) + const [local, others] = splitProps(props, ["messages", "current", "size", "onMessageSelect", "getLabel"]) const content = () => (
    @@ -19,23 +20,36 @@ export function MessageNav( {(message) => { const handleClick = () => local.onMessageSelect(message) + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key !== "Enter" && event.key !== " ") return + event.preventDefault() + local.onMessageSelect(message) + } + return (
  • -
    +
    - diff --git a/packages/ui/src/components/session-message-rail.css b/packages/ui/src/components/session-message-rail.css deleted file mode 100644 index 9f248bed252..00000000000 --- a/packages/ui/src/components/session-message-rail.css +++ /dev/null @@ -1,44 +0,0 @@ -[data-component="session-message-rail"] { - display: contents; -} - -[data-slot="session-message-rail-compact"], -[data-slot="session-message-rail-full"] { - position: absolute; - left: 1.5rem; - margin-top: 0.625rem; - top: 0; - bottom: 8rem; - overflow-y: auto; -} - -[data-slot="session-message-rail-compact"] { - display: flex; -} - -[data-slot="session-message-rail-full"] { - display: none; -} - -@container (min-width: 88rem) { - [data-slot="session-message-rail-compact"] { - display: none; - } - [data-slot="session-message-rail-full"] { - display: flex; - } -} - -[data-component="session-message-rail"] [data-slot="session-message-rail-full"] { - transform: none; -} - -[data-component="session-message-rail"][data-wide] [data-slot="session-message-rail-full"] { - margin-top: 0.125rem; - left: calc(((100% - min(100%, 50rem)) / 2) - 1.5rem); - transform: translateX(-100%); -} - -[data-component="session-message-rail"]:not([data-wide]) [data-slot="session-message-rail-full"] { - margin-top: 0.625rem; -} diff --git a/packages/ui/src/components/session-message-rail.tsx b/packages/ui/src/components/session-message-rail.tsx deleted file mode 100644 index 1935a4f930e..00000000000 --- a/packages/ui/src/components/session-message-rail.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { UserMessage } from "@opencode-ai/sdk/v2" -import { ComponentProps, Show, splitProps } from "solid-js" -import { MessageNav } from "./message-nav" -import "./session-message-rail.css" - -export interface SessionMessageRailProps extends ComponentProps<"div"> { - messages: UserMessage[] - current?: UserMessage - wide?: boolean - onMessageSelect: (message: UserMessage) => void -} - -export function SessionMessageRail(props: SessionMessageRailProps) { - const [local, others] = splitProps(props, ["messages", "current", "wide", "onMessageSelect", "class", "classList"]) - - return ( - 1}> -
    -
    - -
    -
    - -
    -
    -
    - ) -} From befd0f16362678dcd99cd9118cbcb044997c9511 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:05:40 -0600 Subject: [PATCH 154/192] feat(app): new session layout --- packages/app/src/pages/layout.tsx | 53 +++++----- packages/app/src/pages/session.tsx | 4 +- .../enterprise/src/routes/share/[shareID].tsx | 4 +- packages/ui/src/components/session-turn.css | 28 ++---- packages/ui/src/components/session-turn.tsx | 96 ++++++++++++------- 5 files changed, 107 insertions(+), 78 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2f71570f472..5312ff0a2ab 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1296,7 +1296,13 @@ export default function Layout(props: ParentProps) { ) } - const SessionItem = (props: { session: Session; slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => { + const SessionItem = (props: { + session: Session + slug: string + mobile?: boolean + dense?: boolean + popover?: boolean + }): JSX.Element => { const notification = useNotification() const notifications = createMemo(() => notification.session.unseen(props.session.id)) const hasError = createMemo(() => notifications().some((n) => n.type === "error")) @@ -1335,6 +1341,7 @@ export default function Layout(props: ParentProps) { ) const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined) const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened()) + const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) const isActive = createMemo(() => props.session.id === params.id) const messageLabel = (message: Message) => { @@ -1370,23 +1377,14 @@ export default function Layout(props: ParentProps) {
    - - props.session.title} - onSave={(next) => renameSession(props.session, next)} - class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - stopPropagation - /> - + props.session.title} + onSave={(next) => renameSession(props.session, next)} + class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" + displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" + stopPropagation + /> {(summary) => (
    @@ -1396,7 +1394,7 @@ export default function Layout(props: ParentProps) {
    - )) + ) return (
    + {item} + + } > Loading messages…
    }> @@ -1730,6 +1732,7 @@ export default function Layout(props: ParentProps) { slug={base64Encode(props.project.worktree)} dense mobile={props.mobile} + popover={false} /> )} @@ -1746,7 +1749,13 @@ export default function Layout(props: ParentProps) {
    {(session) => ( - + )}
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index b1c844f0c63..5f282ac8577 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1233,6 +1233,7 @@ export default function Page() { > diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index d657ddc1229..483db4d9324 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -295,13 +295,13 @@ export default function () { {(message) => ( setStore("expandedSteps", message.id, (v) => !v)} classes={{ root: "min-w-0 w-full relative", - content: - "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]", + content: "flex flex-col justify-between !overflow-visible", container: "px-4", }} /> diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 1e3cc0b2921..f7ab971794a 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -29,23 +29,6 @@ gap: 28px; overflow-anchor: none; - [data-slot="session-turn-user-badges"] { - position: absolute; - right: 0; - display: flex; - gap: 6px; - padding-left: 16px; - background: linear-gradient(to right, transparent, var(--background-stronger) 12px); - opacity: 0; - transition: opacity 0.15s ease; - pointer-events: none; - } - - &:hover [data-slot="session-turn-user-badges"] { - opacity: 1; - pointer-events: auto; - } - [data-slot="session-turn-badge"] { display: inline-flex; align-items: center; @@ -71,7 +54,7 @@ [data-slot="session-turn-response-trigger"] { position: sticky; - top: 32px; + top: calc(var(--sticky-header-height, 0px)); background-color: var(--background-stronger); z-index: 20; width: calc(100% + 9px); @@ -88,10 +71,17 @@ } [data-slot="session-turn-message-content"] { - margin-top: -18px; + margin-top: 0; max-width: 100%; } + [data-slot="session-turn-user-badges"] { + display: flex; + align-items: center; + gap: 6px; + padding-left: 16px; + } + [data-slot="session-turn-message-title"] { width: 100%; font-size: var(--font-size-large); diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index ae1321bac14..8b807af82b9 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -119,6 +119,7 @@ function AssistantMessageItem(props: { export function SessionTurn( props: ParentProps<{ sessionID: string + sessionTitle?: string messageID: string lastUserMessageID?: string stepsExpanded?: boolean @@ -330,7 +331,9 @@ export function SessionTurn( const response = createMemo(() => lastTextPart()?.text) const responsePartId = createMemo(() => lastTextPart()?.id) - const hasDiffs = createMemo(() => message()?.summary?.diffs?.length) + const sessionInfo = createMemo(() => data.store.session.find((item) => item.id === props.sessionID)) + const sessionTitle = createMemo(() => props.sessionTitle ?? sessionInfo()?.title) + const hasDiffs = createMemo(() => (data.store.session_diff?.[props.sessionID]?.length ?? 0) > 0) const hideResponsePart = createMemo(() => !working() && !!responsePartId()) const [responseCopied, setResponseCopied] = createSignal(false) @@ -376,6 +379,7 @@ export function SessionTurn( diffLimit: diffInit, status: rawStatus(), duration: duration(), + titleShown: false, }) createEffect( @@ -389,6 +393,18 @@ export function SessionTurn( ), ) + createEffect(() => { + if (!sessionTitle()) { + setStore("titleShown", false) + return + } + if (store.titleShown) return + const first = allMessages().find((item) => item?.role === "user") + if (!first) return + if (first.id !== props.messageID) return + setStore("titleShown", true) + }) + createEffect(() => { const r = retry() if (!r) { @@ -482,40 +498,53 @@ export function SessionTurn( - {/* Title (sticky) */} -
setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title"> -
-
- - - - - -

{msg().summary?.title}

-
-
-
-
- - {(msg() as UserMessage).agent} - - - - - {(msg() as UserMessage).model?.modelID} - - - {(msg() as UserMessage).variant || "default"} + +
setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title"> +
+
+ + + + + +

{sessionTitle()}

+
+
+
-
+ + + +
+ + {(msg() as UserMessage).agent} + + + + + {(msg() as UserMessage).model?.modelID} + + + + {(msg() as UserMessage).variant} + +
+
{/* User Message */}
+ {/* Trigger (sticky) */}
setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger"> @@ -612,7 +641,7 @@ export function SessionTurn( setStore("diffsOpen", value) }} > - + {(diff) => ( @@ -658,13 +687,13 @@ export function SessionTurn( )} - store.diffLimit}> + store.diffLimit}>
From 7811e01c8efc57d56b91547463c707baf2eb6815 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sun, 18 Jan 2026 05:26:24 -0600 Subject: [PATCH 155/192] fix(app): new layout improvements --- packages/app/src/pages/session.tsx | 135 ++++++++++------ packages/ui/src/components/message-part.tsx | 35 ++++- packages/ui/src/components/session-turn.css | 95 +++++++++--- packages/ui/src/components/session-turn.tsx | 161 +++++--------------- 4 files changed, 239 insertions(+), 187 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 5f282ac8577..31f9eac9c27 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -18,6 +18,7 @@ import { useCodeComponent } from "@opencode-ai/ui/context/code" import { SessionTurn } from "@opencode-ai/ui/session-turn" import { createAutoScroll } from "@opencode-ai/ui/hooks" import { SessionReview } from "@opencode-ai/ui/session-review" +import { Mark } from "@opencode-ai/ui/logo" import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" @@ -787,17 +788,14 @@ export default function Page() { .filter((tab) => tab !== "context"), ) - const reviewTab = createMemo(() => hasReview() || tabs().active() === "review") - const mobileReview = createMemo(() => !isDesktop() && hasReview() && store.mobileTab === "review") + const mobileReview = createMemo(() => !isDesktop() && view().reviewPanel.opened() && store.mobileTab === "review") - const showTabs = createMemo( - () => view().reviewPanel.opened() && (hasReview() || tabs().all().length > 0 || contextOpen()), - ) + const showTabs = createMemo(() => view().reviewPanel.opened()) const activeTab = createMemo(() => { const active = tabs().active() if (active) return active - if (reviewTab()) return "review" + if (hasReview()) return "review" const first = openedTabs()[0] if (first) return first @@ -1095,8 +1093,8 @@ export default function Page() {
- {/* Mobile tab bar - only shown on mobile when there are diffs */} - + {/* Mobile tab bar - only shown on mobile when user opened review */} + setStore("mobileTab", "review")} > - {reviewCount()} Files Changed + + {reviewCount()} Files Changed + Review + @@ -1138,26 +1139,36 @@ export default function Page() { when={!mobileReview()} fallback={
- Loading changes...
} - > - { - const value = file.tab(path) - tabs().open(value) - file.load(path) - }} - classes={{ - root: "pb-[calc(var(--prompt-height,8rem)+24px)]", - header: "px-4", - container: "px-4", - }} - /> -
+ + + Loading changes...
} + > + { + const value = file.tab(path) + tabs().open(value) + file.load(path) + }} + classes={{ + root: "pb-[calc(var(--prompt-height,8rem)+32px)]", + header: "px-4", + container: "px-4", + }} + /> + + + +
+ +
No changes in this session yet.
+
+
+
} > @@ -1170,11 +1181,29 @@ export default function Page() { }} onClick={autoScroll.handleInteraction} class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar" + style={{ "--session-title-height": info()?.title ? "40px" : "0px" }} > + +
+
+

{info()?.title}

+
+
+
+
- +
@@ -1386,26 +1415,36 @@ export default function Page() {
- +
- Loading changes...
} - > - { - const value = file.tab(path) - tabs().open(value) - file.load(path) - }} - /> -
+ + + Loading changes...
} + > + { + const value = file.tab(path) + tabs().open(value) + file.load(path) + }} + /> +
+ + +
+ +
No changes in this session yet.
+
+
+
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 47403786b22..b3fd01c2d8e 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -46,6 +46,7 @@ import { checksum } from "@opencode-ai/util/encode" import { Tooltip } from "./tooltip" import { IconButton } from "./icon-button" import { createAutoScroll } from "../hooks" +import { createResizeObserver } from "@solid-primitives/resize-observer" interface Diagnostic { range: { @@ -297,6 +298,23 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) { const dialog = useDialog() const [copied, setCopied] = createSignal(false) + const [expanded, setExpanded] = createSignal(false) + const [canExpand, setCanExpand] = createSignal(false) + let textRef: HTMLDivElement | undefined + + const updateCanExpand = () => { + const el = textRef + if (!el) return + if (expanded()) return + setCanExpand(el.scrollHeight > el.clientHeight + 2) + } + + createResizeObserver( + () => textRef, + () => { + updateCanExpand() + }, + ) const textPart = createMemo( () => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined, @@ -304,6 +322,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp const text = createMemo(() => textPart()?.text || "") + createEffect(() => { + text() + updateCanExpand() + }) + const files = createMemo(() => (props.parts?.filter((p) => p.type === "file") as FilePart[]) ?? []) const attachments = createMemo(() => @@ -335,7 +358,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp } return ( -
+
0}>
@@ -365,8 +388,16 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
-
+
(textRef = el)}> +
diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index f7ab971794a..a3c87c576dd 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -44,23 +44,33 @@ } } - [data-slot="session-turn-sticky-title"] { - width: 100%; + [data-slot="session-turn-sticky"] { + width: calc(100% + 9px); position: sticky; - top: 0; + top: var(--session-title-height, 0px); + z-index: 20; background-color: var(--background-stronger); - z-index: 21; + margin-left: -9px; + padding-left: 9px; + padding-bottom: 12px; + display: flex; + flex-direction: column; + gap: 12px; + + &::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: var(--background-stronger); + z-index: -1; + } } [data-slot="session-turn-response-trigger"] { - position: sticky; - top: calc(var(--sticky-header-height, 0px)); - background-color: var(--background-stronger); - z-index: 20; - width: calc(100% + 9px); - margin-left: -9px; - padding-left: 9px; - padding-bottom: 8px; + width: fit-content; } [data-slot="session-turn-message-header"] { @@ -75,6 +85,61 @@ max-width: 100%; } + [data-component="user-message"] [data-slot="user-message-text"] { + max-height: var(--user-message-collapsed-height, 64px); + } + + [data-component="user-message"][data-expanded="true"] [data-slot="user-message-text"] { + max-height: none; + } + + [data-component="user-message"][data-can-expand="true"] [data-slot="user-message-text"] { + padding-right: 36px; + padding-bottom: 28px; + } + + [data-component="user-message"] [data-slot="user-message-text"] [data-slot="user-message-expand"] { + display: none; + position: absolute; + bottom: 6px; + right: 6px; + padding: 0; + } + + [data-component="user-message"][data-can-expand="true"] + [data-slot="user-message-text"] + [data-slot="user-message-expand"], + [data-component="user-message"][data-expanded="true"] + [data-slot="user-message-text"] + [data-slot="user-message-expand"] { + display: inline-flex; + align-items: center; + justify-content: center; + height: 22px; + width: 22px; + border: none; + border-radius: 6px; + background: transparent; + cursor: pointer; + color: var(--text-weak); + + [data-slot="icon-svg"] { + transition: transform 0.15s ease; + } + } + + [data-component="user-message"][data-expanded="true"] + [data-slot="user-message-text"] + [data-slot="user-message-expand"] + [data-slot="icon-svg"] { + transform: rotate(180deg); + } + + [data-component="user-message"] [data-slot="user-message-text"] [data-slot="user-message-expand"]:hover { + background: var(--surface-raised-base); + color: var(--text-base); + } + [data-slot="session-turn-user-badges"] { display: flex; align-items: center; @@ -266,11 +331,7 @@ } [data-component="sticky-accordion-header"] { - top: var(--sticky-header-height, 40px); - - &[data-expanded]::before { - top: calc(-1 * var(--sticky-header-height, 40px)); - } + position: static; } [data-slot="session-turn-accordion-trigger-content"] { diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 8b807af82b9..e5fe4ba1ce8 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -13,17 +13,13 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path" import { Binary } from "@opencode-ai/util/binary" import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js" -import { createResizeObserver } from "@solid-primitives/resize-observer" import { DiffChanges } from "./diff-changes" -import { Typewriter } from "./typewriter" import { Message, Part } from "./message-part" import { Markdown } from "./markdown" import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" import { FileIcon } from "./file-icon" import { Icon } from "./icon" -import { ProviderIcon } from "./provider-icon" -import type { IconName } from "./provider-icons/types" import { IconButton } from "./icon-button" import { Tooltip } from "./tooltip" import { Card } from "./card" @@ -331,8 +327,6 @@ export function SessionTurn( const response = createMemo(() => lastTextPart()?.text) const responsePartId = createMemo(() => lastTextPart()?.id) - const sessionInfo = createMemo(() => data.store.session.find((item) => item.id === props.sessionID)) - const sessionTitle = createMemo(() => props.sessionTitle ?? sessionInfo()?.title) const hasDiffs = createMemo(() => (data.store.session_diff?.[props.sessionID]?.length ?? 0) > 0) const hideResponsePart = createMemo(() => !working() && !!responsePartId()) @@ -371,15 +365,11 @@ export function SessionTurn( const diffBatch = 20 const [store, setStore] = createStore({ - stickyTitleRef: undefined as HTMLDivElement | undefined, - stickyTriggerRef: undefined as HTMLDivElement | undefined, - stickyHeaderHeight: 0, retrySeconds: 0, diffsOpen: [] as string[], diffLimit: diffInit, status: rawStatus(), duration: duration(), - titleShown: false, }) createEffect( @@ -393,18 +383,6 @@ export function SessionTurn( ), ) - createEffect(() => { - if (!sessionTitle()) { - setStore("titleShown", false) - return - } - if (store.titleShown) return - const first = allMessages().find((item) => item?.role === "user") - if (!first) return - if (first.id !== props.messageID) return - setStore("titleShown", true) - }) - createEffect(() => { const r = retry() if (!r) { @@ -420,22 +398,6 @@ export function SessionTurn( onCleanup(() => clearInterval(timer)) }) - createResizeObserver( - () => store.stickyTitleRef, - ({ height }) => { - const triggerHeight = store.stickyTriggerRef?.offsetHeight ?? 0 - setStore("stickyHeaderHeight", height + triggerHeight + 8) - }, - ) - - createResizeObserver( - () => store.stickyTriggerRef, - ({ height }) => { - const titleHeight = store.stickyTitleRef?.offsetHeight ?? 0 - setStore("stickyHeaderHeight", titleHeight + height + 8) - }, - ) - createEffect(() => { const timer = setInterval(() => { setStore("duration", duration()) @@ -491,99 +453,58 @@ export function SessionTurn( data-message={msg().id} data-slot="session-turn-message-container" class={props.classes?.container} - style={{ "--sticky-header-height": `${store.stickyHeaderHeight}px` }} > - -
setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title"> -
-
+
+ {/* User Message */} +
+ +
+ + {/* Trigger (sticky) */} + +
+
+ · + {store.duration} + 0}> + + +
-
- - - -
- - {(msg() as UserMessage).agent} - - - - - {(msg() as UserMessage).model?.modelID} - - - - {(msg() as UserMessage).variant} - -
-
- {/* User Message */} -
- +
- - {/* Trigger (sticky) */} - -
setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger"> - -
-
{/* Response */} 0}>
From c720a2163c38e7ac08cd130fbba84772ccb582b6 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:38:41 -0600 Subject: [PATCH 156/192] chore: cleanup --- packages/ui/src/components/session-turn.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index a3c87c576dd..556b357c3b8 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -331,7 +331,11 @@ } [data-component="sticky-accordion-header"] { - position: static; + top: var(--sticky-header-height, 40px); + &[data-expanded]::before { + top: calc(-1 * var(--sticky-header-height, 40px)); + } + /* position: static; */ } [data-slot="session-turn-accordion-trigger-content"] { From eb779a7cc5c1728471489a3304a6203716bd4d47 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:54:56 -0600 Subject: [PATCH 157/192] chore: cleanup --- packages/ui/src/components/session-turn.css | 13 +++-- packages/ui/src/components/session-turn.tsx | 60 +++++++++++++++++++-- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 556b357c3b8..5f8c0a16f6a 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -1,4 +1,6 @@ [data-component="session-turn"] { + --session-turn-sticky-height: 0px; + --sticky-header-height: calc(var(--session-title-height, 0px) + var(--session-turn-sticky-height, 0px) + 12px); /* flex: 1; */ height: 100%; min-height: 0; @@ -44,6 +46,12 @@ } } + [data-slot="session-turn-attachments"] { + width: 100%; + min-width: 0; + align-self: stretch; + } + [data-slot="session-turn-sticky"] { width: calc(100% + 9px); position: sticky; @@ -331,11 +339,10 @@ } [data-component="sticky-accordion-header"] { - top: var(--sticky-header-height, 40px); + top: var(--sticky-header-height, 0px); &[data-expanded]::before { - top: calc(-1 * var(--sticky-header-height, 40px)); + top: calc(-1 * var(--sticky-header-height, 0px)); } - /* position: static; */ } [data-slot="session-turn-accordion-trigger-content"] { diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index e5fe4ba1ce8..a918f0ae4fd 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -1,5 +1,6 @@ import { AssistantMessage, + FilePart, Message as MessageType, Part as PartType, type PermissionRequest, @@ -29,6 +30,7 @@ import { Spinner } from "./spinner" import { createStore } from "solid-js/store" import { DateTime, DurationUnit, Interval } from "luxon" import { createAutoScroll } from "../hooks" +import { createResizeObserver } from "@solid-primitives/resize-observer" function computeStatusFromPart(part: PartType | undefined): string | undefined { if (!part) return undefined @@ -75,6 +77,12 @@ function same(a: readonly T[], b: readonly T[]) { return a.every((x, i) => x === b[i]) } +function isAttachment(part: PartType | undefined) { + if (part?.type !== "file") return false + const mime = (part as FilePart).mime ?? "" + return mime.startsWith("image/") || mime === "application/pdf" +} + function AssistantMessageItem(props: { message: AssistantMessage responsePartId: string | undefined @@ -133,6 +141,7 @@ export function SessionTurn( const emptyMessages: MessageType[] = [] const emptyParts: PartType[] = [] + const emptyFiles: FilePart[] = [] const emptyAssistant: AssistantMessage[] = [] const emptyPermissions: PermissionRequest[] = [] const emptyPermissionParts: { part: ToolPart; message: AssistantMessage }[] = [] @@ -180,6 +189,19 @@ export function SessionTurn( return data.store.part[msg.id] ?? emptyParts }) + const attachmentParts = createMemo(() => { + const msgParts = parts() + if (msgParts.length === 0) return emptyFiles + return msgParts.filter((part) => isAttachment(part)) as FilePart[] + }) + + const stickyParts = createMemo(() => { + const msgParts = parts() + if (msgParts.length === 0) return emptyParts + if (attachmentParts().length === 0) return msgParts + return msgParts.filter((part) => !isAttachment(part)) + }) + const assistantMessages = createMemo( () => { const msg = message() @@ -331,6 +353,15 @@ export function SessionTurn( const hideResponsePart = createMemo(() => !working() && !!responsePartId()) const [responseCopied, setResponseCopied] = createSignal(false) + const [rootRef, setRootRef] = createSignal() + const [stickyRef, setStickyRef] = createSignal() + + const updateStickyHeight = (height: number) => { + const root = rootRef() + if (!root) return + const next = Math.ceil(height) + root.style.setProperty("--session-turn-sticky-height", `${next}px`) + } const handleCopyResponse = async () => { const content = response() if (!content) return @@ -361,6 +392,24 @@ export function SessionTurn( onUserInteracted: props.onUserInteracted, }) + createResizeObserver( + () => stickyRef(), + ({ height }) => { + updateStickyHeight(height) + }, + ) + + createEffect(() => { + const root = rootRef() + if (!root) return + const sticky = stickyRef() + if (!sticky) { + root.style.setProperty("--session-turn-sticky-height", "0px") + return + } + updateStickyHeight(sticky.getBoundingClientRect().height) + }) + const diffInit = 20 const diffBatch = 20 @@ -438,7 +487,7 @@ export function SessionTurn( }) return ( -
+
-
+ 0}> +
+ +
+
+
{/* User Message */}
- +
{/* Trigger (sticky) */} From c7f0cb3d2d516e1a4673df1cf5be4ee983a78bfb Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 19 Jan 2026 15:20:28 +0000 Subject: [PATCH 158/192] fix: remove focus outline from dropdown menu --- packages/ui/src/components/dropdown-menu.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ui/src/components/dropdown-menu.css b/packages/ui/src/components/dropdown-menu.css index d2dc03c500e..cba041613ea 100644 --- a/packages/ui/src/components/dropdown-menu.css +++ b/packages/ui/src/components/dropdown-menu.css @@ -11,6 +11,11 @@ z-index: 50; transform-origin: var(--kb-menu-content-transform-origin); + &:focus, + &:focus-visible { + outline: none; + } + &[data-closed] { animation: dropdown-menu-close 0.15s ease-out; } From 89be504abcb74093342e1ead17d0b7bce5a44ba8 Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 19 Jan 2026 15:43:50 +0000 Subject: [PATCH 159/192] update: align edit project dialog padding and avatar styles --- .../app/src/components/dialog-edit-project.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 2f0f7db1f68..8160821b226 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -78,7 +78,7 @@ export function DialogEditProject(props: { project: LocalProject }) { return ( -
+
( )} From e12b94d91aee643dc04048a11412cd2a88bddd9e Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 19 Jan 2026 15:45:13 +0000 Subject: [PATCH 160/192] update: adjust edit project icon helper text --- packages/app/src/components/dialog-edit-project.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 8160821b226..a2a0ba9db6c 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -130,9 +130,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
-
- Click or drag an image - Recommended: 128x128px +
+ Recommended size 128x128px
From 494e8d5be94c72950476027d08fc6a2faafb5f94 Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 19 Jan 2026 15:57:47 +0000 Subject: [PATCH 161/192] update: tweak edit project icon container --- packages/app/src/components/dialog-edit-project.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index a2a0ba9db6c..d47dd9dcc40 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -94,10 +94,11 @@ export function DialogEditProject(props: { project: LocalProject }) {
Date: Mon, 19 Jan 2026 15:59:11 +0000 Subject: [PATCH 162/192] update: constrain edit project dialog width --- packages/app/src/components/dialog-edit-project.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index d47dd9dcc40..81ceb305db7 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -77,7 +77,7 @@ export function DialogEditProject(props: { project: LocalProject }) { } return ( - +
Date: Mon, 19 Jan 2026 16:00:21 +0000 Subject: [PATCH 163/192] update: tighten edit project color spacing --- packages/app/src/components/dialog-edit-project.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 81ceb305db7..f6e07df6078 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -140,7 +140,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
-
+
{(color) => ( - +
+ +
+
+ +
diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 2d680b28bbb..98f96c8e802 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -64,6 +64,8 @@ const icons = { help: ``, "settings-gear": ``, dash: ``, + "cloud-upload": ``, + trash: ``, } export interface IconProps extends ComponentProps<"svg"> { From b72a00eaa32e0655d6292ff5f3f79f0ce9231420 Mon Sep 17 00:00:00 2001 From: David Hill Date: Mon, 19 Jan 2026 17:10:18 +0000 Subject: [PATCH 165/192] fix text field border showing through focus ring --- packages/ui/src/components/text-field.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/text-field.css b/packages/ui/src/components/text-field.css index a739c4eb21a..15f5fd4fbb1 100644 --- a/packages/ui/src/components/text-field.css +++ b/packages/ui/src/components/text-field.css @@ -52,6 +52,7 @@ background: var(--input-base); &:focus-within { + border-color: transparent; /* border/shadow-xs/select */ box-shadow: 0 0 0 3px var(--border-weak-selected), From dd0906be8c52d460d856ed4cf89e5fe193aa06db Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 11:21:54 -0600 Subject: [PATCH 166/192] tweak: apply patch description --- packages/opencode/src/tool/apply_patch.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/apply_patch.txt b/packages/opencode/src/tool/apply_patch.txt index e195cd9cb18..5b2d95608c5 100644 --- a/packages/opencode/src/tool/apply_patch.txt +++ b/packages/opencode/src/tool/apply_patch.txt @@ -1,4 +1,4 @@ -Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON. Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope: +Use the `apply_patch` tool to edit files. Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope: *** Begin Patch [ one or more file sections ] From fc50b2962c24dc37fa131759cd56460fbc1f43fa Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:28:18 -0600 Subject: [PATCH 167/192] fix(app): make terminal sessions scoped to workspace --- packages/app/src/context/terminal.tsx | 57 ++++++++++++++++----------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index a7753069cf9..709d7b899ac 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -25,11 +25,11 @@ type TerminalCacheEntry = { dispose: VoidFunction } -function createTerminalSession(sdk: ReturnType, dir: string, id: string | undefined) { - const legacy = `${dir}/terminal${id ? "/" + id : ""}.v1` +function createTerminalSession(sdk: ReturnType, dir: string, session?: string) { + const legacy = session ? [`${dir}/terminal/${session}.v1`, `${dir}/terminal.v1`] : [`${dir}/terminal.v1`] const [store, setStore, _, ready] = persisted( - Persist.scoped(dir, id, "terminal", [legacy]), + Persist.workspace(dir, "terminal", legacy), createStore<{ active?: string all: LocalPTY[] @@ -43,17 +43,28 @@ function createTerminalSession(sdk: ReturnType, dir: string, id: all: createMemo(() => Object.values(store.all)), active: createMemo(() => store.active), new() { + const parse = (title: string) => { + const match = title.match(/^Terminal (\d+)$/) + if (!match) return + const value = Number(match[1]) + if (!Number.isFinite(value) || value <= 0) return + return value + } + const existingTitleNumbers = new Set( - store.all.map((pty) => { - const match = pty.titleNumber - return match + store.all.flatMap((pty) => { + const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined + if (direct !== undefined) return [direct] + const parsed = parse(pty.title) + if (parsed === undefined) return [] + return [parsed] }), ) - let nextNumber = 1 - while (existingTitleNumbers.has(nextNumber)) { - nextNumber++ - } + const nextNumber = + Array.from({ length: existingTitleNumbers.size + 1 }, (_, index) => index + 1).find( + (number) => !existingTitleNumbers.has(number), + ) ?? 1 sdk.client.pty .create({ title: `Terminal ${nextNumber}` }) @@ -166,8 +177,8 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont } } - const load = (dir: string, id: string | undefined) => { - const key = `${dir}:${id ?? WORKSPACE_KEY}` + const load = (dir: string, session?: string) => { + const key = `${dir}:${WORKSPACE_KEY}` const existing = cache.get(key) if (existing) { cache.delete(key) @@ -176,7 +187,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont } const entry = createRoot((dispose) => ({ - value: createTerminalSession(sdk, dir, id), + value: createTerminalSession(sdk, dir, session), dispose, })) @@ -185,18 +196,18 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont return entry.value } - const session = createMemo(() => load(params.dir!, params.id)) + const workspace = createMemo(() => load(params.dir!, params.id)) return { - ready: () => session().ready(), - all: () => session().all(), - active: () => session().active(), - new: () => session().new(), - update: (pty: Partial & { id: string }) => session().update(pty), - clone: (id: string) => session().clone(id), - open: (id: string) => session().open(id), - close: (id: string) => session().close(id), - move: (id: string, to: number) => session().move(id, to), + ready: () => workspace().ready(), + all: () => workspace().all(), + active: () => workspace().active(), + new: () => workspace().new(), + update: (pty: Partial & { id: string }) => workspace().update(pty), + clone: (id: string) => workspace().clone(id), + open: (id: string) => workspace().open(id), + close: (id: string) => workspace().close(id), + move: (id: string, to: number) => workspace().move(id, to), } }, }) From 092428633fe05b33c26a94549d6e65d2235da514 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:44:20 -0600 Subject: [PATCH 168/192] fix(app): layout jumping --- packages/ui/src/components/list.tsx | 38 ++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index b8a8f746072..874638c5a51 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -35,6 +35,25 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) mouseActive: false, }) + const scrollIntoView = (container: HTMLDivElement, node: HTMLElement, block: "center" | "nearest") => { + const containerRect = container.getBoundingClientRect() + const nodeRect = node.getBoundingClientRect() + const top = nodeRect.top - containerRect.top + container.scrollTop + const bottom = top + nodeRect.height + const viewTop = container.scrollTop + const viewBottom = viewTop + container.clientHeight + const target = + block === "center" + ? top - container.clientHeight / 2 + nodeRect.height / 2 + : top < viewTop + ? top + : bottom > viewBottom + ? bottom - container.clientHeight + : viewTop + const max = Math.max(0, container.scrollHeight - container.clientHeight) + container.scrollTop = Math.max(0, Math.min(target, max)) + } + const { filter, grouped, flat, active, setActive, onKeyDown, onInput } = useFilteredList(props) const searchProps = () => (typeof props.search === "object" ? props.search : {}) @@ -65,24 +84,31 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) ) createEffect(() => { - if (!scrollRef()) return + const scroll = scrollRef() + if (!scroll) return if (!props.current) return const key = props.key(props.current) requestAnimationFrame(() => { - const element = scrollRef()?.querySelector(`[data-key="${CSS.escape(key)}"]`) - element?.scrollIntoView({ block: "center" }) + const element = scroll.querySelector(`[data-key="${CSS.escape(key)}"]`) + if (!(element instanceof HTMLElement)) return + scrollIntoView(scroll, element, "center") }) }) createEffect(() => { const all = flat() if (store.mouseActive || all.length === 0) return + const scroll = scrollRef() + if (!scroll) return if (active() === props.key(all[0])) { - scrollRef()?.scrollTo(0, 0) + scroll.scrollTo(0, 0) return } - const element = scrollRef()?.querySelector(`[data-key="${CSS.escape(active()!)}"]`) - element?.scrollIntoView({ block: "center" }) + const key = active() + if (!key) return + const element = scroll.querySelector(`[data-key="${CSS.escape(key)}"]`) + if (!(element instanceof HTMLElement)) return + scrollIntoView(scroll, element, "center") }) createEffect(() => { From 3fd0043d1907b0e565cee73fff0f4725f7c8c0d5 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 12:18:17 -0600 Subject: [PATCH 169/192] chore: handle fields other than reasoning_content in interleaved block --- packages/opencode/src/provider/transform.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index f6b7ec8cbcc..c983bf32c4f 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -123,11 +123,8 @@ export namespace ProviderTransform { return result } - if ( - model.capabilities.interleaved && - typeof model.capabilities.interleaved === "object" && - model.capabilities.interleaved.field === "reasoning_content" - ) { + if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) { + const field = model.capabilities.interleaved.field return msgs.map((msg) => { if (msg.role === "assistant" && Array.isArray(msg.content)) { const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning") @@ -136,7 +133,7 @@ export namespace ProviderTransform { // Filter out reasoning parts from content const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning") - // Include reasoning_content directly on the message for all assistant messages + // Include reasoning_content | reasoning_details directly on the message for all assistant messages if (reasoningText) { return { ...msg, @@ -145,7 +142,7 @@ export namespace ProviderTransform { ...msg.providerOptions, openaiCompatible: { ...(msg.providerOptions as any)?.openaiCompatible, - reasoning_content: reasoningText, + [field]: reasoningText, }, }, } From c2f9fd5fef5454a1e7cedab14d78267809d10fe9 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:44:35 -0600 Subject: [PATCH 170/192] fix(app): reload instance after workspace reset --- packages/app/src/components/prompt-input.tsx | 11 ++++- packages/app/src/pages/layout.tsx | 49 ++++++++++++-------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 2f85652a93e..c74edd94e6b 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1056,7 +1056,16 @@ export const PromptInput: Component = (props) => { let session = info() if (!session && isNewSession) { - session = await client.session.create().then((x) => x.data ?? undefined) + session = await client.session + .create() + .then((x) => x.data ?? undefined) + .catch((err) => { + showToast({ + title: "Failed to create session", + description: errorMessage(err), + }) + return undefined + }) if (session) navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) } if (!session) return diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 5312ff0a2ab..81177d1386d 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -965,10 +965,23 @@ export default function Layout(props: ParentProps) { if (!current) return if (directory === current.worktree) return - const reset = globalSDK.client.worktree + const progress = showToast({ + persistent: true, + title: "Resetting workspace", + description: "This may take a minute.", + }) + const dismiss = () => toaster.dismiss(progress) + + const sessions = await globalSDK.client.session + .list({ directory }) + .then((x) => x.data ?? []) + .catch(() => []) + + const result = await globalSDK.client.worktree .reset({ directory: current.worktree, worktreeResetInput: { directory } }) .then((x) => x.data) .catch((err) => { + dismiss() showToast({ title: "Failed to reset workspace", description: errorMessage(err), @@ -976,21 +989,16 @@ export default function Layout(props: ParentProps) { return false }) - const href = `/${base64Encode(directory)}/session` - navigate(href) - layout.mobileSidebar.hide() - - void (async () => { - const sessions = await globalSDK.client.session - .list({ directory }) - .then((x) => x.data ?? []) - .catch(() => []) - - if (sessions.length === 0) return + if (!result) { + dismiss() + return + } - const archivedAt = Date.now() - await Promise.all( - sessions.map((session) => + const archivedAt = Date.now() + await Promise.all( + sessions + .filter((session) => session.time.archived === undefined) + .map((session) => globalSDK.client.session .update({ sessionID: session.id, @@ -999,11 +1007,14 @@ export default function Layout(props: ParentProps) { }) .catch(() => undefined), ), - ) - })() + ) - const result = await reset - if (!result) return + await globalSDK.client.instance.dispose({ directory }).catch(() => undefined) + dismiss() + + const href = `/${base64Encode(directory)}/session` + navigate(href) + layout.mobileSidebar.hide() showToast({ title: "Workspace reset", From c47699536fe9742cf5aa37f2c51168abeb90cb8d Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:56:24 -0500 Subject: [PATCH 171/192] fix: Don't unnecessarily wrap lines and introduce an unneeded empty line (resolves #9489) (#9488) --- packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 5c37a493dfa..a8671f46699 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -337,6 +337,7 @@ function Option(props: { fg={props.active ? fg : props.current ? theme.primary : theme.text} attributes={props.active ? TextAttributes.BOLD : undefined} overflow="hidden" + wrapMode="none" paddingLeft={3} > {Locale.truncate(props.title, 61)} From 889c60d63b585a276080f20c40c2d73ff715ea94 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Mon, 19 Jan 2026 15:04:59 -0500 Subject: [PATCH 172/192] fix(web): rename favicons to v2 for cache busting (#9492) --- packages/app/index.html | 8 ++--- packages/app/public/apple-touch-icon-v2.png | 1 + packages/app/public/favicon-96x96-v2.png | 1 + packages/app/public/favicon-v2.ico | 1 + packages/app/public/favicon-v2.svg | 1 + packages/app/src/entry.tsx | 2 +- packages/app/src/pages/layout.tsx | 2 +- packages/console/function/src/auth.ts | 2 +- packages/desktop/index.html | 8 ++--- packages/desktop/src/index.tsx | 2 +- packages/docs/docs.json | 2 +- packages/docs/favicon-v2.svg | 19 ++++++++++++ .../assets/favicon/apple-touch-icon-v2.png | Bin 0 -> 1541 bytes .../src/assets/favicon/favicon-96x96-v2.png | Bin 0 -> 536 bytes packages/ui/src/assets/favicon/favicon-v2.ico | Bin 0 -> 15086 bytes packages/ui/src/assets/favicon/favicon-v2.svg | 7 +++++ packages/ui/src/components/favicon.tsx | 6 ++-- packages/web/astro.config.mjs | 28 ++++++++++++++++++ packages/web/public/apple-touch-icon-v2.png | 1 + packages/web/public/favicon-96x96-v2.png | 1 + packages/web/public/favicon-v2.ico | 1 + packages/web/public/favicon-v2.svg | 1 + 22 files changed, 78 insertions(+), 16 deletions(-) create mode 120000 packages/app/public/apple-touch-icon-v2.png create mode 120000 packages/app/public/favicon-96x96-v2.png create mode 120000 packages/app/public/favicon-v2.ico create mode 120000 packages/app/public/favicon-v2.svg create mode 100644 packages/docs/favicon-v2.svg create mode 100644 packages/ui/src/assets/favicon/apple-touch-icon-v2.png create mode 100644 packages/ui/src/assets/favicon/favicon-96x96-v2.png create mode 100644 packages/ui/src/assets/favicon/favicon-v2.ico create mode 100644 packages/ui/src/assets/favicon/favicon-v2.svg create mode 120000 packages/web/public/apple-touch-icon-v2.png create mode 120000 packages/web/public/favicon-96x96-v2.png create mode 120000 packages/web/public/favicon-v2.ico create mode 120000 packages/web/public/favicon-v2.svg diff --git a/packages/app/index.html b/packages/app/index.html index 450807a42e0..1e516cbbb1d 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -4,10 +4,10 @@ OpenCode - - - - + + + + diff --git a/packages/app/public/apple-touch-icon-v2.png b/packages/app/public/apple-touch-icon-v2.png new file mode 120000 index 00000000000..c0d4353db47 --- /dev/null +++ b/packages/app/public/apple-touch-icon-v2.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/apple-touch-icon-v2.png \ No newline at end of file diff --git a/packages/app/public/favicon-96x96-v2.png b/packages/app/public/favicon-96x96-v2.png new file mode 120000 index 00000000000..b3129f6bf91 --- /dev/null +++ b/packages/app/public/favicon-96x96-v2.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-96x96-v2.png \ No newline at end of file diff --git a/packages/app/public/favicon-v2.ico b/packages/app/public/favicon-v2.ico new file mode 120000 index 00000000000..d8527270af6 --- /dev/null +++ b/packages/app/public/favicon-v2.ico @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v2.ico \ No newline at end of file diff --git a/packages/app/public/favicon-v2.svg b/packages/app/public/favicon-v2.svg new file mode 120000 index 00000000000..2600394ceae --- /dev/null +++ b/packages/app/public/favicon-v2.svg @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v2.svg \ No newline at end of file diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index 28741098c8e..8c4662926ad 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -37,7 +37,7 @@ const platform: Platform = { .then(() => { const notification = new Notification(title, { body: description ?? "", - icon: "https://opencode.ai/favicon-96x96.png", + icon: "https://opencode.ai/favicon-96x96-v2.png", }) notification.onclick = () => { window.focus() diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 81177d1386d..2f3b39d8628 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1284,7 +1284,7 @@ export default function Layout(props: ParentProps) {
OpenCode - - - - + + + + diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 6cd77d7d557..a06270b13fe 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -253,7 +253,7 @@ const createPlatform = (password: Accessor): Platform => ({ .then(() => { const notification = new Notification(title, { body: description ?? "", - icon: "https://opencode.ai/favicon-96x96.png", + icon: "https://opencode.ai/favicon-96x96-v2.png", }) notification.onclick = () => { const win = getCurrentWindow() diff --git a/packages/docs/docs.json b/packages/docs/docs.json index 4461f8253b7..93dff10f8c2 100644 --- a/packages/docs/docs.json +++ b/packages/docs/docs.json @@ -7,7 +7,7 @@ "light": "#07C983", "dark": "#15803D" }, - "favicon": "/favicon.svg", + "favicon": "/favicon-v2.svg", "navigation": { "tabs": [ { diff --git a/packages/docs/favicon-v2.svg b/packages/docs/favicon-v2.svg new file mode 100644 index 00000000000..b785c738bf1 --- /dev/null +++ b/packages/docs/favicon-v2.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/favicon/apple-touch-icon-v2.png b/packages/ui/src/assets/favicon/apple-touch-icon-v2.png new file mode 100644 index 0000000000000000000000000000000000000000..70fd01b0ea38ea8b3d0429a6438f5b8e85aba94a GIT binary patch literal 1541 zcmaJ>eKb^Q7{4Ur0=8rcXMfNrMx!Z`2J5r}m{7q=oI5Z4;q`f(Bo2BGNsu z_o*M^xIMiCe~;%%@XdW@9C6U|=3EF(Xy3J&Mnv)+onDt<%fnSpp|orWz33X39|qvAImHaraYT6ua6s5eeo7(qmH2LMX#~Pu2;Tnp)=eVBAS5%uY|{(3wuB zAapme=5QR?*2MU+i^ao>T30eu5Zd{wQ@+ZGno1dH3BTocen+^M)MNL^v8uk#$Zq?I z%@%6)NV%y0;oD~kM$#S1?GRjgwP!pQ{x`l^OZ@Y-Sb<3otdEN&B0Gw!W}D2I@{l2u zuwn>3!;aHkC&9xO4Yv!~RpprP?wSlwBI39;_4!?-dnmpw?!Q=fcUHoZd-O?O@D$h* z=K60K1pZXi%Q6$dLa4UWf-c&ORa8`Hrl;5SPRgF&Q|Ccw(z0?*!*ML*@LQ+JW*hD? z?2vx^2D4PXwiOfte^e8Al1$(yFq5Bs%ER5I;%cyvNtNy-14|BNmk*%)-+J>w&c+K* zW&AZr?A1wdT(c&ASOJ96+iQQ)0*#~_viFD-7&-Fs-ZXFmEm^+iZ{yhneh(>TdI0q1 zphvmNM}e(UhHh{p@bicBH8DVEei4QF{>+VWB|@NSEyiUGGldQ^HRWMybq+#oPK!0uXpM50A2a#48$6R-P=pz-9TF_%;W)Wir+`pO z;&-{gD-Y?eIszbgl4xEEB)ld$b}mzG+8CiJvNPw%n)GWv#32?OS;Zv~kk3xHn{&b| zE}4Ibb)U&^ePw0=+`*3yIV#4D9X`1)`WssZ;sKSF^R_e95r8e<4G#X6TGF;U2gwt) zKDZ1cGYrlbdhM?EJiL7E{(8`j%3;MHDxlY(61Om5hyb@Ik-@-yk4tU@fFAj*wDAVH z-*y$fV-fi0zyIZ*;8}->ds25n1QnR7434S=nsQtG(gg;+2t=G8gLy*FJ7|SKsNwRR ztp>l{!s@~ZOy7fcw+>K3a) z2t9JGBVr0PnAp|meV%Gu3p!47j_uJa=@{{jVjoU;G` literal 0 HcmV?d00001 diff --git a/packages/ui/src/assets/favicon/favicon-96x96-v2.png b/packages/ui/src/assets/favicon/favicon-96x96-v2.png new file mode 100644 index 0000000000000000000000000000000000000000..15266d28f159a74d5c54634d1b3ef548e97f6c8c GIT binary patch literal 536 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U_9&T;uuoF_-2kH*C7W17V-E6 zFXn$)Jn1}VWz)Rk!VA93%@(%Up4f0XPqg-HiS7LRa(xF^{&eKjU^sez7Ly2raKq!= z2Brk2f<3$e3_c8f+ZnkSICw-dnG+r;>$uDw$0;LPsnB{na7mCjKzib*uaihe}mzNs&#KTgN`2~7sCBeS(pZpU>|VHo`hk0|4|7)d1$)nnOUS-2?|0x9zfA3XrUYknTj~WYE{IAqoDKnpTg@KE zlmGwSOe(+SALH)78&qol`;+06E9QCjbBd literal 0 HcmV?d00001 diff --git a/packages/ui/src/assets/favicon/favicon-v2.ico b/packages/ui/src/assets/favicon/favicon-v2.ico new file mode 100644 index 0000000000000000000000000000000000000000..34ca0b9c01b23ca30f64ed17bd0705bb08cca4b1 GIT binary patch literal 15086 zcmeHOK~BRk5L`qb5Qz`ug4APAoOwpYiDO^FTX+>;32UH|yXxAWdV|$=ELq9glikTM zu~I9DJjkQ$cB02wKA%NiL}Z@-;z{I7$0j|d`NKx!LjhaG=q2*5J&k*9nw}PRYCsRr z1M~nraO)meR&HzSw=(_J-v!9OtN{H}UW3c(_0Nvdf4_fQknY_n`CsNAMEivG8uh3C zEncF3%0BU)BTu&$&2x;G$EzDP?$yn?wVZo-qIi0Fyt*x%bBveAs~a`$)y=uJoO^kq zczSufx-Fb@jF-o&8#NwPH!2T$Ur}|V@{s?plB0AORhRyWeSh%uM8{D-pm>xIibwgN zc$5!{NBN+5ln;tW`Ji}|4~j?mpm>xIibwenf7v=r-KZNqKo8IZV?E$~7sT=ZQ6PP% zHEVy>&hNCwJ_}rnu2+BbJ>TMc|MoT?8nb?%_pILeUiZ0ki;vd-T+Mup^-mR`{`7y3 z2Tt9zRKH_G@ilH}J`~UT<>$ldiN-a3Yx8J5i?8Lc&D(rvT+3gZN9$R9EkEbW{--{- zt&eUG3~gzjLjT3=XRqR(_9^T-W#ig&XYn>}y*~f#kj{_oUaq(IiQa$j^~ZYlX!F|G N;%#j4*B + + + + \ No newline at end of file diff --git a/packages/ui/src/components/favicon.tsx b/packages/ui/src/components/favicon.tsx index 3462384d458..abb0e1f78c6 100644 --- a/packages/ui/src/components/favicon.tsx +++ b/packages/ui/src/components/favicon.tsx @@ -3,9 +3,9 @@ import { Link, Meta } from "@solidjs/meta" export const Favicon = () => { return ( <> - - - + + + diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index 99a1c3bd80c..9be189cede8 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -32,6 +32,34 @@ export default defineConfig({ solidJs(), starlight({ title: "OpenCode", + favicon: "/favicon-v2.svg", + head: [ + { + tag: "link", + attrs: { + rel: "icon", + href: "/favicon-v2.ico", + sizes: "32x32", + }, + }, + { + tag: "link", + attrs: { + rel: "icon", + type: "image/png", + href: "/favicon-96x96-v2.png", + sizes: "96x96", + }, + }, + { + tag: "link", + attrs: { + rel: "apple-touch-icon", + href: "/apple-touch-icon-v2.png", + sizes: "180x180", + }, + }, + ], lastUpdated: true, expressiveCode: { themes: ["github-light", "github-dark"] }, social: [ diff --git a/packages/web/public/apple-touch-icon-v2.png b/packages/web/public/apple-touch-icon-v2.png new file mode 120000 index 00000000000..c0d4353db47 --- /dev/null +++ b/packages/web/public/apple-touch-icon-v2.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/apple-touch-icon-v2.png \ No newline at end of file diff --git a/packages/web/public/favicon-96x96-v2.png b/packages/web/public/favicon-96x96-v2.png new file mode 120000 index 00000000000..b3129f6bf91 --- /dev/null +++ b/packages/web/public/favicon-96x96-v2.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-96x96-v2.png \ No newline at end of file diff --git a/packages/web/public/favicon-v2.ico b/packages/web/public/favicon-v2.ico new file mode 120000 index 00000000000..d8527270af6 --- /dev/null +++ b/packages/web/public/favicon-v2.ico @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v2.ico \ No newline at end of file diff --git a/packages/web/public/favicon-v2.svg b/packages/web/public/favicon-v2.svg new file mode 120000 index 00000000000..2600394ceae --- /dev/null +++ b/packages/web/public/favicon-v2.svg @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v2.svg \ No newline at end of file From c3393ecc6c0a1482669b945e109af1d98f25a5ee Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:16:25 +0100 Subject: [PATCH 173/192] fix(app): give feedback when trying to paste a unsupported filetype (#9452) --- packages/app/src/components/prompt-input.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index c74edd94e6b..56bbdc8cb55 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -300,7 +300,8 @@ export const PromptInput: Component = (props) => { event.stopPropagation() const items = Array.from(clipboardData.items) - const imageItems = items.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type)) + const fileItems = items.filter((item) => item.kind === "file") + const imageItems = fileItems.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type)) if (imageItems.length > 0) { for (const item of imageItems) { @@ -310,7 +311,16 @@ export const PromptInput: Component = (props) => { return } + if (fileItems.length > 0) { + showToast({ + title: "Unsupported paste", + description: "Only images or PDFs can be pasted here.", + }) + return + } + const plainText = clipboardData.getData("text/plain") ?? "" + if (!plainText) return addPart({ type: "text", content: plainText, start: 0, end: 0 }) } From d19e76d96c7316dffb1bca1593fcc80bcdc0a9ff Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:43:32 +0100 Subject: [PATCH 174/192] fix: keyboard nav when mouse hovered over list (#9500) --- packages/ui/src/components/list.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 874638c5a51..6929f6b7347 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -58,6 +58,8 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) const searchProps = () => (typeof props.search === "object" ? props.search : {}) + const moved = (event: MouseEvent) => event.movementX !== 0 || event.movementY !== 0 + createEffect(() => { if (props.filter !== undefined) { onInput(props.filter) @@ -227,7 +229,8 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) data-selected={item === props.current} onClick={() => handleSelect(item, i())} type="button" - onMouseMove={() => { + onMouseMove={(event) => { + if (!moved(event)) return setStore("mouseActive", true) setActive(props.key(item)) }} From 091e88c1e157c53552e215efd05122ddd973de4f Mon Sep 17 00:00:00 2001 From: Joseph Campuzano Date: Mon, 19 Jan 2026 14:46:17 -0600 Subject: [PATCH 175/192] fix(opencode): sets input mode based on whether mouse vs keyboard is in use to prevent mouse events firing (#9449) --- .../cmd/tui/component/prompt/autocomplete.tsx | 23 ++++++++++++++++++- .../src/cli/cmd/tui/ui/dialog-select.tsx | 21 +++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index e27c32dfb2e..b6ca88410c8 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -85,6 +85,7 @@ export function Autocomplete(props: { index: 0, selected: 0, visible: false as AutocompleteRef["visible"], + input: "keyboard" as "keyboard" | "mouse", }) const [positionTick, setPositionTick] = createSignal(0) @@ -128,6 +129,14 @@ export function Autocomplete(props: { return props.input().getTextRange(store.index + 1, props.input().cursorOffset) }) + // When the filter changes due to how TUI works, the mousemove might still be triggered + // via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard so + // that the mouseover event doesn't trigger when filtering. + createEffect(() => { + filter(); + setStore("input", "keyboard") + }) + function insertPart(text: string, part: PromptInfo["parts"][number]) { const input = props.input() const currentCursorOffset = input.cursorOffset @@ -525,11 +534,13 @@ export function Autocomplete(props: { const isNavDown = name === "down" || (ctrlOnly && name === "n") if (isNavUp) { + setStore("input", "keyboard") move(-1) e.preventDefault() return } if (isNavDown) { + setStore("input", "keyboard") move(1) e.preventDefault() return @@ -612,7 +623,17 @@ export function Autocomplete(props: { paddingRight={1} backgroundColor={index === store.selected ? theme.primary : undefined} flexDirection="row" - onMouseOver={() => moveTo(index)} + onMouseMove={() => { + setStore("input", "mouse") + }} + onMouseOver={() => { + if (store.input !== "mouse") return + moveTo(index) + }} + onMouseDown={() => { + setStore("input", "mouse") + moveTo(index) + }} onMouseUp={() => select()} > diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index a8671f46699..f7c2fed85c7 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -52,6 +52,7 @@ export function DialogSelect(props: DialogSelectProps) { const [store, setStore] = createStore({ selected: 0, filter: "", + input: "keyboard" as "keyboard" | "mouse", }) createEffect( @@ -83,6 +84,14 @@ export function DialogSelect(props: DialogSelectProps) { return result }) + // When the filter changes due to how TUI works, the mousemove might still be triggered + // via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard + // that the mouseover event doesn't trigger when filtering. + createEffect(() => { + filtered(); + setStore("input", "keyboard") + }) + const grouped = createMemo(() => { const result = pipe( filtered(), @@ -157,12 +166,15 @@ export function DialogSelect(props: DialogSelectProps) { const keybind = useKeybind() useKeyboard((evt) => { + setStore("input", "keyboard") + if (evt.name === "up" || (evt.ctrl && evt.name === "p")) move(-1) if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1) if (evt.name === "pageup") move(-10) if (evt.name === "pagedown") move(10) if (evt.name === "home") moveTo(0) if (evt.name === "end") moveTo(flat().length - 1) + if (evt.name === "return") { const option = selected() if (option) { @@ -259,11 +271,20 @@ export function DialogSelect(props: DialogSelectProps) { { + setStore("input", "mouse") + }} onMouseUp={() => { option.onSelect?.(dialog) props.onSelect?.(option) }} onMouseOver={() => { + if (store.input !== "mouse") return + const index = flat().findIndex((x) => isDeepEqual(x.value, option.value)) + if (index === -1) return + moveTo(index) + }} + onMouseDown={() => { const index = flat().findIndex((x) => isDeepEqual(x.value, option.value)) if (index === -1) return moveTo(index) From 88c5a7fe9e7b8fd14bc2065051779ffa89789387 Mon Sep 17 00:00:00 2001 From: Ronan Kearns <90280289+kearns-cu@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:46:32 -0500 Subject: [PATCH 176/192] fix(tui): clarify resume session tip (#9490) --- packages/opencode/src/cli/cmd/tui/component/tips.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/tips.tsx b/packages/opencode/src/cli/cmd/tui/component/tips.tsx index fe2e7ca2169..3f0318e2690 100644 --- a/packages/opencode/src/cli/cmd/tui/component/tips.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/tips.tsx @@ -106,7 +106,7 @@ const TIPS = [ "Use plugins to send OS notifications when sessions complete", "Create a plugin to prevent OpenCode from reading sensitive files", "Use {highlight}opencode run{/highlight} for non-interactive scripting", - "Use {highlight}opencode run --continue{/highlight} to resume the last session", + "Use {highlight}opencode --continue{/highlight} to resume the last session", "Use {highlight}opencode run -f file.ts{/highlight} to attach files via CLI", "Use {highlight}--format json{/highlight} for machine-readable output in scripts", "Run {highlight}opencode serve{/highlight} for headless API access to OpenCode", From e29120317f8820a86054bb20e8982f0947fa03cd Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Jan 2026 20:47:09 +0000 Subject: [PATCH 177/192] chore: generate --- .../opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx | 2 +- packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index b6ca88410c8..718929d445b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -133,7 +133,7 @@ export function Autocomplete(props: { // via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard so // that the mouseover event doesn't trigger when filtering. createEffect(() => { - filter(); + filter() setStore("input", "keyboard") }) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index f7c2fed85c7..618bf3b3cb6 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -88,7 +88,7 @@ export function DialogSelect(props: DialogSelectProps) { // via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard // that the mouseover event doesn't trigger when filtering. createEffect(() => { - filtered(); + filtered() setStore("input", "keyboard") }) From 769c97af086e5edf0efb431e902eceb54dc668cb Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 14:49:51 -0600 Subject: [PATCH 178/192] chore: rm double conditional --- packages/opencode/src/file/ripgrep.ts | 46 +++++++++++++-------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 834cbee1ed1..0d18173565e 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -162,34 +162,32 @@ export namespace Ripgrep { }) } if (config.extension === "zip") { - if (config.extension === "zip") { - const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()]))) - const entries = await zipFileReader.getEntries() - let rgEntry: any - for (const entry of entries) { - if (entry.filename.endsWith("rg.exe")) { - rgEntry = entry - break - } + const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()]))) + const entries = await zipFileReader.getEntries() + let rgEntry: any + for (const entry of entries) { + if (entry.filename.endsWith("rg.exe")) { + rgEntry = entry + break } + } - if (!rgEntry) { - throw new ExtractionFailedError({ - filepath: archivePath, - stderr: "rg.exe not found in zip archive", - }) - } + if (!rgEntry) { + throw new ExtractionFailedError({ + filepath: archivePath, + stderr: "rg.exe not found in zip archive", + }) + } - const rgBlob = await rgEntry.getData(new BlobWriter()) - if (!rgBlob) { - throw new ExtractionFailedError({ - filepath: archivePath, - stderr: "Failed to extract rg.exe from zip archive", - }) - } - await Bun.write(filepath, await rgBlob.arrayBuffer()) - await zipFileReader.close() + const rgBlob = await rgEntry.getData(new BlobWriter()) + if (!rgBlob) { + throw new ExtractionFailedError({ + filepath: archivePath, + stderr: "Failed to extract rg.exe from zip archive", + }) } + await Bun.write(filepath, await rgBlob.arrayBuffer()) + await zipFileReader.close() } await fs.unlink(archivePath) if (!platformKey.endsWith("-win32")) await fs.chmod(filepath, 0o755) From ecc51ddb4e8a04495da45126a706a7effee5bf8d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:23:59 -0600 Subject: [PATCH 179/192] fix(app): hash nav --- packages/app/src/pages/layout.tsx | 5 +- packages/app/src/pages/session.tsx | 135 +++++++++++++++++++++++------ 2 files changed, 112 insertions(+), 28 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2f3b39d8628..a8f9b162fe4 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1429,10 +1429,11 @@ export default function Layout(props: ParentProps) { getLabel={messageLabel} onMessageSelect={(message) => { if (!isActive()) { - navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`) + sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`) + navigate(`${props.slug}/session/${props.session.id}`) return } - window.location.hash = `message-${message.id}` + window.history.replaceState(null, "", `#message-${message.id}`) window.dispatchEvent(new HashChangeEvent("hashchange")) }} size="normal" diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 31f9eac9c27..fdb9f268cd3 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,4 @@ -import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js" +import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on, createSignal } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { Dynamic } from "solid-js/web" @@ -167,6 +167,7 @@ export default function Page() { const sdk = useSDK() const prompt = usePrompt() const permission = usePermission() + const [pendingMessage, setPendingMessage] = createSignal(undefined) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const view = createMemo(() => layout.view(sessionKey())) @@ -943,17 +944,30 @@ export default function Page() { window.history.replaceState(null, "", `#${anchor(id)}`) } + createEffect(() => { + const sessionID = params.id + if (!sessionID) return + const raw = sessionStorage.getItem("opencode.pendingMessage") + if (!raw) return + const parts = raw.split("|") + const pendingSessionID = parts[0] + const messageID = parts[1] + if (!pendingSessionID || !messageID) return + if (pendingSessionID !== sessionID) return + + sessionStorage.removeItem("opencode.pendingMessage") + setPendingMessage(messageID) + }) + const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => { const root = scroller - if (!root) { - el.scrollIntoView({ behavior, block: "start" }) - return - } + if (!root) return false const a = el.getBoundingClientRect() const b = root.getBoundingClientRect() const top = a.top - b.top + root.scrollTop root.scrollTo({ top, behavior }) + return true } const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => { @@ -967,7 +981,15 @@ export default function Page() { requestAnimationFrame(() => { const el = document.getElementById(anchor(message.id)) - if (el) scrollToElement(el, behavior) + if (!el) { + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + scrollToElement(next, behavior) + }) + return + } + scrollToElement(el, behavior) }) updateHash(message.id) @@ -975,10 +997,57 @@ export default function Page() { } const el = document.getElementById(anchor(message.id)) - if (el) scrollToElement(el, behavior) + if (!el) { + updateHash(message.id) + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + if (!scrollToElement(next, behavior)) return + }) + return + } + if (scrollToElement(el, behavior)) { + updateHash(message.id) + return + } + + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + if (!scrollToElement(next, behavior)) return + }) updateHash(message.id) } + const applyHash = (behavior: ScrollBehavior) => { + const hash = window.location.hash.slice(1) + if (!hash) { + autoScroll.forceScrollToBottom() + return + } + + const match = hash.match(/^message-(.+)$/) + if (match) { + const msg = visibleUserMessages().find((m) => m.id === match[1]) + if (msg) { + scrollToMessage(msg, behavior) + return + } + + // If we have a message hash but the message isn't loaded/rendered yet, + // don't fall back to "bottom". We'll retry once messages arrive. + return + } + + const target = document.getElementById(hash) + if (target) { + scrollToElement(target, behavior) + return + } + + autoScroll.forceScrollToBottom() + } + const getActiveMessageId = (container: HTMLDivElement) => { const cutoff = container.scrollTop + 100 const nodes = container.querySelectorAll("[data-message-id]") @@ -1019,29 +1088,43 @@ export default function Page() { if (!sessionID || !ready) return requestAnimationFrame(() => { - const hash = window.location.hash.slice(1) - if (!hash) { - autoScroll.forceScrollToBottom() - return - } + applyHash("auto") + }) + }) - const hashTarget = document.getElementById(hash) - if (hashTarget) { - scrollToElement(hashTarget, "auto") - return - } + // Retry message navigation once the target message is actually loaded. + createEffect(() => { + const sessionID = params.id + const ready = messagesReady() + if (!sessionID || !ready) return + // dependencies + visibleUserMessages().length + store.turnStart + + const targetId = pendingMessage() ?? (() => { + const hash = window.location.hash.slice(1) const match = hash.match(/^message-(.+)$/) - if (match) { - const msg = visibleUserMessages().find((m) => m.id === match[1]) - if (msg) { - scrollToMessage(msg, "auto") - return - } - } + if (!match) return undefined + return match[1] + })() + if (!targetId) return + if (store.messageId === targetId) return + + const msg = visibleUserMessages().find((m) => m.id === targetId) + if (!msg) return + if (pendingMessage() === targetId) setPendingMessage(undefined) + requestAnimationFrame(() => scrollToMessage(msg, "auto")) + }) - autoScroll.forceScrollToBottom() - }) + createEffect(() => { + const sessionID = params.id + const ready = messagesReady() + if (!sessionID || !ready) return + + const handler = () => requestAnimationFrame(() => applyHash("auto")) + window.addEventListener("hashchange", handler) + onCleanup(() => window.removeEventListener("hashchange", handler)) }) createEffect(() => { From cac35bc52d9a8a2c9ca673510b2266d1c13ee141 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:05:49 -0600 Subject: [PATCH 180/192] fix(app): global terminal/review pane toggles --- packages/app/src/context/layout.tsx | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index a8da156092b..a8a8ce1e9fe 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -33,8 +33,6 @@ type SessionTabs = { type SessionView = { scroll: Record reviewOpen?: string[] - terminalOpened?: boolean - reviewPanelOpened?: boolean } export type LocalProject = Partial & { worktree: string; expanded: boolean } @@ -78,9 +76,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, terminal: { height: 280, + opened: false, }, review: { diffStyle: "split" as ReviewDiffStyle, + panelOpened: true, }, session: { width: 600, @@ -172,7 +172,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const current = store.sessionView[sessionKey] const keep = meta.active ?? sessionKey if (!current) { - setStore("sessionView", sessionKey, { scroll: next, terminalOpened: false, reviewPanelOpened: true }) + setStore("sessionView", sessionKey, { scroll: next }) prune(keep) return } @@ -379,31 +379,31 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( touch(sessionKey) scroll.seed(sessionKey) const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} }) - const terminalOpened = createMemo(() => s().terminalOpened ?? false) - const reviewPanelOpened = createMemo(() => s().reviewPanelOpened ?? true) + const terminalOpened = createMemo(() => store.terminal?.opened ?? false) + const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true) function setTerminalOpened(next: boolean) { - const current = store.sessionView[sessionKey] + const current = store.terminal if (!current) { - setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: next, reviewPanelOpened: true }) + setStore("terminal", { height: 280, opened: next }) return } - const value = current.terminalOpened ?? false + const value = current.opened ?? false if (value === next) return - setStore("sessionView", sessionKey, "terminalOpened", next) + setStore("terminal", "opened", next) } function setReviewPanelOpened(next: boolean) { - const current = store.sessionView[sessionKey] + const current = store.review if (!current) { - setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: false, reviewPanelOpened: next }) + setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next }) return } - const value = current.reviewPanelOpened ?? true + const value = current.panelOpened ?? true if (value === next) return - setStore("sessionView", sessionKey, "reviewPanelOpened", next) + setStore("review", "panelOpened", next) } return { @@ -444,8 +444,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( if (!current) { setStore("sessionView", sessionKey, { scroll: {}, - terminalOpened: false, - reviewPanelOpened: true, reviewOpen: open, }) return From a4d1824412c57d733c33c58e19551f0818c82e8a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:59:41 -0600 Subject: [PATCH 181/192] fix(app): no more favicons --- .../src/components/dialog-edit-project.tsx | 4 +- packages/app/src/context/layout.tsx | 62 ++++++++++++------- packages/app/src/pages/layout.tsx | 2 +- packages/opencode/src/project/project.ts | 4 ++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 1 + packages/sdk/js/src/v2/gen/types.gen.ts | 2 + 6 files changed, 51 insertions(+), 24 deletions(-) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 091f00702f3..7acb766f808 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -22,7 +22,7 @@ export function DialogEditProject(props: { project: LocalProject }) { const [store, setStore] = createStore({ name: defaultName(), color: props.project.icon?.color || "pink", - iconUrl: props.project.icon?.url || "", + iconUrl: props.project.icon?.override || "", saving: false, }) @@ -74,7 +74,7 @@ export function DialogEditProject(props: { project: LocalProject }) { await globalSDK.client.project.update({ projectID: props.project.id, name, - icon: { color: store.color, url: store.iconUrl }, + icon: { color: store.color, override: store.iconUrl }, }) setStore("saving", false) dialog.close() diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index a8a8ce1e9fe..d7d09aa3999 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -208,10 +208,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }) }) - const usedColors = new Set() + const [colors, setColors] = createStore>({}) - function pickAvailableColor(): AvatarColorKey { - const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c)) + function pickAvailableColor(used: Set): AvatarColorKey { + const available = AVATAR_COLOR_KEYS.filter((c) => !used.has(c)) if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)] return available[Math.floor(Math.random() * available.length)] } @@ -222,24 +222,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const metadata = projectID ? globalSync.data.project.find((x) => x.id === projectID) : globalSync.data.project.find((x) => x.worktree === project.worktree) - return [ - { - ...(metadata ?? {}), - ...project, - icon: { url: metadata?.icon?.url, color: metadata?.icon?.color }, + return { + ...(metadata ?? {}), + ...project, + icon: { + url: metadata?.icon?.url, + override: metadata?.icon?.override, + color: metadata?.icon?.color, }, - ] - } - - function colorize(project: LocalProject) { - if (project.icon?.color) return project - const color = pickAvailableColor() - usedColors.add(color) - project.icon = { ...project.icon, color } - if (project.id) { - globalSdk.client.project.update({ projectID: project.id, icon: { color } }) } - return project } const roots = createMemo(() => { @@ -277,8 +268,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }) }) - const enriched = createMemo(() => server.projects.list().flatMap(enrich)) - const list = createMemo(() => enriched().flatMap(colorize)) + const enriched = createMemo(() => server.projects.list().map(enrich)) + const list = createMemo(() => { + const projects = enriched() + return projects.map((project) => { + const color = project.icon?.color ?? colors[project.worktree] + if (!color) return project + const icon = project.icon ? { ...project.icon, color } : { color } + return { ...project, icon } + }) + }) + + createEffect(() => { + const projects = enriched() + if (projects.length === 0) return + + const used = new Set() + for (const project of projects) { + const color = project.icon?.color ?? colors[project.worktree] + if (color) used.add(color) + } + + for (const project of projects) { + if (project.icon?.color) continue + if (colors[project.worktree]) continue + const color = pickAvailableColor(used) + used.add(color) + setColors(project.worktree, color) + if (!project.id) continue + void globalSdk.client.project.update({ projectID: project.id, icon: { color } }) + } + }) onMount(() => { Promise.all( diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index a8f9b162fe4..9daac949e43 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1284,7 +1284,7 @@ export default function Layout(props: ParentProps) {
Date: Mon, 19 Jan 2026 14:46:32 -0600 Subject: [PATCH 182/192] fix(app): fade under sticky elements --- packages/ui/src/components/session-turn.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 5f8c0a16f6a..034d3024707 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -75,6 +75,17 @@ background-color: var(--background-stronger); z-index: -1; } + + &::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 32px; + background: linear-gradient(to bottom, var(--background-stronger), transparent); + pointer-events: none; + } } [data-slot="session-turn-response-trigger"] { From 69b3b35ea5ab576439e2d919b6b2c19b360b910d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Jan 2026 21:00:39 +0000 Subject: [PATCH 183/192] chore: generate --- packages/app/src/pages/session.tsx | 14 ++++++++------ packages/sdk/openapi.json | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index fdb9f268cd3..3b405ef0773 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1102,12 +1102,14 @@ export default function Page() { visibleUserMessages().length store.turnStart - const targetId = pendingMessage() ?? (() => { - const hash = window.location.hash.slice(1) - const match = hash.match(/^message-(.+)$/) - if (!match) return undefined - return match[1] - })() + const targetId = + pendingMessage() ?? + (() => { + const hash = window.location.hash.slice(1) + const match = hash.match(/^message-(.+)$/) + if (!match) return undefined + return match[1] + })() if (!targetId) return if (store.messageId === targetId) return diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 08dd98fd9bc..c1be820f262 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -231,6 +231,9 @@ "url": { "type": "string" }, + "override": { + "type": "string" + }, "color": { "type": "string" } @@ -5796,6 +5799,9 @@ "url": { "type": "string" }, + "override": { + "type": "string" + }, "color": { "type": "string" } From d605a78a0547bee0ac1883eb09509f2ee0c5b815 Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:15:43 +0100 Subject: [PATCH 184/192] fix(app): change keybind for cycling thinking effort (#9508) --- packages/app/src/pages/session.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 3b405ef0773..ec3b0ac30d9 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -531,7 +531,7 @@ export default function Page() { title: "Cycle thinking effort", description: "Switch to the next effort level", category: "Model", - keybind: "shift+mod+t", + keybind: "shift+mod+d", onSelect: () => { local.model.variant.cycle() }, From 79ae749ed8d72fc55cbb47435a717c0653511980 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:28:33 -0600 Subject: [PATCH 185/192] fix(app): don't change resize handle on hover --- packages/ui/src/components/resize-handle.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ui/src/components/resize-handle.css b/packages/ui/src/components/resize-handle.css index 088bf921574..e4c8d474e53 100644 --- a/packages/ui/src/components/resize-handle.css +++ b/packages/ui/src/components/resize-handle.css @@ -5,10 +5,8 @@ &::after { content: ""; position: absolute; - background-color: var(--color-border-strong-base); opacity: 0; transition: opacity 0.15s ease-in-out; - border-radius: 2px; } &:hover::after, From 673e79f457ed75f6077d7c5ad71906a7c8ce415c Mon Sep 17 00:00:00 2001 From: Spoon <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:44:58 +0100 Subject: [PATCH 186/192] tweak(batch): up restrictive max batch tool from `10` to `25` (#9275) --- packages/opencode/src/tool/batch.ts | 8 ++++---- packages/opencode/src/tool/batch.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/tool/batch.ts b/packages/opencode/src/tool/batch.ts index 8bffbd54a28..ba34eb48f5c 100644 --- a/packages/opencode/src/tool/batch.ts +++ b/packages/opencode/src/tool/batch.ts @@ -33,8 +33,8 @@ export const BatchTool = Tool.define("batch", async () => { const { Session } = await import("../session") const { Identifier } = await import("../id/id") - const toolCalls = params.tool_calls.slice(0, 10) - const discardedCalls = params.tool_calls.slice(10) + const toolCalls = params.tool_calls.slice(0, 25) + const discardedCalls = params.tool_calls.slice(25) const { ToolRegistry } = await import("./registry") const availableTools = await ToolRegistry.tools({ modelID: "", providerID: "" }) @@ -139,14 +139,14 @@ export const BatchTool = Tool.define("batch", async () => { state: { status: "error", input: call.parameters, - error: "Maximum of 10 tools allowed in batch", + error: "Maximum of 25 tools allowed in batch", time: { start: now, end: now }, }, }) results.push({ success: false as const, tool: call.tool, - error: new Error("Maximum of 10 tools allowed in batch"), + error: new Error("Maximum of 25 tools allowed in batch"), }) } diff --git a/packages/opencode/src/tool/batch.txt b/packages/opencode/src/tool/batch.txt index b1b6a6010f5..565eb4dd433 100644 --- a/packages/opencode/src/tool/batch.txt +++ b/packages/opencode/src/tool/batch.txt @@ -6,7 +6,7 @@ Payload Format (JSON array): [{"tool": "read", "parameters": {"filePath": "src/index.ts", "limit": 350}},{"tool": "grep", "parameters": {"pattern": "Session\\.updatePart", "include": "src/**/*.ts"}},{"tool": "bash", "parameters": {"command": "git status", "description": "Shows working tree status"}}] Notes: -- 1–10 tool calls per batch +- 1–20 tool calls per batch - All calls start in parallel; ordering NOT guaranteed - Partial failures do not stop other tool calls - Do NOT use the batch tool within another batch tool. From 4e04bee0c91e445e692ef312a9b8136189fceb16 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:45:55 -0600 Subject: [PATCH 187/192] fix(app): favicon --- packages/app/src/pages/layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 9daac949e43..d0d7e0b2f0c 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -540,7 +540,7 @@ export default function Layout(props: ParentProps) { running: number } - const prefetchChunk = 200 + const prefetchChunk = 600 const prefetchConcurrency = 1 const prefetchPendingLimit = 6 const prefetchToken = { value: 0 } @@ -1284,7 +1284,7 @@ export default function Layout(props: ParentProps) {
Date: Mon, 19 Jan 2026 21:55:27 +0000 Subject: [PATCH 188/192] release: v1.1.26 --- 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 | 4 ++-- packages/sdk/js/package.json | 4 ++-- 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, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index e5892a7745d..53d80630b84 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -71,7 +71,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -105,7 +105,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -132,7 +132,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -156,7 +156,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -180,7 +180,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -209,7 +209,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -238,7 +238,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -254,7 +254,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.25", + "version": "1.1.26", "bin": { "opencode": "./bin/opencode", }, @@ -358,7 +358,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -378,7 +378,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.25", + "version": "1.1.26", "devDependencies": { "@hey-api/openapi-ts": "0.90.4", "@tsconfig/node22": "catalog:", @@ -389,7 +389,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -402,7 +402,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -443,7 +443,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "zod": "catalog:", }, @@ -454,7 +454,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.25", + "version": "1.1.26", "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 2a754c96735..736a2625040 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.25", + "version": "1.1.26", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 2f44637fc89..83ff605cffa 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index a9bb2706d48..eb86fb525b1 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.1.25", + "version": "1.1.26", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 6ada8abb05b..6cfdaab7579 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.25", + "version": "1.1.26", "$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 7fe57cc79ad..ab2fd76f8b1 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.25", + "version": "1.1.26", "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 9114ff5621b..0b95f75487e 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index e80a58b2d72..1a40af1c799 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.25", + "version": "1.1.26", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index dccac3f933b..2327c61e667 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.1.25" +version = "1.1.26" 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.1.25/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/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.1.25/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/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.1.25/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index ad83a519c27..e5b0f62b927 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.25", + "version": "1.1.26", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index e1918193470..297abc8598a 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.25", + "version": "1.1.26", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 27349018083..52f392a8179 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.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index f3b12aa8c9f..5ce97dbf7e7 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.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index d544b89e38a..37b116ec2b7 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 0b490591c35..7079384a54a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index a1d2ac6b567..45c6d43f0a5 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.25", + "version": "1.1.26", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index aef7c0c706d..db1adbf926f 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.1.25", + "version": "1.1.26", "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 ad4735765e0..0a48840db0c 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.1.25", + "version": "1.1.26", "publisher": "sst-dev", "repository": { "type": "git", From 93638d1eb402eb4d79560ecbc635b6720973adf2 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 19 Jan 2026 14:44:29 -0800 Subject: [PATCH 189/192] sync: record last synced tag v1.1.26 --- .github/last-synced-tag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/last-synced-tag b/.github/last-synced-tag index 3fe93612e6f..7f9004ba0b8 100644 --- a/.github/last-synced-tag +++ b/.github/last-synced-tag @@ -1 +1 @@ -v1.1.25 +v1.1.26 From 96648156e9da5dcf765e2e7b5f0f118ec971ec68 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 19 Jan 2026 16:11:06 -0800 Subject: [PATCH 190/192] refactor: update Autocomplete API and Editor.open handling - Rename onActiveChange to onMove in font/theme pickers - Fix duplicate 'opened' property in layout context - Update Editor.open to use result object pattern (result?.ok) - Add slashes() method to dialog-command for slash filtering - Remove SessionMessageRail from session page - Update enterprise share page to shuvcode branding --- bun.lock | 743 +++++++++++++++++- packages/app/package.json | 2 +- packages/app/src/components/font-picker.tsx | 4 +- packages/app/src/components/theme-picker.tsx | 12 +- packages/app/src/context/layout.tsx | 1 - packages/app/src/pages/session.tsx | 12 - packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- .../enterprise/src/routes/share/[shareID].tsx | 14 +- packages/opencode/package.json | 2 +- .../cli/cmd/tui/component/dialog-command.tsx | 13 +- .../cli/cmd/tui/component/prompt/index.tsx | 5 +- .../src/cli/cmd/tui/routes/session/index.tsx | 6 +- packages/ui/package.json | 2 +- 14 files changed, 771 insertions(+), 49 deletions(-) diff --git a/bun.lock b/bun.lock index 53d80630b84..a03742bcfec 100644 --- a/bun.lock +++ b/bun.lock @@ -66,6 +66,7 @@ "typescript": "catalog:", "vite": "catalog:", "vite-plugin-icons-spritesheet": "3.0.1", + "vite-plugin-pwa": "1.2.0", "vite-plugin-solid": "catalog:", }, }, @@ -179,7 +180,7 @@ }, }, "packages/desktop": { - "name": "@opencode-ai/desktop", + "name": "@shuvcode/desktop", "version": "1.1.26", "dependencies": { "@opencode-ai/app": "workspace:*", @@ -310,6 +311,7 @@ "decimal.js": "10.5.0", "diff": "catalog:", "fuzzysort": "3.1.0", + "ghostty-opentui": "1.3.7", "gray-matter": "4.0.3", "hono": "catalog:", "hono-openapi": "catalog:", @@ -490,6 +492,7 @@ "tree-sitter-bash", ], "patchedDependencies": { + "ghostty-opentui@1.3.7": "patches/ghostty-opentui@1.3.7.patch", "ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch", }, "overrides": { @@ -599,6 +602,8 @@ "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], + "@apideck/better-ajv-errors": ["@apideck/better-ajv-errors@0.3.6", "", { "dependencies": { "json-schema": "^0.4.0", "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA=="], + "@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="], "@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="], @@ -743,6 +748,10 @@ "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], + "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], + + "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], @@ -755,7 +764,9 @@ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], + "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], @@ -765,22 +776,146 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ=="], + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="], + + "@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA=="], + + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA=="], + + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g=="], + + "@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="], + + "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw=="], + + "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw=="], + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], + "@babel/plugin-syntax-unicode-sets-regex": ["@babel/plugin-syntax-unicode-sets-regex@7.18.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg=="], + + "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], + + "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA=="], + + "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g=="], + + "@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="], + + "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw=="], + + "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.28.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw=="], + + "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ=="], + + "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-replace-supers": "^7.28.6", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q=="], + + "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/template": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ=="], + + "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], + + "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg=="], + + "@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="], + + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA=="], + + "@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="], + + "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg=="], + + "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw=="], + + "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], + + "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="], + + "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], + + "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw=="], + + "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], + + "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A=="], + + "@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="], + + "@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="], + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], + "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.28.5", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew=="], + + "@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="], + + "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], + + "@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="], + + "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg=="], + + "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w=="], + + "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.6", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA=="], + + "@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="], + + "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ=="], + + "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w=="], + + "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], + + "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.28.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg=="], + + "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA=="], + + "@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="], + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw=="], + + "@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg=="], + + "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="], + + "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], + + "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA=="], + + "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], + + "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="], + + "@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw=="], + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], + "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="], + + "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A=="], + + "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], + + "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q=="], + + "@babel/preset-env": ["@babel/preset-env@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.28.6", "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.28.6", "@babel/plugin-transform-async-to-generator": "^7.28.6", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.6", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-class-static-block": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-computed-properties": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.28.6", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.28.6", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.6", "@babel/plugin-transform-exponentiation-operator": "^7.28.6", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.28.6", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.28.6", "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-numeric-separator": "^7.28.6", "@babel/plugin-transform-object-rest-spread": "^7.28.6", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.28.6", "@babel/plugin-transform-private-property-in-object": "^7.28.6", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.28.6", "@babel/plugin-transform-regexp-modifiers": "^7.28.6", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.28.6", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.28.6", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw=="], + + "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], + "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], @@ -1195,8 +1330,6 @@ "@opencode-ai/console-resource": ["@opencode-ai/console-resource@workspace:packages/console/resource"], - "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"], - "@opencode-ai/enterprise": ["@opencode-ai/enterprise@workspace:packages/enterprise"], "@opencode-ai/function": ["@opencode-ai/function@workspace:packages/function"], @@ -1439,6 +1572,14 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + "@rollup/plugin-babel": ["@rollup/plugin-babel@5.3.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0" }, "optionalPeers": ["@types/babel__core"] }, "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q=="], + + "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@15.3.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA=="], + + "@rollup/plugin-replace": ["@rollup/plugin-replace@2.4.2", "", { "dependencies": { "@rollup/pluginutils": "^3.1.0", "magic-string": "^0.25.7" }, "peerDependencies": { "rollup": "^1.20.0 || ^2.0.0" } }, "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg=="], + + "@rollup/plugin-terser": ["@rollup/plugin-terser@0.4.4", "", { "dependencies": { "serialize-javascript": "^6.0.1", "smob": "^1.0.0", "terser": "^5.17.4" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], @@ -1503,6 +1644,8 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@shuvcode/desktop": ["@shuvcode/desktop@workspace:packages/desktop"], + "@sindresorhus/is": ["@sindresorhus/is@7.1.1", "", {}, "sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ=="], "@slack/bolt": ["@slack/bolt@3.22.0", "", { "dependencies": { "@slack/logger": "^4.0.0", "@slack/oauth": "^2.6.3", "@slack/socket-mode": "^1.3.6", "@slack/types": "^2.13.0", "@slack/web-api": "^6.13.0", "@types/express": "^4.16.1", "@types/promise.allsettled": "^1.0.3", "@types/tsscmp": "^1.0.0", "axios": "^1.7.4", "express": "^4.21.0", "path-to-regexp": "^8.1.0", "promise.allsettled": "^1.0.2", "raw-body": "^2.3.3", "tsscmp": "^1.0.6" } }, "sha512-iKDqGPEJDnrVwxSVlFW6OKTkijd7s4qLBeSufoBsTM0reTyfdp/5izIQVkxNfzjHi3o6qjdYbRXkYad5HBsBog=="], @@ -1675,6 +1818,8 @@ "@stripe/stripe-js": ["@stripe/stripe-js@8.6.1", "", {}, "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA=="], + "@surma/rollup-plugin-off-main-thread": ["@surma/rollup-plugin-off-main-thread@2.2.3", "", { "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", "magic-string": "^0.25.0", "string.prototype.matchall": "^4.0.6" } }, "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ=="], + "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="], @@ -1843,6 +1988,8 @@ "@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="], + "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], @@ -1993,6 +2140,8 @@ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + "autoprefixer": ["autoprefixer@10.4.22", "", { "dependencies": { "browserslist": "^4.27.0", "caniuse-lite": "^1.0.30001754", "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], @@ -2017,6 +2166,12 @@ "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], + "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], + + "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], + + "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], + "babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], @@ -2185,6 +2340,8 @@ "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + "common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="], + "compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="], "condense-newlines": ["condense-newlines@0.2.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-whitespace": "^0.3.0", "kind-of": "^3.0.2" } }, "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg=="], @@ -2207,6 +2364,8 @@ "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + "core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], @@ -2221,6 +2380,8 @@ "crossws": ["crossws@0.4.1", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-E7WKBcHVhAVrY6JYD5kteNqVq1GSZxqGrdSiwXR9at+XHi43HJoCQKXcCczR5LBnBquFZPsB3o7HklulKoBU5w=="], + "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="], + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], "css-selector-parser": ["css-selector-parser@3.2.0", "", {}, "sha512-L1bdkNKUP5WYxiW5dW6vA2hd3sL8BdRNLy2FCX0rLVise4eNw9nBdeBuJHxlELieSE2H1f6bYQFfwVUwWCV9rQ=="], @@ -2333,6 +2494,8 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.259", "", {}, "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -2405,6 +2568,8 @@ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], @@ -2447,6 +2612,8 @@ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="], @@ -2459,6 +2626,8 @@ "file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], + "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], @@ -2497,7 +2666,7 @@ "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], @@ -2531,6 +2700,8 @@ "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "get-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="], + "get-port": ["get-port@7.1.0", "", {}, "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -2541,6 +2712,8 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "ghostty-opentui": ["ghostty-opentui@1.3.7", "", { "dependencies": { "strip-ansi": "^7.1.2" }, "peerDependencies": { "@opentui/core": "*" }, "optionalPeers": ["@opentui/core"] }, "sha512-tXlVrFKMiS+VNm48OwQtCefP7i85o04wv2S/NPR5bIzv4yAl2//q7CBa8JEv9bL+5jpZsfMm6z8VJGrTq6Xjvg=="], + "ghostty-web": ["ghostty-web@0.3.0", "", {}, "sha512-SAdSHWYF20GMZUB0n8kh1N6Z4ljMnuUqT8iTB2n5FAPswEV10MejEpLlhW/769GL5+BQa1NYwEg9y/XCckV5+A=="], "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], @@ -2677,6 +2850,8 @@ "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + "idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="], + "ieee754": ["ieee754@1.1.13", "", {}, "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="], "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], @@ -2755,12 +2930,16 @@ "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + "is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], @@ -2769,6 +2948,8 @@ "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + "is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="], + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], @@ -2807,6 +2988,8 @@ "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], + "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], + "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -2845,6 +3028,8 @@ "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], + "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], @@ -2869,6 +3054,8 @@ "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], @@ -2899,6 +3086,8 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], @@ -2917,6 +3106,8 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], + "loglevelnext": ["loglevelnext@6.0.0", "", {}, "sha512-FDl1AI2sJGjHHG3XKJd6sG3/6ncgiGCQ0YkW46nxe7SfqQq6hujd9CvFXIXtkGBUN83KPZ2KSOJK8q5P0bSSRQ=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -3327,6 +3518,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-bytes": ["pretty-bytes@6.1.1", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="], + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], @@ -3357,6 +3550,8 @@ "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], @@ -3403,6 +3598,10 @@ "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=="], + "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], + + "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + "regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], @@ -3411,6 +3610,12 @@ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + + "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="], + + "regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="], "rehype-autolink-headings": ["rehype-autolink-headings@7.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-is-element": "^3.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw=="], @@ -3509,6 +3714,8 @@ "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], + "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], @@ -3557,6 +3764,8 @@ "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "smob": ["smob@1.5.0", "", {}, "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig=="], + "smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="], "socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="], @@ -3577,12 +3786,14 @@ "solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], @@ -3633,6 +3844,8 @@ "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], @@ -3643,12 +3856,16 @@ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="], + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], + "strip-comments": ["strip-comments@2.0.1", "", {}, "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw=="], + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], "stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="], @@ -3679,6 +3896,10 @@ "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], + "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], + + "tempy": ["tempy@0.6.0", "", { "dependencies": { "is-stream": "^2.0.0", "temp-dir": "^2.0.0", "type-fest": "^0.16.0", "unique-string": "^2.0.0" } }, "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw=="], + "terracotta": ["terracotta@1.0.6", "", { "dependencies": { "solid-use": "^0.9.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-yVrmT/Lg6a3tEbeYEJH8ksb1PYkR5FA9k5gr1TchaSNIiA2ZWs5a+koEbePXwlBP0poaV7xViZ/v50bQFcMgqw=="], "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="], @@ -3789,14 +4010,24 @@ "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], + + "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="], + + "unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + "unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="], + "unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="], + "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unifont": ["unifont@0.5.2", "", { "dependencies": { "css-tree": "^3.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.0" } }, "sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg=="], + "unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="], + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], @@ -3829,6 +4060,8 @@ "unzip-stream": ["unzip-stream@0.3.4", "", { "dependencies": { "binary": "^0.3.0", "mkdirp": "^0.5.1" } }, "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw=="], + "upath": ["upath@1.2.0", "", {}, "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="], + "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="], @@ -3863,6 +4096,8 @@ "vite-plugin-icons-spritesheet": ["vite-plugin-icons-spritesheet@3.0.1", "", { "dependencies": { "chalk": "^5.4.1", "glob": "^11.0.1", "node-html-parser": "^7.0.1", "tinyexec": "^0.3.2" }, "peerDependencies": { "vite": ">=5.2.0" } }, "sha512-Cr0+Z6wRMwSwKisWW9PHeTjqmQFv0jwRQQMc3YgAhAgZEe03j21el0P/CA31KN/L5eiL1LhR14VTXl96LetonA=="], + "vite-plugin-pwa": ["vite-plugin-pwa@1.2.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw=="], + "vite-plugin-solid": ["vite-plugin-solid@2.11.10", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw=="], "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], @@ -3901,6 +4136,38 @@ "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], + "workbox-background-sync": ["workbox-background-sync@7.4.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.0" } }, "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w=="], + + "workbox-broadcast-update": ["workbox-broadcast-update@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA=="], + + "workbox-build": ["workbox-build@7.4.0", "", { "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.24.4", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", "@rollup/plugin-babel": "^5.2.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^2.4.1", "@rollup/plugin-terser": "^0.4.3", "@surma/rollup-plugin-off-main-thread": "^2.2.3", "ajv": "^8.6.0", "common-tags": "^1.8.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", "glob": "^11.0.1", "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", "rollup": "^2.79.2", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", "workbox-background-sync": "7.4.0", "workbox-broadcast-update": "7.4.0", "workbox-cacheable-response": "7.4.0", "workbox-core": "7.4.0", "workbox-expiration": "7.4.0", "workbox-google-analytics": "7.4.0", "workbox-navigation-preload": "7.4.0", "workbox-precaching": "7.4.0", "workbox-range-requests": "7.4.0", "workbox-recipes": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0", "workbox-streams": "7.4.0", "workbox-sw": "7.4.0", "workbox-window": "7.4.0" } }, "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA=="], + + "workbox-cacheable-response": ["workbox-cacheable-response@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ=="], + + "workbox-core": ["workbox-core@7.4.0", "", {}, "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ=="], + + "workbox-expiration": ["workbox-expiration@7.4.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.0" } }, "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw=="], + + "workbox-google-analytics": ["workbox-google-analytics@7.4.0", "", { "dependencies": { "workbox-background-sync": "7.4.0", "workbox-core": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ=="], + + "workbox-navigation-preload": ["workbox-navigation-preload@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w=="], + + "workbox-precaching": ["workbox-precaching@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg=="], + + "workbox-range-requests": ["workbox-range-requests@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw=="], + + "workbox-recipes": ["workbox-recipes@7.4.0", "", { "dependencies": { "workbox-cacheable-response": "7.4.0", "workbox-core": "7.4.0", "workbox-expiration": "7.4.0", "workbox-precaching": "7.4.0", "workbox-routing": "7.4.0", "workbox-strategies": "7.4.0" } }, "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ=="], + + "workbox-routing": ["workbox-routing@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ=="], + + "workbox-strategies": ["workbox-strategies@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0" } }, "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg=="], + + "workbox-streams": ["workbox-streams@7.4.0", "", { "dependencies": { "workbox-core": "7.4.0", "workbox-routing": "7.4.0" } }, "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg=="], + + "workbox-sw": ["workbox-sw@7.4.0", "", {}, "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw=="], + + "workbox-window": ["workbox-window@7.4.0", "", { "dependencies": { "@types/trusted-types": "^2.0.2", "workbox-core": "7.4.0" } }, "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw=="], + "workerd": ["workerd@1.20251118.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251118.0", "@cloudflare/workerd-darwin-arm64": "1.20251118.0", "@cloudflare/workerd-linux-64": "1.20251118.0", "@cloudflare/workerd-linux-arm64": "1.20251118.0", "@cloudflare/workerd-windows-64": "1.20251118.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Om5ns0Lyx/LKtYI04IV0bjIrkBgoFNg0p6urzr2asekJlfP18RqFzyqMFZKf0i9Gnjtz/JfAS/Ol6tjCe5JJsQ=="], "wrangler": ["wrangler@4.50.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.11", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251118.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251118.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251118.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-+nuZuHZxDdKmAyXOSrHlciGshCoAPiy5dM+t6mEohWm7HpXvTHmWQGUf/na9jjWlWJHCJYOWzkA1P5HBJqrIEA=="], @@ -4013,6 +4280,8 @@ "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.9", "", { "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.0", "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.13.0", "smol-toml": "^1.4.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-hX2cLC/KW74Io1zIbn92kI482j9J7LleBLGCVU9EP3BeH5MVrnFawOnqD0t/q6D1Z+ZNeQG2gNKMslCcO36wng=="], + "@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=="], @@ -4065,8 +4334,186 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-define-polyfill-provider/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/helper-replace-supers/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/helper-wrap-function/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/helper-wrap-function/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/helper-wrap-function/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-bugfix-safari-class-field-initializer-scope/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/plugin-syntax-import-assertions/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-import-attributes/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-syntax-unicode-sets-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-arrow-functions/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-async-generator-functions/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/plugin-transform-async-to-generator/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-block-scoped-functions/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-block-scoping/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + + "@babel/plugin-transform-class-properties/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + + "@babel/plugin-transform-class-static-block/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-classes/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/plugin-transform-classes/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-classes/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/plugin-transform-computed-properties/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-computed-properties/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-transform-destructuring/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-destructuring/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/plugin-transform-dotall-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-duplicate-keys/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-duplicate-named-capturing-groups-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-dynamic-import/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-explicit-resource-management/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-exponentiation-operator/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-export-namespace-from/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-for-of/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-function-name/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/plugin-transform-function-name/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-function-name/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/plugin-transform-json-strings/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-literals/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-logical-assignment-operators/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-member-expression-literals/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-modules-amd/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-modules-systemjs/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-modules-umd/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-named-capturing-groups-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-new-target/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-nullish-coalescing-operator/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-numeric-separator/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/plugin-transform-object-rest-spread/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-object-rest-spread/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/plugin-transform-object-super/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-object-super/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], + + "@babel/plugin-transform-optional-catch-binding/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-optional-chaining/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-parameters/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + + "@babel/plugin-transform-private-methods/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + + "@babel/plugin-transform-private-property-in-object/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-property-literals/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-regenerator/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-regexp-modifiers/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-reserved-words/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-shorthand-properties/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-spread/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-sticky-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-template-literals/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-typeof-symbol/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-unicode-escapes/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-unicode-property-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-unicode-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/plugin-transform-unicode-sets-regex/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/preset-env/@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + + "@babel/preset-env/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/preset-env/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/preset-env/@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + + "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/preset-modules/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + "@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], @@ -4131,6 +4578,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.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.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-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], @@ -4199,10 +4648,6 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], - - "@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], - "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="], "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], @@ -4223,6 +4668,16 @@ "@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="], + "@rollup/plugin-babel/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], + + "@rollup/plugin-babel/rollup": ["rollup@2.79.2", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ=="], + + "@rollup/plugin-replace/@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], + + "@rollup/plugin-replace/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], + + "@rollup/plugin-replace/rollup": ["rollup@2.79.2", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], @@ -4233,6 +4688,10 @@ "@shikijs/themes/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], + "@shuvcode/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], + + "@shuvcode/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], + "@slack/bolt/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], @@ -4261,6 +4720,8 @@ "@solidjs/start/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=="], + "@surma/rollup-plugin-off-main-thread/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], + "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], @@ -4305,6 +4766,10 @@ "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "babel-plugin-polyfill-corejs2/@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + + "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -4341,6 +4806,10 @@ "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=="], + "esbuild-plugin-copy/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "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.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], @@ -4353,6 +4822,8 @@ "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -4471,6 +4942,8 @@ "sitemap/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], + "source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], @@ -4487,6 +4960,10 @@ "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "tempy/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "tempy/type-fest": ["type-fest@0.16.0", "", {}, "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -4511,6 +4988,10 @@ "which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "workbox-build/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], + + "workbox-build/rollup": ["rollup@2.79.2", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -4577,6 +5058,150 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + + "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/helper-replace-supers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/helper-replace-supers/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/helper-replace-supers/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/helper-replace-supers/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/helper-replace-supers/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/helper-wrap-function/@babel/template/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/helper-wrap-function/@babel/template/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/helper-wrap-function/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/helper-wrap-function/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/helper-wrap-function/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/plugin-transform-classes/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + + "@babel/plugin-transform-classes/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/plugin-transform-classes/@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/plugin-transform-classes/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-transform-classes/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/plugin-transform-classes/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-classes/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-transform-classes/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-transform-computed-properties/@babel/template/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-transform-computed-properties/@babel/template/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-computed-properties/@babel/template/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-transform-destructuring/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + + "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/plugin-transform-function-name/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-transform-function-name/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/plugin-transform-function-name/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-function-name/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-transform-function-name/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + + "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/preset-env/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -4809,8 +5434,6 @@ "@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], - "@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], - "@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="], "@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="], @@ -4835,6 +5458,20 @@ "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], + "@rollup/plugin-babel/@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="], + + "@rollup/plugin-babel/@rollup/pluginutils/estree-walker": ["estree-walker@1.0.1", "", {}, "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="], + + "@rollup/plugin-babel/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "@rollup/plugin-replace/@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="], + + "@rollup/plugin-replace/@rollup/pluginutils/estree-walker": ["estree-walker@1.0.1", "", {}, "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="], + + "@rollup/plugin-replace/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "@shuvcode/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], + "@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "@slack/web-api/p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], @@ -5039,6 +5676,10 @@ "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "source-map/whatwg-url/tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], + + "source-map/whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "tw-to-css/tailwindcss/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=="], @@ -5091,6 +5732,68 @@ "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], + "@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-transform-class-static-block/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-transform-classes/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/plugin-transform-function-name/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/plugin-transform-private-property-in-object/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/preset-env/@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -5165,7 +5868,7 @@ "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], - "@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@shuvcode/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -5221,6 +5924,8 @@ "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "source-map/whatwg-url/tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -5277,6 +5982,18 @@ "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], + "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/preset-env/@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], diff --git a/packages/app/package.json b/packages/app/package.json index f57e1af13f2..5694e2771a4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.26" + "version": "1.1.26", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/components/font-picker.tsx b/packages/app/src/components/font-picker.tsx index 91f2243bb5a..b56f72d243c 100644 --- a/packages/app/src/components/font-picker.tsx +++ b/packages/app/src/components/font-picker.tsx @@ -26,7 +26,7 @@ function DialogSelectFont(props: { originalFont: string }) { dialog.close() } - async function handleActiveChange(font: FontDefinition | undefined) { + async function handleMove(font: FontDefinition | undefined) { if (!font) return const loaded = await ensureFontLoaded(font) @@ -46,7 +46,7 @@ function DialogSelectFont(props: { originalFont: string }) { current={currentFont()} filterKeys={["name", "family"]} onSelect={handleSelect} - onActiveChange={handleActiveChange} + onMove={handleMove} > {(font: FontDefinition) => (
diff --git a/packages/app/src/components/theme-picker.tsx b/packages/app/src/components/theme-picker.tsx index c10d85f8756..149483ade25 100644 --- a/packages/app/src/components/theme-picker.tsx +++ b/packages/app/src/components/theme-picker.tsx @@ -21,7 +21,7 @@ export function DialogSelectTheme(props: { originalTheme: string }) { dialog.close() } - function handleActiveChange(theme: Theme | undefined) { + function handleMove(theme: Theme | undefined) { if (!theme) return setPreviewTheme(theme.id) applyTheme(theme.id) @@ -37,7 +37,7 @@ export function DialogSelectTheme(props: { originalTheme: string }) { current={currentTheme()} filterKeys={["name", "id"]} onSelect={handleSelect} - onActiveChange={handleActiveChange} + onMove={handleMove} > {(theme: Theme) => (
@@ -80,7 +80,13 @@ export function ThemePicker(props: { class?: string; mobile?: boolean }) { } > - diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 0a79194006a..2521efc8df1 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -89,7 +89,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( terminal: { opened: false, height: 280, - opened: false, }, review: { opened: false, diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index c2b95a5f502..65d201efe00 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -18,7 +18,6 @@ import { useCodeComponent } from "@opencode-ai/ui/context/code" import { SessionTurn } from "@opencode-ai/ui/session-turn" import { createAutoScroll } from "@opencode-ai/ui/hooks" import { SessionReview } from "@opencode-ai/ui/session-review" -import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail" import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" @@ -1152,17 +1151,6 @@ export default function Page() { } >
- -
- -
-
{ diff --git a/packages/desktop/package.json b/packages/desktop/package.json index b1a9db0111b..f0a6a369974 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@shuvcode/desktop", "private": true, - "version": "1.1.26" + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 23f3c219125..1a40af1c799 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.26" + "version": "1.1.26", "private": true, "type": "module", "license": "MIT", diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index 483db4d9324..52d81c83e54 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -8,7 +8,7 @@ import { WorkerPoolProvider } from "@opencode-ai/ui/context/worker-pool" import { createAsync, query, useParams } from "@solidjs/router" import { createEffect, createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js" import { Share } from "~/core/share" -import { Logo, Mark } from "@opencode-ai/ui/logo" +import { AsciiLogo, AsciiMark } from "@opencode-ai/ui/logo" import { IconButton } from "@opencode-ai/ui/icon-button" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { createDefaultOptions } from "@opencode-ai/ui/pierre" @@ -211,7 +211,7 @@ export default function () { return ( <> - {info().title} | OpenCode + {info().title} | shuvcode @@ -267,7 +267,7 @@ export default function () {
- +
v{info().version}
@@ -309,7 +309,7 @@ export default function () {
- +
) @@ -321,13 +321,13 @@ export default function () {
- +
diff --git a/packages/opencode/package.json b/packages/opencode/package.json index e89f7490b03..f60132a2cd6 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.26" + "version": "1.1.26", "name": "opencode", "type": "module", "license": "MIT", 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 f7941baa2b1..4270f140174 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx @@ -61,7 +61,7 @@ function init() { trigger(name: string, source?: "prompt") { for (const option of options()) { if (option.value === name) { - option.onSelect?.(dialog, source) + option.onSelect?.(dialog) return } } @@ -83,6 +83,17 @@ function init() { get options() { return options() }, + slashes() { + return options() + .filter((o) => (o as CommandOption).value.startsWith("/")) + .map((o) => ({ + display: (o as any).display ?? (o as CommandOption).value, + value: (o as CommandOption).value, + description: (o as CommandOption).description, + aliases: (o as any).aliases, + onSelect: () => (o as CommandOption).onSelect?.(dialog), + })) as any + }, } return result } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 730da20c265..b6f78ad801a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -247,9 +247,10 @@ export function Prompt(props: PromptProps) { const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text") const value = text - const content = await Editor.open({ value, renderer }) - if (!content) return + const result = await Editor.open({ value, renderer }) + if (!result?.ok) return + const content = result.content input.setText(content) // Update positions for nonTextParts based on their location in new content 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 1294ab849e9..08e913d5009 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -836,8 +836,8 @@ export function Session() { // Open with EDITOR if available const result = await Editor.open({ value: transcript, renderer }) - if (result !== undefined) { - await Bun.write(filepath, result) + if (result?.ok) { + await Bun.write(filepath, result.content) } toast.show({ message: `Session exported to ${filename}`, variant: "success" }) @@ -1119,7 +1119,7 @@ export function Session() { alignItems="flex-end" backgroundColor={RGBA.fromInts(0, 0, 0, 70)} > - + diff --git a/packages/ui/package.json b/packages/ui/package.json index 3965388ad28..7079384a54a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.26" + "version": "1.1.26", "type": "module", "license": "MIT", "exports": { From 56a49552f9b300346d4a32bd3ab6cd00cae60091 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 19 Jan 2026 16:34:08 -0800 Subject: [PATCH 191/192] fix(ci): restrict workflow_dispatch to integration branch only Prevents accidental releases from feature branches by adding branch check to the workflow_dispatch condition. Only integration branch can trigger snapshot releases manually. --- .github/workflows/snapshot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 1d9d1375113..83bae633738 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -20,10 +20,10 @@ permissions: jobs: publish: runs-on: blacksmith-4vcpu-ubuntu-2404 - # Only run if tests passed (workflow_run) or manual dispatch + # Only run if tests passed (workflow_run) or manual dispatch ON INTEGRATION BRANCH # Also skip release commits to prevent infinite loop if: | - (github.event_name == 'workflow_dispatch') || + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/integration') || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && !startsWith(github.event.workflow_run.head_commit.message, 'release:') && From 74c2844c05c8a52b4d235908932b4d2d71d9e4d9 Mon Sep 17 00:00:00 2001 From: shuv Date: Mon, 19 Jan 2026 17:16:26 -0800 Subject: [PATCH 192/192] style: remove unnecessary optional chaining on Editor.open result Apply Greptile review suggestion - Editor.open() always returns a Result object, so optional chaining is unnecessary. --- packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | 2 +- packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index b6f78ad801a..c33407b5452 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -248,7 +248,7 @@ export function Prompt(props: PromptProps) { const value = text const result = await Editor.open({ value, renderer }) - if (!result?.ok) return + if (!result.ok) return const content = result.content input.setText(content) 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 08e913d5009..6217c1b1d0e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -836,7 +836,7 @@ export function Session() { // Open with EDITOR if available const result = await Editor.open({ value: transcript, renderer }) - if (result?.ok) { + if (result.ok) { await Bun.write(filepath, result.content) }