Compare commits

..

38 Commits

Author SHA1 Message Date
opencode 6af6a1295f release: v0.15.19 2025-10-28 08:12:32 +00:00
Dax Raad 22821744ef feat: add OPENCODE_FAKE_VCS flag for VCS testing and update todo tracking instructions 2025-10-28 02:54:29 -04:00
Aiden Cline 872c9467b2 chore: rm unused import 2025-10-28 00:43:29 -05:00
Dax Raad d8249f32a8 do not set temperature for claude models 2025-10-28 01:14:25 -04:00
Aiden Cline 982954cc1b feat (acp): mcp server support, file diffs, some default slash commands (/init, /compact), show todos properly (#3490)
The mcp server support does not mean acp didn't allow u to use mcp servers previously, it means that now you can connect new servers via ACP instead of relying on the opencode defined ones
2025-10-28 00:08:30 -05:00
Frank 4caa458232 acp: fix type error 2025-10-27 21:40:08 -04:00
Frank 6fe8e3973c zen: support 1M claude context 2025-10-27 21:36:10 -04:00
Frank 7816901713 wip: zen doc 2025-10-27 21:25:56 -04:00
Frank 71abca9571 wip: zen 2025-10-27 19:26:28 -04:00
kcrommett 7216a8c86d fix: editor paste functionality for text attachments (#3489) 2025-10-27 17:51:33 -05:00
Jay V e3e16e58c5 docs: edit 2025-10-27 18:16:48 -04:00
Dax Raad a2951a2702 Remove typecheck script from desktop package 2025-10-27 18:03:32 -04:00
Jay V 55453dc606 Add missing dependencies for desktop package 2025-10-27 17:49:31 -04:00
Jay V 198d7f7e5f Merge branch 'doc-acp' into dev 2025-10-27 17:49:24 -04:00
Jay V e3e9fd7aa8 docs: edit 2025-10-27 17:48:17 -04:00
Aiden Cline 3c56dbcf58 chore: rm comment 2025-10-27 16:15:13 -05:00
Aiden Cline ee07ed2dc4 chore: delete unused file 2025-10-27 15:44:12 -05:00
Adam 485e4520e7 wip: desktop work 2025-10-27 15:37:07 -05:00
Adam fc115ea367 wip: desktop work 2025-10-27 15:37:07 -05:00
Adam d03b79e61e wip: desktop work 2025-10-27 15:37:06 -05:00
Adam 0acae8211a wip: desktop work 2025-10-27 15:37:06 -05:00
Aiden Cline 0af4505756 fix: litellm error tool= param must be specified 2025-10-27 14:03:42 -05:00
Aurelien Ribon a606e1d2ec fix: dont set reasoning effort to medium for gpt-5-pro (#3474) 2025-10-27 10:50:57 -05:00
Aiden Cline 0e65700183 update sdk 2025-10-27 10:47:04 -05:00
Aiden Cline e6301ca5d5 tweak: rename event 2025-10-27 10:42:47 -05:00
Bernat Pericàs b562863fcc feat: add session.started event that triggers when a new session is created (#3413) 2025-10-27 07:18:23 -05:00
GitHub Action db85f01eff ignore: update download stats 2025-10-27 2025-10-27 12:04:50 +00:00
Aiden Cline 1a6fd018f6 Revert "fix: Explicitly exit CLI to prevent hanging subprocesses (#3083)"
This reverts commit a9624c0fff.
2025-10-27 01:30:13 -05:00
Aiden Cline fdb5bae3c6 docs: acp 2025-10-27 00:56:00 -05:00
Haris Gušić a9624c0fff fix: Explicitly exit CLI to prevent hanging subprocesses (#3083)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-27 00:03:10 -05:00
Aiden Cline 316d4c9197 wip 2025-10-26 23:49:56 -05:00
Denys Rybalka 5e886c35d5 chore: use stable URLs in PKGBUILD (#3448) 2025-10-26 19:50:45 -05:00
Aiden Cline 5162268f9d docs: update agent frontmatter permission example 2025-10-26 15:04:48 -05:00
Jérôme Benoit 0eb899a950 chore: cleanup versioned zod imports (#3460)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-10-26 14:50:41 -05:00
Dan McGuirk 3241f6b8bb docs: fix typos (#3454) 2025-10-26 14:37:25 -05:00
GitHub Action 2c792f17e6 ignore: update download stats 2025-10-26 2025-10-26 12:04:22 +00:00
Joscha Götzer 7d0c6860cd fix: make build script work cross-platform (#3430)
Co-authored-by: JosXa <info@josxa.dev>
2025-10-26 01:40:17 -05:00
Aiden Cline c70e393c81 Remove claude-haiku-4.5 from default priority for GitHub Copilot session title generation 2025-10-26 01:21:34 -05:00
111 changed files with 3764 additions and 1029 deletions
+2
View File
@@ -119,3 +119,5 @@
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
+52 -15
View File
@@ -37,7 +37,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "0.15.18",
"version": "0.15.19",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -64,7 +64,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "0.15.18",
"version": "0.15.19",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -88,7 +88,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "0.15.18",
"version": "0.15.19",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -109,12 +109,12 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "0.15.18",
"version": "0.15.19",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@pierre/precision-diffs": "catalog:",
"@pierre/precision-diffs": "0.3.5",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/active-element": "2.1.3",
"@solid-primitives/event-bus": "1.1.2",
@@ -141,6 +141,7 @@
"@types/luxon": "3.7.1",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
"opencode": "workspace:*",
"typescript": "catalog:",
"vite": "catalog:",
"vite-plugin-icons-spritesheet": "3.0.1",
@@ -149,7 +150,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "0.15.18",
"version": "0.15.19",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -165,7 +166,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "0.15.18",
"version": "0.15.19",
"bin": {
"opencode": "./bin/opencode",
},
@@ -229,7 +230,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.15.18",
"version": "0.15.19",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -249,7 +250,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.15.18",
"version": "0.15.19",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -260,7 +261,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "0.15.18",
"version": "0.15.19",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -273,10 +274,10 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "0.15.18",
"version": "0.15.19",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/precision-diffs": "0.0.2-alpha.1-1",
"@pierre/precision-diffs": "catalog:",
"@solidjs/meta": "catalog:",
"fuzzysort": "catalog:",
"luxon": "catalog:",
@@ -296,7 +297,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "0.15.18",
"version": "0.15.19",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -343,7 +344,7 @@
"@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.0.2-alpha.1-1",
"@pierre/precision-diffs": "0.3.2",
"@solidjs/meta": "0.29.4",
"@tailwindcss/vite": "4.1.11",
"@tsconfig/bun": "1.0.9",
@@ -937,7 +938,7 @@
"@petamoriken/float16": ["@petamoriken/float16@3.9.2", "", {}, "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="],
"@pierre/precision-diffs": ["@pierre/precision-diffs@0.0.2-alpha.1-1", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" } }, "sha512-T43cwB7gMnbM+tp9p73NptUm4uUOfmrP5ihMOAHWQPpzBa/oeTjqZlmEmSQLpT8WKKnWG0lbKZPtlw7l0gW0Vw=="],
"@pierre/precision-diffs": ["@pierre/precision-diffs@0.3.5", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-qbotIS8CahO/7guljDzU3RVpDfg6WViWe0EB0/SZQi3xHD+nzxxlC+pGoyIFSn+47GG0EKxTnvkfaYANm19FCA=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
@@ -3497,6 +3498,8 @@
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
"@opencode-ai/ui/@pierre/precision-diffs": ["@pierre/precision-diffs@0.3.2", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-HE+wFB0TV+wmjur/J+qI5PsRQl5RN6tCEFTusW0S5FDfZJUIpkxJCacqUxyEI0DriXMKhgGQ+oCQShfaFELdrQ=="],
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="],
"@opencode-ai/web/@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
@@ -3733,6 +3736,8 @@
"nypm/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"opencode/@pierre/precision-diffs": ["@pierre/precision-diffs@0.3.2", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-HE+wFB0TV+wmjur/J+qI5PsRQl5RN6tCEFTusW0S5FDfZJUIpkxJCacqUxyEI0DriXMKhgGQ+oCQShfaFELdrQ=="],
"opencode/ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="],
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
@@ -4067,6 +4072,10 @@
"@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"@opencode-ai/ui/@pierre/precision-diffs/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="],
"@opencode-ai/ui/@pierre/precision-diffs/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="],
"@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="],
"@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="],
@@ -4233,6 +4242,10 @@
"nypm/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"opencode/@pierre/precision-diffs/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="],
"opencode/@pierre/precision-diffs/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="],
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
@@ -4413,6 +4426,18 @@
"@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"@opencode-ai/ui/@pierre/precision-diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@opencode-ai/ui/@pierre/precision-diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
"@opencode-ai/ui/@pierre/precision-diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
"@opencode-ai/ui/@pierre/precision-diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="],
"@opencode-ai/ui/@pierre/precision-diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="],
"@opencode-ai/ui/@pierre/precision-diffs/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
"@vercel/nft/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -4435,6 +4460,18 @@
"nitropack/serve-static/send/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"opencode/@pierre/precision-diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"opencode/@pierre/precision-diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
"opencode/@pierre/precision-diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
"opencode/@pierre/precision-diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="],
"opencode/@pierre/precision-diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="],
"opencode/@pierre/precision-diffs/shiki/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
+2 -2
View File
@@ -104,7 +104,7 @@ To test locally:
- `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow.
- `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow.
- `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment.
- `MOCK_TOKEN`: A GitHub persontal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
- `MOCK_TOKEN`: A GitHub personal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
- `MOCK_EVENT`: Mock GitHub event payload (see templates below).
- `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/github/index.ts` runs your local version of `opencode`.
@@ -118,7 +118,7 @@ Replace:
- `"owner":"sst"` with repo owner
- `"repo":"hello-world"` with repo name
- `"actor":"fwang"` with the GitHub username of commentor
- `"actor":"fwang"` with the GitHub username of commenter
- `"number":4` with the GitHub issue id
- `"body":"hey opencode, summarize thread"` with comment body
+3 -3
View File
@@ -25,7 +25,7 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.0.2-alpha.1-1",
"@pierre/precision-diffs": "0.3.2",
"@solidjs/meta": "0.29.4",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
@@ -53,8 +53,8 @@
"turbo": "2.5.6"
},
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/script": "workspace:*"
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*"
},
"repository": {
"type": "git",
+1 -1
View File
@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "0.15.18"
"version": "0.15.19"
},
"dependencies": {
"@ibm/plex": "6.4.1",
@@ -0,0 +1,5 @@
export class AuthError extends Error {}
export class CreditsError extends Error {}
export class MonthlyLimitError extends Error {}
export class UserLimitError extends Error {}
export class ModelError extends Error {}
@@ -0,0 +1 @@
export type Format = "anthropic" | "openai" | "oa-compat"
@@ -1,67 +1,41 @@
import { z } from "zod"
import type { APIEvent } from "@solidjs/start/server"
import path from "node:path"
import { and, Database, eq, isNull, lt, or, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { BillingTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js"
import { Resource } from "@opencode-ai/console-resource"
import { Billing } from "../../../../core/src/billing"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { ZenData } from "@opencode-ai/console-core/model.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
import { logger } from "./logger"
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError } from "./error"
import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
import { Format } from "./format"
import { anthropicHelper } from "./provider/anthropic"
import { openaiHelper } from "./provider/openai"
import { oaCompatHelper } from "./provider/openai-compatible"
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type Model = ZenData["models"][string]
export async function handler(
input: APIEvent,
opts: {
modifyBody?: (body: any) => any
setAuthHeader: (headers: Headers, apiKey: string) => void
format: Format
parseApiKey: (headers: Headers) => string | undefined
onStreamPart: (chunk: string) => void
getStreamUsage: () => any
normalizeUsage: (body: any) => {
inputTokens: number
outputTokens: number
reasoningTokens?: number
cacheReadTokens?: number
cacheWrite5mTokens?: number
cacheWrite1hTokens?: number
}
},
) {
class AuthError extends Error {}
class CreditsError extends Error {}
class MonthlyLimitError extends Error {}
class UserLimitError extends Error {}
class ModelError extends Error {}
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type Model = ZenData["models"][string]
const FREE_WORKSPACES = [
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
]
const logger = {
metric: (values: Record<string, any>) => {
console.log(`_metric:${JSON.stringify(values)}`)
},
log: console.log,
debug: (message: string) => {
if (Resource.App.stage === "production") return
console.debug(message)
},
}
try {
const url = new URL(input.request.url)
const body = await input.request.json()
logger.debug(JSON.stringify(body))
logger.metric({
is_tream: !!body.stream,
session: input.request.headers.get("x-opencode-session"),
@@ -78,22 +52,28 @@ export async function handler(
// Request to model provider
const startTimestamp = Date.now()
const res = await fetch(path.posix.join(providerInfo.api, url.pathname.replace(/^\/zen\/v1/, "") + url.search), {
const reqUrl = providerInfo.modifyUrl(providerInfo.api)
const reqBody = JSON.stringify(
providerInfo.modifyBody({
...createBodyConverter(opts.format, providerInfo.format)(body),
model: providerInfo.model,
}),
)
logger.debug("REQUEST URL: " + reqUrl)
logger.debug("REQUEST: " + reqBody)
const res = await fetch(reqUrl, {
method: "POST",
headers: (() => {
const headers = input.request.headers
headers.delete("host")
headers.delete("content-length")
opts.setAuthHeader(headers, providerInfo.apiKey)
providerInfo.modifyHeaders(headers, body, providerInfo.apiKey)
Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
headers.set(k, headers.get(v)!)
})
return headers
})(),
body: JSON.stringify({
...(opts.modifyBody?.(body) ?? body),
model: providerInfo.model,
}),
body: reqBody,
})
// Scrub response headers
@@ -104,14 +84,19 @@ export async function handler(
resHeaders.set(k, v)
}
}
logger.debug("STATUS: " + res.status + " " + res.statusText)
if (res.status === 400 || res.status === 503) {
logger.debug("RESPONSE: " + (await res.text()))
}
// Handle non-streaming response
if (!body.stream) {
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
const json = await res.json()
const body = JSON.stringify(json)
const body = JSON.stringify(responseConverter(json))
logger.metric({ response_length: body.length })
logger.debug(body)
await trackUsage(authInfo, modelInfo, providerInfo.id, json.usage)
logger.debug("RESPONSE: " + body)
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
await reload(authInfo)
return new Response(body, {
status: res.status,
@@ -121,10 +106,13 @@ export async function handler(
}
// Handle streaming response
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
const usageParser = providerInfo.createUsageParser()
const stream = new ReadableStream({
start(c) {
const reader = res.body?.getReader()
const decoder = new TextDecoder()
const encoder = new TextEncoder()
let buffer = ""
let responseLength = 0
@@ -136,9 +124,9 @@ export async function handler(
response_length: responseLength,
"timestamp.last_byte": Date.now(),
})
const usage = opts.getStreamUsage()
const usage = usageParser.retrieve()
if (usage) {
await trackUsage(authInfo, modelInfo, providerInfo.id, usage)
await trackUsage(authInfo, modelInfo, providerInfo, usage)
await reload(authInfo)
}
c.close()
@@ -158,12 +146,21 @@ export async function handler(
const parts = buffer.split("\n\n")
buffer = parts.pop() ?? ""
for (const part of parts) {
logger.debug(part)
opts.onStreamPart(part.trim())
for (let part of parts) {
logger.debug("PART: " + part)
part = part.trim()
usageParser.parse(part)
if (providerInfo.format !== opts.format) {
part = streamConverter(part)
c.enqueue(encoder.encode(part + "\n\n"))
}
}
c.enqueue(value)
if (providerInfo.format === opts.format) {
c.enqueue(value)
}
return pump()
}) || Promise.resolve()
@@ -235,7 +232,11 @@ export async function handler(
throw new ModelError(`Provider ${provider.id} not supported`)
}
return { ...provider, ...zenData.providers[provider.id] }
return {
...provider,
...zenData.providers[provider.id],
...(provider.id === "anthropic" ? anthropicHelper : provider.id === "openai" ? openaiHelper : oaCompatHelper),
}
}
async function authenticate(
@@ -356,11 +357,11 @@ export async function handler(
async function trackUsage(
authInfo: Awaited<ReturnType<typeof authenticate>>,
modelInfo: ReturnType<typeof validateModel>,
providerId: string,
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
usage: any,
) {
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
opts.normalizeUsage(usage)
providerInfo.normalizeUsage(usage)
const modelCost =
modelInfo.cost200K &&
@@ -421,7 +422,7 @@ export async function handler(
workspaceID: authInfo.workspaceID,
id: Identifier.create("usage"),
model: modelInfo.id,
provider: providerId,
provider: providerInfo.id,
inputTokens,
outputTokens,
reasoningTokens,
@@ -0,0 +1,12 @@
import { Resource } from "@opencode-ai/console-resource"
export const logger = {
metric: (values: Record<string, any>) => {
console.log(`_metric:${JSON.stringify(values)}`)
},
log: console.log,
debug: (message: string) => {
if (Resource.App.stage === "production") return
console.debug(message)
},
}
@@ -0,0 +1,621 @@
import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider"
type Usage = {
cache_creation?: {
ephemeral_5m_input_tokens?: number
ephemeral_1h_input_tokens?: number
}
cache_creation_input_tokens?: number
cache_read_input_tokens?: number
input_tokens?: number
output_tokens?: number
server_tool_use?: {
web_search_requests?: number
}
}
export const anthropicHelper = {
format: "anthropic",
modifyUrl: (providerApi: string) => providerApi + "/messages",
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
headers.set("x-api-key", apiKey)
headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
if (body.model.startsWith("claude-sonnet-")) {
headers.set("anthropic-beta", "context-1m-2025-08-07")
}
},
modifyBody: (body: Record<string, any>) => {
return {
...body,
service_tier: "standard_only",
}
},
createUsageParser: () => {
let usage: Usage
return {
parse: (chunk: string) => {
const data = chunk.split("\n")[1]
if (!data.startsWith("data: ")) return
let json
try {
json = JSON.parse(data.slice(6))
} catch (e) {
return
}
const usageUpdate = json.usage ?? json.message?.usage
if (!usageUpdate) return
usage = {
...usage,
...usageUpdate,
cache_creation: {
...usage?.cache_creation,
...usageUpdate.cache_creation,
},
server_tool_use: {
...usage?.server_tool_use,
...usageUpdate.server_tool_use,
},
}
},
retrieve: () => usage,
}
},
normalizeUsage: (usage: Usage) => ({
inputTokens: usage.input_tokens ?? 0,
outputTokens: usage.output_tokens ?? 0,
reasoningTokens: undefined,
cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
}),
} satisfies ProviderHelper
export function fromAnthropicRequest(body: any): CommonRequest {
if (!body || typeof body !== "object") return body
const msgs: any[] = []
const sys = Array.isArray(body.system) ? body.system : undefined
if (sys && sys.length > 0) {
for (const s of sys) {
if (!s) continue
if ((s as any).type !== "text") continue
if (typeof (s as any).text !== "string") continue
if ((s as any).text.length === 0) continue
msgs.push({ role: "system", content: (s as any).text })
}
}
const toImg = (src: any) => {
if (!src || typeof src !== "object") return undefined
if ((src as any).type === "url" && typeof (src as any).url === "string")
return { type: "image_url", image_url: { url: (src as any).url } }
if (
(src as any).type === "base64" &&
typeof (src as any).media_type === "string" &&
typeof (src as any).data === "string"
)
return { type: "image_url", image_url: { url: `data:${(src as any).media_type};base64,${(src as any).data}` } }
return undefined
}
const inMsgs = Array.isArray(body.messages) ? body.messages : []
for (const m of inMsgs) {
if (!m || !(m as any).role) continue
if ((m as any).role === "user") {
const partsIn = Array.isArray((m as any).content) ? (m as any).content : []
const partsOut: any[] = []
for (const p of partsIn) {
if (!p || !(p as any).type) continue
if ((p as any).type === "text" && typeof (p as any).text === "string")
partsOut.push({ type: "text", text: (p as any).text })
if ((p as any).type === "image") {
const ip = toImg((p as any).source)
if (ip) partsOut.push(ip)
}
if ((p as any).type === "tool_result") {
const id = (p as any).tool_use_id
const content =
typeof (p as any).content === "string" ? (p as any).content : JSON.stringify((p as any).content)
msgs.push({ role: "tool", tool_call_id: id, content })
}
}
if (partsOut.length > 0) {
if (partsOut.length === 1 && partsOut[0].type === "text") msgs.push({ role: "user", content: partsOut[0].text })
else msgs.push({ role: "user", content: partsOut })
}
continue
}
if ((m as any).role === "assistant") {
const partsIn = Array.isArray((m as any).content) ? (m as any).content : []
const texts: string[] = []
const tcs: any[] = []
for (const p of partsIn) {
if (!p || !(p as any).type) continue
if ((p as any).type === "text" && typeof (p as any).text === "string") texts.push((p as any).text)
if ((p as any).type === "tool_use") {
const name = (p as any).name
const id = (p as any).id
const inp = (p as any).input
const input = (() => {
if (typeof inp === "string") return inp
try {
return JSON.stringify(inp ?? {})
} catch {
return String(inp ?? "")
}
})()
tcs.push({ id, type: "function", function: { name, arguments: input } })
}
}
const out: any = { role: "assistant", content: texts.join("") }
if (tcs.length > 0) out.tool_calls = tcs
msgs.push(out)
continue
}
}
const tools = Array.isArray(body.tools)
? body.tools
.filter((t: any) => t && typeof t === "object" && "input_schema" in t)
.map((t: any) => ({
type: "function",
function: { name: (t as any).name, description: (t as any).description, parameters: (t as any).input_schema },
}))
: undefined
const tcin = body.tool_choice
const tc = (() => {
if (!tcin) return undefined
if ((tcin as any).type === "auto") return "auto"
if ((tcin as any).type === "any") return "required"
if ((tcin as any).type === "tool" && typeof (tcin as any).name === "string")
return { type: "function" as const, function: { name: (tcin as any).name } }
return undefined
})()
const stop = (() => {
const v = body.stop_sequences
if (!v) return undefined
if (Array.isArray(v)) return v.length === 1 ? v[0] : v
if (typeof v === "string") return v
return undefined
})()
return {
max_tokens: body.max_tokens,
temperature: body.temperature,
top_p: body.top_p,
stop,
messages: msgs,
stream: !!body.stream,
tools,
tool_choice: tc,
}
}
export function toAnthropicRequest(body: CommonRequest) {
if (!body || typeof body !== "object") return body
const sysIn = Array.isArray(body.messages) ? body.messages.filter((m: any) => m && m.role === "system") : []
let ccCount = 0
const cc = () => {
ccCount++
return ccCount <= 4 ? { cache_control: { type: "ephemeral" } } : {}
}
const system = sysIn
.filter((m: any) => typeof m.content === "string" && m.content.length > 0)
.map((m: any) => ({ type: "text", text: m.content, ...cc() }))
const msgsIn = Array.isArray(body.messages) ? body.messages : []
const msgsOut: any[] = []
const toSrc = (p: any) => {
if (!p || typeof p !== "object") return undefined
if ((p as any).type === "image_url" && (p as any).image_url) {
const u = (p as any).image_url.url ?? (p as any).image_url
if (typeof u === "string" && u.startsWith("data:")) {
const m = u.match(/^data:([^;]+);base64,(.*)$/)
if (m) return { type: "base64", media_type: m[1], data: m[2] }
}
if (typeof u === "string") return { type: "url", url: u }
}
return undefined
}
for (const m of msgsIn) {
if (!m || !(m as any).role) continue
if ((m as any).role === "user") {
if (typeof (m as any).content === "string") {
msgsOut.push({
role: "user",
content: [{ type: "text", text: (m as any).content, ...cc() }],
})
} else if (Array.isArray((m as any).content)) {
const parts: any[] = []
for (const p of (m as any).content) {
if (!p || !(p as any).type) continue
if ((p as any).type === "text" && typeof (p as any).text === "string")
parts.push({ type: "text", text: (p as any).text, ...cc() })
if ((p as any).type === "image_url") {
const s = toSrc(p)
if (s) parts.push({ type: "image", source: s, ...cc() })
}
}
if (parts.length > 0) msgsOut.push({ role: "user", content: parts })
}
continue
}
if ((m as any).role === "assistant") {
const out: any = { role: "assistant", content: [] as any[] }
if (typeof (m as any).content === "string" && (m as any).content.length > 0) {
;(out.content as any[]).push({ type: "text", text: (m as any).content, ...cc() })
}
if (Array.isArray((m as any).tool_calls)) {
for (const tc of (m as any).tool_calls) {
if ((tc as any).type === "function" && (tc as any).function) {
let input: any
const a = (tc as any).function.arguments
if (typeof a === "string") {
try {
input = JSON.parse(a)
} catch {
input = a
}
} else input = a
const id = (tc as any).id || `toolu_${Math.random().toString(36).slice(2)}`
;(out.content as any[]).push({
type: "tool_use",
id,
name: (tc as any).function.name,
input,
...cc(),
})
}
}
}
if ((out.content as any[]).length > 0) msgsOut.push(out)
continue
}
if ((m as any).role === "tool") {
msgsOut.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: (m as any).tool_call_id,
content: (m as any).content,
...cc(),
},
],
})
continue
}
}
const tools = Array.isArray(body.tools)
? body.tools
.filter((t: any) => t && typeof t === "object" && (t as any).type === "function")
.map((t: any) => ({
name: (t as any).function.name,
description: (t as any).function.description,
input_schema: (t as any).function.parameters,
...cc(),
}))
: undefined
const tcIn = body.tool_choice
const tool_choice = (() => {
if (!tcIn) return undefined
if (tcIn === "auto") return { type: "auto" }
if (tcIn === "required") return { type: "any" }
if ((tcIn as any).type === "function" && (tcIn as any).function?.name)
return { type: "tool", name: (tcIn as any).function.name }
return undefined
})()
const stop_sequences = (() => {
const v = body.stop
if (!v) return undefined
if (Array.isArray(v)) return v
if (typeof v === "string") return [v]
return undefined
})()
return {
max_tokens: body.max_tokens ?? 32_000,
temperature: body.temperature,
top_p: body.top_p,
system: system.length > 0 ? system : undefined,
messages: msgsOut,
stream: !!body.stream,
tools,
tool_choice,
stop_sequences,
}
}
export function fromAnthropicResponse(resp: any): CommonResponse {
if (!resp || typeof resp !== "object") return resp
if (Array.isArray((resp as any).choices)) return resp
const isAnthropic = typeof (resp as any).type === "string" && (resp as any).type === "message"
if (!isAnthropic) return resp
const idIn = (resp as any).id
const id =
typeof idIn === "string" ? idIn.replace(/^msg_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}`
const model = (resp as any).model
const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : []
const text = blocks
.filter((b) => b && b.type === "text" && typeof (b as any).text === "string")
.map((b: any) => b.text)
.join("")
const tcs = blocks
.filter((b) => b && b.type === "tool_use")
.map((b: any) => {
const name = (b as any).name
const args = (() => {
const inp = (b as any).input
if (typeof inp === "string") return inp
try {
return JSON.stringify(inp ?? {})
} catch {
return String(inp ?? "")
}
})()
const tid =
typeof (b as any).id === "string" && (b as any).id.length > 0
? (b as any).id
: `toolu_${Math.random().toString(36).slice(2)}`
return { id: tid, type: "function" as const, function: { name, arguments: args } }
})
const finish = (r: string | null) => {
if (r === "end_turn") return "stop"
if (r === "tool_use") return "tool_calls"
if (r === "max_tokens") return "length"
if (r === "content_filter") return "content_filter"
return null
}
const u = (resp as any).usage
const usage = (() => {
if (!u) return undefined as any
const pt = typeof (u as any).input_tokens === "number" ? (u as any).input_tokens : undefined
const ct = typeof (u as any).output_tokens === "number" ? (u as any).output_tokens : undefined
const total = pt != null && ct != null ? pt + ct : undefined
const cached =
typeof (u as any).cache_read_input_tokens === "number" ? (u as any).cache_read_input_tokens : undefined
const details = cached != null ? { cached_tokens: cached } : undefined
return {
prompt_tokens: pt,
completion_tokens: ct,
total_tokens: total,
...(details ? { prompt_tokens_details: details } : {}),
}
})()
return {
id,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model,
choices: [
{
index: 0,
message: {
role: "assistant",
...(text && text.length > 0 ? { content: text } : {}),
...(tcs.length > 0 ? { tool_calls: tcs } : {}),
},
finish_reason: finish((resp as any).stop_reason ?? null),
},
],
...(usage ? { usage } : {}),
}
}
export function toAnthropicResponse(resp: CommonResponse) {
if (!resp || typeof resp !== "object") return resp
if (!Array.isArray((resp as any).choices)) return resp
const choice = (resp as any).choices[0]
if (!choice) return resp
const message = choice.message
if (!message) return resp
const content: any[] = []
if (typeof message.content === "string" && message.content.length > 0)
content.push({ type: "text", text: message.content })
if (Array.isArray(message.tool_calls)) {
for (const tc of message.tool_calls) {
if ((tc as any).type === "function" && (tc as any).function) {
let input: any
try {
input = JSON.parse((tc as any).function.arguments)
} catch {
input = (tc as any).function.arguments
}
content.push({ type: "tool_use", id: (tc as any).id, name: (tc as any).function.name, input })
}
}
}
const stop_reason = (() => {
const r = choice.finish_reason
if (r === "stop") return "end_turn"
if (r === "tool_calls") return "tool_use"
if (r === "length") return "max_tokens"
if (r === "content_filter") return "content_filter"
return null
})()
const usage = (() => {
const u = (resp as any).usage
if (!u) return undefined
return {
input_tokens: u.prompt_tokens,
output_tokens: u.completion_tokens,
cache_read_input_tokens: u.prompt_tokens_details?.cached_tokens,
}
})()
return {
id: (resp as any).id,
type: "message",
role: "assistant",
content: content.length > 0 ? content : [{ type: "text", text: "" }],
model: (resp as any).model,
stop_reason,
usage,
}
}
export function fromAnthropicChunk(chunk: string): CommonChunk | string {
// Anthropic sends two lines per part: "event: <type>\n" + "data: <json>"
const lines = chunk.split("\n")
const dataLine = lines.find((l) => l.startsWith("data: "))
if (!dataLine) return chunk
let json
try {
json = JSON.parse(dataLine.slice(6))
} catch {
return chunk
}
const out: CommonChunk = {
id: json.id ?? json.message?.id ?? "",
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: json.model ?? json.message?.model ?? "",
choices: [],
}
if (json.type === "content_block_start") {
const cb = json.content_block
if (cb?.type === "text") {
out.choices.push({ index: json.index ?? 0, delta: { role: "assistant", content: "" }, finish_reason: null })
} else if (cb?.type === "tool_use") {
out.choices.push({
index: json.index ?? 0,
delta: {
tool_calls: [
{ index: json.index ?? 0, id: cb.id, type: "function", function: { name: cb.name, arguments: "" } },
],
},
finish_reason: null,
})
}
}
if (json.type === "content_block_delta") {
const d = json.delta
if (d?.type === "text_delta") {
out.choices.push({ index: json.index ?? 0, delta: { content: d.text }, finish_reason: null })
} else if (d?.type === "input_json_delta") {
out.choices.push({
index: json.index ?? 0,
delta: { tool_calls: [{ index: json.index ?? 0, function: { arguments: d.partial_json } }] },
finish_reason: null,
})
}
}
if (json.type === "message_delta") {
const d = json.delta
const finish_reason = (() => {
const r = d?.stop_reason
if (r === "end_turn") return "stop"
if (r === "tool_use") return "tool_calls"
if (r === "max_tokens") return "length"
if (r === "content_filter") return "content_filter"
return null
})()
out.choices.push({ index: 0, delta: {}, finish_reason })
}
if (json.usage) {
const u = json.usage
out.usage = {
prompt_tokens: u.input_tokens,
completion_tokens: u.output_tokens,
total_tokens: (u.input_tokens || 0) + (u.output_tokens || 0),
...(u.cache_read_input_tokens ? { prompt_tokens_details: { cached_tokens: u.cache_read_input_tokens } } : {}),
}
}
return out
}
export function toAnthropicChunk(chunk: CommonChunk): string {
if (!chunk.choices || !Array.isArray(chunk.choices) || chunk.choices.length === 0) {
return JSON.stringify({})
}
const choice = chunk.choices[0]
const delta = choice.delta
if (!delta) return JSON.stringify({})
const result: any = {}
if (delta.content) {
result.type = "content_block_delta"
result.index = 0
result.delta = { type: "text_delta", text: delta.content }
}
if (delta.tool_calls) {
for (const tc of delta.tool_calls) {
if (tc.function?.name) {
result.type = "content_block_start"
result.index = tc.index ?? 0
result.content_block = { type: "tool_use", id: tc.id, name: tc.function.name, input: {} }
} else if (tc.function?.arguments) {
result.type = "content_block_delta"
result.index = tc.index ?? 0
result.delta = { type: "input_json_delta", partial_json: tc.function.arguments }
}
}
}
if (choice.finish_reason) {
const stop_reason = (() => {
const r = choice.finish_reason
if (r === "stop") return "end_turn"
if (r === "tool_calls") return "tool_use"
if (r === "length") return "max_tokens"
if (r === "content_filter") return "content_filter"
return null
})()
result.type = "message_delta"
result.delta = { stop_reason, stop_sequence: null }
}
if (chunk.usage) {
const u = chunk.usage
result.usage = {
input_tokens: u.prompt_tokens,
output_tokens: u.completion_tokens,
cache_read_input_tokens: u.prompt_tokens_details?.cached_tokens,
}
}
return JSON.stringify(result)
}
@@ -0,0 +1,541 @@
import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider"
type Usage = {
prompt_tokens?: number
completion_tokens?: number
total_tokens?: number
// used by moonshot
cached_tokens?: number
// used by xai
prompt_tokens_details?: {
text_tokens?: number
audio_tokens?: number
image_tokens?: number
cached_tokens?: number
}
completion_tokens_details?: {
reasoning_tokens?: number
audio_tokens?: number
accepted_prediction_tokens?: number
rejected_prediction_tokens?: number
}
}
export const oaCompatHelper = {
format: "oa-compat",
modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
headers.set("authorization", `Bearer ${apiKey}`)
},
modifyBody: (body: Record<string, any>) => {
return {
...body,
...(body.stream ? { stream_options: { include_usage: true } } : {}),
}
},
createUsageParser: () => {
let usage: Usage
return {
parse: (chunk: string) => {
if (!chunk.startsWith("data: ")) return
let json
try {
json = JSON.parse(chunk.slice(6)) as { usage?: Usage }
} catch (e) {
return
}
if (!json.usage) return
usage = json.usage
},
retrieve: () => usage,
}
},
normalizeUsage: (usage: Usage) => {
const inputTokens = usage.prompt_tokens ?? 0
const outputTokens = usage.completion_tokens ?? 0
const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? undefined
const cacheReadTokens = usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined
return {
inputTokens: inputTokens - (cacheReadTokens ?? 0),
outputTokens,
reasoningTokens,
cacheReadTokens,
cacheWrite5mTokens: undefined,
cacheWrite1hTokens: undefined,
}
},
} satisfies ProviderHelper
export function fromOaCompatibleRequest(body: any): CommonRequest {
if (!body || typeof body !== "object") return body
const msgsIn = Array.isArray(body.messages) ? body.messages : []
const msgsOut: any[] = []
for (const m of msgsIn) {
if (!m || !m.role) continue
if (m.role === "system") {
if (typeof m.content === "string" && m.content.length > 0) msgsOut.push({ role: "system", content: m.content })
continue
}
if (m.role === "user") {
if (typeof m.content === "string") {
msgsOut.push({ role: "user", content: m.content })
} else if (Array.isArray(m.content)) {
const parts: any[] = []
for (const p of m.content) {
if (!p || !p.type) continue
if (p.type === "text" && typeof p.text === "string") parts.push({ type: "text", text: p.text })
if (p.type === "image_url") parts.push({ type: "image_url", image_url: p.image_url })
}
if (parts.length === 1 && parts[0].type === "text") msgsOut.push({ role: "user", content: parts[0].text })
else if (parts.length > 0) msgsOut.push({ role: "user", content: parts })
}
continue
}
if (m.role === "assistant") {
const out: any = { role: "assistant" }
if (typeof m.content === "string") out.content = m.content
if (Array.isArray(m.tool_calls)) out.tool_calls = m.tool_calls
msgsOut.push(out)
continue
}
if (m.role === "tool") {
msgsOut.push({ role: "tool", tool_call_id: m.tool_call_id, content: m.content })
continue
}
}
return {
max_tokens: body.max_tokens,
temperature: body.temperature,
top_p: body.top_p,
stop: body.stop,
messages: msgsOut,
stream: !!body.stream,
tools: Array.isArray(body.tools) ? body.tools : undefined,
tool_choice: body.tool_choice,
}
}
export function toOaCompatibleRequest(body: CommonRequest) {
if (!body || typeof body !== "object") return body
const msgsIn = Array.isArray(body.messages) ? body.messages : []
const msgsOut: any[] = []
const toImg = (p: any) => {
if (!p || typeof p !== "object") return undefined
if (p.type === "image_url" && p.image_url) return { type: "image_url", image_url: p.image_url }
const s = (p as any).source
if (!s || typeof s !== "object") return undefined
if (s.type === "url" && typeof s.url === "string") return { type: "image_url", image_url: { url: s.url } }
if (s.type === "base64" && typeof s.media_type === "string" && typeof s.data === "string")
return { type: "image_url", image_url: { url: `data:${s.media_type};base64,${s.data}` } }
return undefined
}
for (const m of msgsIn) {
if (!m || !m.role) continue
if (m.role === "system") {
if (typeof m.content === "string" && m.content.length > 0) msgsOut.push({ role: "system", content: m.content })
continue
}
if (m.role === "user") {
if (typeof m.content === "string") {
msgsOut.push({ role: "user", content: m.content })
continue
}
if (Array.isArray(m.content)) {
const parts: any[] = []
for (const p of m.content) {
if (!p || !p.type) continue
if (p.type === "text" && typeof p.text === "string") parts.push({ type: "text", text: p.text })
const ip = toImg(p)
if (ip) parts.push(ip)
}
if (parts.length === 1 && parts[0].type === "text") msgsOut.push({ role: "user", content: parts[0].text })
else if (parts.length > 0) msgsOut.push({ role: "user", content: parts })
}
continue
}
if (m.role === "assistant") {
const out: any = { role: "assistant" }
if (typeof m.content === "string") out.content = m.content
if (Array.isArray(m.tool_calls)) out.tool_calls = m.tool_calls
msgsOut.push(out)
continue
}
if (m.role === "tool") {
msgsOut.push({ role: "tool", tool_call_id: m.tool_call_id, content: m.content })
continue
}
}
const tools = Array.isArray(body.tools)
? body.tools.map((tool: any) => ({
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
},
}))
: undefined
return {
model: body.model,
max_tokens: body.max_tokens,
temperature: body.temperature,
top_p: body.top_p,
stop: body.stop,
messages: msgsOut,
stream: !!body.stream,
tools,
tool_choice: body.tool_choice,
response_format: (body as any).response_format,
}
}
export function fromOaCompatibleResponse(resp: any): CommonResponse {
if (!resp || typeof resp !== "object") return resp
if (!Array.isArray((resp as any).choices)) return resp
const choice = (resp as any).choices[0]
if (!choice) return resp
const message = choice.message
if (!message) return resp
const content: any[] = []
if (typeof message.content === "string" && message.content.length > 0) {
content.push({ type: "text", text: message.content })
}
if (Array.isArray(message.tool_calls)) {
for (const toolCall of message.tool_calls) {
if (toolCall.type === "function" && toolCall.function) {
let input
try {
input = JSON.parse(toolCall.function.arguments)
} catch {
input = toolCall.function.arguments
}
content.push({
type: "tool_use",
id: toolCall.id,
name: toolCall.function.name,
input,
})
}
}
}
const stopReason = (() => {
const reason = choice.finish_reason
if (reason === "stop") return "stop"
if (reason === "tool_calls") return "tool_calls"
if (reason === "length") return "length"
if (reason === "content_filter") return "content_filter"
return null
})()
const usage = (() => {
const u = (resp as any).usage
if (!u) return undefined
return {
prompt_tokens: u.prompt_tokens,
completion_tokens: u.completion_tokens,
total_tokens: u.total_tokens,
...(u.prompt_tokens_details?.cached_tokens
? { prompt_tokens_details: { cached_tokens: u.prompt_tokens_details.cached_tokens } }
: {}),
}
})()
return {
id: (resp as any).id,
object: "chat.completion" as const,
created: Math.floor(Date.now() / 1000),
model: (resp as any).model,
choices: [
{
index: 0,
message: {
role: "assistant" as const,
...(content.length > 0 && content.some((c) => c.type === "text")
? {
content: content
.filter((c) => c.type === "text")
.map((c: any) => c.text)
.join(""),
}
: {}),
...(content.length > 0 && content.some((c) => c.type === "tool_use")
? {
tool_calls: content
.filter((c) => c.type === "tool_use")
.map((c: any) => ({
id: c.id,
type: "function" as const,
function: {
name: c.name,
arguments: typeof c.input === "string" ? c.input : JSON.stringify(c.input),
},
})),
}
: {}),
},
finish_reason: stopReason,
},
],
...(usage ? { usage } : {}),
}
}
export function toOaCompatibleResponse(resp: CommonResponse) {
if (!resp || typeof resp !== "object") return resp
if (Array.isArray((resp as any).choices)) return resp
const isAnthropic = typeof (resp as any).type === "string" && (resp as any).type === "message"
if (!isAnthropic) return resp
const idIn = (resp as any).id
const id =
typeof idIn === "string" ? idIn.replace(/^msg_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}`
const model = (resp as any).model
const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : []
const text = blocks
.filter((b) => b && b.type === "text" && typeof b.text === "string")
.map((b) => b.text)
.join("")
const tcs = blocks
.filter((b) => b && b.type === "tool_use")
.map((b) => {
const name = (b as any).name
const args = (() => {
const inp = (b as any).input
if (typeof inp === "string") return inp
try {
return JSON.stringify(inp ?? {})
} catch {
return String(inp ?? "")
}
})()
const tid =
typeof (b as any).id === "string" && (b as any).id.length > 0
? (b as any).id
: `toolu_${Math.random().toString(36).slice(2)}`
return { id: tid, type: "function" as const, function: { name, arguments: args } }
})
const finish = (r: string | null) => {
if (r === "end_turn") return "stop"
if (r === "tool_use") return "tool_calls"
if (r === "max_tokens") return "length"
if (r === "content_filter") return "content_filter"
return null
}
const u = (resp as any).usage
const usage = (() => {
if (!u) return undefined as any
const pt = typeof u.input_tokens === "number" ? u.input_tokens : undefined
const ct = typeof u.output_tokens === "number" ? u.output_tokens : undefined
const total = pt != null && ct != null ? pt + ct : undefined
const cached = typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : undefined
const details = cached != null ? { cached_tokens: cached } : undefined
return {
prompt_tokens: pt,
completion_tokens: ct,
total_tokens: total,
...(details ? { prompt_tokens_details: details } : {}),
}
})()
return {
id,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model,
choices: [
{
index: 0,
message: {
role: "assistant",
...(text && text.length > 0 ? { content: text } : {}),
...(tcs.length > 0 ? { tool_calls: tcs } : {}),
},
finish_reason: finish((resp as any).stop_reason ?? null),
},
],
...(usage ? { usage } : {}),
}
}
export function fromOaCompatibleChunk(chunk: string): CommonChunk | string {
if (!chunk.startsWith("data: ")) return chunk
let json
try {
json = JSON.parse(chunk.slice(6))
} catch {
return chunk
}
if (!json.choices || !Array.isArray(json.choices) || json.choices.length === 0) {
return chunk
}
const choice = json.choices[0]
const delta = choice.delta
if (!delta) return chunk
const result: CommonChunk = {
id: json.id ?? "",
object: "chat.completion.chunk",
created: json.created ?? Math.floor(Date.now() / 1000),
model: json.model ?? "",
choices: [],
}
if (delta.content) {
result.choices.push({
index: choice.index ?? 0,
delta: { content: delta.content },
finish_reason: null,
})
}
if (delta.tool_calls) {
for (const toolCall of delta.tool_calls) {
result.choices.push({
index: choice.index ?? 0,
delta: {
tool_calls: [
{
index: toolCall.index ?? 0,
id: toolCall.id,
type: toolCall.type ?? "function",
function: toolCall.function,
},
],
},
finish_reason: null,
})
}
}
if (choice.finish_reason) {
result.choices.push({
index: choice.index ?? 0,
delta: {},
finish_reason: choice.finish_reason,
})
}
if (json.usage) {
const usage = json.usage
result.usage = {
prompt_tokens: usage.prompt_tokens,
completion_tokens: usage.completion_tokens,
total_tokens: usage.total_tokens,
...(usage.prompt_tokens_details?.cached_tokens
? { prompt_tokens_details: { cached_tokens: usage.prompt_tokens_details.cached_tokens } }
: {}),
}
}
return result
}
export function toOaCompatibleChunk(chunk: CommonChunk): string {
const result: any = {
id: chunk.id,
object: "chat.completion.chunk",
created: chunk.created,
model: chunk.model,
choices: [],
}
if (!chunk.choices || chunk.choices.length === 0) {
return `data: ${JSON.stringify(result)}`
}
const choice = chunk.choices[0]
const delta = choice.delta
if (delta?.role) {
result.choices.push({
index: choice.index,
delta: { role: delta.role },
finish_reason: null,
})
}
if (delta?.content) {
result.choices.push({
index: choice.index,
delta: { content: delta.content },
finish_reason: null,
})
}
if (delta?.tool_calls) {
for (const tc of delta.tool_calls) {
result.choices.push({
index: choice.index,
delta: {
tool_calls: [
{
index: tc.index,
id: tc.id,
type: tc.type,
function: tc.function,
},
],
},
finish_reason: null,
})
}
}
if (choice.finish_reason) {
result.choices.push({
index: choice.index,
delta: {},
finish_reason: choice.finish_reason,
})
}
if (chunk.usage) {
result.usage = {
prompt_tokens: chunk.usage.prompt_tokens,
completion_tokens: chunk.usage.completion_tokens,
total_tokens: chunk.usage.total_tokens,
...(chunk.usage.prompt_tokens_details?.cached_tokens
? {
prompt_tokens_details: { cached_tokens: chunk.usage.prompt_tokens_details.cached_tokens },
}
: {}),
}
}
return `data: ${JSON.stringify(result)}`
}
@@ -0,0 +1,600 @@
import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider"
type Usage = {
input_tokens?: number
input_tokens_details?: {
cached_tokens?: number
}
output_tokens?: number
output_tokens_details?: {
reasoning_tokens?: number
}
total_tokens?: number
}
export const openaiHelper = {
format: "openai",
modifyUrl: (providerApi: string) => providerApi + "/responses",
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
headers.set("authorization", `Bearer ${apiKey}`)
},
modifyBody: (body: Record<string, any>) => {
return body
},
createUsageParser: () => {
let usage: Usage
return {
parse: (chunk: string) => {
const [event, data] = chunk.split("\n")
if (event !== "event: response.completed") return
if (!data.startsWith("data: ")) return
let json
try {
json = JSON.parse(data.slice(6)) as { response?: { usage?: Usage } }
} catch (e) {
return
}
if (!json.response?.usage) return
usage = json.response.usage
},
retrieve: () => usage,
}
},
normalizeUsage: (usage: Usage) => {
const inputTokens = usage.input_tokens ?? 0
const outputTokens = usage.output_tokens ?? 0
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? undefined
const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? undefined
return {
inputTokens: inputTokens - (cacheReadTokens ?? 0),
outputTokens: outputTokens - (reasoningTokens ?? 0),
reasoningTokens,
cacheReadTokens,
cacheWrite5mTokens: undefined,
cacheWrite1hTokens: undefined,
}
},
} satisfies ProviderHelper
export function fromOpenaiRequest(body: any): CommonRequest {
if (!body || typeof body !== "object") return body
const toImg = (p: any) => {
if (!p || typeof p !== "object") return undefined
if ((p as any).type === "image_url" && (p as any).image_url)
return { type: "image_url", image_url: (p as any).image_url }
if ((p as any).type === "input_image" && (p as any).image_url)
return { type: "image_url", image_url: (p as any).image_url }
const s = (p as any).source
if (!s || typeof s !== "object") return undefined
if ((s as any).type === "url" && typeof (s as any).url === "string")
return { type: "image_url", image_url: { url: (s as any).url } }
if (
(s as any).type === "base64" &&
typeof (s as any).media_type === "string" &&
typeof (s as any).data === "string"
)
return { type: "image_url", image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` } }
return undefined
}
const msgs: any[] = []
const inMsgs = Array.isArray(body.input) ? body.input : Array.isArray(body.messages) ? body.messages : []
for (const m of inMsgs) {
if (!m) continue
// Responses API items without role:
if (!(m as any).role && (m as any).type) {
if ((m as any).type === "function_call") {
const name = (m as any).name
const a = (m as any).arguments
const args = typeof a === "string" ? a : JSON.stringify(a ?? {})
msgs.push({
role: "assistant",
tool_calls: [{ id: (m as any).id, type: "function", function: { name, arguments: args } }],
})
}
if ((m as any).type === "function_call_output") {
const id = (m as any).call_id
const out = (m as any).output
const content = typeof out === "string" ? out : JSON.stringify(out)
msgs.push({ role: "tool", tool_call_id: id, content })
}
continue
}
if ((m as any).role === "system" || (m as any).role === "developer") {
const c = (m as any).content
if (typeof c === "string" && c.length > 0) msgs.push({ role: "system", content: c })
if (Array.isArray(c)) {
const t = c.find((p: any) => p && typeof p.text === "string")
if (t && typeof t.text === "string" && t.text.length > 0) msgs.push({ role: "system", content: t.text })
}
continue
}
if ((m as any).role === "user") {
const c = (m as any).content
if (typeof c === "string") {
msgs.push({ role: "user", content: c })
} else if (Array.isArray(c)) {
const parts: any[] = []
for (const p of c) {
if (!p || !(p as any).type) continue
if (((p as any).type === "text" || (p as any).type === "input_text") && typeof (p as any).text === "string")
parts.push({ type: "text", text: (p as any).text })
const ip = toImg(p)
if (ip) parts.push(ip)
if ((p as any).type === "tool_result") {
const id = (p as any).tool_call_id
const content =
typeof (p as any).content === "string" ? (p as any).content : JSON.stringify((p as any).content)
msgs.push({ role: "tool", tool_call_id: id, content })
}
}
if (parts.length === 1 && parts[0].type === "text") msgs.push({ role: "user", content: parts[0].text })
else if (parts.length > 0) msgs.push({ role: "user", content: parts })
}
continue
}
if ((m as any).role === "assistant") {
const c = (m as any).content
const out: any = { role: "assistant" }
if (typeof c === "string" && c.length > 0) out.content = c
if (Array.isArray((m as any).tool_calls)) out.tool_calls = (m as any).tool_calls
msgs.push(out)
continue
}
if ((m as any).role === "tool") {
msgs.push({ role: "tool", tool_call_id: (m as any).tool_call_id, content: (m as any).content })
continue
}
}
const tcIn = body.tool_choice
const tc = (() => {
if (!tcIn) return undefined
if (tcIn === "auto") return "auto"
if (tcIn === "required") return "required"
if ((tcIn as any).type === "function" && (tcIn as any).function?.name)
return { type: "function" as const, function: { name: (tcIn as any).function.name } }
return undefined
})()
const stop = (() => {
const v = body.stop_sequences ?? body.stop
if (!v) return undefined
if (Array.isArray(v)) return v.length === 1 ? v[0] : v
if (typeof v === "string") return v
return undefined
})()
return {
max_tokens: body.max_output_tokens ?? body.max_tokens,
temperature: body.temperature,
top_p: body.top_p,
stop,
messages: msgs,
stream: !!body.stream,
tools: Array.isArray(body.tools) ? body.tools : undefined,
tool_choice: tc,
}
}
export function toOpenaiRequest(body: CommonRequest) {
if (!body || typeof body !== "object") return body
const msgsIn = Array.isArray(body.messages) ? body.messages : []
const input: any[] = []
const toPart = (p: any) => {
if (!p || typeof p !== "object") return undefined
if ((p as any).type === "text" && typeof (p as any).text === "string")
return { type: "input_text", text: (p as any).text }
if ((p as any).type === "image_url" && (p as any).image_url)
return { type: "input_image", image_url: (p as any).image_url }
const s = (p as any).source
if (!s || typeof s !== "object") return undefined
if ((s as any).type === "url" && typeof (s as any).url === "string")
return { type: "input_image", image_url: { url: (s as any).url } }
if (
(s as any).type === "base64" &&
typeof (s as any).media_type === "string" &&
typeof (s as any).data === "string"
)
return { type: "input_image", image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` } }
return undefined
}
for (const m of msgsIn) {
if (!m || !(m as any).role) continue
if ((m as any).role === "system") {
const c = (m as any).content
if (typeof c === "string") input.push({ role: "system", content: c })
continue
}
if ((m as any).role === "user") {
const c = (m as any).content
if (typeof c === "string") {
input.push({ role: "user", content: [{ type: "input_text", text: c }] })
} else if (Array.isArray(c)) {
const parts: any[] = []
for (const p of c) {
const op = toPart(p)
if (op) parts.push(op)
}
if (parts.length > 0) input.push({ role: "user", content: parts })
}
continue
}
if ((m as any).role === "assistant") {
const c = (m as any).content
if (typeof c === "string" && c.length > 0) {
input.push({ role: "assistant", content: [{ type: "output_text", text: c }] })
}
if (Array.isArray((m as any).tool_calls)) {
for (const tc of (m as any).tool_calls) {
if ((tc as any).type === "function" && (tc as any).function) {
const name = (tc as any).function.name
const a = (tc as any).function.arguments
const args = typeof a === "string" ? a : JSON.stringify(a)
input.push({ type: "function_call", call_id: (tc as any).id, name, arguments: args })
}
}
}
continue
}
if ((m as any).role === "tool") {
const out = typeof (m as any).content === "string" ? (m as any).content : JSON.stringify((m as any).content)
input.push({ type: "function_call_output", call_id: (m as any).tool_call_id, output: out })
continue
}
}
const stop_sequences = (() => {
const v = body.stop
if (!v) return undefined
if (Array.isArray(v)) return v
if (typeof v === "string") return [v]
return undefined
})()
const tcIn = body.tool_choice
const tool_choice = (() => {
if (!tcIn) return undefined
if (tcIn === "auto") return "auto"
if (tcIn === "required") return "required"
if ((tcIn as any).type === "function" && (tcIn as any).function?.name)
return { type: "function", function: { name: (tcIn as any).function.name } }
return undefined
})()
const tools = (() => {
if (!Array.isArray(body.tools)) return undefined
return body.tools.map((tool: any) => {
if (tool.type === "function") {
return {
type: "function",
name: tool.function?.name,
description: tool.function?.description,
parameters: tool.function?.parameters,
strict: tool.function?.strict,
}
}
return tool
})
})()
return {
model: body.model,
input,
max_output_tokens: body.max_tokens,
top_p: body.top_p,
stop_sequences,
stream: !!body.stream,
tools,
tool_choice,
include: Array.isArray((body as any).include) ? (body as any).include : undefined,
truncation: (body as any).truncation,
metadata: (body as any).metadata,
store: (body as any).store,
user: (body as any).user,
text: { verbosity: "low" },
reasoning: { effort: "medium" },
}
}
export function fromOpenaiResponse(resp: any): CommonResponse {
if (!resp || typeof resp !== "object") return resp
if (Array.isArray((resp as any).choices)) return resp
const r = (resp as any).response ?? resp
if (!r || typeof r !== "object") return resp
const idIn = (r as any).id
const id =
typeof idIn === "string" ? idIn.replace(/^resp_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}`
const model = (r as any).model ?? (resp as any).model
const out = Array.isArray((r as any).output) ? (r as any).output : []
const text = out
.filter((o: any) => o && o.type === "message" && Array.isArray((o as any).content))
.flatMap((o: any) => (o as any).content)
.filter((p: any) => p && p.type === "output_text" && typeof p.text === "string")
.map((p: any) => p.text)
.join("")
const tcs = out
.filter((o: any) => o && o.type === "function_call")
.map((o: any) => {
const name = (o as any).name
const a = (o as any).arguments
const args = typeof a === "string" ? a : JSON.stringify(a ?? {})
const tid =
typeof (o as any).id === "string" && (o as any).id.length > 0
? (o as any).id
: `toolu_${Math.random().toString(36).slice(2)}`
return { id: tid, type: "function" as const, function: { name, arguments: args } }
})
const finish = (r: string | null) => {
if (r === "stop") return "stop"
if (r === "tool_call" || r === "tool_calls") return "tool_calls"
if (r === "length" || r === "max_output_tokens") return "length"
if (r === "content_filter") return "content_filter"
return null
}
const u = (r as any).usage ?? (resp as any).usage
const usage = (() => {
if (!u) return undefined as any
const pt = typeof (u as any).input_tokens === "number" ? (u as any).input_tokens : undefined
const ct = typeof (u as any).output_tokens === "number" ? (u as any).output_tokens : undefined
const total = pt != null && ct != null ? pt + ct : undefined
const cached = (u as any).input_tokens_details?.cached_tokens
const details = typeof cached === "number" ? { cached_tokens: cached } : undefined
return {
prompt_tokens: pt,
completion_tokens: ct,
total_tokens: total,
...(details ? { prompt_tokens_details: details } : {}),
}
})()
return {
id,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model,
choices: [
{
index: 0,
message: {
role: "assistant",
...(text && text.length > 0 ? { content: text } : {}),
...(tcs.length > 0 ? { tool_calls: tcs } : {}),
},
finish_reason: finish((r as any).stop_reason ?? null),
},
],
...(usage ? { usage } : {}),
}
}
export function toOpenaiResponse(resp: CommonResponse) {
if (!resp || typeof resp !== "object") return resp
if (!Array.isArray((resp as any).choices)) return resp
const choice = (resp as any).choices[0]
if (!choice) return resp
const msg = choice.message
if (!msg) return resp
const outputItems: any[] = []
if (typeof msg.content === "string" && msg.content.length > 0) {
outputItems.push({
id: `msg_${Math.random().toString(36).slice(2)}`,
type: "message",
status: "completed",
role: "assistant",
content: [{ type: "output_text", text: msg.content, annotations: [], logprobs: [] }],
})
}
if (Array.isArray(msg.tool_calls)) {
for (const tc of msg.tool_calls) {
if ((tc as any).type === "function" && (tc as any).function) {
outputItems.push({
id: (tc as any).id,
type: "function_call",
name: (tc as any).function.name,
call_id: (tc as any).id,
arguments: (tc as any).function.arguments,
})
}
}
}
const stop_reason = (() => {
const r = choice.finish_reason
if (r === "stop") return "stop"
if (r === "tool_calls") return "tool_call"
if (r === "length") return "max_output_tokens"
if (r === "content_filter") return "content_filter"
return null
})()
const usage = (() => {
const u = (resp as any).usage
if (!u) return undefined
return {
input_tokens: u.prompt_tokens,
output_tokens: u.completion_tokens,
total_tokens: u.total_tokens,
...(u.prompt_tokens_details?.cached_tokens
? { input_tokens_details: { cached_tokens: u.prompt_tokens_details.cached_tokens } }
: {}),
}
})()
return {
id: (resp as any).id?.replace(/^chatcmpl_/, "resp_") ?? `resp_${Math.random().toString(36).slice(2)}`,
object: "response",
model: (resp as any).model,
output: outputItems,
stop_reason,
usage,
}
}
export function fromOpenaiChunk(chunk: string): CommonChunk | string {
const lines = chunk.split("\n")
const ev = lines[0]
const dl = lines[1]
if (!ev || !dl || !dl.startsWith("data: ")) return chunk
let json: any
try {
json = JSON.parse(dl.slice(6))
} catch {
return chunk
}
const respObj = json.response ?? {}
const out: CommonChunk = {
id: respObj.id ?? json.id ?? "",
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: respObj.model ?? json.model ?? "",
choices: [],
}
const e = ev.replace("event: ", "").trim()
if (e === "response.output_text.delta") {
const d = (json as any).delta ?? (json as any).text ?? (json as any).output_text_delta
if (typeof d === "string" && d.length > 0)
out.choices.push({ index: 0, delta: { content: d }, finish_reason: null })
}
if (e === "response.output_item.added" && (json as any).item?.type === "function_call") {
const name = (json as any).item?.name
const id = (json as any).item?.id
if (typeof name === "string" && name.length > 0) {
out.choices.push({
index: 0,
delta: { tool_calls: [{ index: 0, id, type: "function", function: { name, arguments: "" } }] },
finish_reason: null,
})
}
}
if (e === "response.function_call_arguments.delta") {
const a = (json as any).delta ?? (json as any).arguments_delta
if (typeof a === "string" && a.length > 0) {
out.choices.push({
index: 0,
delta: { tool_calls: [{ index: 0, function: { arguments: a } }] },
finish_reason: null,
})
}
}
if (e === "response.completed") {
const fr = (() => {
const sr = (respObj as any).stop_reason ?? (json as any).stop_reason
if (sr === "stop") return "stop"
if (sr === "tool_call" || sr === "tool_calls") return "tool_calls"
if (sr === "length" || sr === "max_output_tokens") return "length"
if (sr === "content_filter") return "content_filter"
return null
})()
out.choices.push({ index: 0, delta: {}, finish_reason: fr })
const u = (respObj as any).usage ?? (json as any).response?.usage
if (u) {
out.usage = {
prompt_tokens: u.input_tokens,
completion_tokens: u.output_tokens,
total_tokens: (u.input_tokens || 0) + (u.output_tokens || 0),
...(u.input_tokens_details?.cached_tokens
? { prompt_tokens_details: { cached_tokens: u.input_tokens_details.cached_tokens } }
: {}),
}
}
}
return out
}
export function toOpenaiChunk(chunk: CommonChunk): string {
if (!chunk.choices || !Array.isArray(chunk.choices) || chunk.choices.length === 0) {
return ""
}
const choice = chunk.choices[0]
const d = choice.delta
if (!d) return ""
const id = chunk.id
const model = chunk.model
if (d.content) {
const data = { id, type: "response.output_text.delta", delta: d.content, response: { id, model } }
return `event: response.output_text.delta\ndata: ${JSON.stringify(data)}`
}
if (d.tool_calls) {
for (const tc of d.tool_calls) {
if (tc.function?.name) {
const data = {
type: "response.output_item.added",
output_index: 0,
item: { id: tc.id, type: "function_call", name: tc.function.name, call_id: tc.id, arguments: "" },
}
return `event: response.output_item.added\ndata: ${JSON.stringify(data)}`
}
if (tc.function?.arguments) {
const data = {
type: "response.function_call_arguments.delta",
output_index: 0,
delta: tc.function.arguments,
}
return `event: response.function_call_arguments.delta\ndata: ${JSON.stringify(data)}`
}
}
}
if (choice.finish_reason) {
const u = chunk.usage
const usage = u
? {
input_tokens: u.prompt_tokens,
output_tokens: u.completion_tokens,
total_tokens: u.total_tokens,
...(u.prompt_tokens_details?.cached_tokens
? { input_tokens_details: { cached_tokens: u.prompt_tokens_details.cached_tokens } }
: {}),
}
: undefined
const data: any = { id, type: "response.completed", response: { id, model, ...(usage ? { usage } : {}) } }
return `event: response.completed\ndata: ${JSON.stringify(data)}`
}
return ""
}
@@ -0,0 +1,207 @@
import { Format } from "../format"
import {
fromAnthropicChunk,
fromAnthropicRequest,
fromAnthropicResponse,
toAnthropicChunk,
toAnthropicRequest,
toAnthropicResponse,
} from "./anthropic"
import {
fromOpenaiChunk,
fromOpenaiRequest,
fromOpenaiResponse,
toOpenaiChunk,
toOpenaiRequest,
toOpenaiResponse,
} from "./openai"
import {
fromOaCompatibleChunk,
fromOaCompatibleRequest,
fromOaCompatibleResponse,
toOaCompatibleChunk,
toOaCompatibleRequest,
toOaCompatibleResponse,
} from "./openai-compatible"
export type ProviderHelper = {
format: Format
modifyUrl: (providerApi: string) => string
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
modifyBody: (body: Record<string, any>) => Record<string, any>
createUsageParser: () => {
parse: (chunk: string) => void
retrieve: () => any
}
normalizeUsage: (usage: any) => {
inputTokens: number
outputTokens: number
reasoningTokens?: number
cacheReadTokens?: number
cacheWrite5mTokens?: number
cacheWrite1hTokens?: number
}
}
export interface CommonMessage {
role: "system" | "user" | "assistant" | "tool"
content?: string | Array<CommonContentPart>
tool_call_id?: string
tool_calls?: CommonToolCall[]
}
export interface CommonContentPart {
type: "text" | "image_url"
text?: string
image_url?: { url: string }
}
export interface CommonToolCall {
id: string
type: "function"
function: {
name: string
arguments: string
}
}
export interface CommonTool {
type: "function"
function: {
name: string
description?: string
parameters?: Record<string, any>
}
}
export interface CommonUsage {
input_tokens?: number
output_tokens?: number
total_tokens?: number
prompt_tokens?: number
completion_tokens?: number
cache_read_input_tokens?: number
cache_creation?: {
ephemeral_5m_input_tokens?: number
ephemeral_1h_input_tokens?: number
}
input_tokens_details?: {
cached_tokens?: number
}
output_tokens_details?: {
reasoning_tokens?: number
}
}
export interface CommonRequest {
model?: string
max_tokens?: number
temperature?: number
top_p?: number
stop?: string | string[]
messages: CommonMessage[]
stream?: boolean
tools?: CommonTool[]
tool_choice?: "auto" | "required" | { type: "function"; function: { name: string } }
}
export interface CommonResponse {
id: string
object: "chat.completion"
created: number
model: string
choices: Array<{
index: number
message: {
role: "assistant"
content?: string
tool_calls?: CommonToolCall[]
}
finish_reason: "stop" | "tool_calls" | "length" | "content_filter" | null
}>
usage?: {
prompt_tokens?: number
completion_tokens?: number
total_tokens?: number
prompt_tokens_details?: { cached_tokens?: number }
}
}
export interface CommonChunk {
id: string
object: "chat.completion.chunk"
created: number
model: string
choices: Array<{
index: number
delta: {
role?: "assistant"
content?: string
tool_calls?: Array<{
index: number
id?: string
type?: "function"
function?: {
name?: string
arguments?: string
}
}>
}
finish_reason: "stop" | "tool_calls" | "length" | "content_filter" | null
}>
usage?: {
prompt_tokens?: number
completion_tokens?: number
total_tokens?: number
prompt_tokens_details?: { cached_tokens?: number }
}
}
export function createBodyConverter(from: Format, to: Format) {
return (body: any): any => {
if (from === to) return body
let raw: CommonRequest
if (from === "anthropic") raw = fromAnthropicRequest(body)
else if (from === "openai") raw = fromOpenaiRequest(body)
else raw = fromOaCompatibleRequest(body)
if (to === "anthropic") return toAnthropicRequest(raw)
if (to === "openai") return toOpenaiRequest(raw)
if (to === "oa-compat") return toOaCompatibleRequest(raw)
}
}
export function createStreamPartConverter(from: Format, to: Format) {
return (part: any): any => {
if (from === to) return part
let raw: CommonChunk | string
if (from === "anthropic") raw = fromAnthropicChunk(part)
else if (from === "openai") raw = fromOpenaiChunk(part)
else raw = fromOaCompatibleChunk(part)
// If result is a string (error case), pass it through
if (typeof raw === "string") return raw
if (to === "anthropic") return toAnthropicChunk(raw)
if (to === "openai") return toOpenaiChunk(raw)
if (to === "oa-compat") return toOaCompatibleChunk(raw)
}
}
export function createResponseConverter(from: Format, to: Format) {
return (response: any): any => {
if (from === to) return response
let raw: CommonResponse
if (from === "anthropic") raw = fromAnthropicResponse(response)
else if (from === "openai") raw = fromOpenaiResponse(response)
else raw = fromOaCompatibleResponse(response)
if (to === "anthropic") return toAnthropicResponse(raw)
if (to === "openai") return toOpenaiResponse(raw)
if (to === "oa-compat") return toOaCompatibleResponse(raw)
}
}
@@ -1,63 +1,9 @@
import type { APIEvent } from "@solidjs/start/server"
import { handler } from "~/routes/zen/handler"
type Usage = {
prompt_tokens?: number
completion_tokens?: number
total_tokens?: number
// used by moonshot
cached_tokens?: number
// used by xai
prompt_tokens_details?: {
text_tokens?: number
audio_tokens?: number
image_tokens?: number
cached_tokens?: number
}
completion_tokens_details?: {
reasoning_tokens?: number
audio_tokens?: number
accepted_prediction_tokens?: number
rejected_prediction_tokens?: number
}
}
import { handler } from "~/routes/zen/util/handler"
export function POST(input: APIEvent) {
let usage: Usage
return handler(input, {
modifyBody: (body: any) => ({
...body,
...(body.stream ? { stream_options: { include_usage: true } } : {}),
}),
setAuthHeader: (headers: Headers, apiKey: string) => {
headers.set("authorization", `Bearer ${apiKey}`)
},
format: "oa-compat",
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
onStreamPart: (chunk: string) => {
if (!chunk.startsWith("data: ")) return
let json
try {
json = JSON.parse(chunk.slice(6)) as { usage?: Usage }
} catch (e) {
return
}
if (!json.usage) return
usage = json.usage
},
getStreamUsage: () => usage,
normalizeUsage: (usage: Usage) => {
const inputTokens = usage.prompt_tokens ?? 0
const outputTokens = usage.completion_tokens ?? 0
const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? undefined
const cacheReadTokens = usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined
return {
inputTokens: inputTokens - (cacheReadTokens ?? 0),
outputTokens: outputTokens - (reasoningTokens ?? 0),
reasoningTokens,
cacheReadTokens,
}
},
})
}
@@ -1,64 +1,9 @@
import type { APIEvent } from "@solidjs/start/server"
import { handler } from "~/routes/zen/handler"
type Usage = {
cache_creation?: {
ephemeral_5m_input_tokens?: number
ephemeral_1h_input_tokens?: number
}
cache_creation_input_tokens?: number
cache_read_input_tokens?: number
input_tokens?: number
output_tokens?: number
server_tool_use?: {
web_search_requests?: number
}
}
import { handler } from "~/routes/zen/util/handler"
export function POST(input: APIEvent) {
let usage: Usage
return handler(input, {
modifyBody: (body: any) => ({
...body,
service_tier: "standard_only",
}),
setAuthHeader: (headers: Headers, apiKey: string) => headers.set("x-api-key", apiKey),
format: "anthropic",
parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
onStreamPart: (chunk: string) => {
const data = chunk.split("\n")[1]
if (!data.startsWith("data: ")) return
let json
try {
json = JSON.parse(data.slice(6))
} catch (e) {
return
}
// ie. { type: "message_start"; message: { usage: Usage } }
// ie. { type: "message_delta"; usage: Usage }
const usageUpdate = json.usage ?? json.message?.usage
if (!usageUpdate) return
usage = {
...usage,
...usageUpdate,
cache_creation: {
...usage?.cache_creation,
...usageUpdate.cache_creation,
},
server_tool_use: {
...usage?.server_tool_use,
...usageUpdate.server_tool_use,
},
}
},
getStreamUsage: () => usage,
normalizeUsage: (usage: Usage) => ({
inputTokens: usage.input_tokens ?? 0,
outputTokens: usage.output_tokens ?? 0,
cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
}),
})
}
@@ -0,0 +1,60 @@
import type { APIEvent } from "@solidjs/start/server"
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
import { ZenData } from "@opencode-ai/console-core/model.js"
export async function OPTIONS(input: APIEvent) {
return new Response(null, {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
})
}
export async function GET(input: APIEvent) {
const zenData = ZenData.list()
const disabledModels = await authenticate()
return new Response(
JSON.stringify({
object: "list",
data: Object.entries(zenData.models)
.filter(([id]) => !disabledModels.includes(id))
.map(([id, model]) => ({
id: `opencode/${id}`,
object: "model",
created: Math.floor(Date.now() / 1000),
owned_by: "opencode",
})),
}),
{
headers: {
"Content-Type": "application/json",
},
},
)
async function authenticate() {
const apiKey = input.request.headers.get("authorization")?.split(" ")[1]
if (!apiKey) return []
const disabledModels = await Database.use((tx) =>
tx
.select({
model: ModelTable.model,
})
.from(KeyTable)
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
.leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), isNull(ModelTable.timeDeleted)))
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows.map((row) => row.model)),
)
return disabledModels
}
}
@@ -1,52 +1,9 @@
import type { APIEvent } from "@solidjs/start/server"
import { handler } from "~/routes/zen/handler"
type Usage = {
input_tokens?: number
input_tokens_details?: {
cached_tokens?: number
}
output_tokens?: number
output_tokens_details?: {
reasoning_tokens?: number
}
total_tokens?: number
}
import { handler } from "~/routes/zen/util/handler"
export function POST(input: APIEvent) {
let usage: Usage
return handler(input, {
setAuthHeader: (headers: Headers, apiKey: string) => {
headers.set("authorization", `Bearer ${apiKey}`)
},
format: "openai",
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
onStreamPart: (chunk: string) => {
const [event, data] = chunk.split("\n")
if (event !== "event: response.completed") return
if (!data.startsWith("data: ")) return
let json
try {
json = JSON.parse(data.slice(6)) as { response?: { usage?: Usage } }
} catch (e) {
return
}
if (!json.response?.usage) return
usage = json.response.usage
},
getStreamUsage: () => usage,
normalizeUsage: (usage: Usage) => {
const inputTokens = usage.input_tokens ?? 0
const outputTokens = usage.output_tokens ?? 0
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? undefined
const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? undefined
return {
inputTokens: inputTokens - (cacheReadTokens ?? 0),
outputTokens: outputTokens - (reasoningTokens ?? 0),
reasoningTokens,
cacheReadTokens,
}
},
})
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "0.15.18",
"version": "0.15.19",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "0.15.18",
"version": "0.15.19",
"$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": "0.15.18",
"version": "0.15.19",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+5 -5
View File
@@ -1,23 +1,23 @@
{
"name": "@opencode-ai/desktop",
"version": "0.15.18",
"version": "0.15.19",
"description": "",
"type": "module",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"typecheck": "tsgo --noEmit"
"serve": "vite preview"
},
"license": "MIT",
"devDependencies": {
"opencode": "workspace:*",
"@tailwindcss/vite": "catalog:",
"@tsconfig/bun": "1.0.9",
"@types/luxon": "3.7.1",
"@types/node": "catalog:",
"typescript": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-plugin-icons-spritesheet": "3.0.1",
"vite-plugin-solid": "catalog:"
@@ -26,7 +26,7 @@
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@pierre/precision-diffs": "catalog:",
"@pierre/precision-diffs": "0.3.5",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/active-element": "2.1.3",
"@solid-primitives/event-bus": "1.1.2",
@@ -0,0 +1,362 @@
import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart } from "@opencode-ai/sdk"
import type { Tool } from "opencode/tool/tool"
import type { ReadTool } from "opencode/tool/read"
import { children, Component, createMemo, For, Match, Show, Switch, type JSX } from "solid-js"
import { Dynamic } from "solid-js/web"
import { Markdown } from "./markdown"
import { Collapsible, Icon, IconProps } from "@opencode-ai/ui"
import { getDirectory, getFilename } from "@/utils"
import type { ListTool } from "opencode/tool/ls"
import type { GlobTool } from "opencode/tool/glob"
import type { GrepTool } from "opencode/tool/grep"
import type { WebFetchTool } from "opencode/tool/webfetch"
import type { TaskTool } from "opencode/tool/task"
import type { BashTool } from "opencode/tool/bash"
import type { EditTool } from "opencode/tool/edit"
import type { WriteTool } from "opencode/tool/write"
import { DiffChanges } from "./diff-changes"
export function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
return (
<div class="w-full flex flex-col items-start gap-4">
<For each={props.parts}>
{(part) => {
const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
return (
<Show when={component()}>
<Dynamic component={component()} part={part as any} message={props.message} />
</Show>
)
}}
</For>
</div>
)
}
const PART_MAPPING = {
text: TextPart,
tool: ToolPart,
reasoning: ReasoningPart,
}
function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }) {
return null
// return (
// <Show when={props.part.text.trim()}>
// <div>{props.part.text}</div>
// </Show>
// )
}
function TextPart(props: { part: TextPart; message: AssistantMessage }) {
return (
<Show when={props.part.text.trim()}>
<Markdown text={props.part.text.trim()} />
</Show>
)
}
function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
// const sync = useSync()
const component = createMemo(() => {
const render = ToolRegistry.render(props.part.tool) ?? GenericTool
const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
const input = props.part.state.status === "completed" ? props.part.state.input : {}
// const permissions = sync.data.permission[props.message.sessionID] ?? []
// const permissionIndex = permissions.findIndex((x) => x.callID === props.part.callID)
// const permission = permissions[permissionIndex]
return (
<>
<Dynamic
component={render}
input={input}
tool={props.part.tool}
metadata={metadata}
// permission={permission?.metadata ?? {}}
output={props.part.state.status === "completed" ? props.part.state.output : undefined}
/>
{/* <Show when={props.part.state.status === "error"}>{props.part.state.error.replace("Error: ", "")}</Show> */}
</>
)
})
return <Show when={component()}>{component()}</Show>
}
type TriggerTitle = {
title: string
subtitle?: string
args?: string[]
action?: JSX.Element
}
const isTriggerTitle = (val: any): val is TriggerTitle => {
return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node)
}
function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX.Element; children?: JSX.Element }) {
const resolved = children(() => props.children)
return (
<Collapsible>
<Collapsible.Trigger>
<div class="w-full flex items-center self-stretch gap-5 justify-between">
<div class="w-full flex items-center self-stretch gap-5">
<Icon name={props.icon} size="small" />
<Switch>
<Match when={isTriggerTitle(props.trigger)}>
<div class="w-full flex items-center gap-2 justify-between">
<div class="flex items-center gap-2">
<span class="text-12-medium text-text-base capitalize">
{(props.trigger as TriggerTitle).title}
</span>
<Show when={(props.trigger as TriggerTitle).subtitle}>
<span class="text-12-medium text-text-weak">{(props.trigger as TriggerTitle).subtitle}</span>
</Show>
<Show when={(props.trigger as TriggerTitle).args?.length}>
<For each={(props.trigger as TriggerTitle).args}>
{(arg) => <span class="text-12-regular text-text-weaker">{arg}</span>}
</For>
</Show>
</div>
<Show when={(props.trigger as TriggerTitle).action}>{(props.trigger as TriggerTitle).action}</Show>
</div>
</Match>
<Match when={true}>{props.trigger as JSX.Element}</Match>
</Switch>
</div>
<Show when={resolved()}>
<Collapsible.Arrow />
</Show>
</div>
</Collapsible.Trigger>
<Show when={props.children}>
<Collapsible.Content>{props.children}</Collapsible.Content>
</Show>
</Collapsible>
)
}
function GenericTool(props: ToolProps<any>) {
return <BasicTool icon="mcp" trigger={{ title: props.tool }} />
}
type ToolProps<T extends Tool.Info> = {
input: Partial<Tool.InferParameters<T>>
metadata: Partial<Tool.InferMetadata<T>>
// permission: Record<string, any>
tool: string
output?: string
}
const ToolRegistry = (() => {
const state: Record<
string,
{
name: string
render?: Component<ToolProps<any>>
}
> = {}
function register<T extends Tool.Info>(input: { name: string; render?: Component<ToolProps<T>> }) {
state[input.name] = input
return input
}
return {
register,
render(name: string) {
return state[name]?.render
},
}
})()
ToolRegistry.register<typeof ReadTool>({
name: "read",
render(props) {
return (
<BasicTool
icon="glasses"
trigger={{ title: props.tool, subtitle: props.input.filePath ? getFilename(props.input.filePath) : "" }}
/>
)
},
})
ToolRegistry.register<typeof ListTool>({
name: "list",
render(props) {
return (
<BasicTool icon="bullet-list" trigger={{ title: props.tool, subtitle: props.input.path || "/" }}>
<Show when={false && props.output}>
<div class="whitespace-pre">{props.output}</div>
</Show>
</BasicTool>
)
},
})
ToolRegistry.register<typeof GlobTool>({
name: "glob",
render(props) {
return (
<BasicTool
icon="magnifying-glass-menu"
trigger={{
title: props.tool,
subtitle: props.input.path || "/",
args: props.input.pattern ? ["pattern=" + props.input.pattern] : [],
}}
>
<Show when={false && props.output}>
<div class="whitespace-pre">{props.output}</div>
</Show>
</BasicTool>
)
},
})
ToolRegistry.register<typeof GrepTool>({
name: "grep",
render(props) {
const args = []
if (props.input.pattern) args.push("pattern=" + props.input.pattern)
if (props.input.include) args.push("include=" + props.input.include)
return (
<BasicTool
icon="magnifying-glass-menu"
trigger={{
title: props.tool,
subtitle: props.input.path || "/",
args,
}}
>
<Show when={false && props.output}>
<div class="whitespace-pre">{props.output}</div>
</Show>
</BasicTool>
)
},
})
ToolRegistry.register<typeof WebFetchTool>({
name: "webfetch",
render(props) {
return (
<BasicTool
icon="window-cursor"
trigger={{
title: props.tool,
subtitle: props.input.url || "",
args: props.input.format ? ["format=" + props.input.format] : [],
action: (
<div class="size-6 flex items-center justify-center">
<Icon name="square-arrow-top-right" size="small" />
</div>
),
}}
>
<Show when={false && props.output}>
<div class="whitespace-pre">{props.output}</div>
</Show>
</BasicTool>
)
},
})
ToolRegistry.register<typeof TaskTool>({
name: "task",
render(props) {
return (
<BasicTool
icon="task"
trigger={{
title: `${props.input.subagent_type || props.tool} Agent`,
subtitle: props.input.description,
}}
>
<Show when={false && props.output}>
<div class="whitespace-pre">{props.output}</div>
</Show>
</BasicTool>
)
},
})
ToolRegistry.register<typeof BashTool>({
name: "bash",
render(props) {
return (
<BasicTool
icon="console"
trigger={{
title: "Shell",
subtitle: "Ran " + props.input.command,
}}
>
<Show when={false && props.output}>
<div class="whitespace-pre">{props.output}</div>
</Show>
</BasicTool>
)
},
})
ToolRegistry.register<typeof EditTool>({
name: "edit",
render(props) {
return (
<BasicTool
icon="code-lines"
trigger={
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-5">
<div class="text-12-medium text-text-base capitalize">Edit</div>
<div class="flex">
<Show when={props.input.filePath?.includes("/")}>
<span class="text-text-weak">{getDirectory(props.input.filePath!)}/</span>
</Show>
<span class="text-text-strong">{getFilename(props.input.filePath ?? "")}</span>
</div>
</div>
<div class="flex gap-4 items-center justify-end">{/* <DiffChanges diff={diff} /> */}</div>
</div>
}
>
<Show when={false && props.output}>
<div class="whitespace-pre">{props.output}</div>
</Show>
</BasicTool>
)
},
})
ToolRegistry.register<typeof WriteTool>({
name: "write",
render(props) {
return (
<BasicTool
icon="code-lines"
trigger={
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-5">
<div class="text-12-medium text-text-base capitalize">Write</div>
<div class="flex">
<Show when={props.input.filePath?.includes("/")}>
<span class="text-text-weak">{getDirectory(props.input.filePath!)}/</span>
</Show>
<span class="text-text-strong">{getFilename(props.input.filePath ?? "")}</span>
</div>
</div>
<div class="flex gap-4 items-center justify-end">{/* <DiffChanges diff={diff} /> */}</div>
</div>
}
>
<Show when={false && props.output}>
<div class="whitespace-pre">{props.output}</div>
</Show>
</BasicTool>
)
},
})
@@ -0,0 +1,20 @@
import { FileDiff } from "@opencode-ai/sdk"
import { createMemo, Show } from "solid-js"
export function DiffChanges(props: { diff: FileDiff | FileDiff[] }) {
const additions = createMemo(() =>
Array.isArray(props.diff) ? props.diff.reduce((acc, diff) => acc + (diff.additions ?? 0), 0) : props.diff.additions,
)
const deletions = createMemo(() =>
Array.isArray(props.diff) ? props.diff.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0) : props.diff.deletions,
)
const total = createMemo(() => additions() + deletions())
return (
<Show when={total() > 0}>
<div class="flex gap-2 justify-end items-center">
<span class="text-12-mono text-right text-text-diff-add-base">{`+${additions()}`}</span>
<span class="text-12-mono text-right text-text-diff-delete-base">{`-${deletions()}`}</span>
</div>
</Show>
)
}
+21 -4
View File
@@ -2,8 +2,9 @@ import {
type FileContents,
FileDiff,
type DiffLineAnnotation,
type HunkData,
DiffFileRendererOptions,
registerCustomTheme,
// registerCustomTheme,
} from "@pierre/precision-diffs"
import { ComponentProps, createEffect, splitProps } from "solid-js"
@@ -15,8 +16,7 @@ export type DiffProps<T = {}> = Omit<DiffFileRendererOptions<T>, "themes"> & {
classList?: ComponentProps<"div">["classList"]
}
// @ts-expect-error
registerCustomTheme("opencode", () => import("./theme.json"))
// registerCustomTheme("opencode", () => import("./theme.json"))
// interface ThreadMetadata {
// threadId: string
@@ -49,7 +49,7 @@ export function Diff<T>(props: DiffProps<T>) {
// annotations and a container element to hold the diff
createEffect(() => {
const instance = new FileDiff<T>({
theme: "opencode",
theme: "pierre-light",
// Or can also provide a 'themes' prop, which allows the code to adapt
// to your OS light or dark theme
// themes: { dark: 'pierre-night', light: 'pierre-light' },
@@ -98,6 +98,23 @@ export function Diff<T>(props: DiffProps<T>) {
// 'simple':
// Just a subtle bar separator between each hunk
hunkSeparators: "line-info",
// hunkSeparators(hunkData: HunkData) {
// const fragment = document.createDocumentFragment()
// const numCol = document.createElement("div")
// numCol.textContent = `${hunkData.lines}`
// numCol.style.position = "sticky"
// numCol.style.left = "0"
// numCol.style.backgroundColor = "var(--pjs-bg)"
// numCol.style.zIndex = "2"
// fragment.appendChild(numCol)
// const contentCol = document.createElement("div")
// contentCol.textContent = "unmodified lines"
// contentCol.style.position = "sticky"
// contentCol.style.width = "var(--pjs-column-content-width)"
// contentCol.style.left = "var(--pjs-column-number-width)"
// fragment.appendChild(contentCol)
// return fragment
// },
// On lines that have both additions and deletions, we can run a
// separate diff check to mark parts of the lines that change.
// 'none':
-8
View File
@@ -460,13 +460,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return sync.data.message[store.active]?.find((m) => m.id === store.activeMessage)
})
const activeAssistantMessages = createMemo(() => {
if (!store.active || !activeMessage()) return []
return sync.data.message[store.active]?.filter(
(m) => m.role === "assistant" && m.parentID == activeMessage()?.id,
)
})
const model = createMemo(() => {
if (!last()) return
const model = sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID]
@@ -504,7 +497,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return {
active,
activeMessage,
activeAssistantMessages,
lastUserMessage,
cost,
last,
+133 -47
View File
@@ -22,6 +22,10 @@ import { Code } from "@/components/code"
import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { Diff } from "@/components/diff"
import { ProgressCircle } from "@/components/progress-circle"
import { AssistantMessage } from "@/components/assistant-message"
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
import { DiffChanges } from "@/components/diff-changes"
export default function Page() {
const local = useLocal()
@@ -92,7 +96,7 @@ export default function Page() {
}
}
if (event.key.length === 1 && event.key !== "Unidentified") {
if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
inputRef?.focus()
}
}
@@ -392,9 +396,6 @@ export default function Page() {
{(session) => {
const diffs = createMemo(() => session.summary?.diffs ?? [])
const filesChanged = createMemo(() => diffs().length)
const additions = createMemo(() => diffs().reduce((acc, diff) => (acc ?? 0) + (diff.additions ?? 0), 0))
const deletions = createMemo(() => diffs().reduce((acc, diff) => (acc ?? 0) + (diff.deletions ?? 0), 0))
return (
<Tooltip placement="right" value={session.title}>
<div>
@@ -408,12 +409,7 @@ export default function Page() {
</div>
<div class="flex justify-between items-center self-stretch">
<span class="text-12-regular text-text-weak">{`${filesChanged() || "No"} file${filesChanged() !== 1 ? "s" : ""} changed`}</span>
<Show when={additions() || deletions()}>
<div class="flex gap-2 justify-end items-center">
<span class="text-12-mono text-right text-text-diff-add-base">{`+${additions()}`}</span>
<span class="text-12-mono text-right text-text-diff-delete-base">{`-${deletions()}`}</span>
</div>
</Show>
<DiffChanges diff={diffs()} />
</div>
</div>
</Tooltip>
@@ -434,13 +430,12 @@ export default function Page() {
<Tabs onChange={handleTabChange}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List>
<Tabs.Trigger value="chat" class="flex gap-x-1.5 items-center">
<Tabs.Trigger value="chat" class="flex gap-x-4 items-center">
<div>Chat</div>
<Show when={local.session.active()}>
<div class="flex flex-col h-4 px-2 -mr-2 justify-center items-center rounded-full bg-surface-base text-12-medium text-text-strong">
{local.session.context()}%
</div>
</Show>
<Tooltip value={`${local.session.tokens() ?? 0} Tokens`} class="flex items-center gap-1.5">
<ProgressCircle percentage={local.session.context() ?? 0} />
<div class="text-14-regular text-text-weak text-right">{local.session.context() ?? 0}%</div>
</Tooltip>
</Tabs.Trigger>
{/* <Tabs.Trigger value="review">Review</Tabs.Trigger> */}
<SortableProvider ids={local.file.opened().map((file) => file.path)}>
@@ -548,33 +543,114 @@ export default function Page() {
<Show when={local.session.userMessages().length > 1}>
<ul role="list" class="w-60 shrink-0 flex flex-col items-start gap-1">
<For each={local.session.userMessages()}>
{(message) => (
<li
class="group/li flex items-center gap-x-2 py-1 self-stretch cursor-default"
onClick={() => local.session.setActiveMessage(message.id)}
>
<div class="w-[18px] shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 12" fill="none">
<g>
<rect x="0" width="2" height="12" rx="1" fill="#CFCECD" />
<rect x="4" width="2" height="12" rx="1" fill="#CFCECD" />
<rect x="8" width="2" height="12" rx="1" fill="#CFCECD" />
<rect x="12" width="2" height="12" rx="1" fill="#CFCECD" />
<rect x="16" width="2" height="12" rx="1" fill="#CFCECD" />
</g>
</svg>
</div>
<div
data-active={local.session.activeMessage()?.id === message.id}
classList={{
"text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
}}
{(message) => {
const countLines = (text: string) => {
if (!text) return 0
return text.split("\n").length
}
const additions = createMemo(
() =>
message.summary?.diffs.reduce((acc, diff) => acc + (diff.additions ?? 0), 0) ?? 0,
)
const deletions = createMemo(
() =>
message.summary?.diffs.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0) ?? 0,
)
const totalBeforeLines = createMemo(
() =>
message.summary?.diffs.reduce((acc, diff) => acc + countLines(diff.before), 0) ??
0,
)
const blockCounts = createMemo(() => {
const TOTAL_BLOCKS = 5
const adds = additions()
const dels = deletions()
const unchanged = Math.max(0, totalBeforeLines() - dels)
const totalActivity = unchanged + adds + dels
if (totalActivity === 0) {
return { added: 0, deleted: 0, neutral: TOTAL_BLOCKS }
}
const percentAdded = adds / totalActivity
const percentDeleted = dels / totalActivity
const added_raw = percentAdded * TOTAL_BLOCKS
const deleted_raw = percentDeleted * TOTAL_BLOCKS
let added = adds > 0 ? Math.ceil(added_raw) : 0
let deleted = dels > 0 ? Math.ceil(deleted_raw) : 0
let total_allocated = added + deleted
if (total_allocated > TOTAL_BLOCKS) {
if (added_raw < deleted_raw) {
added = Math.floor(added_raw)
} else {
deleted = Math.floor(deleted_raw)
}
total_allocated = added + deleted
if (total_allocated > TOTAL_BLOCKS) {
if (added_raw < deleted_raw) {
deleted = Math.floor(deleted_raw)
} else {
added = Math.floor(added_raw)
}
}
}
const neutral = Math.max(0, TOTAL_BLOCKS - added - deleted)
return { added, deleted, neutral }
})
const ADD_COLOR = "var(--icon-diff-add-base)"
const DELETE_COLOR = "var(--icon-diff-delete-base)"
const NEUTRAL_COLOR = "var(--icon-weak-base)"
const visibleBlocks = createMemo(() => {
const counts = blockCounts()
const blocks = [
...Array(counts.added).fill(ADD_COLOR),
...Array(counts.deleted).fill(DELETE_COLOR),
...Array(counts.neutral).fill(NEUTRAL_COLOR),
]
return blocks.slice(0, 5)
})
return (
<li
class="group/li flex items-center gap-x-2 py-1 self-stretch cursor-default"
onClick={() => local.session.setActiveMessage(message.id)}
>
{message.summary?.title ?? local.session.getMessageText(message)}
</div>
</li>
)}
<div class="w-[18px] shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 12" fill="none">
<g>
<For each={visibleBlocks()}>
{(color, i) => (
<rect x={i() * 4} width="2" height="12" rx="1" fill={color} />
)}
</For>
</g>
</svg>
</div>
<div
data-active={local.session.activeMessage()?.id === message.id}
classList={{
"text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
}}
>
{message.summary?.title ?? local.session.getMessageText(message)}
</div>
</li>
)
}}
</For>
</ul>
</Show>
@@ -585,6 +661,11 @@ export default function Page() {
const title = createMemo(() => message.summary?.title)
const prompt = createMemo(() => local.session.getMessageText(message))
const summary = createMemo(() => message.summary?.body)
const assistantMessages = createMemo(() => {
return sync.data.message[activeSession().id]?.filter(
(m) => m.role === "assistant" && m.parentID == message.id,
) as AssistantMessageType[]
})
return (
<div
@@ -633,10 +714,7 @@ export default function Page() {
</div>
</div>
<div class="flex gap-4 items-center justify-end">
<div class="flex gap-2 justify-end items-center">
<span class="text-12-mono text-right text-text-diff-add-base">{`+${diff.additions}`}</span>
<span class="text-12-mono text-right text-text-diff-delete-base">{`-${diff.deletions}`}</span>
</div>
<DiffChanges diff={diff} />
<Icon name="chevron-grabber-vertical" size="small" />
</div>
</div>
@@ -661,10 +739,18 @@ export default function Page() {
</Show>
</div>
{/* Response */}
<div data-todo="Response (Timeline)">
<div data-todo="Response" class="w-full">
<div class="flex flex-col items-start gap-1 self-stretch">
<h2 class="text-12-medium text-text-weak">Response</h2>
</div>
<div class="w-full flex flex-col items-start self-stretch gap-8">
<For each={assistantMessages()}>
{(assistantMessage) => {
const parts = createMemo(() => sync.data.part[assistantMessage.id])
return <AssistantMessage message={assistantMessage} parts={parts()} />
}}
</For>
</div>
</div>
</div>
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "0.15.18",
"version": "0.15.19",
"$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": "0.15.18",
"version": "0.15.19",
"name": "opencode",
"type": "module",
"private": true,
+6 -1
View File
@@ -1,6 +1,11 @@
#!/usr/bin/env bun
import path from "path"
const dir = new URL("..", import.meta.url).pathname
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const dir = path.resolve(__dirname, "..")
process.chdir(dir)
import { $ } from "bun"
+9 -5
View File
@@ -51,13 +51,16 @@ if (!Script.preview) {
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
// arch
const binaryPkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode-bin'",
`pkgver=${Script.version.split("-")[0]}`,
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
@@ -68,10 +71,10 @@ if (!Script.preview) {
"conflicts=('opencode')",
"depends=('fzf' 'ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.zip::https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-arm64.zip")`,
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.zip::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.zip")`,
`sha256sums_aarch64=('${arm64Sha}')`,
"",
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.zip::https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-x64.zip")`,
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.zip::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.zip")`,
`sha256sums_x86_64=('${x64Sha}')`,
"",
"package() {",
@@ -86,7 +89,8 @@ if (!Script.preview) {
"# Maintainer: adam",
"",
"pkgname='opencode'",
`pkgver=${Script.version.split("-")[0]}`,
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
@@ -98,7 +102,7 @@ if (!Script.preview) {
"depends=('fzf' 'ripgrep')",
"makedepends=('git' 'bun-bin' 'go')",
"",
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v${Script.version}.tar.gz")`,
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
`sha256sums=('SKIP')`,
"",
"build() {",
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bun
import { z } from "zod/v4"
import { z } from "zod"
import { Config } from "../src/config/config"
const file = process.argv[2]
+177 -60
View File
@@ -1,16 +1,19 @@
import type {
Agent as ACPAgent,
AgentSideConnection,
AuthenticateRequest,
CancelNotification,
InitializeRequest,
LoadSessionRequest,
NewSessionRequest,
PermissionOption,
PromptRequest,
SetSessionModelRequest,
SetSessionModeRequest,
SetSessionModeResponse,
import {
type Agent as ACPAgent,
type AgentSideConnection,
type AuthenticateRequest,
type CancelNotification,
type InitializeRequest,
type LoadSessionRequest,
type NewSessionRequest,
type PermissionOption,
type PlanEntry,
type PromptRequest,
type SetSessionModelRequest,
type SetSessionModeRequest,
type SetSessionModeResponse,
type ToolCallContent,
type ToolKind,
} from "@agentclientprotocol/sdk"
import { Log } from "../util/log"
import { ACPSessionManager } from "./session"
@@ -25,24 +28,17 @@ import { Storage } from "@/storage/storage"
import { Command } from "@/command"
import { Agent as Agents } from "@/agent/agent"
import { Permission } from "@/permission"
import { Session } from "@/session"
import { Identifier } from "@/id/id"
import { SessionCompaction } from "@/session/compaction"
import type { Config } from "@/config/config"
import { MCP } from "@/mcp"
import { Todo } from "@/session/todo"
import { z } from "zod"
export namespace ACP {
const log = Log.create({ service: "acp-agent" })
// TODO: mcp servers?
type ToolKind =
| "read"
| "edit"
| "delete"
| "move"
| "search"
| "execute"
| "think"
| "fetch"
| "switch_mode"
| "other"
export class Agent implements ACPAgent {
private sessionManager = new ACPSessionManager()
private connection: AgentSideConnection
@@ -157,6 +153,62 @@ export namespace ACP {
})
break
case "completed":
const kind = toToolKind(part.tool)
const content: ToolCallContent[] = [
{
type: "content",
content: {
type: "text",
text: part.state.output,
},
},
]
if (kind === "edit") {
const input = part.state.input
const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
const newText =
typeof input["newString"] === "string"
? input["newString"]
: typeof input["content"] === "string"
? input["content"]
: ""
content.push({
type: "diff",
path: filePath,
oldText,
newText,
})
}
if (part.tool === "todowrite") {
const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
if (parsedTodos.success) {
await this.connection
.sessionUpdate({
sessionId: acpSession.id,
update: {
sessionUpdate: "plan",
entries: parsedTodos.data.map((todo) => {
const status: PlanEntry["status"] =
todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
return {
priority: "medium",
status,
content: todo.content,
}
}),
},
})
.catch((err) => {
log.error("failed to send session update for todo", { error: err })
})
} else {
log.error("failed to parse todo output", { error: parsedTodos.error })
}
}
await this.connection
.sessionUpdate({
sessionId: acpSession.id,
@@ -164,15 +216,8 @@ export namespace ACP {
sessionUpdate: "tool_call_update",
toolCallId: part.callID,
status: "completed",
content: [
{
type: "content",
content: {
type: "text",
text: part.state.output,
},
},
],
kind,
content,
title: part.state.title,
rawOutput: {
output: part.state.output,
@@ -258,11 +303,14 @@ export namespace ACP {
protocolVersion: 1,
agentCapabilities: {
loadSession: true,
// TODO: map acp mcp
// mcpCapabilities: {
// http: true,
// sse: true,
// },
mcpCapabilities: {
http: true,
sse: true,
},
promptCapabilities: {
embeddedContext: true,
image: true,
},
},
authMethods: [
{
@@ -287,6 +335,7 @@ export namespace ACP {
const model = await defaultModel(this.config)
const session = await this.sessionManager.create(params.cwd, params.mcpServers, model)
log.info("creating_session", { mcpServers: params.mcpServers.length })
const load = await this.loadSession({
cwd: params.cwd,
mcpServers: params.mcpServers,
@@ -325,6 +374,17 @@ export namespace ACP {
name: command.name,
description: command.description ?? "",
}))
const names = new Set(availableCommands.map((c) => c.name))
if (!names.has("init"))
availableCommands.push({
name: "init",
description: "create/update a AGENTS.md",
})
if (!names.has("compact"))
availableCommands.push({
name: "compact",
description: "compact the session",
})
setTimeout(() => {
this.connection.sessionUpdate({
@@ -346,6 +406,35 @@ export namespace ACP {
const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
const mcpServers: Record<string, Config.Mcp> = {}
for (const server of params.mcpServers) {
if ("type" in server) {
mcpServers[server.name] = {
url: server.url,
headers: server.headers.reduce<Record<string, string>>((acc, { name, value }) => {
acc[name] = value
return acc
}, {}),
type: "remote",
}
} else {
mcpServers[server.name] = {
type: "local",
command: [server.command, ...server.args],
environment: server.env.reduce<Record<string, string>>((acc, { name, value }) => {
acc[name] = value
return acc
}, {}),
}
}
}
await Promise.all(
Object.entries(mcpServers).map(async ([key, mcp]) => {
await MCP.add(key, mcp)
}),
)
return {
sessionId,
models: {
@@ -452,25 +541,25 @@ export namespace ACP {
log.info("parts", { parts })
const cmd = await (async () => {
const text = parts.filter((part) => part.type === "text").join("")
const match = text.match(/^\/(\w+)\s*(.*)$/)
if (!match) return
const cmd = (() => {
const text = parts
.filter((p) => p.type === "text")
.map((p) => p.text)
.join("")
.trim()
const [c, args] = match.slice(1)
const command = await Command.get(c)
if (!command) return
return { command, args }
if (!text.startsWith("/")) return
const [name, ...rest] = text.slice(1).split(/\s+/)
return { name, args: rest.join(" ").trim() }
})()
if (cmd) {
await SessionPrompt.command({
sessionID,
command: cmd.command.name,
arguments: cmd.args,
agent,
})
} else {
const done = {
stopReason: "end_turn" as const,
_meta: {},
}
if (!cmd) {
await SessionPrompt.prompt({
sessionID,
model: {
@@ -480,12 +569,40 @@ export namespace ACP {
parts,
agent,
})
return done
}
return {
stopReason: "end_turn" as const,
_meta: {},
const command = await Command.get(cmd.name)
if (command) {
await SessionPrompt.command({
sessionID,
command: command.name,
arguments: cmd.args,
model: model.providerID + "/" + model.modelID,
agent,
})
return done
}
switch (cmd.name) {
case "init":
await Session.initialize({
sessionID,
messageID: Identifier.ascending("message"),
providerID: model.providerID,
modelID: model.modelID,
})
break
case "compact":
await SessionCompaction.run({
sessionID,
providerID: model.providerID,
modelID: model.modelID,
})
break
}
return done
}
async cancel(params: CancelNotification) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { Config } from "../config/config"
import z from "zod/v4"
import z from "zod"
import { Provider } from "../provider/provider"
import { generateObject, type ModelMessage } from "ai"
import PROMPT_GENERATE from "./generate.txt"
@@ -1,147 +0,0 @@
import z from "zod/v4"
import { Auth } from "./index"
import { NamedError } from "../util/error"
export namespace AuthGithubCopilot {
const CLIENT_ID = "Iv1.b507a08c87ecfe98"
const DEVICE_CODE_URL = "https://github.com/login/device/code"
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
const COPILOT_API_KEY_URL = "https://api.github.com/copilot_internal/v2/token"
interface DeviceCodeResponse {
device_code: string
user_code: string
verification_uri: string
expires_in: number
interval: number
}
interface AccessTokenResponse {
access_token?: string
error?: string
error_description?: string
}
interface CopilotTokenResponse {
token: string
expires_at: number
refresh_in: number
endpoints: {
api: string
}
}
export async function authorize() {
const deviceResponse = await fetch(DEVICE_CODE_URL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "GitHubCopilotChat/0.26.7",
},
body: JSON.stringify({
client_id: CLIENT_ID,
scope: "read:user",
}),
})
const deviceData: DeviceCodeResponse = await deviceResponse.json()
return {
device: deviceData.device_code,
user: deviceData.user_code,
verification: deviceData.verification_uri,
interval: deviceData.interval || 5,
expiry: deviceData.expires_in,
}
}
export async function poll(device_code: string) {
const response = await fetch(ACCESS_TOKEN_URL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "GitHubCopilotChat/0.26.7",
},
body: JSON.stringify({
client_id: CLIENT_ID,
device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
})
if (!response.ok) return "failed"
const data: AccessTokenResponse = await response.json()
if (data.access_token) {
// Store the GitHub OAuth token
await Auth.set("github-copilot", {
type: "oauth",
refresh: data.access_token,
access: "",
expires: 0,
})
return "complete"
}
if (data.error === "authorization_pending") return "pending"
if (data.error) return "failed"
return "pending"
}
export async function access() {
const info = await Auth.get("github-copilot")
if (!info || info.type !== "oauth") return
if (info.access && info.expires > Date.now()) return info.access
// Get new Copilot API token
const response = await fetch(COPILOT_API_KEY_URL, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${info.refresh}`,
"User-Agent": "GitHubCopilotChat/0.26.7",
"Editor-Version": "vscode/1.99.3",
"Editor-Plugin-Version": "copilot-chat/0.26.7",
},
})
if (!response.ok) return
const tokenData: CopilotTokenResponse = await response.json()
// Store the Copilot API token
await Auth.set("github-copilot", {
type: "oauth",
refresh: info.refresh,
access: tokenData.token,
expires: tokenData.expires_at * 1000,
})
return tokenData.token
}
export const DeviceCodeError = NamedError.create("DeviceCodeError", z.object({}))
export const TokenExchangeError = NamedError.create(
"TokenExchangeError",
z.object({
message: z.string(),
}),
)
export const AuthenticationError = NamedError.create(
"AuthenticationError",
z.object({
message: z.string(),
}),
)
export const CopilotTokenError = NamedError.create(
"CopilotTokenError",
z.object({
message: z.string(),
}),
)
}
+1 -1
View File
@@ -1,7 +1,7 @@
import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
import z from "zod/v4"
import z from "zod"
export namespace Auth {
export const Oauth = z
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Global } from "../global"
import { Log } from "../util/log"
import path from "path"
+2 -2
View File
@@ -1,5 +1,5 @@
import z from "zod/v4"
import type { ZodType } from "zod/v4"
import z from "zod"
import type { ZodType } from "zod"
import { Log } from "../util/log"
import { Instance } from "../project/instance"
+1 -1
View File
@@ -29,7 +29,7 @@ export const AcpCommand = cmd({
const input = new WritableStream<Uint8Array>({
write(chunk) {
return new Promise<void>((resolve, reject) => {
process.stdout.write(Buffer.from(chunk), (err) => {
process.stdout.write(chunk, (err) => {
if (err) {
reject(err)
} else {
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { EOL } from "os"
import { NamedError } from "../util/error"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
+1 -1
View File
@@ -1,7 +1,7 @@
import { Log } from "../util/log"
import path from "path"
import os from "os"
import z from "zod/v4"
import z from "zod"
import { Filesystem } from "../util/filesystem"
import { ModelsDev } from "../provider/models"
import { mergeDeep, pipe } from "remeda"
+1 -1
View File
@@ -1,7 +1,7 @@
import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
import z from "zod/v4"
import z from "zod"
import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Bus } from "../bus"
import { $ } from "bun"
import type { BunFile } from "bun"
+2 -2
View File
@@ -2,7 +2,7 @@
import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
import z from "zod/v4"
import z from "zod"
import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { $ } from "bun"
@@ -218,7 +218,7 @@ export namespace Ripgrep {
code: "ENOENT",
errno: -2,
path: input.cwd,
});
})
}
const proc = Bun.spawn(args, {
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Bus } from "../bus"
import { Flag } from "../flag/flag"
import { Instance } from "../project/instance"
+1
View File
@@ -9,6 +9,7 @@ export namespace Flag {
export const OPENCODE_DISABLE_LSP_DOWNLOAD = truthy("OPENCODE_DISABLE_LSP_DOWNLOAD")
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
// Experimental
export const OPENCODE_EXPERIMENTAL_WATCHER = truthy("OPENCODE_EXPERIMENTAL_WATCHER")
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { randomBytes } from "crypto"
export namespace Identifier {
+1 -1
View File
@@ -1,5 +1,5 @@
import { spawn } from "bun"
import z from "zod/v4"
import z from "zod"
import { NamedError } from "../util/error"
import { Log } from "../util/log"
import { Bus } from "../bus"
+1 -1
View File
@@ -1,6 +1,6 @@
import path from "path"
import { $ } from "bun"
import z from "zod/v4"
import z from "zod"
import { NamedError } from "../util/error"
import { Bus } from "../bus"
import { Log } from "../util/log"
+1 -1
View File
@@ -4,7 +4,7 @@ import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types
import { Log } from "../util/log"
import { LANGUAGE_EXTENSIONS } from "./language"
import { Bus } from "../bus"
import z from "zod/v4"
import z from "zod"
import type { LSPServer } from "./server"
import { NamedError } from "../util/error"
import { withTimeout } from "../util/timeout"
+1 -1
View File
@@ -2,7 +2,7 @@ import { Log } from "../util/log"
import { LSPClient } from "./client"
import path from "path"
import { LSPServer } from "./server"
import z from "zod/v4"
import z from "zod"
import { Config } from "../config/config"
import { spawn } from "child_process"
import { Instance } from "../project/instance"
+137 -110
View File
@@ -5,7 +5,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { Config } from "../config/config"
import { Log } from "../util/log"
import { NamedError } from "../util/error"
import z from "zod/v4"
import z from "zod"
import { Session } from "../session"
import { Bus } from "../bus"
import { Instance } from "../project/instance"
@@ -26,122 +26,22 @@ export namespace MCP {
const state = Instance.state(
async () => {
const cfg = await Config.get()
const config = cfg.mcp ?? {}
const clients: {
[name: string]: MCPClient
} = {}
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
if (mcp.enabled === false) {
log.info("mcp server disabled", { key })
continue
}
log.info("found", { key, type: mcp.type })
if (mcp.type === "remote") {
const transports = [
{
name: "StreamableHTTP",
transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
requestInit: {
headers: mcp.headers,
},
}),
},
{
name: "SSE",
transport: new SSEClientTransport(new URL(mcp.url), {
requestInit: {
headers: mcp.headers,
},
}),
},
]
let lastError: Error | undefined
for (const { name, transport } of transports) {
const client = await experimental_createMCPClient({
name: "opencode",
transport,
}).catch((error) => {
lastError = error instanceof Error ? error : new Error(String(error))
log.debug("transport connection failed", {
key,
transport: name,
url: mcp.url,
error: lastError.message,
})
return null
})
if (client) {
log.debug("transport connection succeeded", { key, transport: name })
clients[key] = client
break
}
}
if (!clients[key]) {
const errorMessage = lastError
? `MCP server ${key} failed to connect: ${lastError.message}`
: `MCP server ${key} failed to connect to ${mcp.url}`
log.error("remote mcp connection failed", { key, url: mcp.url, error: lastError?.message })
Bus.publish(Session.Event.Error, {
error: {
name: "UnknownError",
data: {
message: errorMessage,
},
},
})
}
}
if (mcp.type === "local") {
const [cmd, ...args] = mcp.command
const client = await experimental_createMCPClient({
name: "opencode",
transport: new StdioClientTransport({
stderr: "ignore",
command: cmd,
args,
env: {
...process.env,
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
...mcp.environment,
},
}),
}).catch((error) => {
const errorMessage =
error instanceof Error
? `MCP server ${key} failed to start: ${error.message}`
: `MCP server ${key} failed to start`
log.error("local mcp startup failed", {
key,
command: mcp.command,
error: error instanceof Error ? error.message : String(error),
})
Bus.publish(Session.Event.Error, {
error: {
name: "UnknownError",
data: {
message: errorMessage,
},
},
})
return null
})
if (client) {
clients[key] = client
}
}
}
for (const [key, client] of Object.entries(clients)) {
const result = await withTimeout(client.tools(), 5000).catch(() => {})
if (!result) {
log.warn("mcp client verification failed, removing client", { key })
delete clients[key]
}
}
await Promise.all(
Object.entries(config).map(async ([key, mcp]) => {
const result = await create(key, mcp).catch(() => undefined)
if (!result) return
clients[key] = result.client
}),
)
return {
clients,
config: cfg.mcp ?? {},
config,
}
},
async (state) => {
@@ -151,6 +51,133 @@ export namespace MCP {
},
)
export async function add(name: string, mcp: Config.Mcp) {
const s = await state()
const result = await create(name, mcp)
if (!result) return
s.clients[name] = result.client
}
async function create(name: string, mcp: Config.Mcp) {
if (mcp.enabled === false) {
log.info("mcp server disabled", { name })
return
}
log.info("found", { name, type: mcp.type })
let mcpClient: MCPClient | undefined
if (mcp.type === "remote") {
const transports = [
{
name: "StreamableHTTP",
transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
requestInit: {
headers: mcp.headers,
},
}),
},
{
name: "SSE",
transport: new SSEClientTransport(new URL(mcp.url), {
requestInit: {
headers: mcp.headers,
},
}),
},
]
let lastError: Error | undefined
for (const { name, transport } of transports) {
const client = await experimental_createMCPClient({
name: "opencode",
transport,
}).catch((error) => {
lastError = error instanceof Error ? error : new Error(String(error))
log.debug("transport connection failed", {
name,
transport: name,
url: mcp.url,
error: lastError.message,
})
return null
})
if (client) {
log.debug("transport connection succeeded", { name, transport: name })
mcpClient = client
break
}
}
if (!mcpClient) {
const errorMessage = lastError
? `MCP server ${name} failed to connect: ${lastError.message}`
: `MCP server ${name} failed to connect to ${mcp.url}`
log.error("remote mcp connection failed", { name, url: mcp.url, error: lastError?.message })
Bus.publish(Session.Event.Error, {
error: {
name: "UnknownError",
data: {
message: errorMessage,
},
},
})
}
}
if (mcp.type === "local") {
const [cmd, ...args] = mcp.command
const client = await experimental_createMCPClient({
name: "opencode",
transport: new StdioClientTransport({
stderr: "ignore",
command: cmd,
args,
env: {
...process.env,
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
...mcp.environment,
},
}),
}).catch((error) => {
const errorMessage =
error instanceof Error
? `MCP server ${name} failed to start: ${error.message}`
: `MCP server ${name} failed to start`
log.error("local mcp startup failed", {
name,
command: mcp.command,
error: error instanceof Error ? error.message : String(error),
})
Bus.publish(Session.Event.Error, {
error: {
name: "UnknownError",
data: {
message: errorMessage,
},
},
})
return null
})
if (client) {
mcpClient = client
}
}
if (!mcpClient) {
log.warn("mcp client not initialized", { name })
return
}
const result = await withTimeout(mcpClient.tools(), 5000).catch(() => {})
if (!result) {
log.warn("mcp client verification failed, dropping client", { name })
return
}
return {
client: mcpClient,
}
}
export async function status() {
return state().then((state) => {
const result: Record<string, "connected" | "failed" | "disabled"> = {}
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Bus } from "../bus"
import { Log } from "../util/log"
import { Identifier } from "../id/id"
+3 -1
View File
@@ -1,9 +1,10 @@
import z from "zod/v4"
import z from "zod"
import { Filesystem } from "../util/filesystem"
import path from "path"
import { $ } from "bun"
import { Storage } from "../storage/storage"
import { Log } from "../util/log"
import { Flag } from "@/flag/flag"
export namespace Project {
const log = Log.create({ service: "project" })
@@ -31,6 +32,7 @@ export namespace Project {
const project: Info = {
id: "global",
worktree: "/",
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
time: {
created: Date.now(),
},
+1 -1
View File
@@ -1,7 +1,7 @@
import { Global } from "../global"
import { Log } from "../util/log"
import path from "path"
import z from "zod/v4"
import z from "zod"
import { data } from "./models-macro" with { type: "macro" }
import { Installation } from "../installation"
+7 -10
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import path from "path"
import { Config } from "../config/config"
import { mergeDeep, sortBy } from "remeda"
@@ -101,7 +101,7 @@ export namespace Provider {
"nova-pro",
"nova-premier",
"claude",
"deepseek"
"deepseek",
].some((m) => modelID.includes(m))
const isGovCloud = region.startsWith("us-gov")
if (modelRequiresPrefix && !isGovCloud) {
@@ -517,14 +517,11 @@ export namespace Provider {
const provider = await state().then((state) => state.providers[providerID])
if (!provider) return
const priority = [
"claude-haiku-4-5",
"claude-haiku-4.5",
"3-5-haiku",
"3.5-haiku",
"gemini-2.5-flash",
"gpt-5-nano",
]
let priority = ["claude-haiku-4-5", "claude-haiku-4.5", "3-5-haiku", "3.5-haiku", "gemini-2.5-flash", "gpt-5-nano"]
// claude-haiku-4.5 is considered a premium model in github copilot, we shouldn't use premium requests for title gen
if (providerID === "github-copilot") {
priority = priority.filter((m) => m !== "claude-haiku-4.5")
}
for (const item of priority) {
for (const model of Object.keys(provider.info.models)) {
if (model.includes(item)) return getModel(providerID, model)
+4 -2
View File
@@ -75,7 +75,7 @@ export namespace ProviderTransform {
export function temperature(_providerID: string, modelID: string) {
if (modelID.toLowerCase().includes("qwen")) return 0.55
if (modelID.toLowerCase().includes("claude")) return 1
if (modelID.toLowerCase().includes("claude")) return undefined
return 0
}
@@ -92,7 +92,9 @@ export namespace ProviderTransform {
}
if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
if (!modelID.includes("codex")) result["reasoningEffort"] = "medium"
if (!modelID.includes("codex") && !modelID.includes("gpt-5-pro")) {
result["reasoningEffort"] = "medium"
}
if (providerID !== "azure") {
result["textVerbosity"] = modelID.includes("codex") ? "medium" : "low"
+1 -1
View File
@@ -5,7 +5,7 @@ import { Hono } from "hono"
import { cors } from "hono/cors"
import { stream, streamSSE } from "hono/streaming"
import { Session } from "../session"
import z from "zod/v4"
import z from "zod"
import { Provider } from "../provider/provider"
import { mapValues } from "remeda"
import { NamedError } from "../util/error"
+7 -2
View File
@@ -7,7 +7,7 @@ import { defer } from "../util/defer"
import { MessageV2 } from "./message-v2"
import { SystemPrompt } from "./system"
import { Bus } from "../bus"
import z from "zod/v4"
import z from "zod"
import type { ModelsDev } from "../provider/models"
import { SessionPrompt } from "./prompt"
import { Flag } from "../flag/flag"
@@ -155,6 +155,7 @@ export namespace SessionCompaction {
error,
})
},
tools: model.info.tool_call ? {} : undefined,
messages: [
...system.map(
(x): ModelMessage => ({
@@ -188,7 +189,11 @@ export namespace SessionCompaction {
case "text-delta":
part.text += value.text
if (value.providerMetadata) part.metadata = value.providerMetadata
if (part.text) await Session.updatePart(part)
if (part.text)
await Session.updatePart({
part,
delta: value.text,
})
continue
case "text-end": {
part.text = part.text.trimEnd()
+10 -1
View File
@@ -1,5 +1,5 @@
import { Decimal } from "decimal.js"
import z from "zod/v4"
import z from "zod"
import { type LanguageModelUsage, type ProviderMetadata } from "ai"
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
@@ -78,6 +78,12 @@ export namespace Session {
export type ShareInfo = z.output<typeof ShareInfo>
export const Event = {
Created: Bus.event(
"session.created",
z.object({
info: Info,
}),
),
Updated: Bus.event(
"session.updated",
z.object({
@@ -167,6 +173,9 @@ export namespace Session {
}
log.info("created", result)
await Storage.write(["session", Instance.project.id, result.id], result)
Bus.publish(Event.Created, {
info: result,
})
const cfg = await Config.get()
if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto"))
share(result.id)
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Instance } from "../project/instance"
import { Log } from "../util/log"
import { NamedError } from "../util/error"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Bus } from "../bus"
import { NamedError } from "../util/error"
import { Message } from "./message"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { NamedError } from "../util/error"
export namespace Message {
+1 -23
View File
@@ -1,7 +1,7 @@
import path from "path"
import os from "os"
import fs from "fs/promises"
import z from "zod/v4"
import z from "zod"
import { Identifier } from "../id/id"
import { MessageV2 } from "./message-v2"
import { Log } from "../util/log"
@@ -191,28 +191,6 @@ export namespace SessionPrompt {
processor,
})
// const permUnsub = (() => {
// const handled = new Set<string>()
// const options = [
// { optionId: "allow_once", kind: "allow_once", name: "Allow once" },
// { optionId: "allow_always", kind: "allow_always", name: "Always allow" },
// { optionId: "reject_once", kind: "reject_once", name: "Reject" },
// ]
// return Bus.subscribe(Permission.Event.Updated, async (event) => {
// const info = event.properties
// if (info.sessionID !== input.sessionID) return
// if (handled.has(info.id)) return
// handled.add(info.id)
// const toolCallId = info.callID ?? info.id
// const metadata = info.metadata ?? {}
// // TODO: emit permission event to bus for ACP to handle
// Permission.respond({ sessionID: info.sessionID, permissionID: info.id, response: "reject" })
// })
// })()
// await using _permSub = defer(() => {
// permUnsub?.()
// })
const params = await Plugin.trigger(
"chat.params",
{
@@ -93,6 +93,8 @@ user: What is the codebase structure?
assistant: [Uses the Task tool]
</example>
IMPORTANT: Always use the TodoWrite tool to plan and track 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.
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Identifier } from "../id/id"
import { Snapshot } from "../snapshot"
import { MessageV2 } from "./message-v2"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Bus } from "../bus"
import { Storage } from "../storage/storage"
+1 -1
View File
@@ -3,7 +3,7 @@ import path from "path"
import fs from "fs/promises"
import { Log } from "../util/log"
import { Global } from "../global"
import z from "zod/v4"
import z from "zod"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { spawn } from "child_process"
import { Tool } from "./tool"
import DESCRIPTION from "./bash.txt"
+1 -1
View File
@@ -3,7 +3,7 @@
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
import z from "zod/v4"
import z from "zod"
import * as path from "path"
import { Tool } from "./tool"
import { LSP } from "../lsp"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import path from "path"
import { Tool } from "./tool"
import DESCRIPTION from "./glob.txt"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Tool } from "./tool"
import { Ripgrep } from "../file/ripgrep"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Tool } from "./tool"
export const InvalidTool = Tool.define("invalid", {
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Tool } from "./tool"
import * as path from "path"
import DESCRIPTION from "./ls.txt"
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Tool } from "./tool"
import path from "path"
import { LSP } from "../lsp"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Tool } from "./tool"
import path from "path"
import { LSP } from "../lsp"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Tool } from "./tool"
import { EditTool } from "./edit"
import DESCRIPTION from "./multiedit.txt"
+24 -23
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import * as path from "path"
import * as fs from "fs/promises"
import { Tool } from "./tool"
@@ -17,7 +17,8 @@ const PatchParams = z.object({
})
export const PatchTool = Tool.define("patch", {
description: "Apply a patch to modify multiple files. Supports adding, updating, and deleting files with context-aware changes.",
description:
"Apply a patch to modify multiple files. Supports adding, updating, and deleting files with context-aware changes.",
parameters: PatchParams,
async execute(params, ctx) {
if (!params.patchText) {
@@ -46,12 +47,12 @@ export const PatchTool = Tool.define("patch", {
type: "add" | "update" | "delete" | "move"
movePath?: string
}> = []
let totalDiff = ""
for (const hunk of hunks) {
const filePath = path.resolve(Instance.directory, hunk.path)
if (!Filesystem.contains(Instance.directory, filePath)) {
throw new Error(`File ${filePath} is not in the current working directory`)
}
@@ -62,30 +63,30 @@ export const PatchTool = Tool.define("patch", {
const oldContent = ""
const newContent = hunk.contents
const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent)
fileChanges.push({
filePath,
oldContent,
newContent,
type: "add",
})
totalDiff += diff + "\n"
}
break
case "update":
// Check if file exists for update
const stats = await fs.stat(filePath).catch(() => null)
if (!stats || stats.isDirectory()) {
throw new Error(`File not found or is directory: ${filePath}`)
}
// Read file and update time tracking (like edit tool does)
await FileTime.assert(ctx.sessionID, filePath)
const oldContent = await fs.readFile(filePath, "utf-8")
let newContent = oldContent
// Apply the update chunks to get new content
try {
const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks)
@@ -93,9 +94,9 @@ export const PatchTool = Tool.define("patch", {
} catch (error) {
throw new Error(`Failed to apply update to ${filePath}: ${error}`)
}
const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent)
fileChanges.push({
filePath,
oldContent,
@@ -103,23 +104,23 @@ export const PatchTool = Tool.define("patch", {
type: hunk.move_path ? "move" : "update",
movePath: hunk.move_path ? path.resolve(Instance.directory, hunk.move_path) : undefined,
})
totalDiff += diff + "\n"
break
case "delete":
// Check if file exists for deletion
await FileTime.assert(ctx.sessionID, filePath)
const contentToDelete = await fs.readFile(filePath, "utf-8")
const deleteDiff = createTwoFilesPatch(filePath, filePath, contentToDelete, "")
fileChanges.push({
filePath,
oldContent: contentToDelete,
newContent: "",
type: "delete",
})
totalDiff += deleteDiff + "\n"
break
}
@@ -141,7 +142,7 @@ export const PatchTool = Tool.define("patch", {
// Apply the changes
const changedFiles: string[] = []
for (const change of fileChanges) {
switch (change.type) {
case "add":
@@ -153,12 +154,12 @@ export const PatchTool = Tool.define("patch", {
await fs.writeFile(change.filePath, change.newContent, "utf-8")
changedFiles.push(change.filePath)
break
case "update":
await fs.writeFile(change.filePath, change.newContent, "utf-8")
changedFiles.push(change.filePath)
break
case "move":
if (change.movePath) {
// Create parent directories for destination
@@ -173,13 +174,13 @@ export const PatchTool = Tool.define("patch", {
changedFiles.push(change.movePath)
}
break
case "delete":
await fs.unlink(change.filePath)
changedFiles.push(change.filePath)
break
}
// Update file time tracking
FileTime.read(ctx.sessionID, change.filePath)
if (change.movePath) {
@@ -193,7 +194,7 @@ export const PatchTool = Tool.define("patch", {
}
// Generate output summary
const relativePaths = changedFiles.map(filePath => path.relative(Instance.worktree, filePath))
const relativePaths = changedFiles.map((filePath) => path.relative(Instance.worktree, filePath))
const summary = `${fileChanges.length} files changed`
return {
@@ -201,7 +202,7 @@ export const PatchTool = Tool.define("patch", {
metadata: {
diff: totalDiff,
},
output: `Patch applied successfully. ${summary}:\n${relativePaths.map(p => ` ${p}`).join("\n")}`,
output: `Patch applied successfully. ${summary}:\n${relativePaths.map((p) => ` ${p}`).join("\n")}`,
}
},
})
})
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import * as fs from "fs"
import * as path from "path"
import { Tool } from "./tool"
+1 -1
View File
@@ -16,7 +16,7 @@ import { Instance } from "../project/instance"
import { Config } from "../config/config"
import path from "path"
import { type ToolDefinition } from "@opencode-ai/plugin"
import z from "zod/v4"
import z from "zod"
import { Plugin } from "../plugin"
export namespace ToolRegistry {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Tool } from "./tool"
import DESCRIPTION from "./task.txt"
import z from "zod/v4"
import z from "zod"
import { Session } from "../session"
import { Bus } from "../bus"
import { MessageV2 } from "../session/message-v2"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Tool } from "./tool"
import DESCRIPTION_WRITE from "./todowrite.txt"
import { Todo } from "../session/todo"
+4 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import type { MessageV2 } from "../session/message-v2"
export namespace Tool {
@@ -32,6 +32,9 @@ export namespace Tool {
}>
}
export type InferParameters<T extends Info> = T extends Info<infer P> ? z.infer<P> : never
export type InferMetadata<T extends Info> = T extends Info<any, infer M> ? M : never
export function define<Parameters extends z.ZodType, Result extends Metadata>(
id: string,
init: Info<Parameters, Result>["init"] | Awaited<ReturnType<Info<Parameters, Result>["init"]>>,
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import { Tool } from "./tool"
import TurndownService from "turndown"
import DESCRIPTION from "./webfetch.txt"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
import * as path from "path"
import { Tool } from "./tool"
import { LSP } from "../lsp"
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod/v4"
import z from "zod"
export abstract class NamedError extends Error {
abstract schema(): z.core.$ZodType
+1 -1
View File
@@ -1,7 +1,7 @@
import path from "path"
import fs from "fs/promises"
import { Global } from "../global"
import z from "zod/v4"
import z from "zod"
export namespace Log {
export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" })
@@ -0,0 +1,71 @@
import { describe, expect, test } from "bun:test"
import path from "path"
import { Session } from "../../src/session"
import { Bus } from "../../src/bus"
import { Log } from "../../src/util/log"
import { Instance } from "../../src/project/instance"
const projectRoot = path.join(__dirname, "../..")
Log.init({ print: false })
describe("session.started event", () => {
test("should emit session.started event when session is created", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
let eventReceived = false
let receivedInfo: Session.Info | undefined
const unsub = Bus.subscribe(Session.Event.Created, (event) => {
eventReceived = true
receivedInfo = event.properties.info as Session.Info
})
const session = await Session.create({})
await new Promise((resolve) => setTimeout(resolve, 100))
unsub()
expect(eventReceived).toBe(true)
expect(receivedInfo).toBeDefined()
expect(receivedInfo?.id).toBe(session.id)
expect(receivedInfo?.projectID).toBe(session.projectID)
expect(receivedInfo?.directory).toBe(session.directory)
expect(receivedInfo?.title).toBe(session.title)
await Session.remove(session.id)
},
})
})
test("session.started event should be emitted before session.updated", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
const events: string[] = []
const unsubStarted = Bus.subscribe(Session.Event.Created, () => {
events.push("started")
})
const unsubUpdated = Bus.subscribe(Session.Event.Updated, () => {
events.push("updated")
})
const session = await Session.create({})
await new Promise((resolve) => setTimeout(resolve, 100))
unsubStarted()
unsubUpdated()
expect(events).toContain("started")
expect(events).toContain("updated")
expect(events.indexOf("started")).toBeLessThan(events.indexOf("updated"))
await Session.remove(session.id)
},
})
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.15.18",
"version": "0.15.19",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from "zod/v4"
import { z } from "zod"
export type ToolContext = {
sessionID: string
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.15.18",
"version": "0.15.19",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+8
View File
@@ -1197,6 +1197,13 @@ export type EventSessionIdle = {
}
}
export type EventSessionCreated = {
type: "session.created"
properties: {
info: Session
}
}
export type EventSessionUpdated = {
type: "session.updated"
properties: {
@@ -1247,6 +1254,7 @@ export type Event =
| EventFileWatcherUpdated
| EventTodoUpdated
| EventSessionIdle
| EventSessionCreated
| EventSessionUpdated
| EventSessionDeleted
| EventSessionError
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "0.15.18",
"version": "0.15.19",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
@@ -48,6 +48,7 @@ type EditorComponent interface {
SetInterruptKeyInDebounce(inDebounce bool)
SetExitKeyInDebounce(inDebounce bool)
RestoreFromHistory(index int)
GetAttachments() []*attachment.Attachment
}
type editorComponent struct {
@@ -471,6 +472,10 @@ func (m *editorComponent) Length() int {
return m.textarea.Length()
}
func (m *editorComponent) GetAttachments() []*attachment.Attachment {
return m.textarea.GetAttachments()
}
func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
value := strings.TrimSpace(m.Value())
if value == "" {
@@ -628,9 +633,7 @@ func (m *editorComponent) SetValueWithAttachments(value string) {
}
if end > start {
filePath := value[start:end]
slog.Debug("test", "filePath", filePath)
if _, err := os.Stat(filepath.Join(util.CwdPath, filePath)); err == nil {
slog.Debug("test", "found", true)
attachment := m.createAttachmentFromFile(filePath)
if attachment != nil {
m.textarea.InsertAttachment(attachment)
+8
View File
@@ -1164,6 +1164,14 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
}
value := a.editor.Value()
// Expand text attachments before opening editor
for _, att := range a.editor.GetAttachments() {
if textSource, ok := att.GetTextSource(); ok {
value = strings.Replace(value, att.Display, textSource.Value, 1)
}
}
updated, cmd := a.editor.Clear()
a.editor = updated.(chat.EditorComponent)
cmds = append(cmds, cmd)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "0.15.18",
"version": "0.15.19",
"type": "module",
"exports": {
".": "./src/components/index.ts",
@@ -24,7 +24,7 @@
},
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/precision-diffs": "0.0.2-alpha.1-1",
"@pierre/precision-diffs": "catalog:",
"@solidjs/meta": "catalog:",
"fuzzysort": "catalog:",
"luxon": "catalog:",
+224 -214
View File
@@ -1,214 +1,224 @@
--background-base: #f8f7f7;
--background-weak: var(--smoke-light-3);
--background-strong: var(--smoke-light-1);
--background-stronger: #fcfcfc;
--base: var(--smoke-light-alpha-2);
--surface-base: var(--smoke-light-alpha-2);
--base2: var(--smoke-light-alpha-2);
--base3: var(--smoke-light-alpha-2);
--surface-inset-base: var(--smoke-light-alpha-3);
--surface-inset-base-hover: var(--smoke-light-alpha-3);
--surface-inset-strong: #1f000017;
--surface-inset-strong-hover: #1f000017;
--surface-raised-base: var(--smoke-light-alpha-1);
--surface-float-base: var(--smoke-dark-1);
--surface-float-base-hover: var(--smoke-dark-2);
--surface-raised-base-hover: var(--smoke-light-alpha-2);
--surface-raised-strong: var(--smoke-light-1);
--surface-raised-strong-hover: var(--white);
--surface-raised-stronger: var(--white);
--surface-raised-stronger-hover: var(--white);
--surface-weak: var(--smoke-light-alpha-3);
--surface-weaker: var(--smoke-light-alpha-4);
--surface-strong: #ffffff;
--surface-raised-stronger-non-alpha: var(--white);
--surface-brand-base: var(--yuzu-light-9);
--surface-brand-hover: var(--yuzu-light-10);
--surface-interactive-base: var(--cobalt-light-3);
--surface-interactive-hover: var(--cobalt-light-4);
--surface-interactive-weak: var(--cobalt-light-2);
--surface-interactive-weak-hover: var(--cobalt-light-3);
--surface-success-base: var(--apple-light-3);
--surface-success-weak: var(--apple-light-2);
--surface-success-strong: var(--apple-light-9);
--surface-warning-base: var(--solaris-light-3);
--surface-warning-weak: var(--solaris-light-2);
--surface-warning-strong: var(--solaris-light-9);
--surface-critical-base: var(--ember-light-3);
--surface-critical-weak: var(--ember-light-2);
--surface-critical-strong: var(--ember-light-9);
--surface-info-base: var(--lilac-light-3);
--surface-info-weak: var(--lilac-light-2);
--surface-info-strong: var(--lilac-light-9);
--surface-diff-skip-base: var(--smoke-light-3);
--surface-diff-unchanged-base: #ffffff00;
--surface-diff-hidden-base: var(--blue-light-3);
--surface-diff-hidden-weak: var(--blue-light-2);
--surface-diff-hidden-weaker: var(--blue-light-1);
--surface-diff-hidden-strong: var(--blue-light-5);
--surface-diff-hidden-stronger: var(--blue-light-9);
--surface-diff-add-base: var(--mint-light-3);
--surface-diff-add-weak: var(--mint-light-2);
--surface-diff-add-weaker: var(--mint-light-1);
--surface-diff-add-strong: var(--mint-light-5);
--surface-diff-add-stronger: var(--mint-light-9);
--surface-diff-delete-base: var(--ember-light-3);
--surface-diff-delete-weak: var(--ember-light-2);
--surface-diff-delete-weaker: var(--ember-light-1);
--surface-diff-delete-strong: var(--ember-light-6);
--surface-diff-delete-stronger: var(--ember-light-9);
--text-base: var(--smoke-light-11);
--input-base: var(--smoke-light-1);
--input-hover: var(--smoke-light-2);
--input-active: var(--cobalt-light-1);
--input-selected: var(--cobalt-light-4);
--input-focus: var(--cobalt-light-1);
--input-disabled: var(--smoke-light-4);
--text-weak: var(--smoke-light-9);
--text-weaker: var(--smoke-light-8);
--text-strong: var(--smoke-light-12);
--text-on-brand-base: var(--smoke-light-alpha-11);
--text-on-interactive-base: var(--smoke-light-1);
--text-on-success-base: var(--smoke-dark-alpha-11);
--text-on-warning-base: var(--smoke-dark-alpha-11);
--text-on-info-base: var(--smoke-dark-alpha-11);
--text-diff-add-base: var(--mint-light-11);
--text-diff-delete-base: var(--ember-light-11);
--text-diff-delete-strong: var(--ember-light-12);
--text-diff-add-strong: var(--mint-light-12);
--text-on-info-weak: var(--smoke-dark-alpha-9);
--text-on-info-strong: var(--smoke-dark-alpha-12);
--text-on-warning-weak: var(--smoke-dark-alpha-9);
--text-on-warning-strong: var(--smoke-dark-alpha-12);
--text-on-success-weak: var(--smoke-dark-alpha-9);
--text-on-success-strong: var(--smoke-dark-alpha-12);
--text-on-brand-weak: var(--smoke-light-alpha-9);
--text-on-brand-weaker: var(--smoke-light-alpha-8);
--text-on-brand-strong: var(--smoke-light-alpha-12);
--button-secondary-base: #fdfcfc;
--border-base: var(--smoke-light-alpha-7);
--border-hover: var(--smoke-light-alpha-8);
--border-active: var(--smoke-light-alpha-9);
--border-selected: var(--cobalt-light-alpha-9);
--border-disabled: var(--smoke-light-alpha-8);
--border-focus: var(--smoke-light-alpha-9);
--border-weak-base: var(--smoke-light-alpha-5);
--border-strong-base: var(--smoke-light-alpha-7);
--border-strong-hover: var(--smoke-light-alpha-8);
--border-strong-active: var(--smoke-light-alpha-7);
--border-strong-selected: var(--cobalt-light-alpha-6);
--border-strong-disabled: var(--smoke-light-alpha-6);
--border-strong-focus: var(--smoke-light-alpha-7);
--border-weak-hover: var(--smoke-light-alpha-6);
--border-weak-active: var(--smoke-light-alpha-7);
--border-weak-selected: var(--cobalt-light-alpha-6);
--border-weak-disabled: var(--smoke-light-alpha-6);
--border-weak-focus: var(--smoke-light-alpha-7);
--border-interactive-base: var(--cobalt-light-7);
--border-interactive-hover: var(--cobalt-light-8);
--border-interactive-active: var(--cobalt-light-9);
--border-interactive-selected: var(--cobalt-light-9);
--border-interactive-disabled: var(--smoke-light-8);
--border-interactive-focus: var(--cobalt-light-9);
--border-success-base: var(--apple-light-6);
--border-success-hover: var(--apple-light-7);
--border-success-selected: var(--apple-light-9);
--border-warning-base: var(--solaris-light-6);
--border-warning-hover: var(--solaris-light-7);
--border-warning-selected: var(--solaris-light-9);
--border-critical-base: var(--ember-light-6);
--border-critical-hover: var(--ember-light-7);
--border-critical-selected: var(--ember-light-9);
--border-info-base: var(--lilac-light-6);
--border-info-hover: var(--lilac-light-7);
--border-info-selected: var(--lilac-light-9);
--icon-base: var(--smoke-light-9);
--icon-hover: var(--smoke-light-11);
--icon-active: var(--smoke-light-12);
--icon-selected: var(--smoke-light-12);
--icon-disabled: var(--smoke-light-8);
--icon-focus: var(--smoke-light-12);
--icon-weak-base: var(--smoke-light-7);
--icon-invert-base: #ffffff;
--icon-weak-hover: var(--smoke-light-8);
--icon-weak-active: var(--smoke-light-9);
--icon-weak-selected: var(--smoke-light-10);
--icon-weak-disabled: var(--smoke-light-6);
--icon-weak-focus: var(--smoke-light-9);
--icon-strong-base: var(--smoke-light-12);
--icon-strong-hover: var(--smoke-light-12);
--icon-strong-active: var(--smoke-light-12);
--icon-strong-selected: var(--smoke-light-12);
--icon-strong-disabled: var(--smoke-light-8);
--icon-strong-focus: var(--smoke-light-12);
--icon-brand-base: var(--smoke-light-12);
--icon-interactive-base: var(--cobalt-light-9);
--icon-success-base: var(--apple-light-7);
--icon-success-hover: var(--apple-light-8);
--icon-success-active: var(--apple-light-11);
--icon-warning-base: var(--amber-light-7);
--icon-warning-hover: var(--amber-light-8);
--icon-warning-active: var(--amber-light-11);
--icon-critical-base: var(--ember-light-7);
--icon-critical-hover: var(--ember-light-8);
--icon-critical-active: var(--ember-light-11);
--icon-info-base: var(--lilac-light-7);
--icon-info-hover: var(--lilac-light-8);
--icon-info-active: var(--lilac-light-11);
--icon-on-brand-base: var(--smoke-light-alpha-11);
--icon-on-brand-hover: var(--smoke-light-alpha-12);
--icon-on-brand-selected: var(--smoke-light-alpha-12);
--icon-on-interactive-base: var(--smoke-light-alpha-9);
--icon-on-interactive-hover: var(--smoke-light-alpha-10);
--icon-on-interactive-selected: var(--smoke-light-alpha-11);
--icon-agent-plan-base: var(--purple-light-9);
--icon-agent-docs-base: var(--amber-light-9);
--icon-agent-ask-base: var(--cyan-light-9);
--icon-agent-build-base: var(--blue-light-9);
--icon-on-success-base: var(--apple-light-alpha-9);
--icon-on-success-hover: var(--apple-light-alpha-10);
--icon-on-success-selected: var(--apple-light-alpha-11);
--icon-on-warning-base: var(--amber-lightalpha-9);
--icon-on-warning-hover: var(--amber-lightalpha-10);
--icon-on-warning-selected: var(--amber-lightalpha-11);
--icon-on-critical-base: var(--ember-light-alpha-9);
--icon-on-critical-hover: var(--ember-light-alpha-10);
--icon-on-critical-selected: var(--ember-light-alpha-11);
--icon-on-info-base: var(--lilac-light-9);
--icon-on-info-hover: var(--lilac-light-alpha-10);
--icon-on-info-selected: var(--lilac-light-alpha-11);
--icon-diff-add-base: var(--mint-light-11);
--icon-diff-add-hover: var(--mint-light-12);
--icon-diff-add-active: var(--mint-light-12);
--icon-diff-delete-base: var(--ember-light-9);
--icon-diff-delete-hover: var(--ember-light-10);
--icon-diff-delete-active: var(--ember-light-11);
--syntax-comment: #ffffff;
--syntax-string: #ffffff;
--syntax-keyword: #ffffff;
--syntax-function: #ffffff;
--syntax-number: #ffffff;
--syntax-operator: #ffffff;
--syntax-variable: #ffffff;
--syntax-type: #ffffff;
--syntax-constant: #ffffff;
--syntax-punctuation: #ffffff;
--syntax-success: #ffffff;
--syntax-warning: #ffffff;
--syntax-critical: #ffffff;
--syntax-info: #ffffff;
--markdown-heading: #ffffff;
--markdown-text: #ffffff;
--markdown-link: #ffffff;
--markdown-link-text: #ffffff;
--markdown-code: #ffffff;
--markdown-block-quote: #ffffff;
--markdown-emph: #ffffff;
--markdown-strong: #ffffff;
--markdown-horizontal-rule: #ffffff;
--markdown-list-item: #ffffff;
--markdown-list-enumeration: #ffffff;
--markdown-image: #ffffff;
--markdown-image-text: #ffffff;
--markdown-code-block: #ffffff;
--border-color: #ffffff;
--background-base: #F8F7F7;
--background-weak: var(--smoke-light-3);
--background-strong: var(--smoke-light-1);
--background-stronger: #FCFCFC;
--base: var(--smoke-light-alpha-2);
--surface-base: var(--smoke-light-alpha-2);
--surface-base-hover: #0500000F;
--surface-base-active: var(--smoke-light-alpha-3);
--surface-base-interactive-active: var(--cobalt-light-alpha-3);
--base2: var(--smoke-light-alpha-2);
--base3: var(--smoke-light-alpha-2);
--surface-inset-base: var(--smoke-light-alpha-2);
--surface-inset-base-hover: var(--smoke-light-alpha-3);
--surface-inset-strong: #1F000017;
--surface-inset-strong-hover: #1F000017;
--surface-raised-base: var(--smoke-light-alpha-1);
--surface-float-base: var(--smoke-dark-1);
--surface-float-base-hover: var(--smoke-dark-2);
--surface-raised-base-hover: var(--smoke-light-alpha-2);
--surface-raised-strong: var(--smoke-light-1);
--surface-raised-strong-hover: var(--white);
--surface-raised-stronger: var(--white);
--surface-raised-stronger-hover: var(--white);
--surface-weak: var(--smoke-light-alpha-3);
--surface-weaker: var(--smoke-light-alpha-4);
--surface-strong: #FFFFFF;
--surface-raised-stronger-non-alpha: var(--white);
--surface-brand-base: var(--yuzu-light-9);
--surface-brand-hover: var(--yuzu-light-10);
--surface-interactive-base: var(--cobalt-light-3);
--surface-interactive-hover: var(--cobalt-light-4);
--surface-interactive-weak: var(--cobalt-light-2);
--surface-interactive-weak-hover: var(--cobalt-light-3);
--surface-success-base: var(--apple-light-3);
--surface-success-weak: var(--apple-light-2);
--surface-success-strong: var(--apple-light-9);
--surface-warning-base: var(--solaris-light-3);
--surface-warning-weak: var(--solaris-light-2);
--surface-warning-strong: var(--solaris-light-9);
--surface-critical-base: var(--ember-light-3);
--surface-critical-weak: var(--ember-light-2);
--surface-critical-strong: var(--ember-light-9);
--surface-info-base: var(--lilac-light-3);
--surface-info-weak: var(--lilac-light-2);
--surface-info-strong: var(--lilac-light-9);
--surface-diff-hidden-base: var(--blue-light-3);
--surface-diff-skip-base: var(--smoke-light-2);
--surface-diff-unchanged-base: #FFFFFF00;
--surface-diff-hidden-weak: var(--blue-light-2);
--surface-diff-hidden-weaker: var(--blue-light-1);
--surface-diff-hidden-strong: var(--blue-light-5);
--surface-diff-hidden-stronger: var(--blue-light-9);
--surface-diff-add-base: var(--mint-light-3);
--surface-diff-add-weak: var(--mint-light-2);
--surface-diff-add-weaker: var(--mint-light-1);
--surface-diff-add-strong: var(--mint-light-5);
--surface-diff-add-stronger: var(--mint-light-9);
--surface-diff-delete-base: var(--ember-light-3);
--surface-diff-delete-weak: var(--ember-light-2);
--surface-diff-delete-weaker: var(--ember-light-1);
--surface-diff-delete-strong: var(--ember-light-6);
--surface-diff-delete-stronger: var(--ember-light-9);
--text-base: var(--smoke-light-11);
--input-base: var(--smoke-light-1);
--input-hover: var(--smoke-light-2);
--input-active: var(--cobalt-light-1);
--input-selected: var(--cobalt-light-4);
--input-focus: var(--cobalt-light-1);
--input-disabled: var(--smoke-light-4);
--text-weak: var(--smoke-light-9);
--text-weaker: var(--smoke-light-8);
--text-strong: var(--smoke-light-12);
--text-interactive-base: var(--cobalt-light-9);
--text-on-brand-base: var(--smoke-light-alpha-11);
--text-on-interactive-base: var(--smoke-light-1);
--text-on-interactive-weak: var(--smoke-dark-alpha-11);
--text-on-success-base: var(--smoke-dark-alpha-11);
--text-on-warning-base: var(--smoke-dark-alpha-11);
--text-on-info-base: var(--smoke-dark-alpha-11);
--text-diff-add-base: var(--mint-light-11);
--text-diff-delete-base: var(--ember-light-11);
--text-diff-delete-strong: var(--ember-light-12);
--text-diff-add-strong: var(--mint-light-12);
--text-on-info-weak: var(--smoke-dark-alpha-9);
--text-on-info-strong: var(--smoke-dark-alpha-12);
--text-on-warning-weak: var(--smoke-dark-alpha-9);
--text-on-warning-strong: var(--smoke-dark-alpha-12);
--text-on-success-weak: var(--smoke-dark-alpha-9);
--text-on-success-strong: var(--smoke-dark-alpha-12);
--text-on-brand-weak: var(--smoke-light-alpha-9);
--text-on-brand-weaker: var(--smoke-light-alpha-8);
--text-on-brand-strong: var(--smoke-light-alpha-12);
--button-secondary-base: #FDFCFC;
--button-secondary-base-hover: #FAF9F9;
--border-base: var(--smoke-light-alpha-7);
--border-hover: var(--smoke-light-alpha-8);
--border-active: var(--smoke-light-alpha-9);
--border-selected: var(--cobalt-light-alpha-9);
--border-disabled: var(--smoke-light-alpha-8);
--border-focus: var(--smoke-light-alpha-9);
--border-weak-base: var(--smoke-light-alpha-5);
--border-strong-base: var(--smoke-light-alpha-7);
--border-strong-hover: var(--smoke-light-alpha-8);
--border-strong-active: var(--smoke-light-alpha-7);
--border-strong-selected: var(--cobalt-light-alpha-6);
--border-strong-disabled: var(--smoke-light-alpha-6);
--border-strong-focus: var(--smoke-light-alpha-7);
--border-weak-hover: var(--smoke-light-alpha-6);
--border-weak-active: var(--smoke-light-alpha-7);
--border-weak-selected: var(--cobalt-light-alpha-5);
--border-weak-disabled: var(--smoke-light-alpha-6);
--border-weak-focus: var(--smoke-light-alpha-7);
--border-interactive-base: var(--cobalt-light-7);
--border-interactive-hover: var(--cobalt-light-8);
--border-interactive-active: var(--cobalt-light-9);
--border-interactive-selected: var(--cobalt-light-9);
--border-interactive-disabled: var(--smoke-light-8);
--border-interactive-focus: var(--cobalt-light-9);
--border-success-base: var(--apple-light-6);
--border-success-hover: var(--apple-light-7);
--border-success-selected: var(--apple-light-9);
--border-warning-base: var(--solaris-light-6);
--border-warning-hover: var(--solaris-light-7);
--border-warning-selected: var(--solaris-light-9);
--border-critical-base: var(--ember-light-6);
--border-critical-hover: var(--ember-light-7);
--border-critical-selected: var(--ember-light-9);
--border-info-base: var(--lilac-light-6);
--border-info-hover: var(--lilac-light-7);
--border-info-selected: var(--lilac-light-9);
--icon-base: var(--smoke-light-9);
--icon-hover: var(--smoke-light-11);
--icon-active: var(--smoke-light-12);
--icon-selected: var(--smoke-light-12);
--icon-disabled: var(--smoke-light-8);
--icon-focus: var(--smoke-light-12);
--icon-weak-base: var(--smoke-light-7);
--icon-invert-base: #FFFFFF;
--icon-weak-hover: var(--smoke-light-8);
--icon-weak-active: var(--smoke-light-9);
--icon-weak-selected: var(--smoke-light-10);
--icon-weak-disabled: var(--smoke-light-6);
--icon-weak-focus: var(--smoke-light-9);
--icon-strong-base: var(--smoke-light-12);
--icon-strong-hover: var(--smoke-light-12);
--icon-strong-active: var(--smoke-light-12);
--icon-strong-selected: var(--smoke-light-12);
--icon-strong-disabled: var(--smoke-light-8);
--icon-strong-focus: var(--smoke-light-12);
--icon-brand-base: var(--smoke-light-12);
--icon-interactive-base: var(--cobalt-light-9);
--icon-success-base: var(--apple-light-7);
--icon-success-hover: var(--apple-light-8);
--icon-success-active: var(--apple-light-11);
--icon-warning-base: var(--amber-light-7);
--icon-warning-hover: var(--amber-light-8);
--icon-warning-active: var(--amber-light-11);
--icon-critical-base: var(--ember-light-7);
--icon-critical-hover: var(--ember-light-8);
--icon-critical-active: var(--ember-light-11);
--icon-info-base: var(--lilac-light-7);
--icon-info-hover: var(--lilac-light-8);
--icon-info-active: var(--lilac-light-11);
--icon-on-brand-base: var(--smoke-light-alpha-11);
--icon-on-brand-hover: var(--smoke-light-alpha-12);
--icon-on-brand-selected: var(--smoke-light-alpha-12);
--icon-on-interactive-base: var(--smoke-light-1);
--icon-agent-plan-base: var(--purple-light-9);
--icon-agent-docs-base: var(--amber-light-9);
--icon-agent-ask-base: var(--cyan-light-9);
--icon-agent-build-base: var(--cobalt-light-9);
--icon-on-success-base: var(--apple-light-alpha-9);
--icon-on-success-hover: var(--apple-light-alpha-10);
--icon-on-success-selected: var(--apple-light-alpha-11);
--icon-on-warning-base: var(--amber-lightalpha-9);
--icon-on-warning-hover: var(--amber-lightalpha-10);
--icon-on-warning-selected: var(--amber-lightalpha-11);
--icon-on-critical-base: var(--ember-light-alpha-9);
--icon-on-critical-hover: var(--ember-light-alpha-10);
--icon-on-critical-selected: var(--ember-light-alpha-11);
--icon-on-info-base: var(--lilac-light-9);
--icon-on-info-hover: var(--lilac-light-alpha-10);
--icon-on-info-selected: var(--lilac-light-alpha-11);
--icon-diff-add-base: var(--mint-light-11);
--icon-diff-add-hover: var(--mint-light-12);
--icon-diff-add-active: var(--mint-light-12);
--icon-diff-delete-base: var(--ember-light-9);
--icon-diff-delete-hover: var(--ember-light-10);
--icon-diff-delete-active: var(--ember-light-11);
--syntax-comment: #8A8A8A;
--syntax-string: #D68C27;
--syntax-keyword: #3B7DD8;
--syntax-function: #D1383D;
--syntax-number: #3D9A57;
--syntax-operator: #D68C27;
--syntax-variable: #B0851F;
--syntax-type: #318795;
--syntax-constant: #953170;
--syntax-punctuation: #1A1A1A;
--syntax-success: var(--apple-dark-10);
--syntax-warning: var(--amber-light-10);
--syntax-critical: var(--ember-dark-9);
--syntax-info: var(--lilac-dark-11);
--markdown-heading: #D68C27;
--markdown-text: #1A1A1A;
--markdown-link: #3B7DD8;
--markdown-link-text: #318795;
--markdown-code: #3D9A57;
--markdown-block-quote: #B0851F;
--markdown-emph: #B0851F;
--markdown-strong: #D68C27;
--markdown-horizontal-rule: #8A8A8A;
--markdown-list-item: #3B7DD8;
--markdown-list-enumeration: #318795;
--markdown-image: #3B7DD8;
--markdown-image-text: #318795;
--markdown-code-block: #1A1A1A;
--border-color: #FFFFFF;
--border-weaker-base: var(--smoke-light-alpha-3);
--border-weaker-hover: var(--smoke-light-alpha-4);
--border-weaker-active: var(--smoke-light-alpha-6);
--border-weaker-selected: var(--cobalt-light-alpha-4);
--border-weaker-disabled: var(--smoke-light-alpha-2);
--border-weaker-focus: var(--smoke-light-alpha-6);
+39 -7
View File
@@ -1,23 +1,55 @@
[data-component="collapsible"] {
width: 100%;
display: flex;
flex-direction: column;
background-color: var(--surface-inset-base);
border: 1px solid var(--border-weaker-base);
transition: background-color 0.15s ease;
border-radius: 8px;
overflow: clip;
[data-slot="trigger"] {
cursor: pointer;
[data-slot="collapsible-trigger"] {
width: 100%;
display: flex;
height: 40px;
padding: 6px 8px 6px 12px;
align-items: center;
align-self: stretch;
cursor: default;
user-select: none;
color: var(--text-base);
/* text-12-medium */
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large); /* 166.667% */
letter-spacing: var(--letter-spacing-normal);
/* &:hover { */
/* background-color: var(--surface-base); */
/* } */
&:focus-visible {
outline: 2px solid var(--border-focus);
outline-offset: 2px;
outline: none;
}
&[data-disabled] {
cursor: not-allowed;
opacity: 0.5;
}
[data-slot="collapsible-arrow"] {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
/* [data-slot="collapsible-arrow-icon"] { */
/* } */
}
}
[data-slot="content"] {
[data-slot="collapsible-content"] {
overflow: hidden;
/* animation: slideUp 250ms ease-out; */
+12 -2
View File
@@ -1,5 +1,6 @@
import { Collapsible as Kobalte, CollapsibleRootProps } from "@kobalte/core/collapsible"
import { ComponentProps, ParentProps, splitProps } from "solid-js"
import { Icon } from "./icon"
export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
class?: string
@@ -21,14 +22,23 @@ function CollapsibleRoot(props: CollapsibleProps) {
}
function CollapsibleTrigger(props: ComponentProps<typeof Kobalte.Trigger>) {
return <Kobalte.Trigger data-slot="trigger" {...props} />
return <Kobalte.Trigger data-slot="collapsible-trigger" {...props} />
}
function CollapsibleContent(props: ComponentProps<typeof Kobalte.Content>) {
return <Kobalte.Content data-slot="content" {...props} />
return <Kobalte.Content data-slot="collapsible-content" {...props} />
}
function CollapsibleArrow(props?: ComponentProps<"div">) {
return (
<div data-slot="collapsible-arrow" {...(props || {})}>
<Icon data-slot="collapsible-arrow-icon" name="chevron-grabber-vertical" size="small" />
</div>
)
}
export const Collapsible = Object.assign(CollapsibleRoot, {
Arrow: CollapsibleArrow,
Trigger: CollapsibleTrigger,
Content: CollapsibleContent,
})
+10
View File
@@ -139,6 +139,16 @@ const newIcons = {
folder: `<path d="M2.08301 2.91675V16.2501H17.9163V5.41675H9.99967L8.33301 2.91675H2.08301Z" stroke="currentColor" stroke-linecap="round"/>`,
"pencil-line": `<path d="M9.58301 17.9166H17.9163M17.9163 5.83325L14.1663 2.08325L2.08301 14.1666V17.9166H5.83301L17.9163 5.83325Z" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-grabber-vertical": `<path d="M6.66675 12.4998L10.0001 15.8332L13.3334 12.4998M6.66675 7.49984L10.0001 4.1665L13.3334 7.49984" stroke="currentColor" stroke-linecap="square"/>`,
mcp: `<g><path d="M0.972656 9.37176L9.5214 1.60019C10.7018 0.527151 12.6155 0.527151 13.7957 1.60019C14.9761 2.67321 14.9761 4.41295 13.7957 5.48599L7.3397 11.3552" stroke="currentColor" stroke-linecap="round"/><path d="M7.42871 11.2747L13.7957 5.48643C14.9761 4.41338 16.8898 4.41338 18.0702 5.48643L18.1147 5.52688C19.2951 6.59993 19.2951 8.33966 18.1147 9.4127L10.3831 16.4414C9.98966 16.7991 9.98966 17.379 10.3831 17.7366L11.9707 19.1799" stroke="currentColor" stroke-linecap="round"/><path d="M11.6587 3.54346L5.33619 9.29119C4.15584 10.3642 4.15584 12.1039 5.33619 13.177C6.51649 14.25 8.43019 14.25 9.61054 13.177L15.9331 7.42923" stroke="currentColor" stroke-linecap="round"/></g>`,
glasses: `<path d="M0.416626 7.91667H1.66663M19.5833 7.91667H18.3333M11.866 7.57987C11.3165 7.26398 10.6793 7.08333 9.99996 7.08333C9.32061 7.08333 8.68344 7.26398 8.13389 7.57987M8.74996 10C8.74996 12.0711 7.07103 13.75 4.99996 13.75C2.92889 13.75 1.24996 12.0711 1.24996 10C1.24996 7.92893 2.92889 6.25 4.99996 6.25C7.07103 6.25 8.74996 7.92893 8.74996 10ZM18.75 10C18.75 12.0711 17.071 13.75 15 13.75C12.9289 13.75 11.25 12.0711 11.25 10C11.25 7.92893 12.9289 6.25 15 6.25C17.071 6.25 18.75 7.92893 18.75 10Z" stroke="currentColor" stroke-linecap="square"/>`,
"bullet-list": `<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="currentColor" stroke-linecap="square"/>`,
"magnifying-glass-menu": `<path d="M2.08325 10.0002H4.58325M2.08325 5.41683H5.41659M2.08325 14.5835H5.41659M16.4583 13.9585L18.7499 16.2502M17.9166 10.0002C17.9166 12.9917 15.4915 15.4168 12.4999 15.4168C9.50838 15.4168 7.08325 12.9917 7.08325 10.0002C7.08325 7.00862 9.50838 4.5835 12.4999 4.5835C15.4915 4.5835 17.9166 7.00862 17.9166 10.0002Z" stroke="currentColor" stroke-linecap="square"/>`,
"window-cursor": `<path d="M17.9166 10.4167V3.75H2.08325V17.0833H10.4166M17.9166 13.5897L11.6666 11.6667L13.5897 17.9167L15.032 15.0321L17.9166 13.5897Z" stroke="currentColor" stroke-width="1.07143" stroke-linecap="square"/><path d="M5.00024 6.125C5.29925 6.12518 5.54126 6.36795 5.54126 6.66699C5.54108 6.96589 5.29914 7.20783 5.00024 7.20801C4.7012 7.20801 4.45843 6.966 4.45825 6.66699C4.45825 6.36784 4.70109 6.125 5.00024 6.125ZM7.91626 6.125C8.21541 6.125 8.45825 6.36784 8.45825 6.66699C8.45808 6.966 8.21531 7.20801 7.91626 7.20801C7.61736 7.20783 7.37542 6.96589 7.37524 6.66699C7.37524 6.36795 7.61726 6.12518 7.91626 6.125ZM10.8333 6.125C11.1324 6.125 11.3752 6.36784 11.3752 6.66699C11.3751 6.966 11.1323 7.20801 10.8333 7.20801C10.5342 7.20801 10.2914 6.966 10.2913 6.66699C10.2913 6.36784 10.5341 6.125 10.8333 6.125Z" fill="currentColor" stroke="currentColor" stroke-width="0.25" stroke-linecap="square"/>`,
task: `<path d="M9.99992 2.0835V17.9168M7.08325 3.75016H2.08325V16.2502H7.08325M12.9166 16.2502H17.9166V3.75016H12.9166" stroke="currentColor" stroke-linecap="square"/>`,
checklist: `<path d="M9.58342 13.7498H17.0834M9.58342 6.24984H17.0834M2.91675 6.6665L4.58341 7.9165L7.08341 4.1665M2.91675 14.1665L4.58341 15.4165L7.08341 11.6665" stroke="currentColor" stroke-linecap="square"/>`,
console: `<path d="M3.75 5.4165L8.33333 9.99984L3.75 14.5832M10.4167 14.5832H16.25" stroke="currentColor" stroke-linecap="square"/>`,
"code-lines": `<path d="M2.08325 3.75H11.2499M14.5833 3.75H17.9166M2.08325 10L7.08325 10M10.4166 10L17.9166 10M2.08325 16.25L8.74992 16.25M12.0833 16.25L17.9166 16.25" stroke="currentColor" stroke-linecap="square" stroke-linejoin="round"/>`,
"square-arrow-top-right": `<path d="M7.91675 2.9165H2.91675V17.0832H17.0834V12.0832M12.0834 2.9165H17.0834V7.9165M9.58342 10.4165L16.6667 3.33317" stroke="currentColor" stroke-linecap="square"/>`,
}
export interface IconProps extends ComponentProps<"svg"> {

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