Compare commits

...

14 Commits

Author SHA1 Message Date
Kit Langton e6fbaa96e1 refactor(core): extract session request preparation 2026-07-15 22:52:19 -04:00
opencode-agent[bot] 6bf92aa7de feat(tui): suggest settings command (#37205)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-07-15 22:31:31 -04:00
Kit Langton 0a5530d0be fix(plugin): make tool values structural (#37202) 2026-07-15 22:25:01 -04:00
Kit Langton dc7e0f6d2e refactor(core): extract session context loading (#37203) 2026-07-15 22:22:44 -04:00
Aiden Cline b4be28d4c3 tweak: adjust compaction to clearly indicate the convo history (#37195) 2026-07-15 19:28:26 -05:00
opencode-agent[bot] 7624c4f7b3 chore: merge dev into v2 (#37170)
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: James Long <jlongster@users.noreply.github.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Victor Navarro <vn4varro@gmail.com>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Nabs <nabil@instafork.com>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com>
Co-authored-by: Mark <geraint0923@users.noreply.github.com>
Co-authored-by: Jay <air@live.ca>
Co-authored-by: BB84 <110078428+BB-84C@users.noreply.github.com>
2026-07-15 18:23:08 -05:00
Aiden Cline 720f062cd0 refactor(core): select system prompts through plugins (#37181) 2026-07-15 18:21:32 -05:00
Dax Raad b5177adb5b feat(api): add command authentication attempts 2026-07-15 19:08:56 -04:00
Dax Raad a772767e9b refactor(api): scope oauth operations by integration 2026-07-15 19:08:56 -04:00
Aiden Cline fd19fa89c4 fix(codemode): linearize reference walks (#37179) 2026-07-15 17:24:36 -05:00
Aiden Cline 0cfa7b39db ignore: adjust agents md 2026-07-15 17:12:57 -05:00
Kit Langton 5e2e0d6965 feat(simulation): add endpoint handshake protocol (#37157) 2026-07-15 17:46:28 -04:00
opencode-agent[bot] f92d84746b refactor(plugin): scope context hook to session (#37175)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-07-15 16:32:00 -04:00
Dax Raad f2f5eb6f16 feat(plugin): restore ai request hook 2026-07-15 15:58:14 -04:00
140 changed files with 3986 additions and 1207 deletions
+1
View File
@@ -1,2 +1,3 @@
packages/core/migration/**/snapshot.json linguist-generated
packages/core/src/database/migration.gen.ts linguist-generated
packages/core/src/**/*.txt text eol=lf
-2
View File
@@ -19,8 +19,6 @@ Valid types are `feat`, `fix`, `docs`, `chore`, `refactor`, and `test`. Scopes a
Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`.
Never bypass Git hooks. Do not use `--no-verify` or otherwise disable, skip, or circumvent commit or push hooks. If a hook fails, fix the failure or stop and report it to the user.
## Style Guide
### General Principles
+66 -154
View File
@@ -30,9 +30,30 @@
"turbo": "2.10.2",
},
},
"packages/ai": {
"name": "@opencode-ai/ai",
"version": "1.17.20",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2",
"aws4fetch": "1.0.20",
"effect": "catalog:",
"google-auth-library": "10.5.0",
},
"devDependencies": {
"@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:",
"@opencode-ai/http-recorder": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
},
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -98,7 +119,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
"version": "1.17.18",
"version": "1.18.2",
"bin": {
"opencode2": "./bin/opencode2.cjs",
},
@@ -158,7 +179,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -168,12 +189,11 @@
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
},
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -209,7 +229,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -236,7 +256,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48",
@@ -258,7 +278,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -282,7 +302,7 @@
},
"packages/console/support": {
"name": "@opencode-ai/console-support",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*",
@@ -302,7 +322,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.17.20",
"version": "1.18.2",
"bin": {
"opencode": "./bin/opencode",
},
@@ -336,10 +356,10 @@
"@modelcontextprotocol/sdk": "1.29.0",
"@npmcli/arborist": "9.4.0",
"@npmcli/config": "10.8.1",
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/effect-sqlite-node": "workspace:*",
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@openrouter/ai-sdk-provider": "2.9.0",
@@ -398,7 +418,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"effect": "catalog:",
@@ -458,7 +478,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -472,7 +492,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"effect": "catalog:",
},
@@ -484,7 +504,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -516,7 +536,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -532,7 +552,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@effect/platform-node-shared": "4.0.0-beta.83",
},
@@ -562,29 +582,9 @@
"@typescript/native-preview": "catalog:",
},
},
"packages/ai": {
"name": "@opencode-ai/ai",
"version": "1.17.20",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2",
"aws4fetch": "1.0.20",
"effect": "catalog:",
"google-auth-library": "10.5.0",
},
"devDependencies": {
"@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:",
"@opencode-ai/http-recorder": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
},
},
"packages/opencode": {
"name": "opencode",
"version": "1.17.20",
"version": "1.18.2",
"bin": {
"opencode": "./bin/opencode",
},
@@ -621,10 +621,10 @@
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/cli": "workspace:*",
"@opencode-ai/client": "workspace:*",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
"@opencode-ai/schema": "workspace:*",
@@ -717,12 +717,14 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/client": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@standard-schema/spec": "^1.1.0",
"effect": "catalog:",
"zod": "catalog:",
},
@@ -804,7 +806,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -819,7 +821,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@effect/platform-node": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -836,7 +838,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -884,8 +886,8 @@
"dependencies": {
"@fontsource/commit-mono": "5.2.5",
"@napi-rs/canvas": "1.0.2",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/core": "workspace:*",
"@opentui/core": "catalog:",
"effect": "catalog:",
},
@@ -897,7 +899,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -910,7 +912,7 @@
},
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -944,7 +946,7 @@
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -963,7 +965,7 @@
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -1004,7 +1006,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@opencode-ai/client": "workspace:*",
"@opencode-ai/core": "workspace:*",
@@ -1034,7 +1036,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1085,7 +1087,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -2167,6 +2169,8 @@
"@openauthjs/openauth": ["@openauthjs/openauth@0.0.0-20250322224806", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-p5IWSRXvABcwocH2dNI0w8c1QJelIOFulwhKk+aLLFfUbs8u1pr7kQbYe8yCSM2+bcLHiwbogpUQc2ovrGwCuw=="],
"@opencode-ai/ai": ["@opencode-ai/ai@workspace:packages/ai"],
"@opencode-ai/app": ["@opencode-ai/app@workspace:packages/app"],
"@opencode-ai/cli": ["@opencode-ai/cli@workspace:packages/cli"],
@@ -2205,8 +2209,6 @@
"@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"],
"@opencode-ai/ai": ["@opencode-ai/ai@workspace:packages/ai"],
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
@@ -2871,7 +2873,7 @@
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-tHhdiWZfG1ZIh2YcRfPJmY2gHcBmqbAzqm3ER4TIDFYsSEqTD5tICT7cgQ/kI8LRakxp12myOYyK68XPn7MnHw=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-M9rMkTar7JcRrvUHsK1271AuWDmrISIPQpQ4TSHmYZ4KMisGnMH0gfjCWnBwdndR7skvvp/UheHhZGvO3Cr8/g=="],
@@ -2945,7 +2947,7 @@
"@smithy/util-stream": ["@smithy/util-stream@4.6.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-PFzBVEBP5k8R+mK/c+VAKmtpUTL+KzBIXWJ6oM0GWOb31K+QgymXV9IW03XLPM1wtkC7oAb9ZBN2aswSSVbNFg=="],
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@smithy/util-waiter": ["@smithy/util-waiter@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-EYviebytZE6vplW0AGwZ2Rc3sNuVR83lfUCNZu11VchUiKhMwJqrRWy7iVDTNEwG/vEwItno591Iad6/prj6Bw=="],
@@ -3007,7 +3009,7 @@
"@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@stoplight/better-ajv-errors": ["@stoplight/better-ajv-errors@1.0.3", "", { "dependencies": { "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA=="],
@@ -6335,10 +6337,6 @@
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
@@ -6395,8 +6393,6 @@
"@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
@@ -6457,26 +6453,18 @@
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-sdk/client-athena/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
"@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="],
"@aws-sdk/client-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
"@aws-sdk/client-firehose/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
"@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="],
"@aws-sdk/client-lambda/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
"@aws-sdk/client-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/client-sso/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/client-sts/@aws-sdk/core": ["@aws-sdk/core@3.775.0", "", { "dependencies": { "@aws-sdk/types": "3.775.0", "@smithy/core": "^3.2.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.0.2", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA=="],
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.782.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.775.0", "@aws-sdk/credential-provider-http": "3.775.0", "@aws-sdk/credential-provider-ini": "3.782.0", "@aws-sdk/credential-provider-process": "3.775.0", "@aws-sdk/credential-provider-sso": "3.782.0", "@aws-sdk/credential-provider-web-identity": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-HZiAF+TCEyKjju9dgysjiPIWgt/+VerGaeEp18mvKLNfgKz1d+/82A2USEpNKTze7v3cMFASx3CvL8yYyF7mJw=="],
@@ -6499,10 +6487,6 @@
"@aws-sdk/client-sts/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.782.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-dMFkUBgh2Bxuw8fYZQoH/u3H4afQ12VSkzEi//qFiDTwbKYq+u+RYjc8GLDM6JSK1BShMu5AVR7HD4ap1TYUnA=="],
"@aws-sdk/client-sts/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
"@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
@@ -6551,10 +6535,6 @@
"@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
"@aws-sdk/middleware-flexible-checksums/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/middleware-sdk-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
"@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
@@ -6629,6 +6609,8 @@
"@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"@hono/standard-validator/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
@@ -6849,6 +6831,10 @@
"@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@opencode-ai/console-app/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
"@opencode-ai/console-app/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
"@opencode-ai/console-mail/@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="],
"@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="],
@@ -6863,10 +6849,6 @@
"@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
"@opencode-ai/ai/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
"@opencode-ai/ai/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@opencode-ai/script/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
@@ -7351,8 +7333,6 @@
"editorconfig/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"electron-builder/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
@@ -7413,8 +7393,6 @@
"fumadocs-core/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="],
"fumadocs-mdx/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"fumadocs-mdx/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"fumadocs-mdx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
@@ -7553,6 +7531,8 @@
"opencode/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="],
"opencode/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
"opencode/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
@@ -7759,46 +7739,6 @@
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/elevenlabs/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/togetherai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"@astrojs/check/yargs/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=="],
@@ -8401,10 +8341,6 @@
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
"@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@stoplight/spectral-core/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
@@ -8479,10 +8415,6 @@
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
@@ -8873,10 +8805,6 @@
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="],
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
@@ -8989,12 +8917,8 @@
"@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="],
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
@@ -9145,8 +9069,6 @@
"@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
"@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
@@ -9329,14 +9251,6 @@
"@vitejs/plugin-react/vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ai-gateway-provider/@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
@@ -9479,8 +9393,6 @@
"@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.17.20",
"version": "1.18.2",
"description": "",
"type": "module",
"exports": {
+13 -12
View File
@@ -32,8 +32,6 @@ const triggerClass =
const showPopover = () => true
export function HelpButton() {
if (import.meta.env.VITE_OPENCODE_CHANNEL !== "dev") return null
const platform = usePlatform()
return (
@@ -56,6 +54,7 @@ export function HelpButton() {
// can remove this after the tabs rollout has been out for a while
export function TabsInfoPopup() {
const settings = useSettings()
const platform = usePlatform()
const [drawerOpen, setDrawerOpen] = createSignal(false)
return (
@@ -114,16 +113,18 @@ export function TabsInfoPopup() {
<DrawerContent>
<div class="flex h-[52px] w-full shrink-0 items-center gap-4 self-stretch border-b border-v2-border-border-muted p-4">
<p class="min-h-0 min-w-0 flex-1 text-[13px] font-[530] leading-5 tracking-[-0.04px] tabular-nums text-v2-text-text-muted">
June 16
July 14
</p>
<DrawerClose
as={IconButtonV2}
type="button"
size="small"
variant="ghost-muted"
aria-label="Close"
icon={<IconV2 name="xmark-small" />}
/>
<Show when={platform.platform !== "desktop" || platform.os !== "windows"}>
<DrawerClose
as={IconButtonV2}
type="button"
size="small"
variant="ghost-muted"
aria-label="Close"
icon={<IconV2 name="xmark-small" />}
/>
</Show>
</div>
<div class="relative flex min-h-0 w-full flex-1 flex-col items-start gap-6 overflow-y-auto p-8">
<p class="w-full shrink-0 self-stretch text-[21px] font-[610] leading-6 tracking-[-0.37px] tabular-nums text-v2-text-text-base">
@@ -147,7 +148,7 @@ export function TabsInfoPopup() {
<p>When you reopen the app, your tabs are still open.</p>
<p>
The new design does not support Git Worktrees yet, it's coming soon. So if you'd prefer to continue using
the previous layout , you can switch between layouts in Settings. Just keep in mind that the new layout
the previous layout, you can switch between layouts in Settings. Just keep in mind that the new layout
will become permanent in a few weeks.
</p>
</div>
@@ -371,7 +371,7 @@
transform: translateY(-50%);
}
.settings-v2-tab-body.settings-v2-models {
.settings-v2-models {
gap: 24px;
}
+1 -1
View File
@@ -392,7 +392,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
id: "tab.new",
category: "tab",
title: language.t("command.session.new"),
keybind: "mod+t",
keybind: "mod+t,mod+n",
hidden: true,
onSelect: openNewTab,
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
"version": "1.17.18",
"version": "1.18.2",
"type": "module",
"license": "MIT",
"bin": {
@@ -46,7 +46,7 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
yield* request(() => timeline.pending("Starting authorization..."))
const started = yield* request((signal) =>
client.integration.connect.oauth(
client.integration.oauth.connect(
{
integrationID,
methodID: method.id,
@@ -59,8 +59,8 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
const attempt = started.data
yield* Effect.addFinalizer(() =>
request(() =>
client.integration.attempt.cancel(
{ attemptID: attempt.attemptID, location },
client.integration.oauth.cancel(
{ integrationID, attemptID: attempt.attemptID, location },
{ signal: AbortSignal.timeout(5_000) },
),
).pipe(Effect.ignore),
@@ -75,7 +75,7 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
}).pipe(Effect.ignore)
yield* request(() => timeline.pending("Waiting for authorization..."))
const status = yield* waitForConsoleLogin(client, attempt.attemptID)
const status = yield* waitForConsoleLogin(client, integrationID, attempt.attemptID)
if (status.status === "failed") yield* Effect.fail(new Error(status.message))
if (status.status === "expired") yield* Effect.fail(new Error("Device code expired"))
@@ -85,11 +85,12 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
const waitForConsoleLogin = Effect.fn("cli.console.login.wait")(function* (
client: OpenCodeClient,
integrationID: string,
attemptID: string,
) {
while (true) {
const response = yield* request((signal) =>
client.integration.attempt.status({ attemptID, location }, { signal }),
client.integration.oauth.status({ integrationID, attemptID, location }, { signal }),
)
if (response.data.status !== "pending") return response.data
yield* Effect.sleep(500)
@@ -32,7 +32,7 @@ export default Runtime.handler(
return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`))
const started = yield* Effect.promise(() =>
client.integration.connect.oauth({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
client.integration.oauth.connect({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
)
const attempt = started.data
if (attempt.mode === "code")
@@ -40,7 +40,7 @@ export default Runtime.handler(
process.stdout.write(attempt.instructions + EOL + attempt.url + EOL)
const result = yield* poll(client, attempt.attemptID)
const result = yield* poll(client, integration.id, attempt.attemptID)
if (result.status === "complete") {
process.stdout.write(`Authenticated with ${input.name}` + EOL)
return
@@ -52,15 +52,16 @@ export default Runtime.handler(
const poll = (
client: OpenCodeClient,
integrationID: string,
attemptID: string,
): Effect.Effect<Exclude<IntegrationAttemptStatus, { status: "pending" }>> =>
Effect.gen(function* () {
const status = yield* Effect.promise(() => client.integration.attempt.status({ attemptID, location })).pipe(
Effect.map((result) => result.data),
)
const status = yield* Effect.promise(() =>
client.integration.oauth.status({ integrationID, attemptID, location }),
).pipe(Effect.map((result) => result.data))
if (status.status === "pending") {
yield* Effect.sleep("1 second")
return yield* poll(client, attemptID)
return yield* poll(client, integrationID, attemptID)
}
return status
})
+59 -21
View File
@@ -419,7 +419,7 @@ export type IntegrationConnectKeyOperation<E = never> = (
input: Endpoint10_2Input,
) => Effect.Effect<Endpoint10_2Output, E>
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
export type Endpoint10_3Input = {
readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
readonly location?: Endpoint10_3Request["query"]["location"]
@@ -427,55 +427,93 @@ export type Endpoint10_3Input = {
readonly inputs: Endpoint10_3Request["payload"]["inputs"]
readonly label?: Endpoint10_3Request["payload"]["label"]
}
export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.oauth"]>>
export type IntegrationConnectOauthOperation<E = never> = (
export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.connect"]>>
export type IntegrationOauthConnectOperation<E = never> = (
input: Endpoint10_3Input,
) => Effect.Effect<Endpoint10_3Output, E>
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
export type Endpoint10_4Input = {
readonly integrationID: Endpoint10_4Request["params"]["integrationID"]
readonly attemptID: Endpoint10_4Request["params"]["attemptID"]
readonly location?: Endpoint10_4Request["query"]["location"]
}
export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.status"]>>
export type IntegrationAttemptStatusOperation<E = never> = (
export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.status"]>>
export type IntegrationOauthStatusOperation<E = never> = (
input: Endpoint10_4Input,
) => Effect.Effect<Endpoint10_4Output, E>
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
export type Endpoint10_5Input = {
readonly integrationID: Endpoint10_5Request["params"]["integrationID"]
readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
readonly location?: Endpoint10_5Request["query"]["location"]
readonly code?: Endpoint10_5Request["payload"]["code"]
}
export type Endpoint10_5Output = EffectValue<
ReturnType<RawClient["server.integration"]["integration.attempt.complete"]>
>
export type IntegrationAttemptCompleteOperation<E = never> = (
export type Endpoint10_5Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.complete"]>>
export type IntegrationOauthCompleteOperation<E = never> = (
input: Endpoint10_5Input,
) => Effect.Effect<Endpoint10_5Output, E>
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
export type Endpoint10_6Input = {
readonly integrationID: Endpoint10_6Request["params"]["integrationID"]
readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
readonly location?: Endpoint10_6Request["query"]["location"]
}
export type Endpoint10_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.cancel"]>>
export type IntegrationAttemptCancelOperation<E = never> = (
export type Endpoint10_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.cancel"]>>
export type IntegrationOauthCancelOperation<E = never> = (
input: Endpoint10_6Input,
) => Effect.Effect<Endpoint10_6Output, E>
type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
export type Endpoint10_7Input = {
readonly integrationID: Endpoint10_7Request["params"]["integrationID"]
readonly location?: Endpoint10_7Request["query"]["location"]
readonly methodID: Endpoint10_7Request["payload"]["methodID"]
readonly label?: Endpoint10_7Request["payload"]["label"]
}
export type Endpoint10_7Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.connect"]>>
export type IntegrationCommandConnectOperation<E = never> = (
input: Endpoint10_7Input,
) => Effect.Effect<Endpoint10_7Output, E>
type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
export type Endpoint10_8Input = {
readonly integrationID: Endpoint10_8Request["params"]["integrationID"]
readonly attemptID: Endpoint10_8Request["params"]["attemptID"]
readonly location?: Endpoint10_8Request["query"]["location"]
}
export type Endpoint10_8Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.status"]>>
export type IntegrationCommandStatusOperation<E = never> = (
input: Endpoint10_8Input,
) => Effect.Effect<Endpoint10_8Output, E>
type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
export type Endpoint10_9Input = {
readonly integrationID: Endpoint10_9Request["params"]["integrationID"]
readonly attemptID: Endpoint10_9Request["params"]["attemptID"]
readonly location?: Endpoint10_9Request["query"]["location"]
}
export type Endpoint10_9Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.cancel"]>>
export type IntegrationCommandCancelOperation<E = never> = (
input: Endpoint10_9Input,
) => Effect.Effect<Endpoint10_9Output, E>
export interface IntegrationApi<E = never> {
readonly list: IntegrationListOperation<E>
readonly get: IntegrationGetOperation<E>
readonly connect: {
readonly key: IntegrationConnectKeyOperation<E>
readonly oauth: IntegrationConnectOauthOperation<E>
readonly connect: { readonly key: IntegrationConnectKeyOperation<E> }
readonly oauth: {
readonly connect: IntegrationOauthConnectOperation<E>
readonly status: IntegrationOauthStatusOperation<E>
readonly complete: IntegrationOauthCompleteOperation<E>
readonly cancel: IntegrationOauthCancelOperation<E>
}
readonly attempt: {
readonly status: IntegrationAttemptStatusOperation<E>
readonly complete: IntegrationAttemptCompleteOperation<E>
readonly cancel: IntegrationAttemptCancelOperation<E>
readonly command: {
readonly connect: IntegrationCommandConnectOperation<E>
readonly status: IntegrationCommandStatusOperation<E>
readonly cancel: IntegrationCommandCancelOperation<E>
}
}
+60 -13
View File
@@ -518,7 +518,7 @@ const Endpoint10_2 = (raw: RawClient["server.integration"]) => (input: Endpoint1
payload: { key: input["key"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
type Endpoint10_3Input = {
readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
readonly location?: Endpoint10_3Request["query"]["location"]
@@ -527,52 +527,99 @@ type Endpoint10_3Input = {
readonly label?: Endpoint10_3Request["payload"]["label"]
}
const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) =>
raw["integration.connect.oauth"]({
raw["integration.oauth.connect"]({
params: { integrationID: input["integrationID"] },
query: { location: input["location"] },
payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
type Endpoint10_4Input = {
readonly integrationID: Endpoint10_4Request["params"]["integrationID"]
readonly attemptID: Endpoint10_4Request["params"]["attemptID"]
readonly location?: Endpoint10_4Request["query"]["location"]
}
const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) =>
raw["integration.attempt.status"]({
params: { attemptID: input["attemptID"] },
raw["integration.oauth.status"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
type Endpoint10_5Input = {
readonly integrationID: Endpoint10_5Request["params"]["integrationID"]
readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
readonly location?: Endpoint10_5Request["query"]["location"]
readonly code?: Endpoint10_5Request["payload"]["code"]
}
const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) =>
raw["integration.attempt.complete"]({
params: { attemptID: input["attemptID"] },
raw["integration.oauth.complete"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
payload: { code: input["code"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
type Endpoint10_6Input = {
readonly integrationID: Endpoint10_6Request["params"]["integrationID"]
readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
readonly location?: Endpoint10_6Request["query"]["location"]
}
const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) =>
raw["integration.attempt.cancel"]({
params: { attemptID: input["attemptID"] },
raw["integration.oauth.cancel"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
type Endpoint10_7Input = {
readonly integrationID: Endpoint10_7Request["params"]["integrationID"]
readonly location?: Endpoint10_7Request["query"]["location"]
readonly methodID: Endpoint10_7Request["payload"]["methodID"]
readonly label?: Endpoint10_7Request["payload"]["label"]
}
const Endpoint10_7 = (raw: RawClient["server.integration"]) => (input: Endpoint10_7Input) =>
raw["integration.command.connect"]({
params: { integrationID: input["integrationID"] },
query: { location: input["location"] },
payload: { methodID: input["methodID"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
type Endpoint10_8Input = {
readonly integrationID: Endpoint10_8Request["params"]["integrationID"]
readonly attemptID: Endpoint10_8Request["params"]["attemptID"]
readonly location?: Endpoint10_8Request["query"]["location"]
}
const Endpoint10_8 = (raw: RawClient["server.integration"]) => (input: Endpoint10_8Input) =>
raw["integration.command.status"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
type Endpoint10_9Input = {
readonly integrationID: Endpoint10_9Request["params"]["integrationID"]
readonly attemptID: Endpoint10_9Request["params"]["attemptID"]
readonly location?: Endpoint10_9Request["query"]["location"]
}
const Endpoint10_9 = (raw: RawClient["server.integration"]) => (input: Endpoint10_9Input) =>
raw["integration.command.cancel"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
const adaptGroup10 = (raw: RawClient["server.integration"]) => ({
list: Endpoint10_0(raw),
get: Endpoint10_1(raw),
connect: { key: Endpoint10_2(raw), oauth: Endpoint10_3(raw) },
attempt: { status: Endpoint10_4(raw), complete: Endpoint10_5(raw), cancel: Endpoint10_6(raw) },
connect: { key: Endpoint10_2(raw) },
oauth: {
connect: Endpoint10_3(raw),
status: Endpoint10_4(raw),
complete: Endpoint10_5(raw),
cancel: Endpoint10_6(raw),
},
command: { connect: Endpoint10_7(raw), status: Endpoint10_8(raw), cancel: Endpoint10_9(raw) },
})
type Endpoint11_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
+66 -21
View File
@@ -84,14 +84,20 @@ import type {
IntegrationGetOutput,
IntegrationConnectKeyInput,
IntegrationConnectKeyOutput,
IntegrationConnectOauthInput,
IntegrationConnectOauthOutput,
IntegrationAttemptStatusInput,
IntegrationAttemptStatusOutput,
IntegrationAttemptCompleteInput,
IntegrationAttemptCompleteOutput,
IntegrationAttemptCancelInput,
IntegrationAttemptCancelOutput,
IntegrationOauthConnectInput,
IntegrationOauthConnectOutput,
IntegrationOauthStatusInput,
IntegrationOauthStatusOutput,
IntegrationOauthCompleteInput,
IntegrationOauthCompleteOutput,
IntegrationOauthCancelInput,
IntegrationOauthCancelOutput,
IntegrationCommandConnectInput,
IntegrationCommandConnectOutput,
IntegrationCommandStatusInput,
IntegrationCommandStatusOutput,
IntegrationCommandCancelInput,
IntegrationCommandCancelOutput,
McpListInput,
McpListOutput,
McpResourceCatalogInput,
@@ -901,8 +907,10 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
oauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectOauthOutput>(
},
oauth: {
connect: (input: IntegrationOauthConnectInput, requestOptions?: RequestOptions) =>
request<IntegrationOauthConnectOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
@@ -914,13 +922,11 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
},
attempt: {
status: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptStatusOutput>(
status: (input: IntegrationOauthStatusInput, requestOptions?: RequestOptions) =>
request<IntegrationOauthStatusOutput>(
{
method: "GET",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
@@ -928,11 +934,11 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
complete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCompleteOutput>(
complete: (input: IntegrationOauthCompleteInput, requestOptions?: RequestOptions) =>
request<IntegrationOauthCompleteOutput>(
{
method: "POST",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}/complete`,
query: { location: input["location"] },
body: { code: input["code"] },
successStatus: 204,
@@ -941,11 +947,50 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
cancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCancelOutput>(
cancel: (input: IntegrationOauthCancelInput, requestOptions?: RequestOptions) =>
request<IntegrationOauthCancelOutput>(
{
method: "DELETE",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
command: {
connect: (input: IntegrationCommandConnectInput, requestOptions?: RequestOptions) =>
request<IntegrationCommandConnectOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command`,
query: { location: input["location"] },
body: { methodID: input["methodID"], label: input["label"] },
successStatus: 200,
declaredStatuses: [400, 401],
empty: false,
},
requestOptions,
),
status: (input: IntegrationCommandStatusInput, requestOptions?: RequestOptions) =>
request<IntegrationCommandStatusOutput>(
{
method: "GET",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
cancel: (input: IntegrationCommandCancelInput, requestOptions?: RequestOptions) =>
request<IntegrationCommandCancelOutput>(
{
method: "DELETE",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
+83 -12
View File
@@ -187,6 +187,8 @@ export type ProviderV2Info = {
export type IntegrationWhen = { key: string; op: "eq" | "neq"; value: string }
export type IntegrationCommandMethod = { id: string; type: "command"; label: string; command: Array<string> }
export type IntegrationKeyMethod = { type: "key"; label?: string }
export type IntegrationEnvMethod = { type: "env"; names: Array<string> }
@@ -214,6 +216,31 @@ export type IntegrationAttemptStatus =
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
export type IntegrationCommandAttempt = {
attemptID: string
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
export type IntegrationCommandAttemptStatus =
| {
status: "pending"
message?: string
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "complete"
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "failed"
message: string
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "expired"
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
export type McpStatusConnected = { status: "connected" }
export type McpStatusPending = { status: "pending" }
@@ -1992,7 +2019,11 @@ export type SessionMessageAssistantTool = {
time: { created: number; ran?: number; completed?: number }
}
export type IntegrationMethod = IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod
export type IntegrationMethod =
| IntegrationOAuthMethod
| IntegrationCommandMethod
| IntegrationKeyMethod
| IntegrationEnvMethod
export type FormFields = [FormField, ...Array<FormField>]
@@ -3284,7 +3315,7 @@ export type IntegrationConnectKeyInput = {
export type IntegrationConnectKeyOutput = void
export type IntegrationConnectOauthInput = {
export type IntegrationOauthConnectInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
@@ -3306,7 +3337,7 @@ export type IntegrationConnectOauthInput = {
}["label"]
}
export type IntegrationConnectOauthOutput = {
export type IntegrationOauthConnectOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: {
attemptID: string
@@ -3317,36 +3348,76 @@ export type IntegrationConnectOauthOutput = {
}
}
export type IntegrationAttemptStatusInput = {
readonly attemptID: { readonly attemptID: string }["attemptID"]
export type IntegrationOauthStatusInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type IntegrationAttemptStatusOutput = {
export type IntegrationOauthStatusOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: IntegrationAttemptStatus
}
export type IntegrationAttemptCompleteInput = {
readonly attemptID: { readonly attemptID: string }["attemptID"]
export type IntegrationOauthCompleteInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly code?: { readonly code?: string | undefined }["code"]
}
export type IntegrationAttemptCompleteOutput = void
export type IntegrationOauthCompleteOutput = void
export type IntegrationAttemptCancelInput = {
readonly attemptID: { readonly attemptID: string }["attemptID"]
export type IntegrationOauthCancelInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type IntegrationAttemptCancelOutput = void
export type IntegrationOauthCancelOutput = void
export type IntegrationCommandConnectInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly methodID: { readonly methodID: string; readonly label?: string | undefined }["methodID"]
readonly label?: { readonly methodID: string; readonly label?: string | undefined }["label"]
}
export type IntegrationCommandConnectOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: IntegrationCommandAttempt
}
export type IntegrationCommandStatusInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type IntegrationCommandStatusOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: IntegrationCommandAttemptStatus
}
export type IntegrationCommandCancelInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type IntegrationCommandCancelOutput = void
export type McpListInput = {
readonly location?: {
+4 -3
View File
@@ -36,9 +36,10 @@ test("exposes every standard HTTP API group", () => {
expect(Object.keys(client.debug)).toEqual(["location"])
expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
expect(Object.keys(client.message)).toEqual(["list"])
expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "attempt"])
expect(Object.keys(client.integration.connect)).toEqual(["key", "oauth"])
expect(Object.keys(client.integration.attempt)).toEqual(["status", "complete", "cancel"])
expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "oauth", "command"])
expect(Object.keys(client.integration.connect)).toEqual(["key"])
expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"])
expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"])
expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/codemode",
"version": "1.17.20",
"version": "1.18.2",
"description": "Effect-native confined code execution over schema-described tools",
"private": true,
"type": "module",
+57 -29
View File
@@ -34,30 +34,51 @@ export const isRuntimeReference = (value: unknown): boolean =>
value instanceof ErrorConstructorReference ||
isCodeModeValue(value)
export const containsRuntimeReference = (value: unknown, seen = new Set<object>()): boolean => {
if (isRuntimeReference(value)) return true
if (value === null || typeof value !== "object") return false
if (seen.has(value)) return false
seen.add(value)
const contains = Array.isArray(value)
? value.some((item) => containsRuntimeReference(item, seen))
: Object.values(value).some((item) => containsRuntimeReference(item, seen))
seen.delete(value)
return contains
function* childValues(value: object): Generator<unknown> {
if (Array.isArray(value)) {
const length = value.length
for (let index = 0; index < length; index++) yield value[index]
return
}
yield* Object.values(value)
}
export const containsRuntimeReference = (value: unknown): boolean => {
const pending: Array<Iterator<unknown>> = [[value].values()]
const seen = new Set<object>()
while (pending.length > 0) {
const next = pending.at(-1)!.next()
if (next.done) {
pending.pop()
continue
}
const current = next.value
if (isRuntimeReference(current)) return true
if (current === null || typeof current !== "object" || seen.has(current)) continue
seen.add(current)
pending.push(childValues(current))
}
return false
}
// CodeMode values are data here, not opaque interpreter references.
export const containsOpaqueReference = (value: unknown, seen = new Set<object>()): boolean => {
if (isCodeModeValue(value)) return false
if (isRuntimeReference(value)) return true
if (value === null || typeof value !== "object") return false
if (seen.has(value)) return false
seen.add(value)
const contains = Array.isArray(value)
? value.some((item) => containsOpaqueReference(item, seen))
: Object.values(value).some((item) => containsOpaqueReference(item, seen))
seen.delete(value)
return contains
export const containsOpaqueReference = (value: unknown): boolean => {
const pending: Array<Iterator<unknown>> = [[value].values()]
const seen = new Set<object>()
while (pending.length > 0) {
const next = pending.at(-1)!.next()
if (next.done) {
pending.pop()
continue
}
const current = next.value
if (isCodeModeValue(current)) continue
if (isRuntimeReference(current)) return true
if (current === null || typeof current !== "object" || seen.has(current)) continue
seen.add(current)
pending.push(childValues(current))
}
return false
}
// Reject cycles before mutation so later boundary walks remain safe.
@@ -66,15 +87,22 @@ export const rejectCircularInsertion = (
value: unknown,
label: string,
node: AstNode,
seen = new Set<object>(),
): void => {
if (value === container)
throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue")
if (value === null || typeof value !== "object" || isRuntimeReference(value) || seen.has(value)) return
seen.add(value)
const items = Array.isArray(value) ? value : Object.values(value)
for (const item of items) rejectCircularInsertion(container, item, label, node, seen)
seen.delete(value)
const pending: Array<Iterator<unknown>> = [[value].values()]
const seen = new Set<object>()
while (pending.length > 0) {
const next = pending.at(-1)!.next()
if (next.done) {
pending.pop()
continue
}
const current = next.value
if (current === container)
throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue")
if (current === null || typeof current !== "object" || isRuntimeReference(current) || seen.has(current)) continue
seen.add(current)
pending.push(Array.isArray(current) ? current[Symbol.iterator]() : childValues(current))
}
}
export const typeofValue = (value: unknown): string => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.17.20",
"version": "1.18.2",
"type": "module",
"license": "MIT",
"scripts": {
+2
View File
@@ -9,6 +9,7 @@ import "./app.css"
import { LanguageProvider } from "~/context/language"
import { I18nProvider, useI18n } from "~/context/i18n"
import { strip } from "~/lib/language"
import { DesktopPromo } from "~/component/desktop-promo"
function AppMeta() {
const i18n = useI18n()
@@ -33,6 +34,7 @@ export default function App() {
<MetaProvider>
<AppMeta />
<Suspense>{props.children}</Suspense>
<DesktopPromo />
</MetaProvider>
</I18nProvider>
</LanguageProvider>
@@ -0,0 +1,103 @@
[data-component="desktop-promo"] {
--promo-background: hsl(0, 20%, 99%);
--promo-background-weak: hsl(0, 8%, 97%);
--promo-text: hsl(0, 1%, 39%);
--promo-text-strong: hsl(0, 5%, 12%);
--promo-border: hsla(0, 100%, 3%, 0.12);
position: fixed;
z-index: 20;
right: 1.5rem;
bottom: 1.5rem;
width: min(28rem, calc(100vw - 2rem));
padding: 4px;
overflow: hidden;
color: var(--promo-text);
border: 1px solid var(--promo-border);
border-radius: 8px;
background: var(--promo-background);
box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 15%);
font-family: var(--font-mono);
@media (prefers-color-scheme: dark) {
--promo-background: hsl(0, 9%, 7%);
--promo-background-weak: hsl(0, 6%, 10%);
--promo-text: hsl(0, 4%, 71%);
--promo-text-strong: hsl(0, 15%, 94%);
--promo-border: hsl(0, 4%, 23%);
}
@media (max-width: 40rem) {
right: 1rem;
bottom: 1rem;
}
[data-slot="desktop-promo-link"] {
display: block;
color: var(--promo-text);
text-decoration: none;
}
[data-slot="desktop-promo-link"]:focus-visible {
outline: 2px solid var(--promo-text-strong);
outline-offset: -3px;
}
video {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 4px;
background: var(--promo-background-weak);
}
[data-slot="desktop-promo-copy"] {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 1rem;
font-size: 0.875rem;
line-height: 1.4;
}
[data-slot="desktop-promo-copy"] strong {
color: var(--promo-text-strong);
font-weight: 500;
}
[data-slot="desktop-promo-close"] {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: grid;
width: 2rem;
height: 2rem;
padding: 0;
place-items: center;
cursor: pointer;
color: white;
border: none;
border-radius: 0.25rem;
background: rgb(0 0 0 / 70%);
opacity: 0;
transition:
opacity 150ms ease,
background 150ms ease;
}
&:hover [data-slot="desktop-promo-close"],
[data-slot="desktop-promo-close"]:focus-visible {
opacity: 1;
}
[data-slot="desktop-promo-close"]:hover {
background: rgb(0 0 0 / 90%);
}
@media (hover: none) {
[data-slot="desktop-promo-close"] {
opacity: 1;
}
}
}
@@ -0,0 +1,48 @@
import "./desktop-promo.css"
import { A, useLocation } from "@solidjs/router"
import { createSignal, Show } from "solid-js"
import { getRequestEvent } from "solid-js/web"
import desktopPromoVideo from "~/asset/lander/desktop-tabs-landscape.mp4"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
import { strip } from "~/lib/language"
export function DesktopPromo() {
const i18n = useI18n()
const language = useLanguage()
const location = useLocation()
const [visible, setVisible] = createSignal(true)
const request = getRequestEvent()?.request
const hostname = request ? new URL(request.url).hostname : typeof window === "object" ? window.location.hostname : ""
const primaryHost =
hostname === "opencode.ai" || hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"
return (
<Show
when={
visible() &&
primaryHost &&
strip(location.pathname) !== "/download" &&
!strip(location.pathname).startsWith("/download/")
}
>
<aside data-component="desktop-promo">
<A href={language.route("/download")} data-slot="desktop-promo-link">
<video src={desktopPromoVideo} autoplay playsinline loop muted preload="metadata" aria-hidden="true" />
<span data-slot="desktop-promo-copy">
<strong>{i18n.t("home.promo.title")}</strong>
<span>
{i18n.t("home.promo.body")} {i18n.t("home.promo.cta")}
</span>
</span>
</A>
<button type="button" data-slot="desktop-promo-close" onClick={() => setVisible(false)}>
<span class="sr-only">{i18n.t("home.promo.close")}</span>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M5 5L15 15M15 5L5 15" stroke="currentColor" stroke-width="1.5" />
</svg>
</button>
</aside>
</Show>
)
}
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "شعار opencode الداكن",
"home.banner.badge": "جديد",
"home.banner.text": "تطبيق سطح المكتب متاح بنسخة تجريبية",
"home.banner.platforms": "على macOS، Windows، وLinux",
"home.banner.text": "نقدّم علامات التبويب لتطبيق سطح المكتب.",
"home.banner.platforms": "متاحة على macOS وWindows وLinux",
"home.banner.downloadNow": "حمّل الآن",
"home.banner.downloadBetaNow": "حمّل النسخة التجريبية لتطبيق سطح المكتب الآن",
"home.promo.title": "نقدّم علامات التبويب لتطبيق سطح المكتب",
"home.promo.body": "نظّم عملك وجلساتك النشطة باستخدام علامات التبويب.",
"home.promo.cta": "نزّل أحدث إصدار للبدء.",
"home.promo.close": "إغلاق إعلان تطبيق سطح المكتب",
"home.hero.title": "وكيل برمجة بالذكاء الاصطناعي مفتوح المصدر",
"home.hero.subtitle.a": "نماذج مجانية مضمّنة أو اربط أي نموذج من أي مزوّد،",
@@ -700,11 +704,11 @@ export const dict = {
"download.title": "OpenCode | تنزيل",
"download.meta.description": "نزّل OpenCode لـ macOS، Windows، وLinux",
"download.hero.title": "تنزيل OpenCode",
"download.hero.title": "تنزيل OpenCode لسطح المكتب",
"download.hero.subtitle": "متاح في نسخة تجريبية لـ macOS، Windows، وLinux",
"download.hero.button": "تنزيل لـ {{os}}",
"download.section.terminal": "OpenCode للطرفية",
"download.section.desktop": "OpenCode لسطح المكتب (Beta)",
"download.section.desktop": "OpenCode لسطح المكتب",
"download.section.extensions": "امتدادات OpenCode",
"download.section.integrations": "تكاملات OpenCode",
"download.action.download": "تنزيل",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "logo opencode escuro",
"home.banner.badge": "Novo",
"home.banner.text": "App desktop disponível em beta",
"home.banner.platforms": "no macOS, Windows e Linux",
"home.banner.text": "Conheça as abas no app desktop.",
"home.banner.platforms": "Disponível no macOS, Windows e Linux",
"home.banner.downloadNow": "Baixar agora",
"home.banner.downloadBetaNow": "Baixe agora o beta do desktop",
"home.promo.title": "Conheça as abas no app desktop",
"home.promo.body": "Organize seu trabalho e suas sessões ativas com abas.",
"home.promo.cta": "Baixe a versão mais recente para começar.",
"home.promo.close": "Fechar anúncio do app desktop",
"home.hero.title": "O agente de codificação de código aberto com IA",
"home.hero.subtitle.a": "Modelos grátis incluídos ou conecte qualquer modelo de qualquer provedor,",
@@ -711,11 +715,11 @@ export const dict = {
"download.title": "OpenCode | Baixar",
"download.meta.description": "Baixe o OpenCode para macOS, Windows e Linux",
"download.hero.title": "Baixar OpenCode",
"download.hero.title": "Baixar OpenCode Desktop",
"download.hero.subtitle": "Disponível em Beta para macOS, Windows e Linux",
"download.hero.button": "Baixar para {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "Extensões OpenCode",
"download.section.integrations": "Integrações OpenCode",
"download.action.download": "Baixar",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "opencode logo dark",
"home.banner.badge": "Ny",
"home.banner.text": "Desktop-app tilgængelig i beta",
"home.banner.platforms": "på macOS, Windows og Linux",
"home.banner.text": "Introduktion af Tabs til Desktop.",
"home.banner.platforms": "Tilgængelig på macOS, Windows og Linux",
"home.banner.downloadNow": "Download nu",
"home.banner.downloadBetaNow": "Download desktop-betaen nu",
"home.promo.title": "Introduktion af Tabs til Desktop",
"home.promo.body": "Organiser dit arbejde og aktive sessioner med faner.",
"home.promo.cta": "Download den nyeste version for at komme i gang.",
"home.promo.close": "Luk meddelelsen om Desktop-appen",
"home.hero.title": "Den open source AI-kodningsagent",
"home.hero.subtitle.a": "Gratis modeller inkluderet, eller forbind enhver model fra enhver udbyder,",
@@ -706,11 +710,11 @@ export const dict = {
"download.title": "OpenCode | Download",
"download.meta.description": "Download OpenCode til macOS, Windows og Linux",
"download.hero.title": "Download OpenCode",
"download.hero.title": "Download OpenCode Desktop",
"download.hero.subtitle": "Tilgængelig i beta til macOS, Windows og Linux",
"download.hero.button": "Download til {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrations",
"download.action.download": "Download",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "OpenCode Logo dunkel",
"home.banner.badge": "Neu",
"home.banner.text": "Desktop-App in der Beta verfügbar",
"home.banner.platforms": "auf macOS, Windows und Linux",
"home.banner.text": "Neu: Tabs für Desktop.",
"home.banner.platforms": "Verfügbar für macOS, Windows und Linux",
"home.banner.downloadNow": "Jetzt herunterladen",
"home.banner.downloadBetaNow": "Desktop-Beta jetzt herunterladen",
"home.promo.title": "Neu: Tabs für Desktop",
"home.promo.body": "Organisiere deine Arbeit und aktiven Sitzungen mit Tabs.",
"home.promo.cta": "Lade die neueste Version herunter, um loszulegen.",
"home.promo.close": "Ankündigung zur Desktop-App schließen",
"home.hero.title": "Der Open-Source AI-Coding-Agent",
"home.hero.subtitle.a": "Kostenlose Modelle inklusive oder verbinde jedes Modell eines beliebigen Anbieters,",
@@ -711,11 +715,11 @@ export const dict = {
"download.title": "OpenCode | Download",
"download.meta.description": "Lade OpenCode für macOS, Windows und Linux herunter",
"download.hero.title": "OpenCode herunterladen",
"download.hero.title": "OpenCode Desktop herunterladen",
"download.hero.subtitle": "In Beta verfügbar für macOS, Windows und Linux",
"download.hero.button": "Download für {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrationen",
"download.action.download": "Download",
+9 -4
View File
@@ -102,11 +102,16 @@ export const dict = {
"temp.logoDarkAlt": "opencode logo dark",
"home.banner.badge": "New",
"home.banner.text": "Desktop app available in beta",
"home.banner.platforms": "on macOS, Windows, and Linux",
"home.banner.text": "Introducing tabs for desktop.",
"home.banner.platforms": "Available on macOS, Windows, and Linux",
"home.banner.downloadNow": "Download now",
"home.banner.downloadBetaNow": "Download the desktop beta now",
"home.promo.title": "Introducing Tabs for Desktop",
"home.promo.body": "Organize your work and active sessions with tabs.",
"home.promo.cta": "Download the latest to get started.",
"home.promo.close": "Dismiss desktop app announcement",
"home.hero.title": "The open source AI coding agent",
"home.hero.subtitle.a": "Free models included or connect any model from any provider,",
"home.hero.subtitle.b": "including Claude, GPT, Gemini and more.",
@@ -703,11 +708,11 @@ export const dict = {
"download.title": "OpenCode | Download",
"download.meta.description": "Download OpenCode for macOS, Windows, and Linux",
"download.hero.title": "Download OpenCode",
"download.hero.title": "Download OpenCode Desktop",
"download.hero.subtitle": "Available in Beta for macOS, Windows, and Linux",
"download.hero.button": "Download for {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrations",
"download.action.download": "Download",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "logo de opencode oscuro",
"home.banner.badge": "Nuevo",
"home.banner.text": "Aplicación de escritorio disponible en beta",
"home.banner.platforms": "en macOS, Windows y Linux",
"home.banner.text": "Presentamos Tabs para Desktop.",
"home.banner.platforms": "Disponible en macOS, Windows y Linux",
"home.banner.downloadNow": "Descargar ahora",
"home.banner.downloadBetaNow": "Descargar la beta de escritorio ahora",
"home.promo.title": "Presentamos Tabs para Desktop",
"home.promo.body": "Organiza tu trabajo y tus sesiones activas con pestañas.",
"home.promo.cta": "Descarga la última versión para empezar.",
"home.promo.close": "Cerrar el anuncio de la aplicación Desktop",
"home.hero.title": "El agente de codificación IA de código abierto",
"home.hero.subtitle.a": "Modelos gratuitos incluidos o conecta cualquier modelo de cualquier proveedor,",
@@ -711,11 +715,11 @@ export const dict = {
"download.title": "OpenCode | Descargar",
"download.meta.description": "Descarga OpenCode para macOS, Windows y Linux",
"download.hero.title": "Descargar OpenCode",
"download.hero.title": "Descargar OpenCode Desktop",
"download.hero.subtitle": "Disponible en Beta para macOS, Windows y Linux",
"download.hero.button": "Descargar para {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "Extensiones OpenCode",
"download.section.integrations": "Integraciones OpenCode",
"download.action.download": "Descargar",
+8 -4
View File
@@ -100,10 +100,14 @@ export const dict = {
"temp.logoDarkAlt": "opencode logo dark",
"home.banner.badge": "Nouveau",
"home.banner.text": "Application desktop disponible en bêta",
"home.banner.platforms": "sur macOS, Windows et Linux",
"home.banner.text": "Découvrez Tabs pour Desktop.",
"home.banner.platforms": "Disponible sur macOS, Windows et Linux",
"home.banner.downloadNow": "Télécharger maintenant",
"home.banner.downloadBetaNow": "Télécharger la bêta desktop maintenant",
"home.promo.title": "Découvrez Tabs pour Desktop",
"home.promo.body": "Organisez votre travail et vos sessions actives avec des onglets.",
"home.promo.cta": "Téléchargez la dernière version pour commencer.",
"home.promo.close": "Fermer lannonce de lapplication Desktop",
"home.hero.title": "L'agent de code IA open source",
"home.hero.subtitle.a":
@@ -718,11 +722,11 @@ export const dict = {
"download.title": "OpenCode | Téléchargement",
"download.meta.description": "Téléchargez OpenCode pour macOS, Windows et Linux",
"download.hero.title": "Télécharger OpenCode",
"download.hero.title": "Télécharger OpenCode Desktop",
"download.hero.subtitle": "Disponible en bêta pour macOS, Windows et Linux",
"download.hero.button": "Télécharger pour {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Bêta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "Extensions OpenCode",
"download.section.integrations": "Intégrations OpenCode",
"download.action.download": "Télécharger",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "logo scuro di opencode",
"home.banner.badge": "Nuovo",
"home.banner.text": "App desktop disponibile in beta",
"home.banner.platforms": "su macOS, Windows e Linux",
"home.banner.text": "Scopri Tabs per Desktop.",
"home.banner.platforms": "Disponibile su macOS, Windows e Linux",
"home.banner.downloadNow": "Scarica ora",
"home.banner.downloadBetaNow": "Scarica ora la beta desktop",
"home.promo.title": "Scopri Tabs per Desktop",
"home.promo.body": "Organizza il tuo lavoro e le sessioni attive con le schede.",
"home.promo.cta": "Scarica lultima versione per iniziare.",
"home.promo.close": "Chiudi lannuncio dellapp Desktop",
"home.hero.title": "L'agente di coding IA open source",
"home.hero.subtitle.a": "Modelli gratuiti inclusi o collega qualsiasi modello da qualsiasi provider,",
@@ -709,11 +713,11 @@ export const dict = {
"download.title": "OpenCode | Download",
"download.meta.description": "Scarica OpenCode per macOS, Windows e Linux",
"download.hero.title": "Scarica OpenCode",
"download.hero.title": "Scarica OpenCode Desktop",
"download.hero.subtitle": "Disponibile in Beta per macOS, Windows e Linux",
"download.hero.button": "Scarica per {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrations",
"download.action.download": "Scarica",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "opencodeのロゴ(ダーク)",
"home.banner.badge": "新着",
"home.banner.text": "デスクトップアプリのベータ版が利用可能",
"home.banner.platforms": "macOS、Windows、Linux で",
"home.banner.text": "デスクトップ版にタブが登場。",
"home.banner.platforms": "macOS、Windows、Linux で利用できます",
"home.banner.downloadNow": "今すぐダウンロード",
"home.banner.downloadBetaNow": "デスクトップベータ版を今すぐダウンロード",
"home.promo.title": "デスクトップ版にタブが登場",
"home.promo.body": "タブで作業とアクティブなセッションを整理できます。",
"home.promo.cta": "始めるには最新版をダウンロードしてください。",
"home.promo.close": "デスクトップアプリのお知らせを閉じる",
"home.hero.title": "オープンソースのAIコーディングエージェント",
"home.hero.subtitle.a": "無料モデルが含まれています。また、任意のプロバイダーの任意のモデルに接続でき、",
@@ -708,11 +712,11 @@ export const dict = {
"download.title": "OpenCode | ダウンロード",
"download.meta.description": "OpenCode を macOS、Windows、Linux 向けにダウンロード",
"download.hero.title": "OpenCode をダウンロード",
"download.hero.title": "OpenCode デスクトップ版をダウンロード",
"download.hero.subtitle": "macOS、Windows、Linux 向けベータ版を利用可能",
"download.hero.button": "{{os}} 向けダウンロード",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrations",
"download.action.download": "ダウンロード",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "opencode 어두운 로고",
"home.banner.badge": "신규",
"home.banner.text": "데스크톱 앱 베타 버전 출시",
"home.banner.platforms": "macOS, Windows, Linux 지원",
"home.banner.text": "데스크톱 탭을 소개합니다.",
"home.banner.platforms": "macOS, Windows, Linux에서 사용할 수 있습니다",
"home.banner.downloadNow": "지금 다운로드",
"home.banner.downloadBetaNow": "데스크톱 베타 다운로드",
"home.promo.title": "데스크톱 탭을 소개합니다",
"home.promo.body": "탭으로 작업과 활성 세션을 정리하세요.",
"home.promo.cta": "최신 버전을 다운로드하여 시작하세요.",
"home.promo.close": "데스크톱 앱 안내 닫기",
"home.hero.title": "오픈 소스 AI 코딩 에이전트",
"home.hero.subtitle.a": "무료 모델이 포함되어 있으며, 어떤 제공자의 모델이든 연결 가능합니다.",
@@ -700,11 +704,11 @@ export const dict = {
"download.title": "OpenCode | 다운로드",
"download.meta.description": "macOS, Windows, Linux용 OpenCode 다운로드",
"download.hero.title": "OpenCode 다운로드",
"download.hero.title": "OpenCode 데스크톱 다운로드",
"download.hero.subtitle": "macOS, Windows, Linux용 베타 버전 사용 가능",
"download.hero.button": "{{os}}용 다운로드",
"download.section.terminal": "OpenCode 터미널",
"download.section.desktop": "OpenCode 데스크톱 (베타)",
"download.section.desktop": "OpenCode 데스크톱",
"download.section.extensions": "OpenCode 확장 프로그램",
"download.section.integrations": "OpenCode 통합",
"download.action.download": "다운로드",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "opencode logo mørk",
"home.banner.badge": "Ny",
"home.banner.text": "Desktop-app tilgjengelig i beta",
"home.banner.platforms": "på macOS, Windows og Linux",
"home.banner.text": "Vi introduserer Tabs for Desktop.",
"home.banner.platforms": "Tilgjengelig på macOS, Windows og Linux",
"home.banner.downloadNow": "Last ned nå",
"home.banner.downloadBetaNow": "Last ned desktop-betaen nå",
"home.promo.title": "Vi introduserer Tabs for Desktop",
"home.promo.body": "Organiser arbeidet ditt og aktive økter med faner.",
"home.promo.cta": "Last ned den nyeste versjonen for å komme i gang.",
"home.promo.close": "Lukk kunngjøringen om Desktop-appen",
"home.hero.title": "Den åpne kildekode AI-kodingsagenten",
"home.hero.subtitle.a":
@@ -707,11 +711,11 @@ export const dict = {
"download.title": "OpenCode | Last ned",
"download.meta.description": "Last ned OpenCode for macOS, Windows og Linux",
"download.hero.title": "Last ned OpenCode",
"download.hero.title": "Last ned OpenCode Desktop",
"download.hero.subtitle": "Tilgjengelig i beta for macOS, Windows og Linux",
"download.hero.button": "Last ned for {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrations",
"download.action.download": "Last ned",
+8 -4
View File
@@ -101,10 +101,14 @@ export const dict = {
"temp.logoDarkAlt": "ciemne logo opencode",
"home.banner.badge": "Nowość",
"home.banner.text": "Aplikacja desktopowa dostępna w wersji beta",
"home.banner.platforms": "na macOS, Windows i Linux",
"home.banner.text": "Przedstawiamy karty w aplikacji desktopowej.",
"home.banner.platforms": "Dostępne na macOS, Windows i Linux",
"home.banner.downloadNow": "Pobierz teraz",
"home.banner.downloadBetaNow": "Pobierz betę wersji desktopowej",
"home.promo.title": "Przedstawiamy karty w aplikacji desktopowej",
"home.promo.body": "Organizuj swoją pracę i aktywne sesje za pomocą kart.",
"home.promo.cta": "Pobierz najnowszą wersję, aby rozpocząć.",
"home.promo.close": "Zamknij ogłoszenie o aplikacji desktopowej",
"home.hero.title": "Open source'owy agent AI do kodowania",
"home.hero.subtitle.a": "Darmowe modele w zestawie lub podłącz dowolny model od dowolnego dostawcy,",
@@ -708,11 +712,11 @@ export const dict = {
"download.title": "OpenCode | Pobierz",
"download.meta.description": "Pobierz OpenCode na macOS, Windows i Linux",
"download.hero.title": "Pobierz OpenCode",
"download.hero.title": "Pobierz OpenCode Desktop",
"download.hero.subtitle": "Dostępne w wersji Beta na macOS, Windows i Linux",
"download.hero.button": "Pobierz na {{os}}",
"download.section.terminal": "Terminal OpenCode",
"download.section.desktop": "Pulpit OpenCode (Beta)",
"download.section.desktop": "Pulpit OpenCode",
"download.section.extensions": "Rozszerzenia OpenCode",
"download.section.integrations": "Integracje OpenCode",
"download.action.download": "Pobierz",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "темный логотип opencode",
"home.banner.badge": "Новое",
"home.banner.text": "Доступно десктопное приложение (бета)",
"home.banner.platforms": "на macOS, Windows и Linux",
"home.banner.text": "Представляем вкладки в десктопном приложении.",
"home.banner.platforms": "Доступно на macOS, Windows и Linux",
"home.banner.downloadNow": "Скачать",
"home.banner.downloadBetaNow": "Скачать бету для десктопа",
"home.promo.title": "Представляем вкладки в десктопном приложении",
"home.promo.body": "Организуйте работу и активные сессии с помощью вкладок.",
"home.promo.cta": "Скачайте последнюю версию, чтобы начать.",
"home.promo.close": "Закрыть объявление о десктопном приложении",
"home.hero.title": "AI-агент с открытым кодом для программирования",
"home.hero.subtitle.a": "Бесплатные модели включены, или подключите любую модель от любого провайдера,",
@@ -716,11 +720,11 @@ export const dict = {
"download.title": "OpenCode | Скачать",
"download.meta.description": "Скачать OpenCode для macOS, Windows и Linux",
"download.hero.title": "Скачать OpenCode",
"download.hero.title": "Скачать OpenCode Desktop",
"download.hero.subtitle": "Доступна бета для macOS, Windows и Linux",
"download.hero.button": "Скачать для {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "Расширения OpenCode",
"download.section.integrations": "Интеграции OpenCode",
"download.action.download": "Скачать",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "โลโก้ opencode แบบมืด",
"home.banner.badge": "ใหม่",
"home.banner.text": "แอปเดสก์ท็อปพร้อมใช้งานในเวอร์ชันเบต้า",
"home.banner.platforms": "บน macOS, Windows และ Linux",
"home.banner.text": "ขอแนะนำแท็บสำหรับเดสก์ท็อป.",
"home.banner.platforms": "พร้อมใช้งานบน macOS, Windows และ Linux",
"home.banner.downloadNow": "ดาวน์โหลดตอนนี้",
"home.banner.downloadBetaNow": "ดาวน์โหลดเบต้าเดสก์ท็อปตอนนี้",
"home.promo.title": "ขอแนะนำแท็บสำหรับเดสก์ท็อป",
"home.promo.body": "จัดระเบียบงานและเซสชันที่ใช้งานอยู่ด้วยแท็บ",
"home.promo.cta": "ดาวน์โหลดเวอร์ชันล่าสุดเพื่อเริ่มต้นใช้งาน",
"home.promo.close": "ปิดประกาศเกี่ยวกับแอปเดสก์ท็อป",
"home.hero.title": "เอเจนต์เขียนโค้ดด้วย AI แบบโอเพนซอร์ส",
"home.hero.subtitle.a": "มีโมเดลฟรีรวมอยู่ หรือเชื่อมต่อโมเดลใดก็ได้จากผู้ให้บริการรายใดก็ได้,",
@@ -703,11 +707,11 @@ export const dict = {
"download.title": "OpenCode | ดาวน์โหลด",
"download.meta.description": "ดาวน์โหลด OpenCode สำหรับ macOS, Windows และ Linux",
"download.hero.title": "ดาวน์โหลด OpenCode",
"download.hero.title": "ดาวน์โหลด OpenCode สำหรับเดสก์ท็อป",
"download.hero.subtitle": "พร้อมใช้งานในเวอร์ชันเบต้าสำหรับ macOS, Windows และ Linux",
"download.hero.button": "ดาวน์โหลดสำหรับ {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "ส่วนขยาย OpenCode",
"download.section.integrations": "การเชื่อมต่อ OpenCode",
"download.action.download": "ดาวน์โหลด",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "opencode koyu logo",
"home.banner.badge": "Yeni",
"home.banner.text": "Masaüstü uygulaması beta olarak mevcut",
"home.banner.platforms": "macOS, Windows ve Linux'ta",
"home.banner.text": "Masaüstü uygulamasında sekmelerle tanışın.",
"home.banner.platforms": "macOS, Windows ve Linux'ta kullanılabilir",
"home.banner.downloadNow": "Şimdi indir",
"home.banner.downloadBetaNow": "Masaüstü betayı şimdi indir",
"home.promo.title": "Masaüstü uygulamasında sekmelerle tanışın",
"home.promo.body": "Çalışmalarınızı ve etkin oturumlarınızı sekmelerle düzenleyin.",
"home.promo.cta": "Başlamak için en son sürümü indirin.",
"home.promo.close": "Masaüstü uygulaması duyurusunu kapat",
"home.hero.title": "Açık kaynaklı yapay zeka kodlama ajanı",
"home.hero.subtitle.a": "Ücretsiz modeller dahil veya herhangi bir sağlayıcıdan herhangi bir modeli bağlayın,",
@@ -711,11 +715,11 @@ export const dict = {
"download.title": "OpenCode | İndir",
"download.meta.description": "OpenCode'u macOS, Windows ve Linux için indirin",
"download.hero.title": "OpenCode'u İndir",
"download.hero.title": "OpenCode Desktop'u İndir",
"download.hero.subtitle": "macOS, Windows ve Linux için Beta olarak sunuluyor",
"download.hero.button": "{{os}} için indir",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.desktop": "OpenCode Desktop",
"download.section.extensions": "OpenCode Eklentileri",
"download.section.integrations": "OpenCode Entegrasyonları",
"download.action.download": "İndir",
+8 -4
View File
@@ -102,10 +102,14 @@ export const dict = {
"temp.logoDarkAlt": "темний логотип opencode",
"home.banner.badge": "Нове",
"home.banner.text": "Десктопний застосунок доступний у бета-версії",
"home.banner.platforms": "на macOS, Windows та Linux",
"home.banner.text": "Представляємо вкладки в десктопному застосунку.",
"home.banner.platforms": "Доступно на macOS, Windows та Linux",
"home.banner.downloadNow": "Завантажити зараз",
"home.banner.downloadBetaNow": "Завантажити бета-версію десктопного застосунку",
"home.promo.title": "Представляємо вкладки в десктопному застосунку",
"home.promo.body": "Упорядковуйте роботу й активні сесії за допомогою вкладок.",
"home.promo.cta": "Завантажте останню версію, щоб почати.",
"home.promo.close": "Закрити оголошення про десктопний застосунок",
"home.hero.title": "Відкритий AI-агент для кодування",
"home.hero.subtitle.a": "Безкоштовні моделі включено або підключіть будь-яку модель від будь-якого провайдера,",
@@ -670,11 +674,11 @@ export const dict = {
"download.title": "OpenCode | Завантажити",
"download.meta.description": "Завантажте OpenCode для macOS, Windows та Linux",
"download.hero.title": "Завантажити OpenCode",
"download.hero.title": "Завантажити OpenCode Desktop",
"download.hero.subtitle": "Доступно в бета-версії для macOS, Windows та Linux",
"download.hero.button": "Завантажити для {{os}}",
"download.section.terminal": "Термінал OpenCode",
"download.section.desktop": "Десктоп OpenCode (Бета)",
"download.section.desktop": "Десктоп OpenCode",
"download.section.extensions": "Розширення OpenCode",
"download.section.integrations": "Інтеграції OpenCode",
"download.action.download": "Завантажити",
+8 -4
View File
@@ -103,10 +103,14 @@ export const dict = {
"temp.logoDarkAlt": "opencode logo 暗色",
"home.banner.badge": "新",
"home.banner.text": "桌面应用 Beta 版现已推出",
"home.banner.platforms": "支持 macOS, Windows, 和 Linux",
"home.banner.text": "桌面版标签页现已推出",
"home.banner.platforms": "适用于 macOSWindows 和 Linux",
"home.banner.downloadNow": "立即下载",
"home.banner.downloadBetaNow": "立即下载桌面 Beta 版",
"home.promo.title": "桌面版标签页现已推出",
"home.promo.body": "使用标签页整理工作和活跃会话。",
"home.promo.cta": "下载最新版本即可开始使用。",
"home.promo.close": "关闭桌面应用公告",
"home.hero.title": "开源 AI 编程代理",
"home.hero.subtitle.a": "内置免费模型,或连接任意提供商的任意模型,",
@@ -682,11 +686,11 @@ export const dict = {
"download.title": "OpenCode | 下载",
"download.meta.description": "下载适用于 macOS, Windows, 和 Linux 的 OpenCode",
"download.hero.title": "下载 OpenCode",
"download.hero.title": "下载 OpenCode 桌面版",
"download.hero.subtitle": "适用于 macOS, Windows, 和 Linux 的 Beta 版",
"download.hero.button": "下载 {{os}} 版",
"download.section.terminal": "OpenCode 终端",
"download.section.desktop": "OpenCode 桌面版 (Beta)",
"download.section.desktop": "OpenCode 桌面版",
"download.section.extensions": "OpenCode 扩展",
"download.section.integrations": "OpenCode 集成",
"download.action.download": "下载",
+8 -4
View File
@@ -103,10 +103,14 @@ export const dict = {
"temp.logoDarkAlt": "opencode 深色標誌",
"home.banner.badge": "新",
"home.banner.text": "桌面應用已推出 Beta",
"home.banner.platforms": "支援 macOS、Windows 與 Linux",
"home.banner.text": "桌面版分頁功能全新推出。",
"home.banner.platforms": "適用於 macOS、Windows 與 Linux",
"home.banner.downloadNow": "立即下載",
"home.banner.downloadBetaNow": "立即下載桌面 Beta 版",
"home.promo.title": "桌面版分頁功能全新推出",
"home.promo.body": "使用分頁整理工作和作用中的工作階段。",
"home.promo.cta": "下載最新版本即可開始使用。",
"home.promo.close": "關閉桌面應用程式公告",
"home.hero.title": "開源 AI 編碼代理",
"home.hero.subtitle.a": "內建免費模型,或連接任意供應商的任意模型,",
@@ -682,11 +686,11 @@ export const dict = {
"download.title": "OpenCode | 下載",
"download.meta.description": "下載適用於 macOS、Windows 與 Linux 的 OpenCode",
"download.hero.title": "下載 OpenCode",
"download.hero.title": "下載 OpenCode 桌面版",
"download.hero.subtitle": "適用於 macOS、Windows 與 Linux 的 Beta 版現已提供",
"download.hero.button": "下載 {{os}} 版",
"download.section.terminal": "OpenCode 終端",
"download.section.desktop": "OpenCode 桌面版Beta",
"download.section.desktop": "OpenCode 桌面版",
"download.section.extensions": "OpenCode 擴充功能",
"download.section.integrations": "OpenCode 整合",
"download.action.download": "下載",
@@ -284,7 +284,7 @@
}
[data-component="content"] {
padding: 6rem 5rem;
padding: 3rem 5rem 6rem;
@media (max-width: 60rem) {
padding: 4rem 1.5rem;
@@ -316,67 +316,43 @@
/* Download Hero Section */
[data-component="download-hero"] {
/* display: grid; */
display: none;
grid-template-columns: 260px 1fr;
display: grid;
grid-template-columns: 255px 1fr;
gap: 4rem;
padding-bottom: 2rem;
padding: 4px;
border: 1px solid var(--color-border-weak);
border-radius: 8px;
margin-bottom: 4rem;
@media (max-width: 50rem) {
grid-template-columns: 1fr;
gap: 1.5rem;
padding-bottom: 2rem;
gap: 0;
margin-bottom: 2rem;
}
[data-component="hero-icon"] {
display: flex;
justify-content: flex-end;
align-items: center;
@media (max-width: 40rem) {
display: none;
}
[data-slot="icon-placeholder"] {
width: 120px;
height: 120px;
background: var(--color-background-weak);
border: 1px solid var(--color-border-weak);
border-radius: 24px;
[data-component="hero-video"] {
video {
display: block;
width: 100%;
aspect-ratio: 1;
object-fit: cover;
border-radius: 4px;
@media (max-width: 50rem) {
width: 80px;
height: 80px;
aspect-ratio: 16 / 9;
}
}
img {
width: 120px;
height: 120px;
border-radius: 24px;
box-shadow:
0 1.467px 2.847px 0 rgba(0, 0, 0, 0.42),
0 0.779px 1.512px 0 rgba(0, 0, 0, 0.34),
0 0.324px 0.629px 0 rgba(0, 0, 0, 0.24);
@media (max-width: 50rem) {
width: 80px;
height: 80px;
border-radius: 16px;
}
}
@media (max-width: 50rem) {
justify-content: flex-start;
}
}
[data-component="hero-text"] {
display: flex;
flex-direction: column;
justify-content: center;
padding-right: 3rem;
@media (max-width: 50rem) {
padding: 1.5rem;
}
h1 {
font-size: 1.5rem;
@@ -11,7 +11,7 @@ import { LocaleLinks } from "~/component/locale-links"
import { config } from "~/config"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
import desktopAppIcon from "../../asset/lander/opencode-desktop-icon.png"
import desktopTabsVideo from "../../asset/lander/desktop-tabs-landscape.mp4"
import type { DownloadPlatform } from "./types"
type OS = "macOS" | "Windows" | "Linux" | null
@@ -93,12 +93,14 @@ export default function Download() {
<div data-component="content">
<section data-component="download-hero">
<div data-component="hero-icon">
<img src={desktopAppIcon} alt="" />
<div data-component="hero-video">
<video src={desktopTabsVideo} autoplay playsinline loop muted preload="metadata" aria-hidden="true" />
</div>
<div data-component="hero-text">
<h1>{i18n.t("download.hero.title")}</h1>
<p>{i18n.t("download.hero.subtitle")}</p>
<p>
{i18n.t("home.promo.body")} {i18n.t("home.promo.cta")}
</p>
<Show when={detectedOS()}>
<a
href={language.route(getDownloadHref(getDownloadPlatform(detectedOS())))}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.17.20",
"version": "1.18.2",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.17.20",
"version": "1.18.2",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.17.20",
"version": "1.18.2",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-support",
"version": "1.17.20",
"version": "1.18.2",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.20",
"version": "1.18.2",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+8 -1
View File
@@ -116,7 +116,14 @@ export namespace FSUtil {
})
const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) {
yield* fs.makeDirectory(path, { recursive: true })
yield* fs.makeDirectory(path, { recursive: true }).pipe(
// Bun on Windows can throw EEXIST here despite recursive mode.
// https://github.com/oven-sh/bun/issues/21901
Effect.catchIf(
(error) => error.reason._tag === "AlreadyExists",
(error) => isDir(path).pipe(Effect.flatMap((exists) => (exists ? Effect.void : Effect.fail(error)))),
),
)
})
const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* (
+326 -79
View File
@@ -12,6 +12,7 @@ import {
Schedule,
Schema,
Scope,
Stream,
SynchronizedRef,
Types,
} from "effect"
@@ -20,6 +21,8 @@ import { Credential } from "./credential"
import { State } from "./state"
import { EventV2 } from "./event"
import { IntegrationConnection } from "./integration/connection"
import { AppProcess } from "./process"
import { ChildProcess } from "effect/unstable/process"
export const ID = Integration.ID
export type ID = Integration.ID
@@ -45,6 +48,9 @@ export type Prompt = Integration.Prompt
export const OAuthMethod = Integration.OAuthMethod
export type OAuthMethod = Integration.OAuthMethod
export const CommandMethod = Integration.CommandMethod
export type CommandMethod = Integration.CommandMethod
export const KeyMethod = Integration.KeyMethod
export type KeyMethod = Integration.KeyMethod
@@ -87,12 +93,17 @@ export interface KeyImplementation {
readonly method: KeyMethod
}
export interface CommandImplementation {
readonly integrationID: ID
readonly method: CommandMethod
}
export interface EnvImplementation {
readonly integrationID: ID
readonly method: EnvMethod
}
export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation
export type Implementation = OAuthImplementation | CommandImplementation | KeyImplementation | EnvImplementation
export const Attempt = Integration.Attempt
export type Attempt = Integration.Attempt
@@ -100,6 +111,12 @@ export type Attempt = Integration.Attempt
export const AttemptStatus = Integration.AttemptStatus
export type AttemptStatus = typeof AttemptStatus.Type
export const CommandAttempt = Integration.CommandAttempt
export type CommandAttempt = Integration.CommandAttempt
export const CommandAttemptStatus = Integration.CommandAttemptStatus
export type CommandAttemptStatus = Integration.CommandAttemptStatus
export class CodeRequiredError extends Schema.TaggedErrorClass<CodeRequiredError>()("Integration.CodeRequired", {
attemptID: AttemptID,
}) {}
@@ -159,17 +176,6 @@ export interface Interface extends State.Transformable<Draft> {
/** User-facing label for the stored credential. */
readonly label?: string
}) => Effect.Effect<void, AuthorizationError>
/** Starts a stateful OAuth attempt. */
readonly oauth: (input: {
/** Integration being authenticated. */
readonly integrationID: ID
/** OAuth method selected by the caller. */
readonly methodID: MethodID
/** Answers to the method's optional prompts. */
readonly inputs: Inputs
/** User-facing label for the credential created on completion. */
readonly label?: string
}) => Effect.Effect<Attempt, AuthorizationError>
/** Updates a stored credential exposed as a connection. */
readonly update: (
credentialID: Credential.ID,
@@ -178,18 +184,39 @@ export interface Interface extends State.Transformable<Draft> {
/** Removes a stored credential connection. */
readonly remove: (credentialID: Credential.ID) => Effect.Effect<void>
}
readonly attempt: {
readonly oauth: {
/** Starts a stateful OAuth attempt. */
readonly connect: (input: {
readonly integrationID: ID
readonly methodID: MethodID
readonly inputs: Inputs
readonly label?: string
}) => Effect.Effect<Attempt, AuthorizationError>
/** Returns the current state of an OAuth attempt. */
readonly status: (attemptID: AttemptID) => Effect.Effect<AttemptStatus>
readonly status: (input: {
readonly integrationID: ID
readonly attemptID: AttemptID
}) => Effect.Effect<AttemptStatus>
/** Completes the attempt and stores its credential. */
readonly complete: (input: {
/** Opaque handle returned by `oauth`. */
readonly integrationID: ID
readonly attemptID: AttemptID
/** Authorization code required by attempts in code mode. */
readonly code?: string
}) => Effect.Effect<void, CodeRequiredError | AuthorizationError>
/** Cancels an attempt and releases its resources. */
readonly cancel: (attemptID: AttemptID) => Effect.Effect<void>
readonly cancel: (input: { readonly integrationID: ID; readonly attemptID: AttemptID }) => Effect.Effect<void>
}
readonly command: {
readonly connect: (input: {
readonly integrationID: ID
readonly methodID: MethodID
readonly label?: string
}) => Effect.Effect<CommandAttempt, AuthorizationError>
readonly status: (input: {
readonly integrationID: ID
readonly attemptID: AttemptID
}) => Effect.Effect<CommandAttemptStatus>
readonly cancel: (input: { readonly integrationID: ID; readonly attemptID: AttemptID }) => Effect.Effect<void>
}
}
@@ -213,19 +240,40 @@ type PendingAttempt = {
}
type TerminalAttempt = {
status: "complete" | "failed" | "expired"
integrationID: ID
message?: string
removeAt: number
time: AttemptTime
}
type AttemptEntry = PendingAttempt | TerminalAttempt
type PendingCommandAttempt = {
status: "pending"
integrationID: ID
label?: string
message?: string
persisting: boolean
scope: Scope.Closeable
time: AttemptTime
}
type TerminalCommandAttempt = {
status: "complete" | "failed" | "expired"
integrationID: ID
message?: string
removeAt: number
time: AttemptTime
}
type CommandAttemptEntry = PendingCommandAttempt | TerminalCommandAttempt
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const credentials = yield* Credential.Service
const events = yield* EventV2.Service
const processes = yield* AppProcess.Service
const scope = yield* Scope.Scope
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
const commandAttempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, CommandAttemptEntry>())
const state = State.create<Data, Draft>({
name: "integration",
initial: () => ({ integrations: new Map<ID, Entry>() }),
@@ -259,8 +307,11 @@ const layer = Layer.effect(
}
const index = current.methods.findIndex((method) => {
if (method.type !== implementation.method.type) return false
if (method.type !== "oauth" || implementation.method.type !== "oauth") return true
return method.id === implementation.method.id
if (method.type === "oauth" && implementation.method.type === "oauth")
return method.id === implementation.method.id
if (method.type === "command" && implementation.method.type === "command")
return method.id === implementation.method.id
return true
})
if (index === -1) current.methods.push(implementation.method as Types.DeepMutable<Method>)
else current.methods[index] = implementation.method as Types.DeepMutable<Method>
@@ -276,8 +327,9 @@ const layer = Layer.effect(
if (!current) return
const index = current.methods.findIndex((candidate) => {
if (candidate.type !== method.type) return false
if (candidate.type !== "oauth" || method.type !== "oauth") return true
return candidate.id === method.id
if (candidate.type === "oauth" && method.type === "oauth") return candidate.id === method.id
if (candidate.type === "command" && method.type === "command") return candidate.id === method.id
return true
})
if (index !== -1) current.methods.splice(index, 1)
if (method.type === "oauth") current.implementations.delete(method.id)
@@ -332,6 +384,7 @@ const layer = Layer.effect(
? { ...match, persisting: true }
: {
status: "failed" as const,
integrationID: match.integrationID,
message: message(exit.cause),
time: match.time,
removeAt: now + terminalRetention,
@@ -362,13 +415,19 @@ const layer = Layer.effect(
)
const settledAt = yield* Clock.currentTimeMillis
const terminal: TerminalAttempt = Exit.isSuccess(persistence)
? { status: "complete", time: attempt.time, removeAt: settledAt + terminalRetention }
? {
status: "complete",
integrationID: attempt.integrationID,
time: attempt.time,
removeAt: settledAt + terminalRetention,
}
: {
status: "failed",
message: message(persistence.cause),
time: attempt.time,
removeAt: settledAt + terminalRetention,
}
integrationID: attempt.integrationID,
message: message(persistence.cause),
time: attempt.time,
removeAt: settledAt + terminalRetention,
}
// Persisting attempts cannot be cancelled, expired, or claimed again.
yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal))
if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause)
@@ -379,6 +438,61 @@ const layer = Layer.effect(
)
})
const settleCommand = Effect.fnUntraced(function* (attemptID: AttemptID, exit: Exit.Exit<string, unknown>) {
return yield* Effect.uninterruptible(
Effect.gen(function* () {
const now = yield* Clock.currentTimeMillis
const attempt = yield* SynchronizedRef.modify(commandAttempts, (current) => {
const match = current.get(attemptID)
if (!match || match.status !== "pending" || match.persisting) return [undefined, current]
const next = Exit.isSuccess(exit)
? { ...match, persisting: true }
: {
status: "failed" as const,
integrationID: match.integrationID,
message: message(exit.cause),
time: match.time,
removeAt: now + terminalRetention,
}
return [match, new Map(current).set(attemptID, next)]
})
if (!attempt) return
if (Exit.isFailure(exit)) {
yield* close(attempt.scope)
return
}
const persistence = yield* credentials
.create({
integrationID: attempt.integrationID,
label: attempt.label,
value: Credential.Key.make({ type: "key", key: exit.value }),
})
.pipe(Effect.asVoid, Effect.exit)
const settledAt = yield* Clock.currentTimeMillis
const terminal: TerminalCommandAttempt = Exit.isSuccess(persistence)
? {
status: "complete",
integrationID: attempt.integrationID,
time: attempt.time,
removeAt: settledAt + terminalRetention,
}
: {
status: "failed",
integrationID: attempt.integrationID,
message: message(persistence.cause),
time: attempt.time,
removeAt: settledAt + terminalRetention,
}
yield* SynchronizedRef.update(commandAttempts, (current) => new Map(current).set(attemptID, terminal))
yield* close(attempt.scope)
if (Exit.isFailure(persistence)) return
yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* events.publish(Event.Updated, {})
}),
)
})
const scrub = Effect.fnUntraced(function* () {
const now = yield* Clock.currentTimeMillis
const expired = yield* SynchronizedRef.modify(attempts, (current) => {
@@ -387,7 +501,35 @@ const layer = Layer.effect(
for (const [id, attempt] of current) {
if (attempt.status === "pending" && !attempt.persisting && attempt.time.expires <= now) {
scopes.push(attempt.scope)
next.set(id, { status: "expired", time: attempt.time, removeAt: now + terminalRetention })
next.set(id, {
status: "expired",
integrationID: attempt.integrationID,
time: attempt.time,
removeAt: now + terminalRetention,
})
continue
}
if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id)
}
return [scopes, next]
})
yield* Effect.forEach(expired, close, { discard: true })
})
const scrubCommands = Effect.fnUntraced(function* () {
const now = yield* Clock.currentTimeMillis
const expired = yield* SynchronizedRef.modify(commandAttempts, (current) => {
const next = new Map(current)
const scopes: Scope.Closeable[] = []
for (const [id, attempt] of current) {
if (attempt.status === "pending" && !attempt.persisting && attempt.time.expires <= now) {
scopes.push(attempt.scope)
next.set(id, {
status: "expired",
integrationID: attempt.integrationID,
time: attempt.time,
removeAt: now + terminalRetention,
})
continue
}
if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id)
@@ -398,6 +540,114 @@ const layer = Layer.effect(
})
yield* scrub().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope))
yield* scrubCommands().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope))
const connectOAuth = Effect.fn("Integration.oauth.connect")(function* (input: {
readonly integrationID: ID
readonly methodID: MethodID
readonly inputs: Inputs
readonly label?: string
}) {
const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
if (!method) {
return yield* Effect.die(new Error(`OAuth method not found: ${input.integrationID}/${input.methodID}`))
}
const attemptScope = yield* Scope.fork(scope)
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
Scope.provide(attemptScope),
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)),
)
const id = AttemptID.create()
const created = yield* Clock.currentTimeMillis
const time = { created, expires: created + attemptLifetime }
yield* SynchronizedRef.update(attempts, (current) =>
new Map(current).set(id, {
status: "pending",
completing: authorization.mode === "auto",
persisting: false,
authorization,
integrationID: input.integrationID,
methodID: input.methodID,
label: input.label,
scope: attemptScope,
time,
}),
)
if (authorization.mode === "auto") {
yield* authorization.callback.pipe(
Effect.exit,
Effect.flatMap((exit) => settle(id, exit)),
Effect.forkIn(attemptScope, { startImmediately: true }),
)
}
return new Attempt({
attemptID: id,
url: authorization.url,
instructions: authorization.instructions,
mode: authorization.mode,
time,
})
})
const connectCommand = Effect.fn("Integration.command.connect")(function* (input: {
readonly integrationID: ID
readonly methodID: MethodID
readonly label?: string
}) {
const method = state
.get()
.integrations.get(input.integrationID)
?.methods.find((method) => method.type === "command" && method.id === input.methodID)
if (!method || method.type !== "command" || !method.command[0]) {
return yield* Effect.die(new Error(`Command method not found: ${input.integrationID}/${input.methodID}`))
}
const attemptScope = yield* Scope.fork(scope)
const attemptID = AttemptID.create()
const created = yield* Clock.currentTimeMillis
const time = { created, expires: created + attemptLifetime }
yield* SynchronizedRef.update(commandAttempts, (current) =>
new Map(current).set(attemptID, {
status: "pending",
integrationID: input.integrationID,
label: input.label,
persisting: false,
scope: attemptScope,
time,
}),
)
yield* processes
.runStream(
ChildProcess.make(method.command[0], method.command.slice(1), {
extendEnv: true,
stdin: "ignore",
}),
{ okExitCodes: [0] },
)
.pipe(
Stream.tap((line) =>
SynchronizedRef.update(commandAttempts, (current) => {
const attempt = current.get(attemptID)
if (!attempt || attempt.status !== "pending") return current
const message = attempt.message ? `${attempt.message}\n${line}` : line
return new Map(current).set(attemptID, { ...attempt, message })
}),
),
Stream.runCollect,
Effect.flatMap((lines) => {
const credential = Array.from(lines).at(-1)
return credential
? Effect.succeed(credential)
: Effect.fail(new Error("Authentication command returned no credential"))
}),
Effect.exit,
Effect.flatMap((exit) => settleCommand(attemptID, exit)),
Effect.forkIn(attemptScope, { startImmediately: true }),
)
return CommandAttempt.make({ attemptID, time })
})
return Service.of({
transform: state.transform,
@@ -451,47 +701,6 @@ const layer = Layer.effect(
yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID })
yield* events.publish(Event.Updated, {})
}),
oauth: Effect.fn("Integration.connection.oauth")(function* (input) {
const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
if (!method) {
return yield* Effect.die(new Error(`OAuth method not found: ${input.integrationID}/${input.methodID}`))
}
const attemptScope = yield* Scope.fork(scope)
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
Scope.provide(attemptScope),
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)),
)
const id = AttemptID.create()
const created = yield* Clock.currentTimeMillis
const time = { created, expires: created + attemptLifetime }
yield* SynchronizedRef.update(attempts, (current) =>
new Map(current).set(id, {
status: "pending",
completing: authorization.mode === "auto",
persisting: false,
authorization,
integrationID: input.integrationID,
methodID: input.methodID,
label: input.label,
scope: attemptScope,
time,
}),
)
if (authorization.mode === "auto") {
yield* authorization.callback.pipe(
Effect.exit,
Effect.flatMap((exit) => settle(id, exit)),
Effect.forkIn(attemptScope, { startImmediately: true }),
)
}
return new Attempt({
attemptID: id,
url: authorization.url,
instructions: authorization.instructions,
mode: authorization.mode,
time,
})
}),
update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) {
const credential = yield* credentials.get(credentialID)
yield* credentials.update(credentialID, updates)
@@ -509,19 +718,22 @@ const layer = Layer.effect(
yield* events.publish(Event.Updated, {})
}),
},
attempt: {
status: Effect.fn("Integration.attempt.status")(function* (attemptID) {
const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID)
if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${attemptID}`))
oauth: {
connect: connectOAuth,
status: Effect.fn("Integration.oauth.status")(function* (input) {
const attempt = (yield* SynchronizedRef.get(attempts)).get(input.attemptID)
if (!attempt || attempt.integrationID !== input.integrationID)
return yield* Effect.die(new Error(`OAuth attempt not found: ${input.attemptID}`))
if (attempt.status === "failed") {
return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
}
return { status: attempt.status, time: attempt.time }
}),
complete: Effect.fn("Integration.attempt.complete")(function* (input) {
complete: Effect.fn("Integration.oauth.complete")(function* (input) {
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
const match = current.get(input.attemptID)
if (!match || match.status !== "pending" || match.completing) return [match, current]
if (!match || match.integrationID !== input.integrationID) return [undefined, current]
if (match.status !== "pending" || match.completing) return [match, current]
if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
})
@@ -540,12 +752,43 @@ const layer = Layer.effect(
yield* settle(input.attemptID, exit)
if (Exit.isFailure(exit)) return yield* exit
}),
cancel: Effect.fn("Integration.attempt.cancel")(function* (attemptID) {
cancel: Effect.fn("Integration.oauth.cancel")(function* (input) {
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
const match = current.get(attemptID)
if (!match || match.status !== "pending" || match.persisting) return [undefined, current]
const match = current.get(input.attemptID)
if (!match || match.integrationID !== input.integrationID || match.status !== "pending" || match.persisting)
return [undefined, current]
const next = new Map(current)
next.delete(attemptID)
next.delete(input.attemptID)
return [match, next]
})
if (attempt) yield* Scope.close(attempt.scope, Exit.void)
}),
},
command: {
connect: connectCommand,
status: Effect.fn("Integration.command.status")(function* (input) {
const attempt = (yield* SynchronizedRef.get(commandAttempts)).get(input.attemptID)
if (!attempt || attempt.integrationID !== input.integrationID)
return yield* Effect.die(new Error(`Command attempt not found: ${input.attemptID}`))
if (attempt.status === "pending") {
return {
status: attempt.status,
...(attempt.message ? { message: attempt.message } : {}),
time: attempt.time,
}
}
if (attempt.status === "failed") {
return { status: attempt.status, message: attempt.message ?? "Authentication failed", time: attempt.time }
}
return { status: attempt.status, time: attempt.time }
}),
cancel: Effect.fn("Integration.command.cancel")(function* (input) {
const attempt = yield* SynchronizedRef.modify(commandAttempts, (current) => {
const match = current.get(input.attemptID)
if (!match || match.integrationID !== input.integrationID || match.status !== "pending" || match.persisting)
return [undefined, current]
const next = new Map(current)
next.delete(input.attemptID)
return [match, next]
})
if (attempt) yield* Scope.close(attempt.scope, Exit.void)
@@ -555,4 +798,8 @@ const layer = Layer.effect(
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [Credential.node, EventV2.node] })
export const node = makeLocationNode({
service: Service,
layer,
deps: [Credential.node, EventV2.node, AppProcess.node],
})
+2
View File
@@ -1,6 +1,7 @@
export * as PluginHooks from "./hooks"
import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk"
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool"
import { Context, Effect, Layer, Scope } from "effect"
import { makeLocationNode } from "../effect/app-node"
@@ -8,6 +9,7 @@ import { State } from "../state"
export interface Domains {
readonly aisdk: AISDKHooks
readonly session: SessionHooks
readonly tool: ToolHooks
}
+54 -7
View File
@@ -63,6 +63,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
return {
options: {},
agent: {
get: (id) => agents.get(AgentV2.ID.make(id)),
list: (input) => {
const ref = locationRef(input)
if (ref && !isCurrentLocation(ref)) return runtime.location.agent.list(ref)
@@ -123,6 +124,8 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
),
},
model: {
get: (providerID, modelID) =>
catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
list: () => response(catalog.model.available()),
default: () => response(catalog.model.default()),
},
@@ -173,21 +176,57 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
key: input.key,
label: input.label,
}),
oauth: (input) =>
},
oauth: {
connect: (input) =>
response(
integration.connection.oauth({
integration.oauth.connect({
integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID),
inputs: input.inputs,
label: input.label,
}),
),
},
attempt: {
status: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
status: (input) =>
response(
integration.oauth.status({
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
}),
),
complete: (input) =>
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
cancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
integration.oauth.complete({
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
code: input.code,
}),
cancel: (input) =>
integration.oauth.cancel({
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
}),
},
command: {
connect: (input) =>
response(
integration.command.connect({
integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID),
label: input.label,
}),
),
status: (input) =>
response(
integration.command.status({
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
}),
),
cancel: (input) =>
integration.command.cancel({
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
}),
},
reload: integration.reload,
connection: {
@@ -267,6 +306,13 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
})
return
}
if (input.method.type === "command") {
draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: Schema.decodeUnknownSync(Integration.CommandMethod)(input.method),
})
return
}
draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: { type: "key", label: input.method.label },
@@ -367,6 +413,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
},
},
session: {
hook: (name, callback) => hooks.register("session", name, callback),
create: (input) =>
runtime.session.create({
id: input?.id,
+2
View File
@@ -50,6 +50,7 @@ import { ModelsDevPlugin } from "./models-dev"
import { ProviderPlugins } from "./provider"
import { PluginRuntime } from "./runtime"
import { SkillPlugin } from "./skill"
import { SystemPromptPlugin } from "./system-prompt"
import { VariantPlugin } from "./variant"
const services = Effect.fn("PluginInternal.services")(function* () {
@@ -121,6 +122,7 @@ const pre = [
AgentPlugin.Plugin,
CommandPlugin.Plugin,
SkillPlugin.Plugin,
...SystemPromptPlugin.Plugins,
ModelsDevPlugin,
...ProviderPlugins,
PatchTool.Plugin,
+45 -9
View File
@@ -59,6 +59,7 @@ export function fromPromise(plugin: Plugin) {
const context2: Context = {
options: host.options,
agent: {
get: (id) => run(host.agent.get(id)),
list: (input) => run(host.agent.list(input)),
transform: transform(host.agent),
reload: () => run(host.agent.reload()),
@@ -70,9 +71,11 @@ export function fromPromise(plugin: Plugin) {
catalog: {
provider: {
list: (input) => run(host.catalog.provider.list(input)),
get: (input) => run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })),
get: (input) =>
run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })),
},
model: {
get: (providerID, modelID) => run(host.catalog.model.get(providerID, modelID)),
list: (input) => run(host.catalog.model.list(input)),
default: (input) =>
run(host.catalog.model.default(input)).then((result) => ({ ...result, data: result.data ?? null })),
@@ -96,35 +99,66 @@ export function fromPromise(plugin: Plugin) {
),
connect: {
key: (input) =>
run(host.integration.connect.key({ ...input, integrationID: Integration.ID.make(input.integrationID) })),
oauth: (input) =>
run(
host.integration.connect.oauth({
host.integration.connect.key({ ...input, integrationID: Integration.ID.make(input.integrationID) }),
),
},
oauth: {
connect: (input) =>
run(
host.integration.oauth.connect({
...input,
integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID),
}),
),
},
attempt: {
status: (input) =>
run(
host.integration.attempt.status({
host.integration.oauth.status({
...input,
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
}),
),
complete: (input) =>
run(
host.integration.attempt.complete({
host.integration.oauth.complete({
...input,
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
}),
),
cancel: (input) =>
run(
host.integration.attempt.cancel({
host.integration.oauth.cancel({
...input,
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
}),
),
},
command: {
connect: (input) =>
run(
host.integration.command.connect({
...input,
integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID),
}),
),
status: (input) =>
run(
host.integration.command.status({
...input,
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
}),
),
cancel: (input) =>
run(
host.integration.command.cancel({
...input,
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
}),
),
@@ -162,6 +196,8 @@ export function fromPromise(plugin: Plugin) {
register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
},
session: {
hook: (name, callback) =>
register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
create: (input) =>
run(
host.session.create(
+47
View File
@@ -0,0 +1,47 @@
export * as SystemPromptPlugin from "./system-prompt"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Effect } from "effect"
import PROMPT_ANTHROPIC from "./system-prompt/anthropic.txt"
import PROMPT_CODEX from "./system-prompt/codex.txt"
import PROMPT_GEMINI from "./system-prompt/gemini.txt"
import PROMPT_GPT from "./system-prompt/gpt.txt"
import PROMPT_KIMI from "./system-prompt/kimi.txt"
import PROMPT_META from "./system-prompt/meta.txt"
import PROMPT_TRINITY from "./system-prompt/trinity.txt"
export const OpenAIPlugin = make("openai", (id) => {
if (id.includes("gpt")) {
if (id.includes("codex")) return PROMPT_CODEX
return PROMPT_GPT
}
if (id.includes("o1") || id.includes("o3")) return PROMPT_GPT
})
export const GooglePlugin = make("google", (id) => (id.includes("gemini-") ? PROMPT_GEMINI : undefined))
export const AnthropicPlugin = make("anthropic", (id) => (id.includes("claude") ? PROMPT_ANTHROPIC : undefined))
export const KimiPlugin = make("kimi", (id) => (id.includes("kimi") ? PROMPT_KIMI : undefined))
export const ArceePlugin = make("arcee", (id) => (id.includes("trinity") ? PROMPT_TRINITY : undefined))
export const MetaPlugin = make("meta", (id) => (id.includes("muse-spark") ? PROMPT_META : undefined))
export const Plugins = [OpenAIPlugin, GooglePlugin, AnthropicPlugin, KimiPlugin, ArceePlugin, MetaPlugin] as const
function make(id: string, select: (modelID: string) => string | undefined) {
return define({
id: `opencode.system-prompt.${id}`,
effect: Effect.fn(`SystemPromptPlugin.${id}`)(function* (ctx) {
yield* ctx.session.hook("context", (event) =>
Effect.gen(function* () {
if ((yield* ctx.agent.get(event.agent))?.system) return
const system = event.system[0]
if (!system) return
const model = yield* ctx.catalog.model.get(event.model.providerID, event.model.id)
const prompt = select(`${model?.modelID ?? event.model.id} ${model?.family ?? ""}`.toLowerCase())
if (!prompt) return
event.system[0] = { ...system, text: prompt }
}),
)
}),
})
}
@@ -0,0 +1,76 @@
You are OpenCode, the best coding agent on the planet.
You are based on a large language model trained by Meta MSL named Muse Spark.
When asked who you are, identify yourself as OpenCode powered by Meta Muse Spark by name.
You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
If the user asks for help or wants to give feedback inform them of the following:
- ctrl+p to list available actions
- To give feedback, users should report the issue at
https://github.com/anomalyco/opencode
When the user directly asks about OpenCode (eg. "can OpenCode do...", "does OpenCode have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific OpenCode feature (eg. implement a hook, write a slash command, or install an MCP server), use the webfetch tool to gather information to answer the question from V2 OpenCode docs. The list of available docs is available at https://v2.opencode.ai/llms.txt
# Tone and style
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use GitHub-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like shell or code comments as means to communicate with the user during the session.
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
# Professional objectivity
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if OpenCode honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
# Task Management
Track tasks and plans explicitly when useful so that you manage your work and give the user visibility into your progress.
Task tracking is also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not track your plan, you may forget to do important tasks - and that is unacceptable.
It is critical that you mark tasks as completed as soon as you are done with them. Do not batch up multiple tasks before marking them as completed.
Examples:
<example>
user: Run the build and fix any type errors
assistant: I'm going to work through the following items:
- Run the build
- Fix any type errors
I'm now going to run the build using shell.
Looks like I found 10 type errors. I'm going to track the 10 fixes and address them one at a time.
Starting the first fix...
Let me start working on the first item...
The first item has been fixed; I'll move on to the second item...
..
..
</example>
In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors.
<example>
user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
assistant: I'll help you implement a usage metrics tracking and export feature. Let me first plan this task.
The plan is:
1. Research existing metrics tracking in the codebase
2. Design the metrics collection system
3. Implement core metrics tracking functionality
4. Create export functionality for different formats
Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.
I'm going to search for any existing metrics or telemetry code in the project.
I've found some existing telemetry code. The first task is complete; I'll start designing our metrics tracking system based on what I've learned...
[Assistant continues implementing the feature step by step, reporting progress as tasks are completed]
</example>
# Doing tasks
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
- Plan and track the task if required
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear.
# Tool usage policy
- When doing file search, prefer to use the subagent tool in order to reduce context usage.
- You should proactively use the subagent tool with specialized agents when the task at hand matches the agent's description.
- When webfetch returns a message about a redirect to a different host, you should immediately make a new webfetch request with the redirect URL provided in the response.
- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple subagent tool calls.
- Use specialized tools instead of shell commands when possible, as this provides a better user experience. For file operations, use dedicated tools: read for reading files instead of cat/head/tail, edit for editing instead of sed/awk, and write for creating files instead of cat with heredoc or echo redirection. Reserve the shell tool exclusively for actual system commands and terminal operations that require shell execution. NEVER use shell echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the subagent tool instead of running search commands directly.
<example>
user: Where are errors from the client handled?
assistant: [Uses the subagent tool to find the files that handle client errors instead of using glob or grep directly]
</example>
<example>
user: What is the codebase structure?
assistant: [Uses the subagent tool]
</example>
IMPORTANT: Plan and track multi-step tasks throughout the conversation.
# Code References
When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location.
<example>
user: Where are errors from the client handled?
assistant: Clients are marked as failed in the `connectToServer` function in src/services/process.ts:712.
</example>
+1
View File
@@ -198,6 +198,7 @@ export const buildPrompt = (input: { readonly previousSummary?: string; readonly
? `Update the anchored summary below using the conversation history above.\nPreserve still-true details, remove stale details, and merge in the new facts.\n<previous-summary>\n${input.previousSummary}\n</previous-summary>`
: "Create a new anchored summary from the conversation history.",
SUMMARY_TEMPLATE,
"The following is the conversation history:",
...input.context,
].join("\n\n")
+118
View File
@@ -0,0 +1,118 @@
export * as SessionContext from "./context"
import { Context, Effect, Layer } from "effect"
import { AgentV2 } from "../agent"
import { Database } from "../database/database"
import { makeLocationNode } from "../effect/app-node"
import { InstructionDiscovery } from "../instruction-discovery"
import { Instructions } from "../instructions/index"
import { InstructionBuiltIns } from "../instructions/builtins"
import { Location } from "../location"
import { McpGuidance } from "../mcp/guidance"
import { PluginSupervisor } from "../plugin/supervisor"
import { ReferenceGuidance } from "../reference/guidance"
import { SkillGuidance } from "../skill/guidance"
import { AgentNotFoundError } from "./error"
import { SessionHistory } from "./history"
import { InstructionEntry } from "./instruction-entry"
import { SessionMessage } from "./message"
import { SessionRunnerModel } from "./runner/model"
import { SessionSchema } from "./schema"
import { SessionStore } from "./store"
export interface Selection {
readonly session: SessionSchema.Info
readonly agent: AgentV2.Selection & { readonly info: AgentV2.Info }
readonly instructions: Instructions.Instructions
}
export interface Loaded {
readonly session: SessionSchema.Info
readonly agent: AgentV2.Selection & { readonly info: AgentV2.Info }
readonly model: SessionRunnerModel.Resolved
readonly initial: string
readonly messages: ReadonlyArray<SessionMessage.Info>
}
export interface Interface {
/** Resolves the Session, selected agent, and current instruction sources before durable Step preparation. */
readonly select: (sessionID: SessionSchema.ID) => Effect.Effect<Selection, AgentNotFoundError>
/** Loads the selected model and active history after instruction sync and pending-input promotion. */
readonly load: (selection: Selection) => Effect.Effect<Loaded, SessionRunnerModel.Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionContext") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const agents = yield* AgentV2.Service
const builtins = yield* InstructionBuiltIns.Service
const db = (yield* Database.Service).db
const discovery = yield* InstructionDiscovery.Service
const entries = yield* InstructionEntry.Service
const location = yield* Location.Service
const mcpGuidance = yield* McpGuidance.Service
const models = yield* SessionRunnerModel.Service
const plugins = yield* PluginSupervisor.Service
const referenceGuidance = yield* ReferenceGuidance.Service
const skillGuidance = yield* SkillGuidance.Service
const store = yield* SessionStore.Service
const select = Effect.fn("SessionContext.select")(function* (sessionID: SessionSchema.ID) {
const session = yield* store.get(sessionID)
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
if (session.location.directory !== location.directory || session.location.workspaceID !== location.workspaceID)
return yield* Effect.interrupt
yield* plugins.flush
const agent = yield* agents.select(session.agent)
if (!agent.info) return yield* new AgentNotFoundError({ sessionID: session.id, agent: session.agent ?? agent.id })
const instructions = yield* Effect.all(
[
builtins.load(sessionID),
discovery.load(),
skillGuidance.load(agent),
referenceGuidance.load(),
mcpGuidance.load(agent),
entries.load(sessionID),
],
{ concurrency: "unbounded" },
).pipe(Effect.map(Instructions.combine))
return { session, agent: { ...agent, info: agent.info }, instructions }
})
const load = Effect.fn("SessionContext.load")(function* (selection: Selection) {
const model = yield* models.resolve(selection.session)
const history = yield* SessionHistory.entriesForRunner(db, selection.session.id, selection.instructions)
return {
session: selection.session,
agent: selection.agent,
model,
initial: history.initial,
messages: history.entries.map((entry) => entry.message),
}
})
return Service.of({ select, load })
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [
AgentV2.node,
Database.node,
InstructionBuiltIns.node,
InstructionDiscovery.node,
InstructionEntry.node,
Location.node,
McpGuidance.node,
PluginSupervisor.node,
ReferenceGuidance.node,
SessionRunnerModel.node,
SessionStore.node,
SkillGuidance.node,
],
})
+121
View File
@@ -0,0 +1,121 @@
export * as SessionRequest from "./request"
import { LLM, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai"
import { SessionError } from "@opencode-ai/schema/session-error"
import { Context, Effect, Layer } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { PluginHooks } from "../plugin/hooks"
import { ToolRegistry } from "../tool/registry"
import { SessionContext } from "./context"
import { SessionModelHeaders } from "./model-headers"
import { MAX_STEPS_PROMPT } from "./runner/max-steps"
import PROMPT_DEFAULT from "./runner/prompt/base.txt"
import { toLLMMessages } from "./runner/to-llm-message"
type ResolvedTool =
| { readonly type: "reject"; readonly error: SessionError.Error }
| { readonly type: "settle"; readonly settle: ToolRegistry.Materialization["settle"] }
interface Prepared {
readonly request: LLMRequest
readonly retryAllowed: boolean
readonly resolveTool: (name: string) => ResolvedTool
}
interface PrepareInput {
readonly context: SessionContext.Loaded
readonly step: number
}
export interface Interface {
readonly prepare: (input: PrepareInput) => Effect.Effect<Prepared>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRequest") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const hooks = yield* PluginHooks.Service
const registry = yield* ToolRegistry.Service
const prepare = Effect.fn("SessionRequest.prepare")(function* (input: PrepareInput) {
const session = input.context.session
const agent = input.context.agent
const resolved = input.context.model
const model = resolved.model
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
const stepLimitReached = agent.info.steps !== undefined && input.step >= agent.info.steps
const executableTools = stepLimitReached ? undefined : yield* registry.materialize(agent.info.permissions)
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
const system = [agent.info.system ? agent.info.system : PROMPT_DEFAULT, input.context.initial]
.filter((part): part is string => part !== undefined && part.length > 0)
.map(SystemPart.make)
const history = toLLMMessages(input.context.messages, resolved.ref, providerMetadataKey)
const messages = stepLimitReached ? [...history, Message.assistant(MAX_STEPS_PROMPT)] : history
const toolsByName = new Map((executableTools?.definitions ?? []).map((tool) => [tool.name, tool]))
// Hooks may reshape available definitions but cannot advertise tools omitted by permissions or the Step limit.
const contextEvent = yield* hooks.trigger("session", "context", {
sessionID: session.id,
agent: agent.id,
model: resolved.ref,
system,
messages,
tools: Object.fromEntries(
Array.from(toolsByName.values(), (tool) => [
tool.name,
{ description: tool.description, input: { ...tool.inputSchema } },
]),
),
})
// Leave hook-removed definitions in the map so calls to them can be rejected before settlement.
const hookedTools = Object.entries(contextEvent.tools).reduce<Array<LLMRequest["tools"][number]>>(
(result, [name, tool]) => {
const registered = toolsByName.get(name)
if (!registered) return result
toolsByName.delete(name)
result.push(Object.assign({}, registered, { description: tool.description, inputSchema: tool.input }))
return result
},
[],
)
const request = LLM.request({
model,
http: {
headers: SessionModelHeaders.make(session),
},
providerOptions: { openai: { promptCacheKey } },
system: contextEvent.system,
messages: contextEvent.messages,
tools: hookedTools,
toolChoice: stepLimitReached ? "none" : undefined,
})
const resolveTool = (name: string): ResolvedTool => {
if (!executableTools)
return {
type: "reject",
error: { type: "tool.execution", message: "Tools are disabled after the maximum agent steps" },
}
if (toolsByName.has(name))
return {
type: "reject",
error: { type: "tool.execution", message: `Tool is not available for this request: ${name}` },
}
return { type: "settle", settle: executableTools.settle }
}
return {
request,
retryAllowed: !stepLimitReached,
resolveTool,
}
})
return Service.of({ prepare })
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [PluginHooks.node, ToolRegistry.node],
})
+29 -117
View File
@@ -1,57 +1,33 @@
export * as SessionRunnerLLM from "./llm"
import {
LLM,
LLMClient,
LLMError,
LLMEvent,
Message,
SystemPart,
isContextOverflowFailure,
type ProviderErrorEvent,
} from "@opencode-ai/ai"
import { LLMClient, LLMError, LLMEvent, isContextOverflowFailure, type ProviderErrorEvent } from "@opencode-ai/ai"
import { SessionError } from "@opencode-ai/schema/session-error"
import { Money } from "@opencode-ai/schema/money"
import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
import { AgentV2 } from "../../agent"
import { Database } from "../../database/database"
import { EventV2 } from "../../event"
import { Location } from "../../location"
import { ModelV2 } from "../../model"
import { PermissionV2 } from "../../permission"
import { Instructions } from "../../instructions/index"
import { InstructionBuiltIns } from "../../instructions/builtins"
import { InstructionDiscovery } from "../../instruction-discovery"
import { SkillGuidance } from "../../skill/guidance"
import { ReferenceGuidance } from "../../reference/guidance"
import { McpGuidance } from "../../mcp/guidance"
import { InstructionEntry } from "../instruction-entry"
import { QuestionTool } from "../../tool/question"
import { ToolRegistry } from "../../tool/registry"
import { ToolOutputStore } from "../../tool-output-store"
import { InstructionState } from "../instruction-state"
import { SessionCompaction } from "../compaction"
import { SessionContext } from "../context"
import { SessionEvent } from "../event"
import { SessionHistory } from "../history"
import { SessionPending } from "../pending"
import { SessionRequest } from "../request"
import { SessionMessage } from "../message"
import { SessionSchema } from "../schema"
import { SessionStore } from "../store"
import { SessionTitle } from "../title"
import { Service } from "./index"
import { SessionRunnerModel } from "./model"
import { createLLMEventPublisher } from "./publish-llm-event"
import { toLLMMessages } from "./to-llm-message"
import { MAX_STEPS_PROMPT } from "./max-steps"
import { SessionRunnerSystemPrompt } from "./system-prompt"
import { Snapshot } from "../../snapshot"
import { makeLocationNode } from "../../effect/app-node"
import { llmClient } from "../../effect/app-node-platform"
import { AgentNotFoundError, StepFailedError } from "../error"
import { StepFailedError } from "../error"
import { toSessionError } from "../to-session-error"
import { SessionRunnerRetry } from "./retry"
import { PluginSupervisor } from "../../plugin/supervisor"
import { SessionModelHeaders } from "../model-headers"
type StepTokens = {
readonly input: number
@@ -87,22 +63,13 @@ const layer = Layer.effect(
Effect.gen(function* () {
const events = yield* EventV2.Service
const llm = yield* LLMClient.Service
const agents = yield* AgentV2.Service
const tools = yield* ToolRegistry.Service
const models = yield* SessionRunnerModel.Service
const store = yield* SessionStore.Service
const location = yield* Location.Service
const builtins = yield* InstructionBuiltIns.Service
const discovery = yield* InstructionDiscovery.Service
const skillGuidance = yield* SkillGuidance.Service
const referenceGuidance = yield* ReferenceGuidance.Service
const mcpGuidance = yield* McpGuidance.Service
const entries = yield* InstructionEntry.Service
const context = yield* SessionContext.Service
const requests = yield* SessionRequest.Service
const snapshots = yield* Snapshot.Service
const db = (yield* Database.Service).db
const compaction = yield* SessionCompaction.Service
const title = yield* SessionTitle.Service
const plugins = yield* PluginSupervisor.Service
// Title generation is a side effect of the first step; it must not delay step continuation.
// Tracked per process so repeated wakes before the second user message arrives don't
// re-fire a redundant LLM call; `SessionTitle` itself is idempotent based on durable history.
@@ -113,9 +80,6 @@ const layer = Layer.effect(
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
return session
})
const isCurrentLocation = (session: SessionSchema.Info) =>
session.location.directory === location.directory && session.location.workspaceID === location.workspaceID
const failInterruptedTools = Effect.fn("SessionRunner.failInterruptedTools")(function* (
sessionID: SessionSchema.ID,
) {
@@ -142,19 +106,6 @@ const layer = Layer.effect(
(reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError),
)
const loadInstructions = (agent: AgentV2.Selection, sessionID: SessionSchema.ID) =>
Effect.all(
[
builtins.load(sessionID),
discovery.load(),
skillGuidance.load(agent),
referenceGuidance.load(),
mcpGuidance.load(agent),
entries.load(sessionID),
],
{ concurrency: "unbounded" },
).pipe(Effect.map(Instructions.combine))
const attemptStep = Effect.fn("SessionRunner.attemptStep")(function* (
sessionID: SessionSchema.ID,
promotion: SessionPending.Delivery | undefined,
@@ -162,55 +113,34 @@ const layer = Layer.effect(
recoverOverflow?: typeof compaction.compact,
assistantMessageID?: SessionMessage.ID,
) {
const session = yield* getSession(sessionID)
if (!isCurrentLocation(session)) return yield* Effect.interrupt
yield* plugins.flush
const agent = yield* agents.select(session.agent)
const agentInfo = agent.info
if (!agentInfo) return yield* new AgentNotFoundError({ sessionID: session.id, agent: session.agent ?? agent.id })
const selected = yield* context.select(sessionID)
// Establish what the model knows before admitting what the user said, so
// a blocked first step leaves pending inputs untouched.
const instructions = yield* loadInstructions(agent, session.id)
yield* InstructionState.prepare(db, events, instructions, session.id)
yield* InstructionState.prepare(db, events, selected.instructions, selected.session.id)
let currentStep = step
if (promotion) {
let promoted = 0
if (promotion === "steer") promoted = yield* SessionPending.promoteSteers(db, events, session.id)
if (promotion === "steer") promoted = yield* SessionPending.promoteSteers(db, events, selected.session.id)
if (promotion === "queue") {
promoted += Number(yield* SessionPending.promoteNextQueued(db, events, session.id))
promoted += yield* SessionPending.promoteSteers(db, events, session.id)
promoted += Number(yield* SessionPending.promoteNextQueued(db, events, selected.session.id))
promoted += yield* SessionPending.promoteSteers(db, events, selected.session.id)
}
if (promoted > 0) currentStep = 1
}
const resolved = yield* models.resolve(session)
const loaded = yield* context.load(selected)
const session = loaded.session
const agent = loaded.agent
const resolved = loaded.model
const model = resolved.model
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
const history = yield* SessionHistory.entriesForRunner(db, session.id, instructions)
const context = history.entries.map((entry) => entry.message)
const compactionInput = { session, messages: context, model }
const compactionInput = { session, messages: loaded.messages, model }
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
const compacted = yield* compaction.compact(compactionInput)
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
return yield* new StepFailedError({ error: compacted.error })
}
const isLastStep = agentInfo.steps !== undefined && currentStep >= agentInfo.steps
const toolMaterialization = isLastStep ? undefined : yield* tools.materialize(agentInfo.permissions)
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
const request = LLM.request({
model,
http: {
headers: SessionModelHeaders.make(session),
},
providerOptions: { openai: { promptCacheKey } },
system: [agentInfo.system ? agentInfo.system : SessionRunnerSystemPrompt.provider(model), history.initial]
.filter((part): part is string => part !== undefined && part.length > 0)
.map(SystemPart.make),
messages: [
...toLLMMessages(context, resolved.ref, providerMetadataKey),
...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : []),
],
tools: toolMaterialization?.definitions ?? [],
toolChoice: isLastStep ? "none" : undefined,
const prepared = yield* requests.prepare({
context: loaded,
step: currentStep,
})
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
const ownedToolFibers: Array<Fiber.Fiber<void, ToolOutputStore.Error>> = []
@@ -222,7 +152,7 @@ const layer = Layer.effect(
// The selected catalog identity, not model.id: route-level ids are provider API
// model ids (for example gpt-5.5-fast resolves to api id gpt-5.5).
model: resolved.ref,
providerMetadataKey,
providerMetadataKey: model.route.providerMetadataKey ?? model.provider,
snapshot: startSnapshot,
assistantMessageID,
})
@@ -232,7 +162,7 @@ const layer = Layer.effect(
const serialized = <A, E, R>(effect: Effect.Effect<A, E, R>) => publication.withPermit(effect)
const publish = (event: LLMEvent, error?: SessionError.Error) => serialized(publisher.publish(event, error))
let overflowFailure: ProviderErrorEvent | undefined
const providerStream = llm.stream(request).pipe(
const providerStream = llm.stream(prepared.request).pipe(
Stream.runForEach((event) =>
Effect.gen(function* () {
if (overflowFailure || publisher.hasProviderError()) return
@@ -244,13 +174,9 @@ const layer = Layer.effect(
}
yield* publish(event)
if (event.type !== "tool-call" || event.providerExecuted) return
if (!toolMaterialization) {
yield* serialized(
publisher.failUnsettledTools({
type: "tool.execution",
message: "Tools are disabled after the maximum agent steps",
}),
)
const tool = prepared.resolveTool(event.name)
if (tool.type === "reject") {
yield* serialized(publisher.failUnsettledTools(tool.error))
return
}
needsContinuation = true
@@ -258,7 +184,7 @@ const layer = Layer.effect(
ownedToolFibers.push(
yield* Effect.uninterruptibleMask((restore) =>
restore(
toolMaterialization.settle({
tool.settle({
sessionID: session.id,
agent: agent.id,
messageID: assistantMessageID,
@@ -337,8 +263,7 @@ const layer = Layer.effect(
recoverOverflow &&
!publisher.hasRetryEvidence() &&
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
(yield* restore(recoverOverflow({ session, messages: context, model }))).status ===
"completed"
(yield* restore(recoverOverflow({ session, messages: loaded.messages, model }))).status === "completed"
)
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const
@@ -349,11 +274,7 @@ const layer = Layer.effect(
const llmFailure = streamFailure instanceof LLMError ? streamFailure : undefined
if (llmFailure && !publisher.hasProviderError()) {
const error = toSessionError(llmFailure)
if (
SessionRunnerRetry.isRetryable(llmFailure) &&
!publisher.hasRetryEvidence() &&
(agentInfo.steps === undefined || currentStep < agentInfo.steps)
) {
if (SessionRunnerRetry.isRetryable(llmFailure) && !publisher.hasRetryEvidence() && prepared.retryAllowed) {
return yield* new SessionRunnerRetry.RetryableFailure({
cause: llmFailure,
assistantMessageID: yield* publisher.startAssistant(),
@@ -583,21 +504,12 @@ export const node = makeLocationNode({
deps: [
EventV2.node,
llmClient,
AgentV2.node,
ToolRegistry.node,
SessionRunnerModel.node,
SessionContext.node,
SessionRequest.node,
SessionStore.node,
Location.node,
InstructionBuiltIns.node,
InstructionDiscovery.node,
SkillGuidance.node,
ReferenceGuidance.node,
McpGuidance.node,
InstructionEntry.node,
SessionCompaction.node,
SessionTitle.node,
Snapshot.node,
Database.node,
PluginSupervisor.node,
],
})
@@ -1,142 +0,0 @@
You are opencode, an agent - please keep going until the users query is completely resolved, before ending your turn and yielding back to the user.
Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.
You MUST iterate and keep going until the problem is solved.
You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me.
Only terminate your turn when you are sure that the problem is solved. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH.
You must use the webfetch tool to recursively gather all information from URL's provided to you by the user, as well as any links you find in the content of those pages.
Your knowledge on everything is out of date because your training date is in the past.
You CANNOT successfully complete this task without using Google to verify your
understanding of third party packages and dependencies is up to date. You must use the webfetch tool to search google for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need.
Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why.
If the user request is "resume" or "continue" or "try again", check the previous conversation history to identify the next incomplete step. Continue from that step, and do not hand back control to the user until the request is complete. Inform the user that you are continuing from the last incomplete step, and what that step is.
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided.
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
You MUST keep working until the problem is completely solved. Do not end your turn until you have completed the necessary work and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input.
# Workflow
1. Fetch any URL's provided by the user using the `webfetch` tool.
2. Understand the problem deeply. Carefully read the issue and think critically about what is required. Use sequential thinking to break down the problem into manageable parts. Consider the following:
- What is the expected behavior?
- What are the edge cases?
- What are the potential pitfalls?
- How does this fit into the larger context of the codebase?
- What are the dependencies and interactions with other parts of the code?
3. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
4. Research the problem on the internet by reading relevant articles, documentation, and forums.
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps.
6. Implement the fix incrementally. Make small, testable code changes.
7. Debug as needed. Use debugging techniques to isolate and resolve issues.
8. Test frequently. Run tests after each change to verify correctness.
9. Iterate until the root cause is fixed and all tests pass.
10. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete.
Refer to the detailed sections below for more information on each step.
## 1. Fetch Provided URLs
- If the user provides a URL, use the `webfetch` tool to retrieve the content of the provided URL.
- After fetching, review the content returned by the webfetch tool.
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
## 2. Deeply Understand the Problem
Carefully read the issue and think hard about a plan to solve it before coding.
## 3. Codebase Investigation
- Explore relevant files and directories.
- Search for key functions, classes, or variables related to the issue.
- Read and understand relevant code snippets.
- Identify the root cause of the problem.
- Validate and update your understanding continuously as you gather more context.
## 4. Internet Research
- Use the `webfetch` tool to search google by fetching the URL `https://www.google.com/search?q=your+search+query`.
- After fetching, review the content returned by the fetch tool.
- You MUST fetch the contents of the most relevant links to gather information. Do not rely on the summary that you find in the search results.
- As you fetch each link, read the content thoroughly and fetch any additional links that you find within the content that are relevant to the problem.
- Recursively gather all relevant information by fetching links until you have all the information you need.
## 5. Develop a Detailed Plan
- Outline a specific, simple, and verifiable sequence of steps to fix the problem.
- Continue through the planned steps instead of ending your turn and asking the user what they want to do next.
## 6. Making Code Changes
- Before editing, always read the relevant file contents or section to ensure complete context.
- Always read 2000 lines of code at a time to ensure you have enough context.
- If a patch is not applied correctly, attempt to reapply it.
- Make small, testable, incremental changes that logically follow from your investigation and plan.
- Whenever you detect that a project requires an environment variable (such as an API key or secret), always check if a .env file exists in the project root. If it does not exist, automatically create a .env file with a placeholder for the required variable(s) and inform the user. Do this proactively, without waiting for the user to request it.
## 7. Debugging
- Make code changes only if you have high confidence they can solve the problem
- When debugging, try to determine the root cause rather than addressing symptoms
- Debug for as long as needed to identify the root cause and identify a fix
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening
- To test hypotheses, you can also add test statements or functions
- Revisit your assumptions if unexpected behavior occurs.
# Communication Guidelines
Always communicate clearly and concisely in a casual, friendly yet professional tone.
<examples>
"Let me fetch the URL you provided to gather more information."
"Ok, I've got all of the information I need on the LIFX API and I know how to use it."
"Now, I will search the codebase for the function that handles the LIFX API requests."
"I need to update several files here - stand by"
"OK! Now let's run the tests to make sure everything is working correctly."
"Whelp - I see we have some problems. Let's fix those up."
</examples>
- Respond with clear, direct answers. Use bullet points and code blocks for structure. - Avoid unnecessary explanations, repetition, and filler.
- Always write code directly to the correct files.
- Do not display code to the user unless they specifically ask for it.
- Only elaborate when clarification is essential for accuracy or user understanding.
# Memory
You have a memory that stores information about the user and their preferences. This memory is used to provide a more personalized experience. You can access and update this memory as needed. The memory is stored in a file called `.github/instructions/memory.instruction.md`. If the file is empty, you'll need to create it.
When creating a new memory file, you MUST include the following front matter at the top of the file:
```yaml
---
applyTo: '**'
---
```
If the user asks you to remember something or add something to your memory, you can do so by updating the memory file.
# Reading Files and Folders
**Always check if you have already read a file, folder, or workspace structure before reading it again.**
- If you have already read the content and it has not changed, do NOT re-read it.
- Only re-read files or folders if:
- You suspect the content has changed since your last read.
- You have made edits to the file or folder.
- You encounter an error that suggests the context may be stale or incomplete.
- Use your internal memory and previous context to avoid redundant reads.
- This will save time, reduce unnecessary operations, and make your workflow more efficient.
# Writing Prompts
If you are asked to write a prompt, you should always generate the prompt in markdown format.
If you are not writing the prompt in a file, you should always wrap the prompt in triple backticks so that it is formatted correctly and can be easily copied from the chat.
# Git
If the user tells you to stage and commit, you may do so.
You are NEVER allowed to stage and commit files automatically.
@@ -1,30 +0,0 @@
export * as SessionRunnerSystemPrompt from "./system-prompt"
import type { Model } from "@opencode-ai/ai"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_BEAST from "./prompt/beast.txt"
import PROMPT_CODEX from "./prompt/codex.txt"
import PROMPT_DEFAULT from "./prompt/default.txt"
import PROMPT_GEMINI from "./prompt/gemini.txt"
import PROMPT_GPT from "./prompt/gpt.txt"
import PROMPT_KIMI from "./prompt/kimi.txt"
import PROMPT_TRINITY from "./prompt/trinity.txt"
export function provider(model: Model) {
const id = model.id.toLowerCase()
if (id.includes("gpt-4") || id.includes("o1") || id.includes("o3")) return normalize(PROMPT_BEAST)
if (id.includes("gpt")) {
if (id.includes("codex")) return normalize(PROMPT_CODEX)
return normalize(PROMPT_GPT)
}
if (id.includes("gemini-")) return normalize(PROMPT_GEMINI)
if (id.includes("claude")) return normalize(PROMPT_ANTHROPIC)
if (id.includes("trinity")) return normalize(PROMPT_TRINITY)
if (id.includes("kimi")) return normalize(PROMPT_KIMI)
return normalize(PROMPT_DEFAULT)
}
function normalize(prompt: string) {
return prompt.replaceAll("\r\n", "\n")
}
+2 -2
View File
@@ -4,7 +4,7 @@ This folder owns Core's one local tool representation, process and Location regi
## Representations
- `tool.ts` defines the opaque canonical `Tool.make({ description, input, output, execute, toModelOutput })` value. Shipped built-ins and plugin tools use the same type.
- `tool.ts` defines the structural canonical `Tool.make({ description, input, output, execute, toModelOutput })` declaration. Shipped built-ins and plugin tools use the same type.
- `tools.ts` exposes the registration-only `Tools.Service` view used by Location producers.
- `registry.ts` stores only canonical Location registrations, derives definitions, invokes tools, and applies generic output bounding.
@@ -12,7 +12,7 @@ Do not add a second executable entry type, registry-owned executor, authorizatio
## Construction
Tool schemas and projection use `input` and `output` terminology. A tool value is opaque: its codecs, executor, definition derivation, and catalog permission declaration are private runtime details.
Tool schemas and projection use `input` and `output` terminology. A tool value carries its schemas, executor, projection, and optional catalog permission directly so separately loaded plugin package instances can exchange it structurally.
Location-scoped built-in layers acquire `PermissionV2.Service` and every other required Location service while the layer is constructed. The executor captures those services. Permission sources are always constructed from the canonical invocation context:
+13
View File
@@ -195,6 +195,19 @@ export const Plugin = {
),
)
.pipe(Effect.orDie)
yield* ctx.session.hook("context", (event) =>
Effect.sync(() => {
const usePatch =
event.model.providerID.toLowerCase() === "openai" || event.model.id.toLowerCase().includes("gpt")
if (usePatch) {
delete event.tools.edit
delete event.tools.write
return
}
delete event.tools.patch
}),
)
}),
}
+27
View File
@@ -215,5 +215,32 @@ export const Plugin = {
),
)
.pipe(Effect.orDie)
yield* ctx.session.hook("context", (event) =>
Effect.gen(function* () {
const tool = event.tools[name]
if (!tool) return
const selected = yield* agents.resolve(event.agent)
if (!selected) return
const available = (yield* agents.list())
.filter(
(agent) =>
agent.mode !== "primary" &&
!agent.hidden &&
PermissionV2.evaluate(name, agent.id, selected.permissions).effect !== "deny",
)
.toSorted((a, b) => a.id.localeCompare(b.id))
if (available.length === 0) return
tool.description = [
tool.description,
"",
"Available subagents:",
...available.map(
(agent) =>
`- ${agent.id}: ${agent.description ?? "This subagent should only be called when explicitly requested."}`,
),
].join("\n")
}),
)
}),
}
+3
View File
@@ -80,6 +80,9 @@ export const Info = Schema.Struct({
description:
"Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
}),
subagent_depth: Schema.optional(NonNegativeInt).annotate({
description: "Maximum subagent nesting depth. Defaults to 1, which prevents subagents from launching subagents.",
}),
username: Schema.optional(Schema.String).annotate({
description: "Custom username to display in conversations instead of system username",
}),
File diff suppressed because one or more lines are too long
+79 -15
View File
@@ -29,6 +29,20 @@ const failingIt = testEffect(
AppNodeBuilder.build(LayerNode.group([Integration.node, EventV2.node]), [[Credential.node, failingCredentialNode]]),
)
function eventually<A, E, R>(
effect: Effect.Effect<A, E, R>,
predicate: (value: A) => boolean,
remaining = 1000,
): Effect.Effect<A, E | Error, R> {
return Effect.gen(function* () {
const value = yield* effect
if (predicate(value)) return value
if (remaining === 0) return yield* Effect.fail(new Error("Timed out waiting for value"))
yield* Effect.promise(() => Bun.sleep(1))
return yield* eventually(effect, predicate, remaining - 1)
})
}
describe("Integration", () => {
it.effect("registers integrations through the editor", () =>
Effect.gen(function* () {
@@ -151,6 +165,51 @@ describe("Integration", () => {
}),
)
it.live("runs command authentication and stores the final output line", () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
const credentials = yield* Credential.Service
const integrationID = Integration.ID.make("company")
const methodID = Integration.MethodID.make("login")
yield* integrations.transform((editor) =>
editor.method.update({
integrationID,
method: {
id: methodID,
type: "command",
label: "Log in",
command: [
process.execPath,
"-e",
'console.log("https://example.com/login"); await Bun.sleep(50); console.log("secret")',
],
},
}),
)
const attempt = yield* integrations.command.connect({ integrationID, methodID, label: "Work" })
const pending = yield* eventually(
integrations.command.status({ integrationID, attemptID: attempt.attemptID }),
(status) => status.status === "pending" && status.message?.includes("https://example.com/login") === true,
)
expect(pending).toMatchObject({ status: "pending", message: "https://example.com/login" })
expect(
yield* eventually(
integrations.command.status({ integrationID, attemptID: attempt.attemptID }),
(status) => status.status === "complete",
),
).toEqual({ status: "complete", time: attempt.time })
expect(yield* credentials.list(integrationID)).toEqual([
expect.objectContaining({
integrationID,
label: "Work",
value: Credential.Key.make({ type: "key", key: "secret" }),
}),
])
}),
)
it.effect("completes code OAuth once and stores the credential", () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
@@ -181,14 +240,14 @@ describe("Integration", () => {
}),
)
const attempt = yield* integrations.connection.oauth({
const attempt = yield* integrations.oauth.connect({
integrationID,
methodID,
inputs: {},
label: "Personal",
})
expect(attempt.mode).toBe("code")
yield* integrations.attempt.complete({ attemptID: attempt.attemptID, code: "1234" })
yield* integrations.oauth.complete({ integrationID, attemptID: attempt.attemptID, code: "1234" })
expect((yield* credentials.list(integrationID))[0]).toEqual(
expect.objectContaining({
@@ -230,12 +289,17 @@ describe("Integration", () => {
}),
)
const attempt = yield* integrations.connection.oauth({ integrationID, methodID, inputs: {} })
expect(yield* integrations.attempt.complete({ attemptID: attempt.attemptID }).pipe(Effect.flip)).toBeInstanceOf(
Integration.CodeRequiredError,
)
const attempt = yield* integrations.oauth.connect({ integrationID, methodID, inputs: {} })
expect(
yield* integrations.oauth.complete({ integrationID, attemptID: attempt.attemptID }).pipe(Effect.flip),
).toBeInstanceOf(Integration.CodeRequiredError)
expect(closed).toBe(false)
yield* integrations.attempt.cancel(attempt.attemptID)
yield* integrations.oauth.cancel({
integrationID: Integration.ID.make("other"),
attemptID: attempt.attemptID,
})
expect(closed).toBe(false)
yield* integrations.oauth.cancel({ integrationID, attemptID: attempt.attemptID })
expect(closed).toBe(true)
expect(yield* credentials.list(integrationID)).toEqual([])
}),
@@ -263,9 +327,9 @@ describe("Integration", () => {
}),
)
const attempt = yield* integrations.connection.oauth({ integrationID, methodID, inputs: {} })
const attempt = yield* integrations.oauth.connect({ integrationID, methodID, inputs: {} })
yield* Effect.yieldNow
expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({
expect(yield* integrations.oauth.status({ integrationID, attemptID: attempt.attemptID })).toEqual({
status: "complete",
time: attempt.time,
})
@@ -301,12 +365,12 @@ describe("Integration", () => {
}),
)
const attempt = yield* integrations.connection.oauth({ integrationID, methodID, inputs: {} })
const exit = yield* integrations.attempt
.complete({ attemptID: attempt.attemptID, code: "1234" })
const attempt = yield* integrations.oauth.connect({ integrationID, methodID, inputs: {} })
const exit = yield* integrations.oauth
.complete({ integrationID, attemptID: attempt.attemptID, code: "1234" })
.pipe(Effect.exit)
expect(Exit.isFailure(exit) && Cause.hasDies(exit.cause)).toBe(true)
expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({
expect(yield* integrations.oauth.status({ integrationID, attemptID: attempt.attemptID })).toEqual({
status: "failed",
message: "credential persistence failed",
time: attempt.time,
@@ -337,11 +401,11 @@ describe("Integration", () => {
}),
)
const attempt = yield* integrations.connection.oauth({ integrationID, methodID, inputs: {} })
const attempt = yield* integrations.oauth.connect({ integrationID, methodID, inputs: {} })
expect(attempt.time.expires - attempt.time.created).toBe(Duration.toMillis(Duration.minutes(10)))
yield* TestClock.adjust(Duration.minutes(10))
yield* Effect.yieldNow
expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({
expect(yield* integrations.oauth.status({ integrationID, attemptID: attempt.attemptID })).toEqual({
status: "expired",
time: attempt.time,
})
+3
View File
@@ -44,6 +44,9 @@ export const registerToolPlugin = <R>(plugin: {
Effect.gen(function* () {
const tools = yield* Tools.Service
const context = host({
session: {
hook: () => Effect.succeed({ dispose: Effect.void }),
},
tool: {
transform: (callback) =>
Effect.gen(function* () {
+7 -2
View File
@@ -187,15 +187,20 @@ function resourceMcpLayer(url: string, onFormCreated?: (form: Form.Info) => Effe
active: unusedIntegration,
resolve: unusedIntegration,
key: unusedIntegration,
oauth: unusedIntegration,
update: unusedIntegration,
remove: unusedIntegration,
},
attempt: {
oauth: {
connect: unusedIntegration,
status: unusedIntegration,
complete: unusedIntegration,
cancel: unusedIntegration,
},
command: {
connect: unusedIntegration,
status: unusedIntegration,
cancel: unusedIntegration,
},
}),
Layer.mock(Credential.Service, {}),
),
+45
View File
@@ -0,0 +1,45 @@
import { describe, expect } from "bun:test"
import { Message, SystemPart } from "@opencode-ai/ai"
import { Agent } from "@opencode-ai/schema/agent"
import { Model } from "@opencode-ai/schema/model"
import { Provider } from "@opencode-ai/schema/provider"
import { Session } from "@opencode-ai/schema/session"
import { Effect, Layer } from "effect"
import { PluginHooks } from "../src/plugin/hooks"
import { testEffect } from "./lib/effect"
const layer = PluginHooks.node.implementation as Layer.Layer<PluginHooks.Service>
const it = testEffect(layer)
describe("PluginHooks", () => {
it.effect("registers scoped session hooks and triggers them sequentially", () =>
Effect.gen(function* () {
const hooks = yield* PluginHooks.Service
const seen: string[] = []
yield* hooks.register("session", "context", (event) =>
Effect.sync(() => {
seen.push("first")
event.system.push(SystemPart.make("second"))
}),
)
yield* hooks.register("session", "context", (event) =>
Effect.sync(() => {
seen.push(event.system[1]?.text ?? "missing")
event.messages = [Message.user("changed")]
}),
)
const event = {
sessionID: Session.ID.make("ses_hooks"),
agent: Agent.ID.make("build"),
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
system: [SystemPart.make("first")],
messages: [Message.user("original")],
tools: {},
}
expect(yield* hooks.trigger("session", "context", event)).toBe(event)
expect(seen).toEqual(["first", "second"])
expect(event.messages).toEqual([Message.user("changed")])
}),
)
})
+63 -19
View File
@@ -5,15 +5,23 @@ import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types"
import type {
IntegrationCommandMethod,
IntegrationEnvMethod,
IntegrationKeyMethod,
IntegrationOAuthMethod,
} from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect"
type Overrides = Partial<Omit<PluginContext, "options">>
type Overrides = Partial<Omit<PluginContext, "options" | "session">> & {
readonly session?: Partial<PluginContext["session"]>
}
export function host(overrides: Overrides = {}): PluginContext {
return {
options: {},
agent: overrides.agent ?? {
get: () => Effect.die("unused agent.get"),
list: () => Effect.die("unused agent.list"),
transform: () => Effect.die("unused agent.transform"),
reload: () => Effect.die("unused agent.reload"),
@@ -27,6 +35,7 @@ export function host(overrides: Overrides = {}): PluginContext {
get: () => Effect.die("unused catalog.provider.get"),
},
model: {
get: () => Effect.die("unused catalog.model.get"),
list: () => Effect.die("unused catalog.model.list"),
default: () => Effect.die("unused catalog.model.default"),
},
@@ -46,12 +55,17 @@ export function host(overrides: Overrides = {}): PluginContext {
get: () => Effect.die("unused integration.get"),
connect: {
key: () => Effect.die("unused integration.connect.key"),
oauth: () => Effect.die("unused integration.connect.oauth"),
},
attempt: {
status: () => Effect.die("unused integration.attempt.status"),
complete: () => Effect.die("unused integration.attempt.complete"),
cancel: () => Effect.die("unused integration.attempt.cancel"),
oauth: {
connect: () => Effect.die("unused integration.oauth.connect"),
status: () => Effect.die("unused integration.oauth.status"),
complete: () => Effect.die("unused integration.oauth.complete"),
cancel: () => Effect.die("unused integration.oauth.cancel"),
},
command: {
connect: () => Effect.die("unused integration.command.connect"),
status: () => Effect.die("unused integration.command.status"),
cancel: () => Effect.die("unused integration.command.cancel"),
},
transform: () => Effect.die("unused integration.transform"),
reload: () => Effect.die("unused integration.reload"),
@@ -77,18 +91,20 @@ export function host(overrides: Overrides = {}): PluginContext {
transform: () => Effect.die("unused tool.transform"),
hook: () => Effect.die("unused tool.hook"),
},
session: overrides.session ?? {
create: () => Effect.die("unused session.create"),
get: () => Effect.die("unused session.get"),
prompt: () => Effect.die("unused session.prompt"),
command: () => Effect.die("unused session.command"),
interrupt: () => Effect.die("unused session.interrupt"),
session: {
hook: overrides.session?.hook ?? (() => Effect.die("unused session.hook")),
create: overrides.session?.create ?? (() => Effect.die("unused session.create")),
get: overrides.session?.get ?? (() => Effect.die("unused session.get")),
prompt: overrides.session?.prompt ?? (() => Effect.die("unused session.prompt")),
command: overrides.session?.command ?? (() => Effect.die("unused session.command")),
interrupt: overrides.session?.interrupt ?? (() => Effect.die("unused session.interrupt")),
},
}
}
export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] {
return {
get: (id) => agent.get(AgentV2.ID.make(id)).pipe(Effect.map((value) => value && agentInfo(value))),
list: () => Effect.die("unused agent.list"),
reload: agent.reload,
transform: (callback) =>
@@ -119,6 +135,10 @@ export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"
get: () => Effect.die("unused catalog.provider.get"),
},
model: {
get: (providerID, modelID) =>
catalog.model
.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))
.pipe(Effect.map((value) => value && modelInfo(value))),
list: () => Effect.die("unused catalog.model.list"),
default: () => Effect.die("unused catalog.model.default"),
},
@@ -190,12 +210,17 @@ export function integrationHost(integration: Integration.Interface): PluginConte
get: () => Effect.die("unused integration.get"),
connect: {
key: () => Effect.die("unused integration.connect.key"),
oauth: () => Effect.die("unused integration.connect.oauth"),
},
attempt: {
status: () => Effect.die("unused integration.attempt.status"),
complete: () => Effect.die("unused integration.attempt.complete"),
cancel: () => Effect.die("unused integration.attempt.cancel"),
oauth: {
connect: () => Effect.die("unused integration.oauth.connect"),
status: () => Effect.die("unused integration.oauth.status"),
complete: () => Effect.die("unused integration.oauth.complete"),
cancel: () => Effect.die("unused integration.oauth.cancel"),
},
command: {
connect: () => Effect.die("unused integration.command.connect"),
status: () => Effect.die("unused integration.command.status"),
cancel: () => Effect.die("unused integration.command.cancel"),
},
reload: integration.reload,
connection: {
@@ -278,6 +303,17 @@ export function integrationHost(integration: Integration.Interface): PluginConte
})
return
}
if (input.method.type === "command") {
draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: {
...input.method,
id: Integration.MethodID.make(input.method.id),
command: [...input.method.command],
},
})
return
}
draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: input.method,
@@ -293,6 +329,7 @@ export function integrationHost(integration: Integration.Interface): PluginConte
function method(value: Integration.Method) {
if (value.type === "env") return { type: value.type, names: [...value.names] }
if (value.type === "key") return { type: value.type, label: value.label }
if (value.type === "command") return { ...value, command: [...value.command] }
return {
type: value.type,
id: value.id,
@@ -305,10 +342,17 @@ function method(value: Integration.Method) {
}
function internalMethod(
value: IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod,
value: IntegrationOAuthMethod | IntegrationCommandMethod | IntegrationKeyMethod | IntegrationEnvMethod,
): Integration.Method {
if (value.type === "env") return value
if (value.type === "key") return value
if (value.type === "command") {
return {
...value,
id: Integration.MethodID.make(value.id),
command: [...value.command],
}
}
return {
...value,
id: Integration.MethodID.make(value.id),
+71
View File
@@ -1,13 +1,21 @@
import { describe, expect } from "bun:test"
import { Message, SystemPart } from "@opencode-ai/ai"
import { Effect, Schema } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { PluginPromise } from "@opencode-ai/core/plugin/promise"
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Plugin } from "@opencode-ai/plugin/v2"
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
import { Model } from "@opencode-ai/schema/model"
import { Provider } from "@opencode-ai/schema/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
@@ -43,6 +51,37 @@ describe("fromPromise", () => {
}),
)
it.effect("forwards direct agent and model reads", () =>
Effect.gen(function* () {
const agents = yield* AgentV2.Service
const catalog = yield* Catalog.Service
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
yield* agents.transform((draft) =>
draft.update(AgentV2.ID.make("reviewer"), (agent) => {
agent.description = "Reviews code"
}),
)
yield* catalog.transform((draft) =>
draft.model.update(ProviderV2.ID.make("test"), ModelV2.ID.make("alias"), (model) => {
model.modelID = ModelV2.ID.make("gpt-5")
}),
)
yield* PluginPromise.fromPromise(
Plugin.define({
id: "promise-direct-reads",
setup: async (ctx) => {
expect(await ctx.agent.get("reviewer")).toMatchObject({ description: "Reviews code" })
expect(await ctx.agent.get("missing")).toBeUndefined()
expect(await ctx.catalog.model.get("test", "alias")).toMatchObject({ modelID: "gpt-5" })
expect(await ctx.catalog.model.get("test", "missing")).toBeUndefined()
},
}),
).effect(host)
}),
)
it.effect("loads a promise plugin and registers a transform hook", () =>
Effect.gen(function* () {
const agents = yield* AgentV2.Service
@@ -72,6 +111,38 @@ describe("fromPromise", () => {
}),
)
it.effect("forwards session context hooks", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const hooks = yield* PluginHooks.Service
const host = yield* PluginHost.make(plugin)
yield* PluginPromise.fromPromise(
Plugin.define({
id: "promise-session-context",
setup: async (ctx) => {
await ctx.session.hook("context", (event) => {
event.system.push(SystemPart.make("Promise hook"))
delete event.tools.echo
})
},
}),
).effect(host)
const event: SessionHooks["context"] = {
sessionID: SessionV2.ID.make("ses_promise_session_context"),
agent: AgentV2.ID.make("build"),
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
system: [SystemPart.make("Initial")],
messages: [Message.user("Hello")],
tools: { echo: { description: "Echo", input: { type: "object" } } },
}
yield* hooks.trigger("session", "context", event)
expect(event.system.map((part) => part.text)).toEqual(["Initial", "Promise hook"])
expect(event.tools).toEqual({})
}),
)
it.effect("disposes a hook registration on request", () =>
Effect.gen(function* () {
const agents = yield* AgentV2.Service
@@ -124,13 +124,17 @@ describe("OpencodePlugin", () => {
Effect.gen(function* () {
yield* addPlugin()
const integrations = yield* Integration.Service
const attempt = yield* integrations.connection.oauth({
integrationID: Integration.ID.make("opencode"),
const integrationID = Integration.ID.make("opencode")
const attempt = yield* integrations.oauth.connect({
integrationID,
methodID: Integration.MethodID.make("device"),
inputs: { server: `${server.url.origin}/console///?ignored=true#ignored` },
})
expect(attempt.url).toBe(`${server.url.origin}/verify`)
yield* eventually(integrations.attempt.status(attempt.attemptID), (status) => status.status === "complete")
yield* eventually(
integrations.oauth.status({ integrationID, attemptID: attempt.attemptID }),
(status) => status.status === "complete",
)
expect(requests).toContain("POST /console/auth/device/code")
expect(requests).toContain("POST /console/auth/device/token")
@@ -147,8 +151,8 @@ describe("OpencodePlugin", () => {
it.effect("rejects non-HTTP OpenCode servers", () =>
Effect.gen(function* () {
yield* addPlugin()
const error = yield* (yield* Integration.Service).connection
.oauth({
const error = yield* (yield* Integration.Service).oauth
.connect({
integrationID: Integration.ID.make("opencode"),
methodID: Integration.MethodID.make("device"),
inputs: { server: "ftp://console.example.com" },
@@ -0,0 +1,161 @@
import { describe, expect, test } from "bun:test"
import { SystemPart } from "@opencode-ai/ai"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt"
import { SessionV2 } from "@opencode-ai/core/session"
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
import { Model } from "@opencode-ai/schema/model"
import { Provider } from "@opencode-ai/schema/provider"
import { Effect } from "effect"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
import PROMPT_META from "../../src/plugin/system-prompt/meta.txt"
import PROMPT_DEFAULT from "../../src/session/runner/prompt/base.txt"
const it = testEffect(PluginTestLayer)
const fallback = PROMPT_DEFAULT
const makeHost = Effect.gen(function* () {
const plugins = yield* PluginV2.Service
return yield* PluginHost.make(plugins)
})
const context = (id: string, system = fallback): SessionHooks["context"] => ({
sessionID: SessionV2.ID.make("ses_system_prompt"),
agent: AgentV2.ID.make("build"),
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make(id) }),
system: [SystemPart.make(system)],
messages: [],
tools: {},
})
describe("SystemPromptPlugin", () => {
test("uses V2 vocabulary in the Meta prompt", () => {
expect(PROMPT_META).toContain("webfetch tool")
expect(PROMPT_META).toContain("subagent tool")
expect(PROMPT_META).toContain("shell tool")
expect(PROMPT_META).toContain("read for reading files")
expect(PROMPT_META).toContain("edit for editing")
expect(PROMPT_META).toContain("write for creating files")
expect(PROMPT_META).toContain("https://v2.opencode.ai/llms.txt")
expect(PROMPT_META).not.toMatch(
/TodoWrite|Task tool|WebFetch|\bBash\b|Read for reading files|Edit for editing|Write for creating files|https:\/\/opencode\.ai\/docs/,
)
})
test("uses granular IDs with a common prefix", () => {
expect(SystemPromptPlugin.Plugins.map((plugin) => plugin.id)).toEqual([
"opencode.system-prompt.openai",
"opencode.system-prompt.google",
"opencode.system-prompt.anthropic",
"opencode.system-prompt.kimi",
"opencode.system-prompt.arcee",
"opencode.system-prompt.meta",
])
})
it.effect("selects model-lab prompts through session context hooks", () =>
Effect.gen(function* () {
const hooks = yield* PluginHooks.Service
const pluginHost = yield* makeHost
yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), {
discard: true,
})
const cases = [
["gpt-5", "You are OpenCode, You and the user share the same workspace"],
["gpt-4.1", "You are OpenCode, You and the user share the same workspace"],
["o3", "You are OpenCode, You and the user share the same workspace"],
["gpt-5-codex", "## Editing constraints"],
["gemini-2.5-pro", "# Core Mandates"],
["claude-sonnet-4", "# Professional objectivity"],
["kimi-k2", "# Prompt and Tool Use"],
["trinity", "what command should I run to list files"],
["meta/muse-spark-1.1", "OpenCode powered by Meta Muse Spark"],
["llama-3.3", "You are opencode, an interactive CLI tool"],
] as const
yield* Effect.forEach(
cases,
([id, expected]) => {
const event = context(id)
return hooks
.trigger("session", "context", event)
.pipe(Effect.tap(() => Effect.sync(() => expect(event.system[0]?.text).toContain(expected))))
},
{ discard: true },
)
}),
)
it.effect("preserves an explicit agent system prompt", () =>
Effect.gen(function* () {
const agents = yield* AgentV2.Service
const hooks = yield* PluginHooks.Service
yield* agents.transform((draft) =>
draft.update(AgentV2.ID.make("build"), (agent) => {
agent.system = "Custom agent prompt"
}),
)
const pluginHost = yield* makeHost
yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), {
discard: true,
})
const event = context("gpt-5", "Custom agent prompt")
yield* hooks.trigger("session", "context", event)
expect(event.system.map((part) => part.text)).toEqual(["Custom agent prompt"])
}),
)
it.effect("allows one model-lab prompt plugin to be enabled independently", () =>
Effect.gen(function* () {
const hooks = yield* PluginHooks.Service
const pluginHost = yield* makeHost
yield* SystemPromptPlugin.GooglePlugin.effect(pluginHost)
const gemini = context("gemini-2.5-pro")
const claude = context("claude-sonnet-4")
yield* hooks.trigger("session", "context", gemini)
yield* hooks.trigger("session", "context", claude)
expect(gemini.system[0]?.text).toContain("# Core Mandates")
expect(claude.system[0]?.text).toBe(fallback)
}),
)
it.effect("selects against the catalog model ID instead of its alias", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const hooks = yield* PluginHooks.Service
const pluginHost = yield* makeHost
yield* catalog.transform((draft) => {
draft.model.update(Provider.ID.make("test"), Model.ID.make("openai-alias"), (model) => {
model.modelID = Model.ID.make("gpt-5")
})
draft.model.update(Provider.ID.make("test"), Model.ID.make("gpt-5-alias"), (model) => {
model.modelID = Model.ID.make("custom-model")
})
draft.model.update(Provider.ID.make("test"), Model.ID.make("codex-family-alias"), (model) => {
model.modelID = Model.ID.make("custom-deployment")
model.family = Model.Family.make("gpt-codex")
})
})
yield* SystemPromptPlugin.OpenAIPlugin.effect(pluginHost)
const physicalOpenAI = context("openai-alias")
const physicalCustom = context("gpt-5-alias")
const familyOpenAI = context("codex-family-alias")
yield* hooks.trigger("session", "context", physicalOpenAI)
yield* hooks.trigger("session", "context", physicalCustom)
yield* hooks.trigger("session", "context", familyOpenAI)
expect(physicalOpenAI.system[0]?.text).toContain("You are OpenCode, You and the user share the same workspace")
expect(physicalCustom.system[0]?.text).toBe(fallback)
expect(familyOpenAI.system[0]?.text).toContain("## Editing constraints")
}),
)
})
@@ -1,6 +1,7 @@
import { HttpRecorder } from "@opencode-ai/http-recorder"
import * as OpenAIChat from "@opencode-ai/ai/protocols/openai-chat"
import { Auth, LLMClient, RequestExecutor } from "@opencode-ai/ai/route"
import { Catalog } from "@opencode-ai/core/catalog"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
@@ -36,11 +37,14 @@ import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
import { McpGuidance } from "@opencode-ai/core/mcp/guidance"
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt"
import { describe, expect } from "bun:test"
import { eq } from "drizzle-orm"
import { Effect, Layer } from "effect"
import path from "node:path"
import { testEffect } from "./lib/effect"
import { agentHost, catalogHost, host } from "./plugin/host"
const cassetteName = "session-runner/openai-chat-streams-text"
const cassetteDirectory = path.resolve(import.meta.dir, "fixtures/recordings")
@@ -77,6 +81,20 @@ const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Ef
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(Instructions.empty) })
const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
const pluginSupervisor = Layer.succeed(PluginSupervisor.Service, PluginSupervisor.Service.of({ flush: Effect.void }))
const promptCatalog = Layer.mock(Catalog.Service, {
provider: {
get: () => Effect.succeed(undefined),
all: () => Effect.succeed([]),
available: () => Effect.succeed([]),
},
model: {
get: () => Effect.succeed(undefined),
all: () => Effect.succeed([]),
available: () => Effect.succeed([]),
default: () => Effect.succeed(undefined),
small: () => Effect.succeed(undefined),
},
})
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
[Snapshot.node, Snapshot.noopLayer],
[LayerNodePlatform.llmClient, client],
@@ -116,6 +134,8 @@ const it = testEffect(
SessionProjector.node,
SessionStore.node,
AgentV2.node,
Catalog.node,
PluginHooks.node,
ToolRegistry.node,
SessionRunnerModel.node,
InstructionBuiltIns.node,
@@ -130,6 +150,7 @@ const it = testEffect(
[
[LayerNodePlatform.llmClient, client],
[PermissionV2.node, permission],
[Catalog.node, promptCatalog],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
[SessionRunnerModel.node, models],
[InstructionBuiltIns.node, systemContext],
@@ -150,11 +171,19 @@ describe("SessionRunnerLLM recorded", () => {
it.effect("executes one recorded V2 prompt through the recorded HTTP transport", () =>
Effect.gen(function* () {
const agents = yield* AgentV2.Service
const catalog = yield* Catalog.Service
const hooks = yield* PluginHooks.Service
yield* agents.transform((draft) =>
draft.update(AgentV2.ID.make("build"), (agent) => {
agent.mode = "primary"
}),
)
const pluginHost = host({
agent: agentHost(agents),
catalog: catalogHost(catalog),
session: { hook: (name, callback) => hooks.register("session", name, callback) },
})
yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), { discard: true })
const { db } = yield* Database.Service
yield* db
.insert(ProjectTable)
@@ -1,21 +0,0 @@
import { describe, expect, test } from "bun:test"
import { Model } from "@opencode-ai/ai"
import * as OpenAIChat from "@opencode-ai/ai/protocols/openai-chat"
import { SessionRunnerSystemPrompt } from "@opencode-ai/core/session/runner/system-prompt"
const prompt = (id: string) =>
SessionRunnerSystemPrompt.provider(Model.make({ id, provider: "test", route: OpenAIChat.route }))
describe("SessionRunnerSystemPrompt", () => {
test("selects the legacy provider-family prompts from the model id", () => {
expect(prompt("gpt-5")).toContain("You are OpenCode, You and the user share the same workspace")
expect(prompt("gpt-4.1")).toContain("THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH")
expect(prompt("o3")).toContain("THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH")
expect(prompt("gpt-5-codex")).toContain("## Editing constraints")
expect(prompt("gemini-2.5-pro")).toContain("# Core Mandates")
expect(prompt("claude-sonnet-4")).toContain("# Professional objectivity")
expect(prompt("kimi-k2")).toContain("# Prompt and Tool Use")
expect(prompt("trinity")).toContain("what command should I run to list files")
expect(prompt("llama-3.3")).toContain("You are opencode, an interactive CLI tool")
})
})
@@ -107,17 +107,6 @@ describe("ToolRegistry", () => {
}),
)
it.effect("reuses model definitions across requests", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
yield* service.register({ echo: make() }, { codemode: false })
const first = yield* toolDefinitions(service)
const second = yield* toolDefinitions(service)
expect(second[0]).toBe(first[0])
}),
)
it.effect("removes a scoped registration", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
+62 -3
View File
@@ -3,7 +3,9 @@ import {
LLMClient,
LLMError,
LLMEvent,
Message,
Model,
SystemPart,
ToolFailure,
TransportReason,
InvalidProviderOutputReason,
@@ -13,6 +15,7 @@ import {
type LLMRequest,
} from "@opencode-ai/ai"
import * as OpenAIChat from "@opencode-ai/ai/protocols/openai-chat"
import { Catalog } from "@opencode-ai/core/catalog"
import { Database } from "@opencode-ai/core/database/database"
import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
@@ -39,9 +42,10 @@ import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator
import { SessionRunner } from "@opencode-ai/core/session/runner"
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { SessionRunnerSystemPrompt } from "@opencode-ai/core/session/runner/system-prompt"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt"
import { QuestionTool } from "@opencode-ai/core/tool/question"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { AgentV2 } from "@opencode-ai/core/agent"
@@ -69,6 +73,8 @@ import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Scope, S
import { TestClock } from "effect/testing"
import { asc, eq } from "drizzle-orm"
import { testEffect } from "./lib/effect"
import { agentHost, catalogHost, host } from "./plugin/host"
import PROMPT_DEFAULT from "../src/session/runner/prompt/base.txt"
const requests: LLMRequest[] = []
let response: LLMEvent[] = []
@@ -134,7 +140,7 @@ const reply = {
],
}
const model = Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route })
const defaultSystem = SessionRunnerSystemPrompt.provider(model)
const defaultSystem = PROMPT_DEFAULT
const replacementModel = Model.make({ id: "replacement", provider: "fake", route: OpenAIChat.route })
const compactModel = Model.make({
id: "compact",
@@ -355,6 +361,20 @@ const pluginSupervisor = Layer.succeed(
flush: Effect.suspend(() => pluginFlushHook),
}),
)
const promptCatalog = Layer.mock(Catalog.Service, {
provider: {
get: () => Effect.succeed(undefined),
all: () => Effect.succeed([]),
available: () => Effect.succeed([]),
},
model: {
get: () => Effect.succeed(undefined),
all: () => Effect.succeed([]),
available: () => Effect.succeed([]),
default: () => Effect.succeed(undefined),
small: () => Effect.succeed(undefined),
},
})
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
[Snapshot.node, Snapshot.noopLayer],
[LayerNodePlatform.llmClient, client],
@@ -395,8 +415,10 @@ const it = testEffect(
SessionProjector.node,
SessionStore.node,
AgentV2.node,
Catalog.node,
ToolRegistry.node,
ToolRegistry.toolsNode,
PluginHooks.node,
echoNode,
SessionRunnerModel.node,
InstructionBuiltIns.node,
@@ -413,6 +435,7 @@ const it = testEffect(
[
[LayerNodePlatform.llmClient, client],
[PermissionV2.node, permission],
[Catalog.node, promptCatalog],
[SessionRunnerModel.node, models],
[InstructionBuiltIns.node, systemContext],
[InstructionDiscovery.node, instructionContext],
@@ -451,6 +474,17 @@ const insertSession = (id: SessionV2.ID) =>
const setup = Effect.gen(function* () {
const { db } = yield* Database.Service
const agents = yield* AgentV2.Service
const catalog = yield* Catalog.Service
const hooks = yield* PluginHooks.Service
const pluginHost = host({
agent: agentHost(agents),
catalog: catalogHost(catalog),
session: { hook: (name, callback) => hooks.register("session", name, callback) },
})
yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), {
discard: true,
})
requests.length = 0
authorizations.length = 0
executions.length = 0
@@ -474,7 +508,6 @@ const setup = Effect.gen(function* () {
toolExecutionsReady = 5
activeToolExecutions = 0
maxActiveToolExecutions = 0
const agents = yield* AgentV2.Service
yield* agents.transform((draft) =>
draft.update(AgentV2.ID.make("build"), (agent) => {
agent.mode = "primary"
@@ -773,6 +806,32 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
})
describe("SessionRunnerLLM", () => {
it.effect("applies session context hooks without exposing unavailable tools", () =>
Effect.gen(function* () {
const session = yield* setup
const hooks = yield* PluginHooks.Service
yield* hooks.register("session", "context", (event) =>
Effect.sync(() => {
event.system = [SystemPart.make("Hooked system")]
event.messages = [Message.user("Hooked message")]
delete event.tools.echo
event.tools.unregistered = { description: "Unavailable", input: { type: "object" } }
}),
)
yield* admit(session, "Original message")
responses = [reply.tool("call-removed", "echo", { text: "blocked" })]
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
expect(requests[0]?.system.map((part) => part.text)).toEqual(["Hooked system"])
expect(requests[0]?.messages).toEqual([Message.user("Hooked message")])
expect(requests[0]?.tools.map((tool) => tool.name)).not.toContain("echo")
expect(requests[0]?.tools.map((tool) => tool.name)).not.toContain("unregistered")
expect(executions).toEqual([])
}),
)
it.effect("advertises and executes a location registered tool", () =>
Effect.gen(function* () {
const session = yield* setup
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.17.20",
"version": "1.18.2",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+2 -2
View File
@@ -169,9 +169,9 @@ and plugin options.
| Capability | Available operations |
| ---------------------- | -------------------------------------------------------------------------------------------- |
| `ctx.agent` | `list`, `transform`, `reload` |
| `ctx.agent` | `list`, `get`, `transform`, `reload` |
| `ctx.catalog.provider` | `list`, `get` |
| `ctx.catalog.model` | `list`, `default` |
| `ctx.catalog.model` | `list`, `get`, `default` |
| `ctx.catalog` | `transform`, `reload` |
| `ctx.command` | `list`, `transform`, `reload` |
| `ctx.integration` | `list`, `get`, `connect`, `attempt`, `transform`, `reload`, and connection lookup/resolution |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.20",
"version": "1.18.2",
"name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.20",
"version": "1.18.2",
"name": "@opencode-ai/effect-sqlite-node",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.17.20",
"version": "1.18.2",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.17.20",
"version": "1.18.2",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.20",
"version": "1.18.2",
"name": "@opencode-ai/http-recorder",
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.20",
"version": "1.18.2",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -17,6 +17,7 @@ Implementation checklist:
- [x] Add OpenTUI UI state extraction for screen, focus, elements, and generated actions.
- [x] Add OpenTUI UI action execution for typing, keys, enter, arrows, focus, and click.
- [x] Add reusable JSON-RPC WebSocket server at the manifest's UI endpoint.
- [x] Add `simulation.handshake` protocol, role, identity, version, and capability negotiation to both control endpoints.
- [x] Expose `ui.state`, `ui.action`, `ui.render`.
- [x] Expose `trace.list`, `trace.clear`, `trace.export`.
- [x] Wire visible V1/full-TUI renderer path through the same action protocol.
@@ -69,6 +69,7 @@ This is important because the frontend has direct access to the renderer, screen
Protocol:
- JSON-RPC 2.0 over WebSocket.
- Clients negotiate protocol version, endpoint role, and capabilities with `simulation.handshake` before using endpoint methods.
- Loopback only.
- `OPENCODE_DRIVE` names a manifest in the opencode-drive registry, or is `1` for the unnamed default endpoints.
- The manifest supplies exact loopback `ui` and `backend` WebSocket endpoints.
@@ -77,6 +78,42 @@ Protocol:
The app should not send JSON-RPC requests back to the driver in the first milestone. The driver sends requests; the app responds and emits notifications/events as useful.
The canonical handshake request is:
```ts
{
jsonrpc: "2.0"
id: string | number | null
method: "simulation.handshake"
params: {
client: {
name: string
version: string
}
expectedRole: "ui" | "backend"
offeredVersions: Array<number>
requiredCapabilities: Array<string>
optionalCapabilities: Array<string>
}
}
```
The response result is:
```ts
{
protocolVersion: 1
role: "ui" | "backend"
server: {
name: string
version: string
}
capabilities: Array<string>
}
```
Capabilities are open strings. Each endpoint advertises only methods and notifications it actually implements. A role mismatch, no supported offered protocol version, or a missing required capability fails the request; unsupported optional capabilities do not.
Initial method groups:
- `ui.state`: return screen, elements, focus, and generated possible actions.
+1 -1
View File
@@ -1152,7 +1152,7 @@ export function options(input: {
}
if (input.model.providerID === "meta" && input.model.api.npm === "@ai-sdk/openai") {
result["reasoningEffort"] = "high"
result["reasoningEffort"] = "xhigh"
result["reasoningSummary"] = "auto"
result["include"] = INCLUDE_ENCRYPTED_REASONING
}
+15 -1
View File
@@ -91,6 +91,21 @@ export const TaskTool = Tool.define(
const cfg = yield* config.get()
const runInBackground = params.background === true
const parent = yield* sessions.get(ctx.sessionID)
let current = parent
let depth = 0
while (current.parentID) {
depth++
current = yield* sessions.get(current.parentID)
}
if (depth >= (cfg.subagent_depth ?? 1)) {
return yield* Effect.fail(
new Error(
`Subagent depth limit reached (${cfg.subagent_depth ?? 1}). Increase "subagent_depth" to allow nested subagents.`,
),
)
}
if (!ctx.extra?.bypassAgentCheck) {
yield* ctx.ask({
permission: id,
@@ -111,7 +126,6 @@ export const TaskTool = Tool.define(
const session = params.task_id
? yield* sessions.get(SessionID.make(params.task_id)).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
: undefined
const parent = yield* sessions.get(ctx.sessionID)
const childPermission = deriveSubagentSessionPermission({
parentSessionPermission: parent.permission ?? [],
subagent: next,
+80
View File
@@ -387,6 +387,86 @@ describe("tool.task", () => {
}),
)
it.instance("prevents subagents from launching subagents by default", () =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const { chat, assistant } = yield* seed()
const child = yield* sessions.create({ parentID: chat.id, title: "child" })
const nestedAssistant = yield* sessions.updateMessage({
...assistant,
id: MessageID.ascending(),
parentID: MessageID.ascending(),
sessionID: child.id,
})
const tool = yield* TaskTool
const def = yield* tool.init()
let asked = false
const exit = yield* def
.execute(
{
description: "inspect bug",
prompt: "look into the cache key path",
subagent_type: "general",
},
{
sessionID: child.id,
messageID: nestedAssistant.id,
agent: "general",
abort: new AbortController().signal,
extra: { promptOps: stubOps() },
messages: [],
metadata: () => Effect.void,
ask: () => Effect.sync(() => (asked = true)),
},
)
.pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
expect(asked).toBe(false)
expect(yield* sessions.children(child.id)).toHaveLength(0)
}),
)
it.instance(
"allows nested subagents up to the configured depth",
() =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const { chat, assistant } = yield* seed()
const child = yield* sessions.create({ parentID: chat.id, title: "child" })
const nestedAssistant = yield* sessions.updateMessage({
...assistant,
id: MessageID.ascending(),
parentID: MessageID.ascending(),
sessionID: child.id,
})
const tool = yield* TaskTool
const def = yield* tool.init()
const result = yield* def.execute(
{
description: "inspect bug",
prompt: "look into the cache key path",
subagent_type: "general",
},
{
sessionID: child.id,
messageID: nestedAssistant.id,
agent: "general",
abort: new AbortController().signal,
extra: { promptOps: stubOps() },
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
},
)
expect((yield* sessions.get(result.metadata.sessionId)).parentID).toBe(child.id)
}),
{ config: { subagent_depth: 2 } },
)
it.instance(
"execute shapes child permissions for task and primary tools",
() =>
+3 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.17.20",
"version": "1.18.2",
"type": "module",
"license": "MIT",
"scripts": {
@@ -25,9 +25,11 @@
],
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/client": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@standard-schema/spec": "^1.1.0",
"effect": "catalog:",
"zod": "catalog:"
},
+7 -5
View File
@@ -80,14 +80,16 @@ yield *
Hooks run sequentially in registration order. Later hooks observe mutations made by earlier hooks.
Session request context is mutable immediately before provider dispatch:
Session context is mutable immediately before provider dispatch:
```ts
yield *
ctx.session.hook("request", (event) => {
event.tools.read.description = "Read a file using narrow line ranges."
delete event.tools.write
})
ctx.session.hook("context", (event) =>
Effect.sync(() => {
event.tools.read.description = "Read a file using narrow line ranges."
delete event.tools.write
}),
)
```
## Reloading A Domain

Some files were not shown because too many files have changed in this diff Show More