Compare commits

..
Author SHA1 Message Date
Aiden Cline 106eeb8536 tweak
test / test (push) Has been cancelled
2025-12-29 10:51:47 -06:00
Aiden Cline 888361892d ci: update changelog gen 2025-12-29 10:48:59 -06:00
Aiden Cline a96f3010a6 Merge branch 'dev' into changelog-updates 2025-12-28 22:57:20 -06:00
Aiden Cline 619127c2ed ci: add changelog script 2025-12-28 22:57:12 -06:00
56 changed files with 322 additions and 1275 deletions
-1
View File
@@ -1,6 +1,5 @@
---
description: ALWAYS use this when writing docs
color: "#38A3EE"
---
You are an expert technical documentation writer
-1
View File
@@ -2,7 +2,6 @@
mode: primary
hidden: true
model: opencode/claude-haiku-4-5
color: "#44BA81"
tools:
"*": false
"github-triage": true
-1
View File
@@ -184,4 +184,3 @@
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
+35 -32
View File
@@ -22,7 +22,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -70,7 +70,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -98,7 +98,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -125,7 +125,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -149,7 +149,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -173,7 +173,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@solid-primitives/storage": "catalog:",
@@ -200,7 +200,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -229,7 +229,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -245,7 +245,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.208",
"version": "1.0.207",
"bin": {
"opencode": "./bin/opencode",
},
@@ -254,7 +254,7 @@
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/amazon-bedrock": "3.0.57",
"@ai-sdk/anthropic": "2.0.56",
"@ai-sdk/anthropic": "2.0.50",
"@ai-sdk/azure": "2.0.73",
"@ai-sdk/cerebras": "1.0.33",
"@ai-sdk/cohere": "2.0.21",
@@ -269,7 +269,7 @@
"@ai-sdk/openai-compatible": "1.0.27",
"@ai-sdk/perplexity": "2.0.22",
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.19",
"@ai-sdk/provider-utils": "3.0.18",
"@ai-sdk/togetherai": "1.0.30",
"@ai-sdk/xai": "2.0.42",
"@clack/prompts": "1.0.0-alpha.1",
@@ -347,7 +347,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -367,7 +367,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.208",
"version": "1.0.207",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -378,7 +378,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -391,7 +391,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -403,10 +403,8 @@
"@solidjs/meta": "catalog:",
"@typescript/native-preview": "catalog:",
"fuzzysort": "catalog:",
"katex": "0.16.27",
"luxon": "catalog:",
"marked": "catalog:",
"marked-katex-extension": "5.1.6",
"marked-shiki": "catalog:",
"remeda": "catalog:",
"shiki": "catalog:",
@@ -418,7 +416,6 @@
"@tailwindcss/vite": "catalog:",
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
"@types/katex": "0.16.7",
"@types/luxon": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:",
@@ -429,7 +426,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"zod": "catalog:",
},
@@ -440,7 +437,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -569,7 +566,7 @@
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
"@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.30", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9bxQbIXnWSN4bNismrza3NvIo+ui/Y3pj3UN6e9vCszCWFCN45RgISi4oDe10RqmzaJ/X8cfO/Tem+K8MT3wGQ=="],
@@ -1777,8 +1774,6 @@
"@types/jsonwebtoken": ["@types/jsonwebtoken@8.5.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg=="],
"@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
"@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
@@ -2791,8 +2786,6 @@
"jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="],
"katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
@@ -2883,8 +2876,6 @@
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
"marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="],
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -3907,24 +3898,38 @@
"@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
"@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
"@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
"@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
@@ -4279,8 +4284,6 @@
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@@ -4305,7 +4308,7 @@
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1766996594,
"narHash": "sha256-SosfgQSqVmOkqVgNYJnxW5FvoIQX4grOcpIKNrIwz4o=",
"lastModified": 1766840161,
"narHash": "sha256-Ss/LHpJJsng8vz1Pe33RSGIWUOcqM1fjrehjUkdrWio=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0744ef1b047f07d31d9962d757ffe38ec14a4d41",
"rev": "3edc4a30ed3903fdf6f90c837f961fa6b49582d1",
"type": "github"
},
"original": {
+1 -1
View File
@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-2i/QMBzp9MalOXur36mXaDDU8R9G/0dODCODEQOnaCU="
"nodeModules": "sha256-lloUZt5mLyNWAcbQrJB4wGUKvKu24WFEhOLfZD5/FMg="
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.0.208",
"version": "1.0.207",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -69,7 +69,7 @@ export function App() {
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id ?? "new"} keyed>
<Show when={p.params.id || true} keyed>
<TerminalProvider>
<PromptProvider>
<Session />
@@ -1,180 +0,0 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { TextField } from "@opencode-ai/ui/text-field"
import { Icon } from "@opencode-ai/ui/icon"
import { createMemo, createSignal, For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useGlobalSDK } from "@/context/global-sdk"
import { type LocalProject, getAvatarColors } from "@/context/layout"
import { Avatar } from "@opencode-ai/ui/avatar"
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
function getFilename(input: string) {
const parts = input.split("/")
return parts[parts.length - 1] || input
}
export function DialogEditProject(props: { project: LocalProject }) {
const dialog = useDialog()
const globalSDK = useGlobalSDK()
const folderName = createMemo(() => getFilename(props.project.worktree))
const defaultName = createMemo(() => props.project.name || folderName())
const [store, setStore] = createStore({
name: defaultName(),
color: props.project.icon?.color || "pink",
iconUrl: props.project.icon?.url || "",
saving: false,
})
const [dragOver, setDragOver] = createSignal(false)
function handleFileSelect(file: File) {
if (!file.type.startsWith("image/")) return
const reader = new FileReader()
reader.onload = (e) => setStore("iconUrl", e.target?.result as string)
reader.readAsDataURL(file)
}
function handleDrop(e: DragEvent) {
e.preventDefault()
setDragOver(false)
const file = e.dataTransfer?.files[0]
if (file) handleFileSelect(file)
}
function handleDragOver(e: DragEvent) {
e.preventDefault()
setDragOver(true)
}
function handleDragLeave() {
setDragOver(false)
}
function handleInputChange(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (file) handleFileSelect(file)
}
function clearIcon() {
setStore("iconUrl", "")
}
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
if (!props.project.id) return
setStore("saving", true)
const name = store.name.trim() === folderName() ? "" : store.name.trim()
await globalSDK.client.project.update({
projectID: props.project.id,
name,
icon: { color: store.color, url: store.iconUrl },
})
setStore("saving", false)
dialog.close()
}
return (
<Dialog title="Edit project">
<form onSubmit={handleSubmit} class="flex flex-col gap-6 px-2.5 pb-3">
<div class="flex flex-col gap-4">
<TextField
autofocus
type="text"
label="Name"
placeholder={folderName()}
value={store.name}
onChange={(v) => setStore("name", v)}
/>
<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">Icon</label>
<div class="flex gap-3 items-start">
<div class="relative">
<div
class="size-16 rounded-lg overflow-hidden border border-dashed transition-colors cursor-pointer"
classList={{
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
"border-border-base hover:border-border-strong": !dragOver(),
}}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={() => document.getElementById("icon-upload")?.click()}
>
<Show
when={store.iconUrl}
fallback={
<div class="size-full flex items-center justify-center">
<Avatar
fallback={store.name || defaultName()}
{...getAvatarColors(store.color)}
class="size-full"
/>
</div>
}
>
<img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
</Show>
</div>
<Show when={store.iconUrl}>
<button
type="button"
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-base border border-border-base flex items-center justify-center hover:bg-surface-raised-base-hover"
onClick={clearIcon}
>
<Icon name="close" class="size-3 text-icon-base" />
</button>
</Show>
</div>
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak">
<span>Click or drag an image</span>
<span>Recommended: 128x128px</span>
</div>
</div>
</div>
<Show when={!store.iconUrl}>
<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">Color</label>
<div class="flex gap-2">
<For each={AVATAR_COLOR_KEYS}>
{(color) => (
<button
type="button"
class="relative size-8 rounded-md transition-all"
classList={{
"ring-2 ring-offset-2 ring-offset-surface-base ring-text-interactive-base":
store.color === color,
}}
style={{ background: getAvatarColors(color).background }}
onClick={() => setStore("color", color)}
>
<Avatar fallback={store.name || defaultName()} {...getAvatarColors(color)} class="size-full" />
</button>
)}
</For>
</div>
</div>
</Show>
</div>
<div class="flex justify-end gap-2">
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
Cancel
</Button>
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
{store.saving ? "Saving..." : "Save"}
</Button>
</div>
</form>
</Dialog>
)
}
@@ -3,7 +3,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { useSync } from "@/context/sync"
import { useParams } from "@solidjs/router"
import { AssistantMessage } from "@opencode-ai/sdk/v2/client"
import { AssistantMessage } from "@opencode-ai/sdk/v2"
export function SessionContextUsage() {
const sync = useSync()
@@ -35,13 +35,19 @@ export function SessionContextUsage() {
{(ctx) => (
<Tooltip
value={
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
<span class="opacity-70 text-right">Tokens</span>
<span class="text-left">{ctx().tokens}</span>
<span class="opacity-70 text-right">Usage</span>
<span class="text-left">{ctx().percentage ?? 0}%</span>
<span class="opacity-70 text-right">Cost</span>
<span class="text-left">{cost()}</span>
<div class="flex flex-col gap-1">
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Tokens</span>
<span class="text-left flex-1">{ctx().tokens}</span>
</div>
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Usage</span>
<span class="text-left flex-1">{ctx().percentage ?? 0}%</span>
</div>
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Cost</span>
<span class="text-left flex-1">{cost()}</span>
</div>
</div>
}
placement="top"
+27 -47
View File
@@ -23,7 +23,7 @@ import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { useGlobalSDK } from "./global-sdk"
import { ErrorPage, type InitError } from "../pages/error"
import { batch, createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
import { createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/util/path"
@@ -71,19 +71,21 @@ function createGlobalSync() {
project: Project[]
provider: ProviderListResponse
provider_auth: ProviderAuthResponse
children: Record<string, State>
}>({
ready: false,
path: { state: "", config: "", worktree: "", directory: "", home: "" },
project: [],
provider: { all: [], connected: [], default: {} },
provider_auth: {},
children: {},
})
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
function child(directory: string) {
if (!directory) console.error("No directory provided")
if (!children[directory]) {
children[directory] = createStore<State>({
setGlobalStore("children", directory, {
project: "",
provider: { all: [], connected: [], default: {} },
config: {},
@@ -103,6 +105,7 @@ function createGlobalSync() {
message: {},
part: {},
})
children[directory] = createStore(globalStore.children[directory])
bootstrapInstance(directory)
}
return children[directory]
@@ -124,7 +127,7 @@ function createGlobalSync() {
const updated = new Date(s.time.updated).getTime()
return updated > fourHoursAgo
})
setStore("session", reconcile(sessions, { key: "id" }))
setStore("session", sessions)
})
.catch((err) => {
console.error("Failed to load sessions", err)
@@ -135,7 +138,7 @@ function createGlobalSync() {
async function bootstrapInstance(directory: string) {
if (!directory) return
const [store, setStore] = child(directory)
const [, setStore] = child(directory)
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
directory,
@@ -167,32 +170,12 @@ function createGlobalSync() {
vcs: () => sdk.vcs.get().then((x) => setStore("vcs", x.data)),
permission: () =>
sdk.permission.list().then((x) => {
const grouped: Record<string, Permission[]> = {}
const grouped: Record<string, typeof x.data> = {}
for (const perm of x.data ?? []) {
const existing = grouped[perm.sessionID]
if (existing) {
existing.push(perm)
continue
}
grouped[perm.sessionID] = [perm]
grouped[perm.sessionID] = grouped[perm.sessionID] ?? []
grouped[perm.sessionID]!.push(perm)
}
batch(() => {
for (const sessionID of Object.keys(store.permission)) {
if (grouped[sessionID]) continue
setStore("permission", sessionID, [])
}
for (const [sessionID, permissions] of Object.entries(grouped)) {
setStore(
"permission",
sessionID,
reconcile(
permissions.slice().sort((a, b) => a.id.localeCompare(b.id)),
{ key: "id" },
),
)
}
})
setStore("permission", grouped)
}),
}
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
@@ -263,7 +246,7 @@ function createGlobalSync() {
setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
break
case "todo.updated":
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos, { key: "id" }))
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos))
break
case "session.status": {
setStore("session_status", event.properties.sessionID, reconcile(event.properties.status))
@@ -345,26 +328,23 @@ function createGlobalSync() {
break
}
case "permission.updated": {
const sessionID = event.properties.sessionID
const permissions = store.permission[sessionID]
const permissions = store.permission[event.properties.sessionID]
if (!permissions) {
setStore("permission", sessionID, [event.properties])
break
setStore("permission", event.properties.sessionID, [event.properties])
} else {
const result = Binary.search(permissions, event.properties.id, (p) => p.id)
setStore(
"permission",
event.properties.sessionID,
produce((draft) => {
if (result.found) {
draft[result.index] = event.properties
return
}
draft.push(event.properties)
}),
)
}
const result = Binary.search(permissions, event.properties.id, (p) => p.id)
if (result.found) {
setStore("permission", sessionID, result.index, reconcile(event.properties))
break
}
setStore(
"permission",
sessionID,
produce((draft) => {
draft.splice(result.index, 0, event.properties)
}),
)
break
}
case "permission.replied": {
-1
View File
@@ -70,7 +70,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
{
...project,
...(metadata ?? {}),
icon: { url: metadata?.icon?.url, color: metadata?.icon?.color },
},
]
}
-130
View File
@@ -1,130 +0,0 @@
import { createEffect, createRoot, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import type { Permission } from "@opencode-ai/sdk/v2/client"
import { persisted } from "@/utils/persist"
type PermissionsBySession = {
[sessionID: string]: Permission[]
}
type PermissionRespondFn = (input: {
sessionID: string
permissionID: string
response: "once" | "always" | "reject"
}) => void
const AUTO_ACCEPT_TYPES = new Set(["edit", "write"])
function shouldAutoAccept(perm: Permission) {
return AUTO_ACCEPT_TYPES.has(perm.type)
}
export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
name: "Permission",
init: (props: { permissions: PermissionsBySession; onRespond: PermissionRespondFn }) => {
const [store, setStore, _, ready] = persisted(
"permission.v1",
createStore({
autoAcceptEdits: {} as Record<string, boolean>,
}),
)
const responded = new Set<string>()
const watches = new Map<string, () => void>()
function respond(perm: Permission) {
if (responded.has(perm.id)) return
responded.add(perm.id)
props.onRespond({
sessionID: perm.sessionID,
permissionID: perm.id,
response: "once",
})
}
function watch(sessionID: string) {
if (watches.has(sessionID)) return
const dispose = createRoot((dispose) => {
createEffect(() => {
if (!store.autoAcceptEdits[sessionID]) return
const permissions = props.permissions[sessionID] ?? []
permissions.length
for (const perm of permissions) {
if (!shouldAutoAccept(perm)) continue
respond(perm)
}
})
return dispose
})
watches.set(sessionID, dispose)
}
function unwatch(sessionID: string) {
const dispose = watches.get(sessionID)
if (!dispose) return
dispose()
watches.delete(sessionID)
}
createEffect(() => {
if (!ready()) return
for (const sessionID in store.autoAcceptEdits) {
if (!store.autoAcceptEdits[sessionID]) continue
watch(sessionID)
}
})
onCleanup(() => {
for (const dispose of watches.values()) dispose()
watches.clear()
})
function enable(sessionID: string) {
setStore("autoAcceptEdits", sessionID, true)
watch(sessionID)
const permissions = props.permissions[sessionID] ?? []
for (const perm of permissions) {
if (!shouldAutoAccept(perm)) continue
respond(perm)
}
}
function disable(sessionID: string) {
setStore("autoAcceptEdits", sessionID, false)
unwatch(sessionID)
}
return {
get permissions() {
return props.permissions
},
respond: props.onRespond,
isAutoAccepting(sessionID: string) {
return store.autoAcceptEdits[sessionID] ?? false
},
toggleAutoAccept(sessionID: string) {
if (store.autoAcceptEdits[sessionID]) {
disable(sessionID)
return
}
enable(sessionID)
},
enableAutoAccept(sessionID: string) {
if (store.autoAcceptEdits[sessionID]) return
enable(sessionID)
},
disableAutoAccept(sessionID: string) {
disable(sessionID)
},
}
},
})
+20 -44
View File
@@ -1,5 +1,5 @@
import { batch, createMemo } from "solid-js"
import { produce, reconcile } from "solid-js/store"
import { produce } from "solid-js/store"
import { createMemo } from "solid-js"
import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { createSimpleContext } from "@opencode-ai/ui/context"
@@ -56,7 +56,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const result = Binary.search(messages, input.messageID, (m) => m.id)
messages.splice(result.index, 0, message)
}
draft.part[input.messageID] = input.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
draft.part[input.messageID] = input.parts.slice()
}),
)
},
@@ -67,46 +67,22 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
retry(() => sdk.client.session.todo({ sessionID })),
retry(() => sdk.client.session.diff({ sessionID })),
])
batch(() => {
setStore(
"session",
produce((draft) => {
const match = Binary.search(draft, sessionID, (s) => s.id)
if (match.found) {
draft[match.index] = session.data!
return
}
draft.splice(match.index, 0, session.data!)
}),
)
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
setStore(
"message",
sessionID,
reconcile(
(messages.data ?? [])
.map((x) => x.info)
.slice()
.sort((a, b) => a.id.localeCompare(b.id)),
{ key: "id" },
),
)
for (const message of messages.data ?? []) {
setStore(
"part",
message.info.id,
reconcile(
message.parts.slice().sort((a, b) => a.id.localeCompare(b.id)),
{ key: "id" },
),
)
}
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
})
setStore(
produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id)
if (match.found) draft.session[match.index] = session.data!
if (!match.found) draft.session.splice(match.index, 0, session.data!)
draft.todo[sessionID] = todo.data ?? []
draft.message[sessionID] = messages
.data!.map((x) => x.info)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
for (const message of messages.data!) {
draft.part[message.info.id] = message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
}
draft.session_diff[sessionID] = diff.data ?? []
}),
)
},
fetch: async (count = 10) => {
setStore("limit", (x) => x + count)
@@ -115,7 +91,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
.slice(0, store.limit)
setStore("session", reconcile(sessions, { key: "id" }))
setStore("session", sessions)
})
},
more: createMemo(() => store.session.length >= store.limit),
+9 -12
View File
@@ -3,7 +3,6 @@ import { useParams } from "@solidjs/router"
import { SDKProvider, useSDK } from "@/context/sdk"
import { SyncProvider, useSync } from "@/context/sync"
import { LocalProvider } from "@/context/local"
import { PermissionProvider } from "@/context/permission"
import { base64Decode } from "@opencode-ai/util/encode"
import { DataProvider } from "@opencode-ai/ui/context"
import { iife } from "@opencode-ai/util/iife"
@@ -20,18 +19,16 @@ export default function Layout(props: ParentProps) {
{iife(() => {
const sync = useSync()
const sdk = useSDK()
const respond = (input: {
sessionID: string
permissionID: string
response: "once" | "always" | "reject"
}) => sdk.client.permission.respond(input)
return (
<PermissionProvider permissions={sync.data.permission} onRespond={respond}>
<DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
</PermissionProvider>
<DataProvider
data={sync.data}
directory={directory()}
onPermissionRespond={(input) => {
sdk.client.permission.respond(input)
}}
>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
)
})}
</SyncProvider>
+3 -9
View File
@@ -49,7 +49,6 @@ import { Header } from "@/components/header"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { DialogEditProject } from "@/components/dialog-edit-project"
import { useCommand, type CommandOption } from "@/context/command"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
@@ -523,7 +522,7 @@ export default function Layout(props: ParentProps) {
const notification = useNotification()
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
@@ -559,7 +558,7 @@ export default function Layout(props: ParentProps) {
}
const ProjectVisual = (props: { project: LocalProject; class?: string }): JSX.Element => {
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
const current = createMemo(() => base64Decode(params.dir ?? ""))
return (
<Switch>
@@ -702,7 +701,7 @@ export default function Layout(props: ParentProps) {
const sortable = createSortable(props.project.worktree)
const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
const [store, setProjectStore] = globalSync.child(props.project.worktree)
const sessions = createMemo(() => store.session.toSorted(sortSessions))
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
@@ -748,11 +747,6 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item
onSelect={() => dialog.show(() => <DialogEditProject project={props.project} />)}
>
<DropdownMenu.ItemLabel>Edit project</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => closeProject(props.project.worktree)}>
<DropdownMenu.ItemLabel>Close project</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
+19 -84
View File
@@ -60,14 +60,6 @@ import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { StatusBar } from "@/components/status-bar"
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
import { usePermission } from "@/context/permission"
import { showToast } from "@opencode-ai/ui/toast"
function same<T>(a: readonly T[], b: readonly T[]) {
if (a === b) return true
if (a.length !== b.length) return false
return a.every((x, i) => x === b[i])
}
export default function Page() {
const layout = useLayout()
@@ -82,40 +74,22 @@ export default function Page() {
const sdk = useSDK()
const prompt = usePrompt()
const permission = usePermission()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey()))
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const revertMessageID = createMemo(() => info()?.revert?.messageID)
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const emptyUserMessages: UserMessage[] = []
const userMessages = createMemo(
() => messages().filter((m) => m.role === "user") as UserMessage[],
emptyUserMessages,
{ equals: same },
)
const visibleUserMessages = createMemo(
() => {
const revert = revertMessageID()
if (!revert) return userMessages()
return userMessages().filter((m) => m.id < revert)
},
emptyUserMessages,
{ equals: same },
)
const lastUserMessage = createMemo(() => visibleUserMessages().at(-1))
createEffect(
on(
() => lastUserMessage()?.id,
() => {
const msg = lastUserMessage()
if (!msg) return
if (msg.agent) local.agent.set(msg.agent)
if (msg.model) local.model.set(msg.model)
},
),
const userMessages = createMemo(() =>
messages()
.filter((m) => m.role === "user")
.sort((a, b) => a.id.localeCompare(b.id)),
)
const visibleUserMessages = createMemo(() => {
const revert = revertMessageID()
if (!revert) return userMessages()
return userMessages().filter((m) => m.id < revert)
})
const lastUserMessage = createMemo(() => visibleUserMessages()?.at(-1))
const [store, setStore] = createStore({
clickTimer: undefined as number | undefined,
@@ -185,37 +159,16 @@ export default function Page() {
),
)
const idle = { type: "idle" as const }
createEffect(
on(
() => params.id,
(id) => {
const status = sync.data.session_status[id ?? ""] ?? idle
batch(() => {
setStore("userInteracted", false)
setStore("stepsExpanded", status.type !== "idle")
})
},
),
)
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
createEffect(
on(
() => status().type,
(type) => {
if (type !== "idle") return
batch(() => {
setStore("userInteracted", false)
setStore("stepsExpanded", false)
})
},
{ defer: true },
),
)
createEffect(() => {
params.id
const status = sync.data.session_status[params.id ?? ""] ?? { type: "idle" }
batch(() => {
setStore("userInteracted", false)
setStore("stepsExpanded", status.type !== "idle")
})
})
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? { type: "idle" })
const working = createMemo(() => status().type !== "idle" && activeMessage()?.id === lastUserMessage()?.id)
createRenderEffect((prev) => {
@@ -351,22 +304,6 @@ export default function Page() {
keybind: "shift+mod+.",
onSelect: () => local.agent.move(-1),
},
{
id: "permissions.autoaccept",
title: params.id && permission.isAutoAccepting(params.id) ? "Stop auto-accepting edits" : "Auto-accept edits",
category: "Permissions",
disabled: !params.id,
onSelect: () => {
if (!params.id) return
permission.toggleAutoAccept(params.id)
showToast({
title: permission.isAutoAccepting(params.id) ? "Auto-accepting edits" : "Stopped auto-accepting edits",
description: permission.isAutoAccepting(params.id)
? "Edit and write permissions will be automatically approved"
: "Edit and write permissions will require approval",
})
},
},
{
id: "session.undo",
title: "Undo",
@@ -638,7 +575,6 @@ export default function Page() {
<SessionTurn
sessionID={params.id!}
messageID={message.id}
lastUserMessageID={lastUserMessage()?.id}
stepsExpanded={store.mobileStepsExpanded[message.id] ?? false}
onStepsExpandedToggle={() => setStore("mobileStepsExpanded", message.id, (x) => !x)}
onUserInteracted={() => setStore("userInteracted", true)}
@@ -695,7 +631,6 @@ export default function Page() {
<SessionTurn
sessionID={params.id!}
messageID={activeMessage()!.id}
lastUserMessageID={lastUserMessage()?.id}
stepsExpanded={store.stepsExpanded}
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
onUserInteracted={() => setStore("userInteracted", true)}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.208",
"version": "1.0.207",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -124,8 +124,6 @@ export async function handler(
res.status !== 200 &&
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
res.status !== 404 &&
// ie. cannot change codex model providers mid-session
!modelInfo.stickyProvider &&
modelInfo.fallbackProvider &&
providerInfo.id !== modelInfo.fallbackProvider
) {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.208",
"version": "1.0.207",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.208",
"version": "1.0.207",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.208",
"version": "1.0.207",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.0.208",
"version": "1.0.207",
"type": "module",
"scripts": {
"typecheck": "tsgo -b",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.208",
"version": "1.0.207",
"private": true,
"type": "module",
"scripts": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.208"
version = "1.0.207"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-linux-arm64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.208",
"version": "1.0.207",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.208",
"version": "1.0.207",
"name": "opencode",
"type": "module",
"private": true,
@@ -50,7 +50,7 @@
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/amazon-bedrock": "3.0.57",
"@ai-sdk/anthropic": "2.0.56",
"@ai-sdk/anthropic": "2.0.50",
"@ai-sdk/azure": "2.0.73",
"@ai-sdk/cerebras": "1.0.33",
"@ai-sdk/cohere": "2.0.21",
@@ -65,7 +65,7 @@
"@ai-sdk/openai-compatible": "1.0.27",
"@ai-sdk/perplexity": "2.0.22",
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.19",
"@ai-sdk/provider-utils": "3.0.18",
"@ai-sdk/togetherai": "1.0.30",
"@ai-sdk/xai": "2.0.42",
"@clack/prompts": "1.0.0-alpha.1",
@@ -7,5 +7,4 @@ Rules:
- Do not explain what the user asked for
- Write in first person (I added..., I fixed...)
- Never ask questions or add new questions
- If the conversation ends with an unanswered question to the user, preserve that exact question
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary
- Only exception: if the conversation ends with an unanswered question to the user, preserve that exact question
@@ -5,7 +5,6 @@ import { DialogSelect } from "@tui/ui/dialog-select"
import { useDialog } from "@tui/ui/dialog"
import { useSDK } from "../context/sdk"
import { DialogPrompt } from "../ui/dialog-prompt"
import { Link } from "../ui/link"
import { useTheme } from "../context/theme"
import { TextAttributes } from "@opentui/core"
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2"
@@ -129,7 +128,7 @@ function AutoMethod(props: AutoMethodProps) {
<text fg={theme.textMuted}>esc</text>
</box>
<box gap={1}>
<Link href={props.authorization.url} fg={theme.primary} />
<text fg={theme.primary}>{props.authorization.url}</text>
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
</box>
<text fg={theme.textMuted}>Waiting for authorization...</text>
@@ -171,7 +170,7 @@ function CodeMethod(props: CodeMethodProps) {
description={() => (
<box gap={1}>
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
<Link href={props.authorization.url} fg={theme.primary} />
<text fg={theme.primary}>{props.authorization.url}</text>
<Show when={error()}>
<text fg={theme.error}>Invalid code</text>
</Show>
@@ -1,28 +0,0 @@
import type { JSX } from "solid-js"
import type { RGBA } from "@opentui/core"
import open from "open"
export interface LinkProps {
href: string
children?: JSX.Element | string
fg?: RGBA
}
/**
* Link component that renders clickable hyperlinks.
* Clicking anywhere on the link text opens the URL in the default browser.
*/
export function Link(props: LinkProps) {
const displayText = props.children ?? props.href
return (
<text
fg={props.fg}
onMouseUp={() => {
open(props.href).catch(() => {})
}}
>
{displayText}
</text>
)
}
-4
View File
@@ -191,10 +191,6 @@ export namespace Config {
cwd: dir,
},
).catch(() => {})
// Install any additional dependencies defined in the package.json
// This allows local plugins and custom tools to use external packages
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
}
const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
+3 -4
View File
@@ -196,14 +196,13 @@ export namespace LSPServer {
}
await fs.rename(extractedPath, finalPath)
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
await $`${npmCmd} install`.cwd(finalPath).quiet()
await $`${npmCmd} run compile`.cwd(finalPath).quiet()
await $`npm install`.cwd(finalPath).quiet()
await $`npm run compile`.cwd(finalPath).quiet()
log.info("installed VS Code ESLint server", { serverPath })
}
const proc = spawn(BunProc.which(), [serverPath, "--stdio"], {
const proc = spawn(BunProc.which(), ["--max-old-space-size=8192", serverPath, "--stdio"], {
cwd: root,
env: {
...process.env,
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.208",
"version": "1.0.207",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -24,4 +24,4 @@
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.208",
"version": "1.0.207",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -29,4 +29,4 @@
"publishConfig": {
"directory": "dist"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.208",
"version": "1.0.207",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+5 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.208",
"version": "1.0.207",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",
@@ -25,16 +25,15 @@
"generate:tailwind": "bun run script/tailwind.ts"
},
"devDependencies": {
"@tailwindcss/vite": "catalog:",
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
"@types/katex": "0.16.7",
"@types/luxon": "catalog:",
"tailwindcss": "catalog:",
"@tsconfig/node22": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-plugin-icons-spritesheet": "3.0.1",
"vite-plugin-solid": "catalog:"
"vite-plugin-solid": "catalog:",
"tailwindcss": "catalog:",
"@tailwindcss/vite": "catalog:"
},
"dependencies": {
"@kobalte/core": "catalog:",
@@ -47,10 +46,8 @@
"@solidjs/meta": "catalog:",
"@typescript/native-preview": "catalog:",
"fuzzysort": "catalog:",
"katex": "0.16.27",
"luxon": "catalog:",
"marked": "catalog:",
"marked-katex-extension": "5.1.6",
"marked-shiki": "catalog:",
"remeda": "catalog:",
"shiki": "catalog:",
@@ -1,63 +0,0 @@
[data-component="image-preview"] {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
[data-slot="image-preview-container"] {
position: relative;
z-index: 50;
width: min(calc(100vw - 32px), 90vw);
max-width: 1200px;
height: min(calc(100vh - 32px), 90vh);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
[data-slot="image-preview-content"] {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-height: 100%;
border-radius: var(--radius-lg);
background: var(--surface-raised-stronger-non-alpha);
box-shadow:
0 15px 45px 0 rgba(19, 16, 16, 0.35),
0 3.35px 10.051px 0 rgba(19, 16, 16, 0.25),
0 0.998px 2.993px 0 rgba(19, 16, 16, 0.2);
overflow: hidden;
&:focus-visible {
outline: none;
}
[data-slot="image-preview-header"] {
display: flex;
padding: 8px 8px 0;
justify-content: flex-end;
align-items: center;
align-self: stretch;
}
[data-slot="image-preview-body"] {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
overflow: auto;
}
[data-slot="image-preview-image"] {
max-width: 100%;
max-height: calc(90vh - 100px);
object-fit: contain;
border-radius: var(--radius-md);
}
}
}
}
@@ -1,24 +0,0 @@
import { Dialog as Kobalte } from "@kobalte/core/dialog"
import { IconButton } from "./icon-button"
export interface ImagePreviewProps {
src: string
alt?: string
}
export function ImagePreview(props: ImagePreviewProps) {
return (
<div data-component="image-preview">
<div data-slot="image-preview-container">
<Kobalte.Content data-slot="image-preview-content">
<div data-slot="image-preview-header">
<Kobalte.CloseButton data-slot="image-preview-close" as={IconButton} icon="close" variant="ghost" />
</div>
<div data-slot="image-preview-body">
<img src={props.src} alt={props.alt ?? "Image preview"} data-slot="image-preview-image" />
</div>
</Kobalte.Content>
</div>
</div>
)
}
+26 -29
View File
@@ -1,5 +1,5 @@
import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
import { createEffect, createSignal, For, onCleanup, type JSX, on, Show } from "solid-js"
import { createEffect, createSignal, For, type JSX, on, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { Icon, type IconProps } from "./icon"
import { IconButton } from "./icon-button"
@@ -116,33 +116,6 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
setScrollRef,
})
function GroupHeader(props: { category: string }): JSX.Element {
const [stuck, setStuck] = createSignal(false)
const [header, setHeader] = createSignal<HTMLDivElement | undefined>(undefined)
createEffect(() => {
const scroll = scrollRef()
const node = header()
if (!scroll || !node) return
const handler = () => {
const rect = node.getBoundingClientRect()
const scrollRect = scroll.getBoundingClientRect()
setStuck(rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
}
scroll.addEventListener("scroll", handler, { passive: true })
handler()
onCleanup(() => scroll.removeEventListener("scroll", handler))
})
return (
<div data-slot="list-header" data-stuck={stuck()} ref={setHeader}>
{props.category}
</div>
)
}
return (
<div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}>
<Show when={!!props.search}>
@@ -184,7 +157,31 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
{(group) => (
<div data-slot="list-group">
<Show when={group.category}>
<GroupHeader category={group.category} />
{(() => {
const [stuck, setStuck] = createSignal(false)
return (
<div
data-slot="list-header"
data-stuck={stuck()}
ref={(el) => {
createEffect(() => {
const scroll = scrollRef()
if (!scroll) return
const handler = () => {
const rect = el.getBoundingClientRect()
const scrollRect = scroll.getBoundingClientRect()
setStuck(rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
}
scroll.addEventListener("scroll", handler, { passive: true })
handler()
return () => scroll.removeEventListener("scroll", handler)
})
}}
>
{group.category}
</div>
)
})()}
</Show>
<div data-slot="list-items">
<For each={group.items}>
+8 -18
View File
@@ -6,7 +6,7 @@
color: var(--text-base);
font-family: var(--font-family-sans);
font-size: var(--font-size-base); /* 14px */
line-height: var(--line-height-x-large);
line-height: var(--line-height-large);
/* Spacing for flow */
> *:first-child {
@@ -64,16 +64,8 @@
list-style-position: inside;
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
li {
margin-bottom: 0.5rem;
margin-bottom: 0.25rem;
}
li::marker {
@@ -125,15 +117,13 @@
:not(pre) > code {
font-family: var(--font-family-mono);
font-feature-settings: var(--font-family-mono--font-feature-settings);
color: var(--syntax-string);
font-weight: var(--font-weight-medium);
/* font-size: 13px; */
font-size: 13px;
/* padding: 2px 2px; */
/* margin: 0 1.5px; */
/* border-radius: 2px; */
/* background: var(--surface-base); */
/* box-shadow: 0 0 0 0.5px var(--border-weak-base); */
padding: 2px 2px;
margin: 0 1.5px;
border-radius: 2px;
background: var(--surface-base);
box-shadow: 0 0 0 0.5px var(--border-weak-base);
}
/* Tables */
+2 -15
View File
@@ -40,10 +40,6 @@
border-color: var(--border-strong-base);
}
&[data-clickable="true"] {
cursor: pointer;
}
&[data-type="image"] {
width: 48px;
height: 48px;
@@ -249,14 +245,6 @@
[data-component="edit-content"] {
border-top: 1px solid var(--border-weaker-base);
max-height: 420px;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
[data-component="write-content"] {
@@ -382,9 +370,9 @@
&[data-permission="true"] {
position: sticky;
top: calc(2px + var(--sticky-header-height, 40px));
top: var(--sticky-header-height, 80px);
bottom: 0px;
z-index: 20;
z-index: 10;
border-radius: 6px;
border: none;
box-shadow: var(--shadow-xs-border-base);
@@ -395,7 +383,6 @@
content: "";
position: absolute;
inset: -1.5px;
top: -5px;
border-radius: 7.5px;
border: 1.5px solid transparent;
background:
+30 -118
View File
@@ -1,15 +1,4 @@
import {
Component,
createEffect,
createMemo,
createSignal,
For,
Match,
Show,
Switch,
onCleanup,
type JSX,
} from "solid-js"
import { Component, createEffect, createMemo, createSignal, For, Match, Show, Switch, type JSX } from "solid-js"
import { Dynamic } from "solid-js/web"
import {
AssistantMessage,
@@ -25,7 +14,6 @@ import {
import { useData } from "../context"
import { useDiffComponent } from "../context/diff"
import { useCodeComponent } from "../context/code"
import { useDialog } from "../context/dialog"
import { BasicTool } from "./basic-tool"
import { GenericTool } from "./basic-tool"
import { Button } from "./button"
@@ -34,7 +22,6 @@ import { Icon } from "./icon"
import { Checkbox } from "./checkbox"
import { DiffChanges } from "./diff-changes"
import { Markdown } from "./markdown"
import { ImagePreview } from "./image-preview"
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
import { checksum } from "@opencode-ai/util/encode"
import { createAutoScroll } from "../hooks"
@@ -93,47 +80,6 @@ export type PartComponent = Component<MessagePartProps>
export const PART_MAPPING: Record<string, PartComponent | undefined> = {}
const TEXT_RENDER_THROTTLE_MS = 100
function same<T>(a: readonly T[], b: readonly T[]) {
if (a === b) return true
if (a.length !== b.length) return false
return a.every((x, i) => x === b[i])
}
function createThrottledValue(getValue: () => string) {
const [value, setValue] = createSignal(getValue())
let timeout: ReturnType<typeof setTimeout> | undefined
let last = 0
createEffect(() => {
const next = getValue()
const now = Date.now()
const remaining = TEXT_RENDER_THROTTLE_MS - (now - last)
if (remaining <= 0) {
if (timeout) {
clearTimeout(timeout)
timeout = undefined
}
last = now
setValue(next)
return
}
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
last = Date.now()
setValue(next)
timeout = undefined
}, remaining)
})
onCleanup(() => {
if (timeout) clearTimeout(timeout)
})
return value
}
function relativizeProjectPaths(text: string, directory?: string) {
if (!text) return ""
if (!directory) return text
@@ -263,21 +209,15 @@ export function Message(props: MessageProps) {
}
export function AssistantMessageDisplay(props: { message: AssistantMessage; parts: PartType[] }) {
const emptyParts: PartType[] = []
const filteredParts = createMemo(
() =>
props.parts.filter((x) => {
return x.type !== "tool" || (x as ToolPart).tool !== "todoread"
}),
emptyParts,
{ equals: same },
)
const filteredParts = createMemo(() => {
return props.parts?.filter((x) => {
return x.type !== "tool" || (x as ToolPart).tool !== "todoread"
})
})
return <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
}
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
const dialog = useDialog()
const textPart = createMemo(
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
)
@@ -300,26 +240,13 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
}),
)
const openImagePreview = (url: string, alt?: string) => {
dialog.show(() => <ImagePreview src={url} alt={alt} />)
}
return (
<div data-component="user-message">
<Show when={attachments().length > 0}>
<div data-slot="user-message-attachments">
<For each={attachments()}>
{(file) => (
<div
data-slot="user-message-attachment"
data-type={file.mime.startsWith("image/") ? "image" : "file"}
data-clickable={file.mime.startsWith("image/") && !!file.url}
onClick={() => {
if (file.mime.startsWith("image/") && file.url) {
openImagePreview(file.url, file.filename)
}
}}
>
<div data-slot="user-message-attachment" data-type={file.mime.startsWith("image/") ? "image" : "file"}>
<Show
when={file.mime.startsWith("image/") && file.url}
fallback={
@@ -435,22 +362,9 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
const part = props.part as ToolPart
const permission = createMemo(() => {
const next = data.store.permission?.[props.message.sessionID]?.[0]
if (!next) return undefined
if (next.callID !== part.callID) return undefined
return next
})
const [showPermission, setShowPermission] = createSignal(false)
createEffect(() => {
const perm = permission()
if (perm) {
const timeout = setTimeout(() => setShowPermission(true), 50)
onCleanup(() => clearTimeout(timeout))
} else {
setShowPermission(false)
}
const sessionID = props.message.sessionID
const permissions = data.store.permission?.[sessionID] ?? []
return permissions.find((p) => p.callID === part.callID)
})
const [forceOpen, setForceOpen] = createSignal(false)
@@ -468,17 +382,13 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
})
}
const emptyInput: Record<string, any> = {}
const emptyMetadata: Record<string, any> = {}
const component = createMemo(() => {
const render = ToolRegistry.render(part.tool) ?? GenericTool
// @ts-expect-error
const metadata = part.state?.metadata ?? {}
const input = part.state?.input ?? {}
const input = () => part.state?.input ?? emptyInput
// @ts-expect-error
const metadata = () => part.state?.metadata ?? emptyMetadata
const render = ToolRegistry.render(part.tool) ?? GenericTool
return (
<div data-component="tool-part-wrapper" data-permission={showPermission()}>
return (
<Switch>
<Match when={part.state.status === "error" && part.state.error}>
{(error) => {
@@ -507,9 +417,9 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
<Match when={true}>
<Dynamic
component={render}
input={input()}
input={input}
tool={part.tool}
metadata={metadata()}
metadata={metadata}
// @ts-expect-error
output={part.state.output}
status={part.state.status}
@@ -519,7 +429,13 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
/>
</Match>
</Switch>
<Show when={showPermission() && permission()}>
)
})
return (
<div data-component="tool-part-wrapper" data-permission={!!permission()}>
<Show when={component()}>{component()}</Show>
<Show when={permission()}>
{(perm) => (
<div data-component="permission-prompt">
<div data-slot="permission-message">{perm().title}</div>
@@ -545,12 +461,11 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
const data = useData()
const part = props.part as TextPart
const displayText = () => relativizeProjectPaths((part.text ?? "").trim(), data.directory)
const throttledText = createThrottledValue(displayText)
return (
<Show when={throttledText()}>
<Show when={displayText()}>
<div data-component="text-part">
<Markdown text={throttledText()} />
<Markdown text={displayText()} />
</div>
</Show>
)
@@ -558,13 +473,10 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
const part = props.part as ReasoningPart
const text = () => part.text.trim()
const throttledText = createThrottledValue(text)
return (
<Show when={throttledText()}>
<Show when={part.text.trim()}>
<div data-component="reasoning-part">
<Markdown text={throttledText()} />
<Markdown text={part.text.trim()} />
</div>
</Show>
)
@@ -711,7 +623,7 @@ ToolRegistry.register({
const sessionId = childSessionId()
if (!sessionId) return undefined
const permissions = data.store.permission?.[sessionId] ?? []
return permissions[0]
return permissions.toSorted((a, b) => a.id.localeCompare(b.id))[0]
})
const childToolPart = createMemo(() => {
+66 -141
View File
@@ -1,17 +1,9 @@
import {
AssistantMessage,
Message as MessageType,
Part as PartType,
type Permission,
TextPart,
ToolPart,
} from "@opencode-ai/sdk/v2/client"
import { AssistantMessage, Part as PartType, TextPart, ToolPart } from "@opencode-ai/sdk/v2/client"
import { useData } from "../context"
import { useDiffComponent } from "../context/diff"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { checksum } from "@opencode-ai/util/encode"
import { Binary } from "@opencode-ai/util/binary"
import { createEffect, createMemo, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { DiffChanges } from "./diff-changes"
import { Typewriter } from "./typewriter"
@@ -68,38 +60,26 @@ function computeStatusFromPart(part: PartType | undefined): string | undefined {
return undefined
}
function same<T>(a: readonly T[], b: readonly T[]) {
if (a === b) return true
if (a.length !== b.length) return false
return a.every((x, i) => x === b[i])
}
function AssistantMessageItem(props: {
message: AssistantMessage
responsePartId: string | undefined
hideResponsePart: boolean
summary: string | undefined
response: string | undefined
lastTextPartId: string | undefined
working: boolean
}) {
const data = useData()
const emptyParts: PartType[] = []
const msgParts = createMemo(() => data.store.part[props.message.id] ?? emptyParts)
const lastTextPart = createMemo(() => {
const parts = msgParts()
for (let i = parts.length - 1; i >= 0; i--) {
const part = parts[i]
if (part?.type === "text") return part as TextPart
}
return undefined
})
const msgParts = createMemo(() => data.store.part[props.message.id] ?? [])
const lastTextPart = createMemo(() =>
msgParts()
.filter((p) => p?.type === "text")
.at(-1),
)
const filteredParts = createMemo(() => {
const parts = msgParts()
if (!props.hideResponsePart) return parts
const responsePartId = props.responsePartId
if (!responsePartId) return parts
if (responsePartId !== lastTextPart()?.id) return parts
return parts.filter((part) => part?.id !== responsePartId)
if (!props.working && !props.summary && props.response && props.lastTextPartId === lastTextPart()?.id) {
return msgParts().filter((p) => p?.id !== lastTextPart()?.id)
}
return msgParts()
})
return <Message message={props.message} parts={filteredParts()} />
@@ -109,7 +89,6 @@ export function SessionTurn(
props: ParentProps<{
sessionID: string
messageID: string
lastUserMessageID?: string
stepsExpanded?: boolean
onStepsExpandedToggle?: () => void
onUserInteracted?: () => void
@@ -123,76 +102,27 @@ export function SessionTurn(
const data = useData()
const diffComponent = useDiffComponent()
const emptyMessages: MessageType[] = []
const emptyParts: PartType[] = []
const emptyAssistant: AssistantMessage[] = []
const emptyPermissions: Permission[] = []
const emptyPermissionParts: { part: ToolPart; message: AssistantMessage }[] = []
const idle = { type: "idle" as const }
const allMessages = createMemo(() => data.store.message[props.sessionID] ?? [])
const userMessages = createMemo(() =>
allMessages()
.filter((m) => m.role === "user")
.sort((a, b) => a.id.localeCompare(b.id)),
)
const allMessages = createMemo(() => data.store.message[props.sessionID] ?? emptyMessages)
const messageIndex = createMemo(() => {
const messages = allMessages()
const result = Binary.search(messages, props.messageID, (m) => m.id)
if (!result.found) return -1
const msg = messages[result.index]
if (msg.role !== "user") return -1
return result.index
})
const message = createMemo(() => {
const index = messageIndex()
if (index < 0) return undefined
const msg = allMessages()[index]
if (!msg || msg.role !== "user") return undefined
return msg
})
const lastUserMessageID = createMemo(() => {
if (props.lastUserMessageID) return props.lastUserMessageID
const messages = allMessages()
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i]
if (msg?.role === "user") return msg.id
}
return undefined
})
const isLastUserMessage = createMemo(() => props.messageID === lastUserMessageID())
const message = createMemo(() => userMessages().find((m) => m.id === props.messageID))
const isLastUserMessage = createMemo(() => message()?.id === userMessages().at(-1)?.id)
const parts = createMemo(() => {
const msg = message()
if (!msg) return emptyParts
return data.store.part[msg.id] ?? emptyParts
if (!msg) return []
return data.store.part[msg.id] ?? []
})
const assistantMessages = createMemo(
() => {
const msg = message()
if (!msg) return emptyAssistant
const messages = allMessages()
const index = messageIndex()
if (index < 0) return emptyAssistant
const result: AssistantMessage[] = []
for (let i = index + 1; i < messages.length; i++) {
const item = messages[i]
if (!item) continue
if (item.role === "user") break
if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage)
}
return result
},
emptyAssistant,
{ equals: same },
)
const assistantMessages = createMemo(() => {
const msg = message()
if (!msg) return [] as AssistantMessage[]
return allMessages().filter((m) => m.role === "assistant" && m.parentID === msg.id) as AssistantMessage[]
})
const lastAssistantMessage = createMemo(() => assistantMessages().at(-1))
@@ -201,7 +131,7 @@ export function SessionTurn(
const lastTextPart = createMemo(() => {
const msgs = assistantMessages()
for (let mi = msgs.length - 1; mi >= 0; mi--) {
const msgParts = data.store.part[msgs[mi].id] ?? emptyParts
const msgParts = data.store.part[msgs[mi].id] ?? []
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
const part = msgParts[pi]
if (part?.type === "text") return part as TextPart
@@ -221,25 +151,20 @@ export function SessionTurn(
return false
})
const permissions = createMemo(() => data.store.permission?.[props.sessionID] ?? emptyPermissions)
const permissionCount = createMemo(() => permissions().length)
const nextPermission = createMemo(() => permissions()[0])
const permissionParts = createMemo(() => {
if (props.stepsExpanded) return emptyPermissionParts
const result: { part: ToolPart; message: AssistantMessage }[] = []
const permissions = data.store.permission?.[props.sessionID] ?? []
if (!permissions.length) return result
const next = nextPermission()
if (!next) return emptyPermissionParts
for (const message of assistantMessages()) {
const parts = data.store.part[message.id] ?? emptyParts
for (const part of parts) {
if (part?.type !== "tool") continue
const tool = part as ToolPart
if (tool.callID === next.callID) return [{ part: tool, message }]
for (const m of assistantMessages()) {
const msgParts = data.store.part[m.id] ?? []
for (const p of msgParts) {
if (p?.type === "tool" && permissions.some((perm) => perm.callID === (p as ToolPart).callID)) {
result.push({ part: p as ToolPart, message: m })
}
}
}
return emptyPermissionParts
return result
})
const shellModePart = createMemo(() => {
@@ -249,7 +174,7 @@ export function SessionTurn(
const msgs = assistantMessages()
if (msgs.length !== 1) return
const msgParts = data.store.part[msgs[0].id] ?? emptyParts
const msgParts = data.store.part[msgs[0].id] ?? []
if (msgParts.length !== 1) return
const assistantPart = msgParts[0]
@@ -264,7 +189,7 @@ export function SessionTurn(
let currentTask: ToolPart | undefined
for (let mi = msgs.length - 1; mi >= 0; mi--) {
const msgParts = data.store.part[msgs[mi].id] ?? emptyParts
const msgParts = data.store.part[msgs[mi].id] ?? []
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
const part = msgParts[pi]
if (!part) continue
@@ -291,12 +216,12 @@ export function SessionTurn(
: undefined
if (taskSessionId) {
const taskMessages = data.store.message[taskSessionId] ?? emptyMessages
const taskMessages = data.store.message[taskSessionId] ?? []
for (let mi = taskMessages.length - 1; mi >= 0; mi--) {
const msg = taskMessages[mi]
if (!msg || msg.role !== "assistant") continue
const msgParts = data.store.part[msg.id] ?? emptyParts
const msgParts = data.store.part[msg.id] ?? []
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
const part = msgParts[pi]
if (part) return computeStatusFromPart(part)
@@ -307,7 +232,12 @@ export function SessionTurn(
return computeStatusFromPart(last)
})
const status = createMemo(() => data.store.session_status[props.sessionID] ?? idle)
const status = createMemo(
() =>
data.store.session_status[props.sessionID] ?? {
type: "idle",
},
)
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
const retry = createMemo(() => {
const s = status()
@@ -315,11 +245,12 @@ export function SessionTurn(
return s
})
const summary = createMemo(() => message()?.summary?.body)
const response = createMemo(() => lastTextPart()?.text)
const responsePartId = createMemo(() => lastTextPart()?.id)
const hasDiffs = createMemo(() => message()?.summary?.diffs?.length)
const hideResponsePart = createMemo(() => !working() && !summary() && !!responsePartId())
const summary = () => message()?.summary?.body
const response = () => {
const part = lastTextPart()
return part?.type === "text" ? (part as TextPart).text : undefined
}
const hasDiffs = () => message()?.summary?.diffs?.length
function duration() {
const msg = message()
@@ -397,13 +328,11 @@ export function SessionTurn(
}
})
createEffect(
on(permissionCount, (count, prev) => {
if (!count) return
if (prev !== undefined && count <= prev) return
createEffect(() => {
if (permissionParts().length > 0) {
autoScroll.forceScrollToBottom()
}),
)
}
})
createEffect(() => {
if (working() || !isLastUserMessage()) return
@@ -413,12 +342,6 @@ export function SessionTurn(
if (summary()) return
if (store.summaryWaitTimedOut) return
const completed = lastAssistantMessage()?.time.completed
if (completed && Date.now() - completed > 2000) {
setStore("summaryWaitTimedOut", true)
return
}
const timer = setTimeout(() => {
setStore("summaryWaitTimedOut", true)
}, 6000)
@@ -554,8 +477,10 @@ export function SessionTurn(
{(assistantMessage) => (
<AssistantMessageItem
message={assistantMessage}
responsePartId={responsePartId()}
hideResponsePart={hideResponsePart()}
summary={summary()}
response={response()}
lastTextPartId={lastTextPart()?.id}
working={working()}
/>
)}
</For>
+1 -1
View File
@@ -1,6 +1,6 @@
[data-component="toast-region"] {
position: fixed;
bottom: 48px;
bottom: 32px;
right: 32px;
z-index: 1000;
display: flex;
-4
View File
@@ -1,5 +1,4 @@
import { marked } from "marked"
import markedKatex from "marked-katex-extension"
import markedShiki from "marked-shiki"
import { bundledLanguages, type BundledLanguage } from "shiki"
import { createSimpleContext } from "./helper"
@@ -379,9 +378,6 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
name: "Marked",
init: () => {
return marked.use(
markedKatex({
throwOnError: false,
}),
markedShiki({
async highlight(code, lang) {
const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [] })
+16 -80
View File
@@ -19,86 +19,36 @@ export function createAutoScroll(options: AutoScrollOptions) {
let autoScrollTimeout: ReturnType<typeof setTimeout> | undefined
let isMouseDown = false
let cleanupListeners: (() => void) | undefined
let scheduledScroll = false
let scheduledForce = false
function distanceFromBottom() {
if (!scrollRef) return 0
return scrollRef.scrollHeight - scrollRef.clientHeight - scrollRef.scrollTop
}
function scrollToBottom() {
if (!scrollRef || store.userScrolled || !options.working()) return
function startAutoScroll() {
isAutoScrolling = true
if (autoScrollTimeout) clearTimeout(autoScrollTimeout)
autoScrollTimeout = setTimeout(() => {
isAutoScrolling = false
}, 1000)
}
function scrollToBottomNow() {
if (!scrollRef || store.userScrolled || !options.working()) return
const distance = distanceFromBottom()
if (distance < 2) return
const behavior = distance > 96 ? "auto" : "smooth"
startAutoScroll()
scrollRef.scrollTo({
top: scrollRef.scrollHeight,
behavior,
behavior: "smooth",
})
}
function forceScrollToBottomNow() {
if (!scrollRef) return
if (store.userScrolled) setStore("userScrolled", false)
const distance = distanceFromBottom()
if (distance < 2) return
startAutoScroll()
scrollRef.scrollTo({
top: scrollRef.scrollHeight,
behavior: "auto",
})
}
function scheduleScrollToBottom(force = false) {
if (typeof requestAnimationFrame === "undefined") {
if (force) {
forceScrollToBottomNow()
return
}
scrollToBottomNow()
return
}
if (force) scheduledForce = true
if (scheduledScroll) return
scheduledScroll = true
requestAnimationFrame(() => {
scheduledScroll = false
const shouldForce = scheduledForce
scheduledForce = false
if (shouldForce) {
forceScrollToBottomNow()
return
}
scrollToBottomNow()
})
}
function scrollToBottom() {
scheduleScrollToBottom(false)
}
function forceScrollToBottom() {
scheduleScrollToBottom(true)
if (!scrollRef) return
setStore("userScrolled", false)
isAutoScrolling = true
if (autoScrollTimeout) clearTimeout(autoScrollTimeout)
autoScrollTimeout = setTimeout(() => {
isAutoScrolling = false
}, 1000)
scrollRef.scrollTo({
top: scrollRef.scrollHeight,
behavior: "smooth",
})
}
function handleScroll() {
@@ -182,20 +132,6 @@ export function createAutoScroll(options: AutoScrollOptions) {
}
})
// Ensure pinned-to-bottom stays pinned during heavy DOM updates
createEffect(() => {
const el = store.contentRef
if (!el) return
const observer = new MutationObserver(() => {
if (store.userScrolled) return
if (!options.working()) return
scheduleScrollToBottom(false)
})
observer.observe(el, { childList: true, subtree: true, characterData: true })
onCleanup(() => observer.disconnect())
})
// Handle content resize
createResizeObserver(
() => store.contentRef,
+1 -9
View File
@@ -51,17 +51,9 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
)
})
function initialActive() {
if (props.current) return props.key(props.current)
const items = flat()
if (items.length === 0) return ""
return props.key(items[0])
}
const list = createList({
items: () => flat().map(props.key),
initialActive: initialActive(),
initialActive: props.current ? props.key(props.current) : props.key(flat()[0]),
loop: true,
})
-2
View File
@@ -4,7 +4,6 @@
@import "./theme.css" layer(theme);
@import "./base.css" layer(base);
@import "katex/dist/katex.min.css" layer(base);
@import "../components/accordion.css" layer(components);
@import "../components/avatar.css" layer(components);
@@ -22,7 +21,6 @@
@import "../components/provider-icon.css" layer(components);
@import "../components/icon.css" layer(components);
@import "../components/icon-button.css" layer(components);
@import "../components/image-preview.css" layer(components);
@import "../components/text-field.css" layer(components);
@import "../components/list.css" layer(components);
@import "../components/logo.css" layer(components);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.208",
"version": "1.0.207",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.208",
"version": "1.0.207",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -75
View File
@@ -106,8 +106,7 @@ You can configure TUI-specific settings through the `tui` option.
"scroll_speed": 3,
"scroll_acceleration": {
"enabled": true
},
"diff_style": "auto"
}
}
}
```
@@ -116,7 +115,6 @@ Available options:
- `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration. **Takes precedence over `scroll_speed`.**
- `scroll_speed` - Custom scroll speed multiplier (default: `1`, minimum: `1`). Ignored if `scroll_acceleration.enabled` is `true`.
- `diff_style` - Control diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows single column.
[Learn more about using the TUI here](/docs/tui).
@@ -180,25 +178,6 @@ You can configure the providers and models you want to use in your OpenCode conf
The `small_model` option configures a separate model for lightweight tasks like title generation. By default, OpenCode tries to use a cheaper model if one is available from your provider, otherwise it falls back to your main model.
Provider options can include `timeout` and `setCacheKey`:
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"anthropic": {
"options": {
"timeout": 600000,
"setCacheKey": true
}
}
}
}
```
- `timeout` - Request timeout in milliseconds (default: 300000). Set to `false` to disable.
- `setCacheKey` - Ensure a cache key is always set for designated provider.
You can also configure [local models](/docs/models#local). [Learn more](/docs/models).
---
@@ -384,42 +363,6 @@ For example, to ensure that the `edit` and `bash` tools require user approval:
---
### Compaction
You can control context compaction behavior through the `compaction` option.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"compaction": {
"auto": true,
"prune": true
}
}
```
- `auto` - Automatically compact the session when context is full (default: `true`).
- `prune` - Remove old tool outputs to save tokens (default: `true`).
---
### Watcher
You can configure file watcher ignore patterns through the `watcher` option.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"watcher": {
"ignore": ["node_modules/**", "dist/**", ".git/**"]
}
}
```
Patterns follow glob syntax. Use this to exclude noisy directories from file watching.
---
### MCP servers
You can configure MCP servers you want to use through the `mcp` option.
@@ -512,23 +455,6 @@ If a provider appears in both `enabled_providers` and `disabled_providers`, the
---
### Experimental
The `experimental` key contains options that are under active development.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"experimental": {}
}
```
:::caution
Experimental options are not stable. They may change or be removed without notice.
:::
---
## Variables
You can use variable substitution in your config files to reference environment variables and file contents.
@@ -35,7 +35,6 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
| [opencode-notificator](https://github.com/panta/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop notifications and sound alerts for permission, completion, and error events |
| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context |
| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Allow OpenCode agents to lazy load prompts on demand with skill discovery and injection |
---
+1 -31
View File
@@ -47,7 +47,7 @@ Browse available plugins in the [ecosystem](/docs/ecosystem#plugins).
**npm plugins** are installed automatically using Bun at startup. Packages and their dependencies are cached in `~/.cache/opencode/node_modules/`.
**Local plugins** are loaded directly from the plugin directory. To use external packages, you must create a `package.json` within your config directory (see [Dependencies](#dependencies)), or publish the plugin to npm and [add it to your config](/docs/config#plugins).
**Local plugins** are loaded directly from the plugin directory. Dependencies are not installed automatically. If your local plugin requires external packages, publish it to npm instead and add it to your config.
---
@@ -71,36 +71,6 @@ functions. Each function receives a context object and returns a hooks object.
---
### Dependencies
Local plugins and custom tools can use external npm packages. Add a `package.json` to your config directory with the dependencies you need.
```json title=".opencode/package.json"
{
"dependencies": {
"shescape": "^2.1.0"
}
}
```
OpenCode runs `bun install` at startup to install these. Your plugins and tools can then import them.
```ts title=".opencode/plugin/my-plugin.ts"
import { escape } from "shescape"
export const MyPlugin = async (ctx) => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
output.args.command = escape(output.args.command)
}
},
}
}
```
---
### Basic structure
```js title=".opencode/plugin/example.js"
+1 -28
View File
@@ -869,33 +869,6 @@ To use Kimi K2 from Moonshot AI:
---
### MiniMax
1. Head over to the [MiniMax API Console](https://platform.minimax.io/login), create an account, and generate an API key.
2. Run the `/connect` command and search for **MiniMax**.
```txt
/connect
```
3. Enter your MiniMax API key.
```txt
┌ API key
└ enter
```
4. Run the `/models` command to select a model like _M2.1_.
```txt
/models
```
---
### Nebius Token Factory
1. Head over to the [Nebius Token Factory console](https://tokenfactory.nebius.com/), create an account, and click **Add Key**.
@@ -1304,7 +1277,7 @@ SAP AI Core provides access to 40+ models from OpenAI, Anthropic, Google, Amazon
└ enter
```
4. Run the `/models` command to select a model like _GLM-4.7_.
4. Run the `/models` command to select a model like _GLM-4.5_.
```txt
/models
+1 -5
View File
@@ -77,11 +77,10 @@ You can also access our models through the following API endpoints.
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| MiniMax M2.1 | minimax-m2.1-free | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| GLM 4.7 | glm-4.7-free | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` |
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.7 | glm-4.7-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 Thinking | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -112,7 +111,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
| --------------------------------- | ------ | ------ | ----------- | ------------ |
| Big Pickle | Free | Free | Free | - |
| Grok Code Fast 1 | Free | Free | Free | - |
| MiniMax M2.1 | Free | Free | Free | - |
| GLM 4.7 | Free | Free | Free | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
@@ -147,7 +145,6 @@ The free models:
- Grok Code Fast 1 is currently free on OpenCode for a limited time. The xAI team is using this time to collect feedback and improve Grok Code.
- GLM 4.7 is currently free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- MiniMax M2.1 is currently free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
<a href={email}>Contact us</a> if you have any questions.
@@ -160,7 +157,6 @@ All our models are hosted in the US. Our providers follow a zero-retention polic
- Grok Code Fast 1: During its free period, collected data may be used to improve Grok Code.
- GLM 4.7: During its free period, collected data may be used to improve the model.
- MiniMax M2.1: During its free period, collected data may be used to improve the model.
- Big Pickle: During its free period, collected data may be used to improve the model.
- OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
- Anthropic APIs: Requests are retained for 30 days in accordance with [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.208",
"version": "1.0.207",
"publisher": "sst-dev",
"repository": {
"type": "git",