Compare commits

..

1 Commits

Author SHA1 Message Date
Aiden Cline
0597d225a3 ignore: add truncation namespace 2026-01-07 01:24:50 -06:00
67 changed files with 501 additions and 35838 deletions

View File

@@ -172,7 +172,7 @@ jobs:
- name: Install tauri-cli from portable appimage branch
if: contains(matrix.settings.host, 'ubuntu')
run: |
cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch truly-portable-appimage --force
echo "Installed tauri-cli version:"
cargo tauri --version

View File

@@ -28,8 +28,7 @@ curl -fsSL https://opencode.ai/install | bash
npm i -g opencode-ai@latest # or bun/pnpm/yarn
scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date)
brew install opencode # macOS and Linux (official brew formula, updated less frequently)
brew install opencode # macOS and Linux
paru -S opencode-bin # Arch Linux
mise use -g opencode # Any OS
nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch

View File

@@ -28,8 +28,7 @@ curl -fsSL https://opencode.ai/install | bash
npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn
scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS 和 Linux(推荐,始终保持最新)
brew install opencode # macOS 和 Linux官方 brew formula更新频率较低
brew install opencode # macOS 和 Linux
paru -S opencode-bin # Arch Linux
mise use -g opencode # 任意系统
nix run nixpkgs#opencode # 或用 github:anomalyco/opencode 获取最新 dev 分支

View File

@@ -28,8 +28,7 @@ curl -fsSL https://opencode.ai/install | bash
npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn
scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS 與 Linux(推薦,始終保持最新)
brew install opencode # macOS 與 Linux官方 brew formula更新頻率較低
brew install opencode # macOS 與 Linux
paru -S opencode-bin # Arch Linux
mise use -g github:anomalyco/opencode # 任何作業系統
nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支

View File

@@ -193,4 +193,3 @@
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |

View File

@@ -22,7 +22,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.1.6",
"version": "1.1.4",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -70,7 +70,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.1.6",
"version": "1.1.4",
"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.1.6",
"version": "1.1.4",
"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.1.6",
"version": "1.1.4",
"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.1.6",
"version": "1.1.4",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -173,10 +173,9 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.1.6",
"version": "1.1.4",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
@@ -202,7 +201,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.1.6",
"version": "1.1.4",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -231,7 +230,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.1.6",
"version": "1.1.4",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -247,7 +246,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.1.6",
"version": "1.1.4",
"bin": {
"opencode": "./bin/opencode",
},
@@ -277,7 +276,7 @@
"@clack/prompts": "1.0.0-alpha.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.25.2",
"@modelcontextprotocol/sdk": "1.15.1",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
@@ -350,7 +349,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.1.6",
"version": "1.1.4",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -370,7 +369,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.1.6",
"version": "1.1.4",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -381,7 +380,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.1.6",
"version": "1.1.4",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -394,7 +393,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.1.6",
"version": "1.1.4",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -433,7 +432,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.1.6",
"version": "1.1.4",
"dependencies": {
"zod": "catalog:",
},
@@ -444,7 +443,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.1.6",
"version": "1.1.4",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -911,8 +910,6 @@
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.1", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-x/nDTupOnV9VuSeNIiJpgIpc915GHduhyseJeMTnI0JMsXaObmpa0rgPr3ASVEYMLgpvqozIEG1RTOOnal6zLQ=="],
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
"@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="],
"@hono/zod-validator": ["@hono/zod-validator@0.4.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="],
@@ -1097,7 +1094,7 @@
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.15.1", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w=="],
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
@@ -1905,9 +1902,7 @@
"ai": ["ai@5.0.97", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
@@ -2411,7 +2406,7 @@
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
@@ -2787,9 +2782,7 @@
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
@@ -3391,8 +3384,6 @@
"remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
@@ -3767,6 +3758,8 @@
"update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
@@ -4065,11 +4058,9 @@
"@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
"@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
"@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="],
@@ -4413,6 +4404,8 @@
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
"uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],

View File

@@ -76,7 +76,6 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
"checkout.session.completed",
"checkout.session.expired",
"charge.refunded",
"invoice.payment_succeeded",
"customer.created",
"customer.deleted",
"customer.updated",

View File

@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-rNGq0yjL5ZHYVg+zyV4nFPug4gqhKhyOnfebaufyd34="
"nodeModules": "sha256-Vi6auFnjZ6Ko7yGy73kyjE3gToreuhD81mZgcnxxxww="
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.1.6",
"version": "1.1.4",
"description": "",
"type": "module",
"exports": {

View File

@@ -1,5 +1,5 @@
import "@/index.css"
import { ErrorBoundary, Show, lazy, type ParentProps } from "solid-js"
import { ErrorBoundary, Show, Suspense, lazy, type ParentProps } from "solid-js"
import { Router, Route, Navigate } from "@solidjs/router"
import { MetaProvider } from "@solidjs/meta"
import { Font } from "@opencode-ai/ui/font"
@@ -20,12 +20,10 @@ import { FileProvider } from "@/context/file"
import { NotificationProvider } from "@/context/notification"
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { CommandProvider } from "@/context/command"
import { Logo } from "@opencode-ai/ui/logo"
import Layout from "@/pages/layout"
import DirectoryLayout from "@/pages/directory-layout"
import { ErrorPage } from "./pages/error"
import { iife } from "@opencode-ai/util/iife"
import { Suspense } from "solid-js"
const Home = lazy(() => import("@/pages/home"))
const Session = lazy(() => import("@/pages/session"))
@@ -33,7 +31,7 @@ const Loading = () => <div class="size-full flex items-center justify-center tex
declare global {
interface Window {
__OPENCODE__?: { updaterEnabled?: boolean; port?: number; serverReady?: boolean }
__OPENCODE__?: { updaterEnabled?: boolean; port?: number }
}
}
@@ -49,25 +47,6 @@ const defaultServerUrl = iife(() => {
return window.location.origin
})
export function AppBaseProviders(props: ParentProps) {
return (
<MetaProvider>
<Font />
<ThemeProvider>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<DialogProvider>
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<CodeComponentProvider component={Code}>{props.children}</CodeComponentProvider>
</DiffComponentProvider>
</MarkedProvider>
</DialogProvider>
</ErrorBoundary>
</ThemeProvider>
</MetaProvider>
)
}
function ServerKey(props: ParentProps) {
const server = useServer()
return (
@@ -77,56 +56,71 @@ function ServerKey(props: ParentProps) {
)
}
export function AppInterface() {
export function App() {
return (
<ServerProvider defaultUrl={defaultServerUrl}>
<ServerKey>
<GlobalSDKProvider>
<GlobalSyncProvider>
<Router
root={(props) => (
<PermissionProvider>
<LayoutProvider>
<NotificationProvider>
<CommandProvider>
<Layout>{props.children}</Layout>
</CommandProvider>
</NotificationProvider>
</LayoutProvider>
</PermissionProvider>
)}
>
<Route
path="/"
component={() => (
<Suspense fallback={<Loading />}>
<Home />
</Suspense>
)}
/>
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id ?? "new"} keyed>
<TerminalProvider>
<FileProvider>
<PromptProvider>
<Suspense fallback={<Loading />}>
<Session />
</Suspense>
</PromptProvider>
</FileProvider>
</TerminalProvider>
</Show>
)}
/>
</Route>
</Router>
</GlobalSyncProvider>
</GlobalSDKProvider>
</ServerKey>
</ServerProvider>
<MetaProvider>
<Font />
<ThemeProvider>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<DialogProvider>
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<CodeComponentProvider component={Code}>
<ServerProvider defaultUrl={defaultServerUrl}>
<ServerKey>
<GlobalSDKProvider>
<GlobalSyncProvider>
<Router
root={(props) => (
<PermissionProvider>
<LayoutProvider>
<NotificationProvider>
<CommandProvider>
<Layout>{props.children}</Layout>
</CommandProvider>
</NotificationProvider>
</LayoutProvider>
</PermissionProvider>
)}
>
<Route
path="/"
component={() => (
<Suspense fallback={<Loading />}>
<Home />
</Suspense>
)}
/>
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id ?? "new"} keyed>
<TerminalProvider>
<FileProvider>
<PromptProvider>
<Suspense fallback={<Loading />}>
<Session />
</Suspense>
</PromptProvider>
</FileProvider>
</TerminalProvider>
</Show>
)}
/>
</Route>
</Router>
</GlobalSyncProvider>
</GlobalSDKProvider>
</ServerKey>
</ServerProvider>
</CodeComponentProvider>
</DiffComponentProvider>
</MarkedProvider>
</DialogProvider>
</ErrorBoundary>
</ThemeProvider>
</MetaProvider>
)
}

View File

@@ -248,8 +248,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
}
const isFocused = createFocusSignal(() => editorRef)
createEffect(() => {
params.id
editorRef.focus()
@@ -260,6 +258,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onCleanup(() => clearInterval(interval))
})
const isFocused = createFocusSignal(() => editorRef)
const [composing, setComposing] = createSignal(false)
const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
@@ -293,13 +292,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const clipboardData = event.clipboardData
if (!clipboardData) return
event.preventDefault()
event.stopPropagation()
const items = Array.from(clipboardData.items)
const imageItems = items.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type))
if (imageItems.length > 0) {
event.preventDefault()
event.stopPropagation()
for (const item of imageItems) {
const file = item.getAsFile()
if (file) await addImageAttachment(file)
@@ -307,6 +305,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return
}
event.preventDefault()
event.stopPropagation()
const plainText = clipboardData.getData("text/plain") ?? ""
addPart({ type: "text", content: plainText, start: 0, end: 0 })
}
@@ -347,11 +347,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
onMount(() => {
editorRef.addEventListener("paste", handlePaste)
document.addEventListener("dragover", handleGlobalDragOver)
document.addEventListener("dragleave", handleGlobalDragLeave)
document.addEventListener("drop", handleGlobalDrop)
})
onCleanup(() => {
editorRef.removeEventListener("paste", handlePaste)
document.removeEventListener("dragover", handleGlobalDragOver)
document.removeEventListener("dragleave", handleGlobalDragLeave)
document.removeEventListener("drop", handleGlobalDrop)
@@ -1506,7 +1508,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}}
contenteditable="true"
onInput={handleInput}
onPaste={handlePaste}
onCompositionStart={() => setComposing(true)}
onCompositionEnd={() => setComposing(false)}
onKeyDown={handleKeyDown}

View File

@@ -1,6 +1,6 @@
// @refresh reload
import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface } from "@/app"
import { App } from "@/app"
import { Platform, PlatformProvider } from "@/context/platform"
import pkg from "../package.json"
@@ -55,9 +55,7 @@ const platform: Platform = {
render(
() => (
<PlatformProvider value={platform}>
<AppBaseProviders>
<AppInterface />
</AppBaseProviders>
<App />
</PlatformProvider>
),
root!,

View File

@@ -1,2 +1,2 @@
export { PlatformProvider, type Platform } from "./context/platform"
export { AppBaseProviders, AppInterface } from "./app"
export { App } from "./app"

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.1.6",
"version": "1.1.4",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -2,7 +2,6 @@ import { Billing } from "@opencode-ai/console-core/billing.js"
import type { APIEvent } from "@solidjs/start/server"
import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable, PaymentTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js"
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -147,242 +146,6 @@ export async function POST(input: APIEvent) {
.where(eq(BillingTable.workspaceID, workspaceID))
})
}
if (body.type === "invoice.payment_succeeded" && body.data.object.billing_reason === "subscription_cycle") {
const invoiceID = body.data.object.id as string
const amountInCents = body.data.object.amount_paid
const customerID = body.data.object.customer as string
const subscriptionID = body.data.object.parent?.subscription_details?.subscription as string
if (!customerID) throw new Error("Customer ID not found")
if (!invoiceID) throw new Error("Invoice ID not found")
if (!subscriptionID) throw new Error("Subscription ID not found")
const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
expand: ["payments"],
})
const paymentID = invoice.payments?.data[0].payment.payment_intent as string
if (!paymentID) throw new Error("Payment ID not found")
const workspaceID = await Database.use((tx) =>
tx
.select({ workspaceID: BillingTable.workspaceID })
.from(BillingTable)
.where(eq(BillingTable.customerID, customerID))
.then((rows) => rows[0]?.workspaceID),
)
if (!workspaceID) throw new Error("Workspace ID not found for customer")
await Database.use((tx) =>
tx.insert(PaymentTable).values({
workspaceID,
id: Identifier.create("payment"),
amount: centsToMicroCents(amountInCents),
paymentID,
invoiceID,
customerID,
}),
)
}
if (body.type === "customer.subscription.created") {
const data = {
id: "evt_1Smq802SrMQ2Fneksse5FMNV",
object: "event",
api_version: "2025-07-30.basil",
created: 1767766916,
data: {
object: {
id: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
object: "subscription",
application: null,
application_fee_percent: null,
automatic_tax: {
disabled_reason: null,
enabled: false,
liability: null,
},
billing_cycle_anchor: 1770445200,
billing_cycle_anchor_config: null,
billing_mode: {
flexible: {
proration_discounts: "included",
},
type: "flexible",
updated_at: 1770445200,
},
billing_thresholds: null,
cancel_at: null,
cancel_at_period_end: false,
canceled_at: null,
cancellation_details: {
comment: null,
feedback: null,
reason: null,
},
collection_method: "charge_automatically",
created: 1770445200,
currency: "usd",
customer: "cus_TkKmZZvysJ2wej",
customer_account: null,
days_until_due: null,
default_payment_method: null,
default_source: "card_1Smq7u2SrMQ2FneknjyOa7sq",
default_tax_rates: [],
description: null,
discounts: [],
ended_at: null,
invoice_settings: {
account_tax_ids: null,
issuer: {
type: "self",
},
},
items: {
object: "list",
data: [
{
id: "si_TkKnBKXFX76t0O",
object: "subscription_item",
billing_thresholds: null,
created: 1770445200,
current_period_end: 1772864400,
current_period_start: 1770445200,
discounts: [],
metadata: {},
plan: {
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
object: "plan",
active: true,
amount: 20000,
amount_decimal: "20000",
billing_scheme: "per_unit",
created: 1767725082,
currency: "usd",
interval: "month",
interval_count: 1,
livemode: false,
metadata: {},
meter: null,
nickname: null,
product: "prod_Tk9LjWT1n0DgYm",
tiers_mode: null,
transform_usage: null,
trial_period_days: null,
usage_type: "licensed",
},
price: {
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
object: "price",
active: true,
billing_scheme: "per_unit",
created: 1767725082,
currency: "usd",
custom_unit_amount: null,
livemode: false,
lookup_key: null,
metadata: {},
nickname: null,
product: "prod_Tk9LjWT1n0DgYm",
recurring: {
interval: "month",
interval_count: 1,
meter: null,
trial_period_days: null,
usage_type: "licensed",
},
tax_behavior: "unspecified",
tiers_mode: null,
transform_quantity: null,
type: "recurring",
unit_amount: 20000,
unit_amount_decimal: "20000",
},
quantity: 1,
subscription: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
tax_rates: [],
},
],
has_more: false,
total_count: 1,
url: "/v1/subscription_items?subscription=sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
},
latest_invoice: "in_1Smq7x2SrMQ2FnekSJesfPwE",
livemode: false,
metadata: {},
next_pending_invoice_item_invoice: null,
on_behalf_of: null,
pause_collection: null,
payment_settings: {
payment_method_options: null,
payment_method_types: null,
save_default_payment_method: "off",
},
pending_invoice_item_interval: null,
pending_setup_intent: null,
pending_update: null,
plan: {
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
object: "plan",
active: true,
amount: 20000,
amount_decimal: "20000",
billing_scheme: "per_unit",
created: 1767725082,
currency: "usd",
interval: "month",
interval_count: 1,
livemode: false,
metadata: {},
meter: null,
nickname: null,
product: "prod_Tk9LjWT1n0DgYm",
tiers_mode: null,
transform_usage: null,
trial_period_days: null,
usage_type: "licensed",
},
quantity: 1,
schedule: null,
start_date: 1770445200,
status: "active",
test_clock: "clock_1Smq6n2SrMQ2FnekQw4yt2PZ",
transfer_data: null,
trial_end: null,
trial_settings: {
end_behavior: {
missing_payment_method: "create_invoice",
},
},
trial_start: null,
},
},
livemode: false,
pending_webhooks: 0,
request: {
id: "req_6YO9stvB155WJD",
idempotency_key: "581ba059-6f86-49b2-9c49-0d8450255322",
},
type: "customer.subscription.created",
}
}
if (body.type === "customer.subscription.deleted") {
const subscriptionID = body.data.object.id
if (!subscriptionID) throw new Error("Subscription ID not found")
const workspaceID = await Database.use((tx) =>
tx
.select({ workspaceID: BillingTable.workspaceID })
.from(BillingTable)
.where(eq(BillingTable.subscriptionID, subscriptionID))
.then((rows) => rows[0]?.workspaceID),
)
if (!workspaceID) throw new Error("Workspace ID not found for subscription")
await Database.transaction(async (tx) => {
await tx.update(BillingTable).set({ subscriptionID: null }).where(eq(BillingTable.workspaceID, workspaceID))
await tx.update(UserTable).set({ timeSubscribed: null }).where(eq(UserTable.workspaceID, workspaceID))
})
}
})()
.then((message) => {
return Response.json({ message: message ?? "done" }, { status: 200 })

View File

@@ -1,8 +1,2 @@
.root {
[data-slot="title-row"] {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-4);
}
}

View File

@@ -1,57 +1,11 @@
import { action, useParams, useAction, useSubmission, json } from "@solidjs/router"
import { createStore } from "solid-js/store"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { withActor } from "~/context/auth.withActor"
import { queryBillingInfo } from "../../common"
import styles from "./black-section.module.css"
const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
"use server"
return json(
await withActor(
() =>
Billing.generateSessionUrl({ returnUrl })
.then((data) => ({ error: undefined, data }))
.catch((e) => ({
error: e.message as string,
data: undefined,
})),
workspaceID,
),
{ revalidate: queryBillingInfo.key },
)
}, "sessionUrl")
export function BlackSection() {
const params = useParams()
const sessionAction = useAction(createSessionUrl)
const sessionSubmission = useSubmission(createSessionUrl)
const [store, setStore] = createStore({
sessionRedirecting: false,
})
async function onClickSession() {
const result = await sessionAction(params.id!, window.location.href)
if (result.data) {
setStore("sessionRedirecting", true)
window.location.href = result.data
}
}
return (
<section class={styles.root}>
<div data-slot="section-title">
<h2>Subscription</h2>
<div data-slot="title-row">
<p>You are subscribed to OpenCode Black for $200 per month.</p>
<button
data-color="primary"
disabled={sessionSubmission.pending || store.sessionRedirecting}
onClick={onClickSession}
>
{sessionSubmission.pending || store.sessionRedirecting ? "Loading..." : "Manage Subscription"}
</button>
</div>
<h2>Black</h2>
<p>You are subscribed to Black.</p>
</div>
</section>
)

View File

@@ -1 +0,0 @@
ALTER TABLE `billing` ADD CONSTRAINT `global_subscription_id` UNIQUE(`subscription_id`);

File diff suppressed because it is too large Load Diff

View File

@@ -316,13 +316,6 @@
"when": 1767759322451,
"tag": "0044_tiny_captain_midlands",
"breakpoints": true
},
{
"idx": 45,
"version": "5",
"when": 1767765497502,
"tag": "0045_cuddly_diamondback",
"breakpoints": true
}
]
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.1.6",
"version": "1.1.4",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -82,14 +82,6 @@ const invoice = invoices.data[0]
const invoiceID = invoice?.id
const paymentID = invoice?.payments?.data[0]?.payment.payment_intent as string | undefined
// Get the default payment method from the customer
const paymentMethodID = (customer.invoice_settings.default_payment_method ?? subscription.default_payment_method) as
| string
| null
const paymentMethod = paymentMethodID ? await Billing.stripe().paymentMethods.retrieve(paymentMethodID) : null
const paymentMethodLast4 = paymentMethod?.card?.last4 ?? null
const paymentMethodType = paymentMethod?.type ?? null
// Look up the user by email via AuthTable
const auth = await Database.use((tx) =>
tx
@@ -124,15 +116,12 @@ await Billing.stripe().customers.update(customerID, {
})
await Database.transaction(async (tx) => {
// Set customer id, subscription id, and payment method on workspace billing
// Set customer id and subscription id on workspace billing
await tx
.update(BillingTable)
.set({
customerID,
subscriptionID,
paymentMethodID,
paymentMethodLast4,
paymentMethodType,
})
.where(eq(BillingTable.workspaceID, workspaceID))
@@ -158,9 +147,6 @@ await Database.transaction(async (tx) => {
console.log(`Successfully onboarded workspace ${workspaceID}`)
console.log(` Customer ID: ${customerID}`)
console.log(` Subscription ID: ${subscriptionID}`)
console.log(
` Payment Method: ${paymentMethodID ?? "(none)"} (${paymentMethodType ?? "unknown"} ending in ${paymentMethodLast4 ?? "????"})`,
)
console.log(` User ID: ${user.id}`)
console.log(` Invoice ID: ${invoiceID ?? "(none)"}`)
console.log(` Payment ID: ${paymentID ?? "(none)"}`)

View File

@@ -23,11 +23,7 @@ export const BillingTable = mysqlTable(
timeReloadLockedTill: utc("time_reload_locked_till"),
subscriptionID: varchar("subscription_id", { length: 28 }),
},
(table) => [
...workspaceIndexes(table),
uniqueIndex("global_customer_id").on(table.customerID),
uniqueIndex("global_subscription_id").on(table.subscriptionID),
],
(table) => [...workspaceIndexes(table), uniqueIndex("global_customer_id").on(table.customerID)],
)
export const PaymentTable = mysqlTable(

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.1.6",
"version": "1.1.4",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.1.6",
"version": "1.1.4",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.1.6",
"version": "1.1.4",
"type": "module",
"license": "MIT",
"scripts": {
@@ -14,7 +14,6 @@
},
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",

View File

@@ -1177,21 +1177,6 @@ dependencies = [
"new_debug_unreachable",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -1199,7 +1184,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -1267,7 +1251,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@@ -2792,7 +2775,6 @@ dependencies = [
name = "opencode-desktop"
version = "0.0.0"
dependencies = [
"futures",
"gtk",
"listeners",
"semver",

View File

@@ -36,7 +36,6 @@ serde_json = "1"
tokio = "1.48.0"
listeners = "0.3"
tauri-plugin-os = "2"
futures = "0.3.31"
semver = "1.0.27"
[target.'cfg(target_os = "linux")'.dependencies]

View File

@@ -12,19 +12,5 @@
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.personal-information.addressbook</key>
<true/>
<key>com.apple.security.personal-information.calendars</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.personal-information.photos-library</key>
<true/>
</dict>
</plist>

View File

@@ -2,7 +2,6 @@ mod cli;
mod window_customizer;
use cli::{get_sidecar_path, install_cli, sync_cli};
use futures::FutureExt;
use std::{
collections::VecDeque,
net::{SocketAddr, TcpListener},
@@ -10,9 +9,10 @@ use std::{
time::{Duration, Instant},
};
use tauri::{
path::BaseDirectory, AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl,
WebviewWindow,
path::BaseDirectory, AppHandle, LogicalSize, Manager, RunEvent, WebviewUrl, WebviewWindow,
};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_shell::ShellExt;
use tokio::net::TcpSocket;
@@ -20,26 +20,7 @@ use tokio::net::TcpSocket;
use crate::window_customizer::PinchZoomDisablePlugin;
#[derive(Clone)]
struct ServerState {
child: Arc<Mutex<Option<CommandChild>>>,
status: futures::future::Shared<tokio::sync::oneshot::Receiver<Result<(), String>>>,
}
impl ServerState {
pub fn new(
child: Option<CommandChild>,
status: tokio::sync::oneshot::Receiver<Result<(), String>>,
) -> Self {
Self {
child: Arc::new(Mutex::new(child)),
status: status.shared(),
}
}
pub fn set_child(&self, child: Option<CommandChild>) {
*self.child.lock().unwrap() = child;
}
}
struct ServerState(Arc<Mutex<Option<CommandChild>>>);
#[derive(Clone)]
struct LogState(Arc<Mutex<VecDeque<String>>>);
@@ -54,7 +35,7 @@ fn kill_sidecar(app: AppHandle) {
};
let Some(server_state) = server_state
.child
.0
.lock()
.expect("Failed to acquire mutex lock")
.take()
@@ -68,6 +49,25 @@ fn kill_sidecar(app: AppHandle) {
println!("Killed server");
}
#[tauri::command]
async fn copy_logs_to_clipboard(app: AppHandle) -> Result<(), String> {
let log_state = app.try_state::<LogState>().ok_or("Log state not found")?;
let logs = log_state
.0
.lock()
.map_err(|_| "Failed to acquire log lock")?;
let log_text = logs.iter().cloned().collect::<Vec<_>>().join("");
app.clipboard()
.write_text(log_text)
.map_err(|e| format!("Failed to copy to clipboard: {}", e))?;
Ok(())
}
#[tauri::command]
async fn get_logs(app: AppHandle) -> Result<String, String> {
let log_state = app.try_state::<LogState>().ok_or("Log state not found")?;
@@ -79,15 +79,6 @@ async fn get_logs(app: AppHandle) -> Result<String, String> {
Ok(logs.iter().cloned().collect::<Vec<_>>().join(""))
}
#[tauri::command]
async fn ensure_server_started(state: State<'_, ServerState>) -> Result<(), String> {
state
.status
.clone()
.await
.map_err(|_| "Failed to get server status".to_string())?
}
fn get_sidecar_port() -> u32 {
option_env!("OPENCODE_PORT")
.map(|s| s.to_string())
@@ -139,7 +130,7 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild {
.args([
"-il",
"-c",
&format!("\"{}\" serve --port={}", sidecar.display(), port),
&format!("{} serve --port={}", sidecar.display(), port),
])
.spawn()
.expect("Failed to spawn opencode")
@@ -218,8 +209,9 @@ pub fn run() {
.plugin(PinchZoomDisablePlugin)
.invoke_handler(tauri::generate_handler![
kill_sidecar,
install_cli,
ensure_server_started
copy_logs_to_clipboard,
get_logs,
install_cli
])
.setup(move |app| {
let app = app.handle().clone();
@@ -227,93 +219,94 @@ pub fn run() {
// Initialize log state
app.manage(LogState(Arc::new(Mutex::new(VecDeque::new()))));
// Get port and create window immediately for faster perceived startup
let port = get_sidecar_port();
let primary_monitor = app.primary_monitor().ok().flatten();
let size = primary_monitor
.map(|m| m.size().to_logical(m.scale_factor()))
.unwrap_or(LogicalSize::new(1920, 1080));
// Create window immediately with serverReady = false
let mut window_builder =
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
.title("OpenCode")
.inner_size(size.width as f64, size.height as f64)
.decorations(true)
.zoom_hotkeys_enabled(true)
.disable_drag_drop_handler()
.initialization_script(format!(
r#"
window.__OPENCODE__ ??= {{}};
window.__OPENCODE__.updaterEnabled = {updater_enabled};
window.__OPENCODE__.port = {port};
"#
));
#[cfg(target_os = "macos")]
{
window_builder = window_builder
.title_bar_style(tauri::TitleBarStyle::Overlay)
.hidden_title(true);
}
let app = app.clone();
tauri::async_runtime::spawn(async move {
let port = get_sidecar_port();
let window = window_builder.build().expect("Failed to create window");
let should_spawn_sidecar = !is_server_running(port).await;
let (tx, rx) = tokio::sync::oneshot::channel();
app.manage(ServerState::new(None, rx));
let child = if should_spawn_sidecar {
let child = spawn_sidecar(&app, port);
{
let app = app.clone();
tauri::async_runtime::spawn(async move {
let should_spawn_sidecar = !is_server_running(port).await;
let timestamp = Instant::now();
loop {
if timestamp.elapsed() > Duration::from_secs(7) {
let res = app.dialog()
.message("Failed to spawn OpenCode Server. Copy logs using the button below and send them to the team for assistance.")
.title("Startup Failed")
.buttons(MessageDialogButtons::OkCancelCustom("Copy Logs And Exit".to_string(), "Exit".to_string()))
.blocking_show_with_result();
let (child, res) = if should_spawn_sidecar {
let child = spawn_sidecar(&app, port);
if matches!(&res, MessageDialogResult::Custom(name) if name == "Copy Logs And Exit") {
match copy_logs_to_clipboard(app.clone()).await {
Ok(()) => println!("Logs copied to clipboard successfully"),
Err(e) => println!("Failed to copy logs to clipboard: {}", e),
}
}
let timestamp = Instant::now();
let res = loop {
if timestamp.elapsed() > Duration::from_secs(7) {
break Err(format!(
"Failed to spawn OpenCode Server. Logs:\n{}",
get_logs(app.clone()).await.unwrap()
));
}
app.exit(1);
tokio::time::sleep(Duration::from_millis(10)).await;
return;
}
if is_server_running(port).await {
// give the server a little bit more time to warm up
tokio::time::sleep(Duration::from_millis(10)).await;
tokio::time::sleep(Duration::from_millis(10)).await;
break Ok(());
}
};
if is_server_running(port).await {
// give the server a little bit more time to warm up
tokio::time::sleep(Duration::from_millis(10)).await;
println!("Server ready after {:?}", timestamp.elapsed());
break;
}
}
(Some(child), res)
} else {
(None, Ok(()))
};
println!("Server ready after {:?}", timestamp.elapsed());
app.state::<ServerState>().set_child(child);
Some(child)
} else {
None
};
if res.is_ok() {
let _ = window.eval("window.__OPENCODE__.serverReady = true;");
}
let primary_monitor = app.primary_monitor().ok().flatten();
let size = primary_monitor
.map(|m| m.size().to_logical(m.scale_factor()))
.unwrap_or(LogicalSize::new(1920, 1080));
let _ = tx.send(res);
});
let mut window_builder =
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
.title("OpenCode")
.inner_size(size.width as f64, size.height as f64)
.decorations(true)
.zoom_hotkeys_enabled(true)
.disable_drag_drop_handler()
.initialization_script(format!(
r#"
window.__OPENCODE__ ??= {{}};
window.__OPENCODE__.updaterEnabled = {updater_enabled};
window.__OPENCODE__.port = {port};
"#
));
#[cfg(target_os = "macos")]
{
window_builder = window_builder
.title_bar_style(tauri::TitleBarStyle::Overlay)
.hidden_title(true);
}
window_builder.build().expect("Failed to create window");
app.manage(ServerState(Arc::new(Mutex::new(child))));
});
}
{
let app = app.clone();
tauri::async_runtime::spawn(async move {
if let Err(e) = sync_cli(app) {
eprintln!("Failed to sync CLI: {e}");
}
});
let app = app.clone();
tauri::async_runtime::spawn(async move {
if let Err(e) = sync_cli(app) {
eprintln!("Failed to sync CLI: {e}");
}
});
}
Ok(())

View File

@@ -1,24 +1,21 @@
// @refresh reload
import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
import { App, PlatformProvider, Platform } from "@opencode-ai/app"
import { open, save } from "@tauri-apps/plugin-dialog"
import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { type as ostype } from "@tauri-apps/plugin-os"
import { AsyncStorage } from "@solid-primitives/storage"
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
import { Store } from "@tauri-apps/plugin-store"
import { UPDATER_ENABLED } from "./updater"
import { createMenu } from "./menu"
import { check, Update } from "@tauri-apps/plugin-updater"
import { invoke } from "@tauri-apps/api/core"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
import { relaunch } from "@tauri-apps/plugin-process"
import { AsyncStorage } from "@solid-primitives/storage"
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
import { Store } from "@tauri-apps/plugin-store"
import { Logo } from "@opencode-ai/ui/logo"
import { Suspense, createResource, ParentProps } from "solid-js"
import { UPDATER_ENABLED } from "./updater"
import { createMenu } from "./menu"
import pkg from "../package.json"
import { Show } from "solid-js"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -272,36 +269,7 @@ render(() => {
{ostype() === "macos" && (
<div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
)}
<AppBaseProviders>
<ServerGate>
<AppInterface />
</ServerGate>
</AppBaseProviders>
<App />
</PlatformProvider>
)
}, root!)
// Gate component that waits for the server to be ready
function ServerGate(props: ParentProps) {
const [status] = createResource(async () => {
if (window.__OPENCODE__?.serverReady) return
return await invoke("ensure_server_started")
})
return (
// Not using suspense as not all components are compatible with it (undefined refs)
<Show
when={status.state !== "pending"}
fallback={
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
<Logo class="w-xl opacity-12 animate-pulse" />
<div class="mt-8 text-14-regular text-text-weak">Starting server...</div>
</div>
}
>
{/* Trigger error boundary without rendering the returned value */}
{(status(), null)}
{props.children}
</Show>
)
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.1.6",
"version": "1.1.4",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.1.6"
version = "1.1.4"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.4/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.4/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.4/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.4/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.4/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.1.6",
"version": "1.1.4",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.1.6",
"version": "1.1.4",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -72,7 +72,7 @@
"@clack/prompts": "1.0.0-alpha.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.25.2",
"@modelcontextprotocol/sdk": "1.15.1",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",

View File

@@ -95,6 +95,7 @@ export namespace Agent {
options: {},
mode: "subagent",
native: true,
hidden: true,
},
explore: {
name: "explore",
@@ -140,7 +141,6 @@ export namespace Agent {
options: {},
native: true,
hidden: true,
temperature: 0.5,
permission: PermissionNext.merge(
defaults,
PermissionNext.fromConfig({

View File

@@ -12,11 +12,8 @@ Your output must be:
</task>
<rules>
- Title must be grammatically correct and read naturally - no word salad
- Never include tool names in the title (e.g. "read tool", "bash tool", "edit tool")
- Focus on the main topic or question the user needs to retrieve
- Vary your phrasing - avoid repetitive patterns like always starting with "Analyzing"
- When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it
- Use -ing verbs for actions (Debugging, Implementing, Analyzing)
- Keep exact: technical terms, numbers, filenames, HTTP codes
- Remove: the, this, my, a, an
- Never assume tech stack
@@ -32,12 +29,8 @@ Your output must be:
<examples>
"debug 500 errors in production" → Debugging production 500 errors
"refactor user service" → Refactoring user service
"why is app.js failing" → app.js failure investigation
"implement rate limiting" → Rate limiting implementation
"how do I connect postgres to my API" → Postgres API connection
"why is app.js failing" → Analyzing app.js failure
"implement rate limiting" → Implementing rate limiting
"how do I connect postgres to my API" → Connecting Postgres to API
"best practices for React hooks" → React hooks best practices
"@src/auth.ts can you add refresh token support" → Auth refresh token support
"@utils/parser.ts this is broken" → Parser bug fix
"look at @config.json" → Config review
"@App.tsx add dark mode toggle" → Dark mode toggle in App
</examples>

View File

@@ -653,10 +653,8 @@ export function Autocomplete(props: {
})
const height = createMemo(() => {
const count = options().length || 1
if (!store.visible) return Math.min(10, count)
positionTick()
return Math.min(10, count, Math.max(1, props.anchor().y))
if (options().length) return Math.min(10, options().length)
return 1
})
let scroll: ScrollBoxRenderable

View File

@@ -288,11 +288,11 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
createEffect(() => {
const theme = sync.data.config.theme
console.log("theme", theme)
if (theme) setStore("active", theme)
})
function init() {
resolveSystemTheme()
createEffect(() => {
getCustomThemes()
.then((custom) => {
setStore(
@@ -309,18 +309,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
setStore("ready", true)
}
})
}
onMount(init)
})
function resolveSystemTheme() {
console.log("resolveSystemTheme")
console.log("resolved system theme")
renderer
.getPalette({
size: 16,
})
.then((colors) => {
console.log(colors.palette)
if (!colors.palette[0]) {
if (store.active === "system") {
setStore(
@@ -344,9 +341,11 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
}
const renderer = useRenderer()
process.on("SIGUSR2", async () => {
renderer.clearPaletteCache()
init()
resolveSystemTheme()
const sdk = useSDK()
sdk.event.on("server.instance.disposed", () => {
resolveSystemTheme()
})
const values = createMemo(() => {

View File

@@ -61,10 +61,6 @@
"dark": "darkStep11",
"light": "lightStep11"
},
"selectedListItemText": {
"dark": "#0a0a0a",
"light": "#ffffff"
},
"background": {
"dark": "transparent",
"light": "transparent"

View File

@@ -77,10 +77,6 @@
"dark": "darkStep11",
"light": "lightStep11"
},
"selectedListItemText": {
"dark": "#0a0a0a",
"light": "#ffffff"
},
"background": {
"dark": "darkStep1",
"light": "lightStep1"

View File

@@ -37,40 +37,14 @@ export namespace Config {
export const state = Instance.state(async () => {
const auth = await Auth.all()
let result = await global()
// Load remote/well-known config first as the base layer (lowest precedence)
// This allows organizations to provide default configs that users can override
let result: Info = {}
for (const [key, value] of Object.entries(auth)) {
if (value.type === "wellknown") {
process.env[value.key] = value.token
log.debug("fetching remote config", { url: `${key}/.well-known/opencode` })
const response = await fetch(`${key}/.well-known/opencode`)
if (!response.ok) {
throw new Error(`failed to fetch remote config from ${key}: ${response.status}`)
}
const wellknown = (await response.json()) as any
const remoteConfig = wellknown.config ?? {}
// Add $schema to prevent load() from trying to write back to a non-existent file
if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
result = mergeConfigConcatArrays(
result,
await load(JSON.stringify(remoteConfig), `${key}/.well-known/opencode`),
)
log.debug("loaded remote config from well-known", { url: key })
}
}
// Global user config overrides remote config
result = mergeConfigConcatArrays(result, await global())
// Custom config path overrides global
// Override with custom config if provided
if (Flag.OPENCODE_CONFIG) {
result = mergeConfigConcatArrays(result, await loadFile(Flag.OPENCODE_CONFIG))
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
// Project config has highest precedence (overrides global and remote)
for (const file of ["opencode.jsonc", "opencode.json"]) {
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
for (const resolved of found.toReversed()) {
@@ -78,12 +52,19 @@ export namespace Config {
}
}
// Inline config content has highest precedence
if (Flag.OPENCODE_CONFIG_CONTENT) {
result = mergeConfigConcatArrays(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
}
for (const [key, value] of Object.entries(auth)) {
if (value.type === "wellknown") {
process.env[value.key] = value.token
const wellknown = (await fetch(`${key}/.well-known/opencode`).then((x) => x.json())) as any
result = mergeConfigConcatArrays(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
}
}
result.agent = result.agent || {}
result.mode = result.mode || {}
result.plugin = result.plugin || []

View File

@@ -111,8 +111,8 @@ export namespace Installation {
)
async function getBrewFormula() {
const tapFormula = await $`brew list --formula anomalyco/tap/opencode`.throws(false).quiet().text()
if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode"
const tapFormula = await $`brew list --formula sst/tap/opencode`.throws(false).quiet().text()
if (tapFormula.includes("opencode")) return "sst/tap/opencode"
const coreFormula = await $`brew list --formula opencode`.throws(false).quiet().text()
if (coreFormula.includes("opencode")) return "opencode"
return "opencode"

View File

@@ -0,0 +1,210 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { Log } from "../util/log"
import { Identifier } from "../id/id"
import { Plugin } from "../plugin"
import { Instance } from "../project/instance"
import { Wildcard } from "../util/wildcard"
export namespace Permission {
const log = Log.create({ service: "permission" })
function toKeys(pattern: Info["pattern"], type: string): string[] {
return pattern === undefined ? [type] : Array.isArray(pattern) ? pattern : [pattern]
}
function covered(keys: string[], approved: Record<string, boolean>): boolean {
const pats = Object.keys(approved)
return keys.every((k) => pats.some((p) => Wildcard.match(k, p)))
}
export const Info = z
.object({
id: z.string(),
type: z.string(),
pattern: z.union([z.string(), z.array(z.string())]).optional(),
sessionID: z.string(),
messageID: z.string(),
callID: z.string().optional(),
message: z.string(),
metadata: z.record(z.string(), z.any()),
time: z.object({
created: z.number(),
}),
})
.meta({
ref: "Permission",
})
export type Info = z.infer<typeof Info>
export const Event = {
Updated: BusEvent.define("permission.updated", Info),
Replied: BusEvent.define(
"permission.replied",
z.object({
sessionID: z.string(),
permissionID: z.string(),
response: z.string(),
}),
),
}
const state = Instance.state(
() => {
const pending: {
[sessionID: string]: {
[permissionID: string]: {
info: Info
resolve: () => void
reject: (e: any) => void
}
}
} = {}
const approved: {
[sessionID: string]: {
[permissionID: string]: boolean
}
} = {}
return {
pending,
approved,
}
},
async (state) => {
for (const pending of Object.values(state.pending)) {
for (const item of Object.values(pending)) {
item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID, item.info.metadata))
}
}
},
)
export function pending() {
return state().pending
}
export function list() {
const { pending } = state()
const result: Info[] = []
for (const items of Object.values(pending)) {
for (const item of Object.values(items)) {
result.push(item.info)
}
}
return result.sort((a, b) => a.id.localeCompare(b.id))
}
export async function ask(input: {
type: Info["type"]
message: Info["message"]
pattern?: Info["pattern"]
callID?: Info["callID"]
sessionID: Info["sessionID"]
messageID: Info["messageID"]
metadata: Info["metadata"]
}) {
const { pending, approved } = state()
log.info("asking", {
sessionID: input.sessionID,
messageID: input.messageID,
toolCallID: input.callID,
pattern: input.pattern,
})
const approvedForSession = approved[input.sessionID] || {}
const keys = toKeys(input.pattern, input.type)
if (covered(keys, approvedForSession)) return
const info: Info = {
id: Identifier.ascending("permission"),
type: input.type,
pattern: input.pattern,
sessionID: input.sessionID,
messageID: input.messageID,
callID: input.callID,
message: input.message,
metadata: input.metadata,
time: {
created: Date.now(),
},
}
switch (
await Plugin.trigger("permission.ask", info, {
status: "ask",
}).then((x) => x.status)
) {
case "deny":
throw new RejectedError(info.sessionID, info.id, info.callID, info.metadata)
case "allow":
return
}
pending[input.sessionID] = pending[input.sessionID] || {}
return new Promise<void>((resolve, reject) => {
pending[input.sessionID][info.id] = {
info,
resolve,
reject,
}
Bus.publish(Event.Updated, info)
})
}
export const Response = z.enum(["once", "always", "reject"])
export type Response = z.infer<typeof Response>
export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) {
log.info("response", input)
const { pending, approved } = state()
const match = pending[input.sessionID]?.[input.permissionID]
if (!match) return
delete pending[input.sessionID][input.permissionID]
Bus.publish(Event.Replied, {
sessionID: input.sessionID,
permissionID: input.permissionID,
response: input.response,
})
if (input.response === "reject") {
match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata))
return
}
match.resolve()
if (input.response === "always") {
approved[input.sessionID] = approved[input.sessionID] || {}
const approveKeys = toKeys(match.info.pattern, match.info.type)
for (const k of approveKeys) {
approved[input.sessionID][k] = true
}
const items = pending[input.sessionID]
if (!items) return
for (const item of Object.values(items)) {
const itemKeys = toKeys(item.info.pattern, item.info.type)
if (covered(itemKeys, approved[input.sessionID])) {
respond({
sessionID: item.info.sessionID,
permissionID: item.info.id,
response: input.response,
})
}
}
}
}
export class RejectedError extends Error {
constructor(
public readonly sessionID: string,
public readonly permissionID: string,
public readonly toolCallID?: string,
public readonly metadata?: Record<string, any>,
public readonly reason?: string,
) {
super(
reason !== undefined
? reason
: `The user rejected permission to use this specific tool call. You may try again with different parameters.`,
)
}
}
}

View File

@@ -127,30 +127,11 @@ export namespace PermissionNext {
throw new DeniedError(ruleset.filter((r) => Wildcard.match(request.permission, r.permission)))
if (rule.action === "ask") {
const id = input.id ?? Identifier.ascending("permission")
const info: Request = {
id,
...request,
}
const { Plugin } = await import("@/plugin")
const hook = await Plugin.trigger(
"permission.ask",
{
id,
type: request.permission,
pattern: request.patterns,
sessionID: request.sessionID,
messageID: request.tool?.messageID ?? "",
callID: request.tool?.callID,
title: request.permission,
metadata: request.metadata,
time: { created: Date.now() },
},
{ status: "ask" as "ask" | "deny" | "allow" },
).catch(() => ({ status: "ask" as const }))
if (hook.status === "deny")
throw new DeniedError(ruleset.filter((r) => Wildcard.match(request.permission, r.permission)))
if (hook.status === "allow") continue
return new Promise<void>((resolve, reject) => {
const info: Request = {
id,
...request,
}
s.pending[id] = {
info,
resolve,

View File

@@ -246,12 +246,7 @@ export namespace Server {
},
)
.use(async (c, next) => {
let directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
try {
directory = decodeURIComponent(directory)
} catch {
// fallback to original value
}
const directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
return Instance.provide({
directory,
init: InstanceBootstrap,

View File

@@ -23,7 +23,6 @@ import { CodeSearchTool } from "./codesearch"
import { Flag } from "@/flag/flag"
import { Log } from "@/util/log"
import { LspTool } from "./lsp"
import { Truncate } from "../session/truncation"
export namespace ToolRegistry {
const log = Log.create({ service: "tool.registry" })
@@ -65,11 +64,10 @@ export namespace ToolRegistry {
description: def.description,
execute: async (args, ctx) => {
const result = await def.execute(args as any, ctx)
const out = Truncate.output(result)
return {
title: "",
output: out.truncated ? out.content : result,
metadata: { truncated: out.truncated },
output: result,
metadata: {},
}
},
}),

View File

@@ -2,7 +2,6 @@ import z from "zod"
import type { MessageV2 } from "../session/message-v2"
import type { Agent } from "../agent/agent"
import type { PermissionNext } from "../permission/next"
import { Truncate } from "../session/truncation"
export namespace Tool {
interface Metadata {
@@ -53,7 +52,7 @@ export namespace Tool {
init: async (ctx) => {
const toolInfo = init instanceof Function ? await init(ctx) : init
const execute = toolInfo.execute
toolInfo.execute = async (args, ctx) => {
toolInfo.execute = (args, ctx) => {
try {
toolInfo.parameters.parse(args)
} catch (error) {
@@ -65,16 +64,7 @@ export namespace Tool {
{ cause: error },
)
}
const result = await execute(args, ctx)
const truncated = Truncate.output(result.output)
return {
...result,
output: truncated.content,
metadata: {
...result.metadata,
truncated: truncated.truncated,
},
}
return execute(args, ctx)
}
return toolInfo
},

View File

@@ -82,7 +82,7 @@ test("general agent denies todo tools", async () => {
const general = await Agent.get("general")
expect(general).toBeDefined()
expect(general?.mode).toBe("subagent")
expect(general?.hidden).toBeUndefined()
expect(general?.hidden).toBe(true)
expect(evalPerm(general, "todoread")).toBe("deny")
expect(evalPerm(general, "todowrite")).toBe("deny")
},

View File

@@ -1,7 +1,6 @@
import { test, expect, mock, afterEach } from "bun:test"
import { test, expect } from "bun:test"
import { Config } from "../../src/config/config"
import { Instance } from "../../src/project/instance"
import { Auth } from "../../src/auth"
import { tmpdir } from "../fixture/fixture"
import path from "path"
import fs from "fs/promises"
@@ -914,234 +913,3 @@ test("permission config preserves key order", async () => {
},
})
})
// MCP config merging tests
test("project config can override MCP server enabled status", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
// Simulates a base config (like from remote .well-known) with disabled MCP
await Bun.write(
path.join(dir, "opencode.jsonc"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
mcp: {
jira: {
type: "remote",
url: "https://jira.example.com/mcp",
enabled: false,
},
wiki: {
type: "remote",
url: "https://wiki.example.com/mcp",
enabled: false,
},
},
}),
)
// Project config enables just jira
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
mcp: {
jira: {
type: "remote",
url: "https://jira.example.com/mcp",
enabled: true,
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
// jira should be enabled (overridden by project config)
expect(config.mcp?.jira).toEqual({
type: "remote",
url: "https://jira.example.com/mcp",
enabled: true,
})
// wiki should still be disabled (not overridden)
expect(config.mcp?.wiki).toEqual({
type: "remote",
url: "https://wiki.example.com/mcp",
enabled: false,
})
},
})
})
test("MCP config deep merges preserving base config properties", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
// Base config with full MCP definition
await Bun.write(
path.join(dir, "opencode.jsonc"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
mcp: {
myserver: {
type: "remote",
url: "https://myserver.example.com/mcp",
enabled: false,
headers: {
"X-Custom-Header": "value",
},
},
},
}),
)
// Override just enables it, should preserve other properties
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
mcp: {
myserver: {
type: "remote",
url: "https://myserver.example.com/mcp",
enabled: true,
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.mcp?.myserver).toEqual({
type: "remote",
url: "https://myserver.example.com/mcp",
enabled: true,
headers: {
"X-Custom-Header": "value",
},
})
},
})
})
test("local .opencode config can override MCP from project config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
// Project config with disabled MCP
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
mcp: {
docs: {
type: "remote",
url: "https://docs.example.com/mcp",
enabled: false,
},
},
}),
)
// Local .opencode directory config enables it
const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
await Bun.write(
path.join(opencodeDir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
mcp: {
docs: {
type: "remote",
url: "https://docs.example.com/mcp",
enabled: true,
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.mcp?.docs?.enabled).toBe(true)
},
})
})
test("project config overrides remote well-known config", async () => {
const originalFetch = globalThis.fetch
let fetchedUrl: string | undefined
const mockFetch = mock((url: string | URL | Request) => {
const urlStr = url.toString()
if (urlStr.includes(".well-known/opencode")) {
fetchedUrl = urlStr
return Promise.resolve(
new Response(
JSON.stringify({
config: {
mcp: {
jira: {
type: "remote",
url: "https://jira.example.com/mcp",
enabled: false,
},
},
},
}),
{ status: 200 },
),
)
}
return originalFetch(url)
})
globalThis.fetch = mockFetch as unknown as typeof fetch
const originalAuthAll = Auth.all
Auth.all = mock(() =>
Promise.resolve({
"https://example.com": {
type: "wellknown" as const,
key: "TEST_TOKEN",
token: "test-token",
},
}),
)
try {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
// Project config enables jira (overriding remote default)
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
mcp: {
jira: {
type: "remote",
url: "https://jira.example.com/mcp",
enabled: true,
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
// Verify fetch was called for wellknown config
expect(fetchedUrl).toBe("https://example.com/.well-known/opencode")
// Project config (enabled: true) should override remote (enabled: false)
expect(config.mcp?.jira?.enabled).toBe(true)
},
})
} finally {
globalThis.fetch = originalFetch
Auth.all = originalAuthAll
}
})

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.1.6",
"version": "1.1.4",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.1.6",
"version": "1.1.4",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -19,11 +19,9 @@ export function createOpencodeClient(config?: Config & { directory?: string }) {
}
if (config?.directory) {
const isNonASCII = /[^\x00-\x7F]/.test(config.directory)
const encodedDirectory = isNonASCII ? encodeURIComponent(config.directory) : config.directory
config.headers = {
...config.headers,
"x-opencode-directory": encodedDirectory,
"x-opencode-directory": config.directory,
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.1.6",
"version": "1.1.4",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.1.6",
"version": "1.1.4",
"type": "module",
"license": "MIT",
"exports": {

View File

@@ -1,6 +1,6 @@
import fuzzysort from "fuzzysort"
import { entries, flatMap, groupBy, map, pipe } from "remeda"
import { createEffect, createMemo, createResource, on } from "solid-js"
import { createMemo, createResource } from "solid-js"
import { createStore } from "solid-js/store"
import { createList } from "solid-list"
@@ -86,14 +86,9 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
}
}
createEffect(
on(grouped, () => {
reset()
}),
)
const onInput = (value: string) => {
setStore("filter", value)
reset()
}
return {

View File

@@ -372,17 +372,3 @@ input:where([type="button"], [type="reset"], [type="submit"]),
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}
/*
Prevent iOS Safari from auto-zooming on input focus.
iOS WebKit zooms on any input with font-size < 16px as an accessibility feature.
*/
@media (hover: none) and (pointer: coarse) {
input,
select,
textarea,
[contenteditable="true"] {
font-size: 16px !important;
}
}

View File

@@ -9,7 +9,6 @@ import catppuccinThemeJson from "./themes/catppuccin.json"
import ayuThemeJson from "./themes/ayu.json"
import oneDarkProThemeJson from "./themes/onedarkpro.json"
import shadesOfPurpleThemeJson from "./themes/shadesofpurple.json"
import nightowlThemeJson from "./themes/nightowl.json"
export const oc1Theme = oc1ThemeJson as DesktopTheme
export const tokyonightTheme = tokyoThemeJson as DesktopTheme
@@ -21,7 +20,6 @@ export const catppuccinTheme = catppuccinThemeJson as DesktopTheme
export const ayuTheme = ayuThemeJson as DesktopTheme
export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme
export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme
export const nightowlTheme = nightowlThemeJson as DesktopTheme
export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
"oc-1": oc1Theme,
@@ -34,5 +32,4 @@ export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
ayu: ayuTheme,
onedarkpro: oneDarkProTheme,
shadesofpurple: shadesOfPurpleTheme,
nightowl: nightowlTheme,
}

View File

@@ -41,5 +41,4 @@ export {
ayuTheme,
oneDarkProTheme,
shadesOfPurpleTheme,
nightowlTheme,
} from "./default-themes"

View File

@@ -1,131 +0,0 @@
{
"$schema": "https://opencode.ai/desktop-theme.json",
"name": "Night Owl",
"id": "nightowl",
"light": {
"seeds": {
"neutral": "#f0f0f0",
"primary": "#4876d6",
"success": "#2aa298",
"warning": "#c96765",
"error": "#de3d3b",
"info": "#4876d6",
"interactive": "#4876d6",
"diffAdd": "#2aa298",
"diffDelete": "#de3d3b"
},
"overrides": {
"background-base": "#fbfbfb",
"background-weak": "#f0f0f0",
"background-strong": "#ffffff",
"background-stronger": "#ffffff",
"border-weak-base": "#d9d9d9",
"border-weak-hover": "#cccccc",
"border-weak-active": "#bfbfbf",
"border-weak-selected": "#4876d6",
"border-weak-disabled": "#e6e6e6",
"border-weak-focus": "#4876d6",
"border-base": "#c0c0c0",
"border-hover": "#b3b3b3",
"border-active": "#a6a6a6",
"border-selected": "#4876d6",
"border-disabled": "#d9d9d9",
"border-focus": "#4876d6",
"border-strong-base": "#90a7b2",
"border-strong-hover": "#7d9aa6",
"border-strong-active": "#6a8d9a",
"border-strong-selected": "#4876d6",
"border-strong-disabled": "#c0c0c0",
"border-strong-focus": "#4876d6",
"surface-diff-add-base": "#eaf8f6",
"surface-diff-delete-base": "#fbe9e9",
"surface-diff-hidden-base": "#e8f0fc",
"text-base": "#403f53",
"text-weak": "#7a8181",
"text-strong": "#1a1a1a",
"syntax-string": "#c96765",
"syntax-primitive": "#aa0982",
"syntax-property": "#4876d6",
"syntax-type": "#994cc3",
"syntax-constant": "#2aa298",
"syntax-info": "#4876d6",
"markdown-heading": "#4876d6",
"markdown-text": "#403f53",
"markdown-link": "#4876d6",
"markdown-link-text": "#2aa298",
"markdown-code": "#2aa298",
"markdown-block-quote": "#7a8181",
"markdown-emph": "#994cc3",
"markdown-strong": "#c96765",
"markdown-horizontal-rule": "#90a7b2",
"markdown-list-item": "#4876d6",
"markdown-list-enumeration": "#2aa298",
"markdown-image": "#4876d6",
"markdown-image-text": "#2aa298",
"markdown-code-block": "#403f53"
}
},
"dark": {
"seeds": {
"neutral": "#011627",
"primary": "#82aaff",
"success": "#c5e478",
"warning": "#ecc48d",
"error": "#ef5350",
"info": "#82aaff",
"interactive": "#82aaff",
"diffAdd": "#c5e478",
"diffDelete": "#ef5350"
},
"overrides": {
"background-base": "#011627",
"background-weak": "#0b253a",
"background-strong": "#001122",
"background-stronger": "#000c17",
"border-weak-base": "#1d3b53",
"border-weak-hover": "#234561",
"border-weak-active": "#2a506f",
"border-weak-selected": "#82aaff",
"border-weak-disabled": "#0f2132",
"border-weak-focus": "#82aaff",
"border-base": "#3a5a75",
"border-hover": "#456785",
"border-active": "#507494",
"border-selected": "#82aaff",
"border-disabled": "#1a3347",
"border-focus": "#82aaff",
"border-strong-base": "#5f7e97",
"border-strong-hover": "#6e8da6",
"border-strong-active": "#7d9cb5",
"border-strong-selected": "#82aaff",
"border-strong-disabled": "#2c4a63",
"border-strong-focus": "#82aaff",
"surface-diff-add-base": "#0a2e1a",
"surface-diff-delete-base": "#2d1b1b",
"surface-diff-hidden-base": "#0b253a",
"text-base": "#d6deeb",
"text-weak": "#5f7e97",
"text-strong": "#ffffff",
"syntax-string": "#ecc48d",
"syntax-primitive": "#f78c6c",
"syntax-property": "#82aaff",
"syntax-type": "#c5e478",
"syntax-constant": "#7fdbca",
"syntax-info": "#82aaff",
"markdown-heading": "#82aaff",
"markdown-text": "#d6deeb",
"markdown-link": "#82aaff",
"markdown-link-text": "#7fdbca",
"markdown-code": "#c5e478",
"markdown-block-quote": "#5f7e97",
"markdown-emph": "#c792ea",
"markdown-strong": "#ecc48d",
"markdown-horizontal-rule": "#5f7e97",
"markdown-list-item": "#82aaff",
"markdown-list-enumeration": "#7fdbca",
"markdown-image": "#82aaff",
"markdown-image-text": "#7fdbca",
"markdown-code-block": "#d6deeb"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.1.6",
"version": "1.1.4",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.1.6",
"version": "1.1.4",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

View File

@@ -32,74 +32,21 @@ different order of precedence.
Configuration files are **merged together**, not replaced.
:::
Configuration files are merged together, not replaced. Settings from the following config locations are combined. Later configs override earlier ones only for conflicting keys. Non-conflicting settings from all configs are preserved.
Configuration files are merged together, not replaced. Settings from the following config locations are combined. Where later configs override earlier ones only for conflicting keys. Non-conflicting settings from all configs are preserved.
For example, if your global config sets `theme: "opencode"` and `autoupdate: true`, and your project config sets `model: "anthropic/claude-sonnet-4-5"`, the final configuration will include all three settings.
---
### Precedence order
Config sources are loaded in this order (later sources override earlier ones):
1. **Remote config** (from `.well-known/opencode`) - organizational defaults
2. **Global config** (`~/.config/opencode/opencode.json`) - user preferences
3. **Custom config** (`OPENCODE_CONFIG` env var) - custom overrides
4. **Project config** (`opencode.json` in project) - project-specific settings
5. **`.opencode` directories** - agents, commands, plugins
6. **Inline config** (`OPENCODE_CONFIG_CONTENT` env var) - runtime overrides
This means project configs can override global defaults, and global configs can override remote organizational defaults.
---
### Remote
Organizations can provide default configuration via the `.well-known/opencode` endpoint. This is fetched automatically when you authenticate with a provider that supports it.
Remote config is loaded first, serving as the base layer. All other config sources (global, project) can override these defaults.
For example, if your organization provides MCP servers that are disabled by default:
```json title="Remote config from .well-known/opencode"
{
"mcp": {
"jira": {
"type": "remote",
"url": "https://jira.example.com/mcp",
"enabled": false
}
}
}
```
You can enable specific servers in your local config:
```json title="opencode.json"
{
"mcp": {
"jira": {
"type": "remote",
"url": "https://jira.example.com/mcp",
"enabled": true
}
}
}
```
---
### Global
Place your global OpenCode config in `~/.config/opencode/opencode.json`. Use global config for user-wide preferences like themes, providers, or keybinds.
Global config overrides remote organizational defaults.
Place your global OpenCode config in `~/.config/opencode/opencode.json`. You'll want to use the global config for things like themes, providers, or keybinds.
---
### Per project
Add `opencode.json` in your project root. Project config has the highest precedence among standard config files - it overrides both global and remote configs.
You can also add a `opencode.json` in your project. Settings from this config are merged with and can override the global config. This is useful for configuring providers or modes specific to your project.
:::tip
Place project specific config in the root of your project.
@@ -113,20 +60,20 @@ This is also safe to be checked into Git and uses the same schema as the global
### Custom path
Specify a custom config file path using the `OPENCODE_CONFIG` environment variable.
You can also specify a custom config file path using the `OPENCODE_CONFIG` environment variable.
```bash
export OPENCODE_CONFIG=/path/to/my/custom-config.json
opencode run "Hello world"
```
Custom config is loaded between global and project configs in the precedence order.
Settings from this config are merged with and **can override** the global and project configs.
---
### Custom directory
Specify a custom config directory using the `OPENCODE_CONFIG_DIR`
You can specify a custom config directory using the `OPENCODE_CONFIG_DIR`
environment variable. This directory will be searched for agents, commands,
modes, and plugins just like the standard `.opencode` directory, and should
follow the same structure.

View File

@@ -76,11 +76,9 @@ You can also install it with the following commands:
- **Using Homebrew on macOS and Linux**
```bash
brew install anomalyco/tap/opencode
brew install opencode
```
> We recommend using the OpenCode tap for the most up to date releases. The official `brew install opencode` formula is maintained by the Homebrew team and is updated less frequently.
- **Using Paru on Arch Linux**
```bash

View File

@@ -44,29 +44,6 @@ You can also disable a server by setting `enabled` to `false`. This is useful if
---
### Overriding remote defaults
Organizations can provide default MCP servers via their `.well-known/opencode` endpoint. These servers may be disabled by default, allowing users to opt-in to the ones they need.
To enable a specific server from your organization's remote config, add it to your local config with `enabled: true`:
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"jira": {
"type": "remote",
"url": "https://jira.example.com/mcp",
"enabled": true
}
}
}
```
Your local config values override the remote defaults. See [config precedence](/docs/config#precedence-order) for more details.
---
## Local
Add local MCP servers using `type` to `"local"` within the MCP object.

View File

@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.1.6",
"version": "1.1.4",
"publisher": "sst-dev",
"repository": {
"type": "git",