diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index ab2f9c0f2..6c38495d1 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -4,7 +4,7 @@ on: push: branches: - dev - - windows + - fix-build - v0 concurrency: ${{ github.workflow }}-${{ github.ref }} diff --git a/README.md b/README.md index 551456f32..2a5e9cea6 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ scoop bucket add extras; scoop install extras/opencode # Windows choco install opencode # Windows brew install opencode # macOS and Linux paru -S opencode-bin # Arch Linux +mise use --pin -g ubi:sst/opencode # Any OS ``` > [!TIP] @@ -58,6 +59,10 @@ For more info on how to configure OpenCode [**head over to our docs**](https://o If you're interested in contributing to OpenCode, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request. +### Building on OpenCode + +If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in anyway. + ### FAQ #### How is this different than Claude Code? diff --git a/STATS.md b/STATS.md index 627771e9b..0cf4d185f 100644 --- a/STATS.md +++ b/STATS.md @@ -140,3 +140,5 @@ | 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) | | 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) | | 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) | +| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) | +| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) | diff --git a/bun.lock b/bun.lock index 28add27ef..3b827ff7e 100644 --- a/bun.lock +++ b/bun.lock @@ -40,7 +40,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.65", + "version": "1.0.68", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -67,7 +67,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.65", + "version": "1.0.68", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -91,7 +91,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.65", + "version": "1.0.68", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -115,7 +115,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.65", + "version": "1.0.68", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -155,7 +155,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.65", + "version": "1.0.68", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -171,7 +171,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.65", + "version": "1.0.68", "bin": { "opencode": "./bin/opencode", }, @@ -189,8 +189,8 @@ "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", - "@opentui/core": "0.1.42", - "@opentui/solid": "0.1.42", + "@opentui/core": "0.1.45", + "@opentui/solid": "0.1.45", "@parcel/watcher": "2.5.1", "@pierre/precision-diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -249,7 +249,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.65", + "version": "1.0.68", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -269,7 +269,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.65", + "version": "1.0.68", "devDependencies": { "@hey-api/openapi-ts": "0.81.0", "@tsconfig/node22": "catalog:", @@ -280,7 +280,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.65", + "version": "1.0.68", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -293,7 +293,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.65", + "version": "1.0.68", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -323,7 +323,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.65", + "version": "1.0.68", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -966,21 +966,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.42", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.42", "@opentui/core-darwin-x64": "0.1.42", "@opentui/core-linux-arm64": "0.1.42", "@opentui/core-linux-x64": "0.1.42", "@opentui/core-win32-arm64": "0.1.42", "@opentui/core-win32-x64": "0.1.42", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-oV2xHBB2HaNiGvaV6R0C8GmniNJSsLKop4APq4FrLyCYberc6vZcATSHcA5YT9krdvHbBDOOn9RI2oaVJYRbUQ=="], + "@opentui/core": ["@opentui/core@0.1.45", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.45", "@opentui/core-darwin-x64": "0.1.45", "@opentui/core-linux-arm64": "0.1.45", "@opentui/core-linux-x64": "0.1.45", "@opentui/core-win32-arm64": "0.1.45", "@opentui/core-win32-x64": "0.1.45", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-iGzU1rZNfBfnw9TEHppvt1B9gtOC/jl0KU3fezCoXoMRCcy3STYLhZbEsYM6cX6AnRK85DqTzC6E6eWWUwGMlA=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.42", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sk5b/kh/y8HUJ7stGA5ydkajJX/z2OiGqSm+wn6XIoqdDavxQaFoQOt1PCuCqaxqZWJcXZ6OmISDVagZPUsPuw=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.45", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7pzgeYTkR88bufNY4ZnUBnqjsmR9wjx/wH81YyAtc8Hnp/6l+tU+1DHEJEseIArJKRIETwMMNs0W4oxSyQEAJg=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.42", "", { "os": "darwin", "cpu": "x64" }, "sha512-b0FKTw+t/wlJg4u+wTurWzbQe47gExkjguaGSUua0m0vybrkkvbUvmrADr+yivCjxcPAhSZ3lOOVU3uZuWsNqw=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.45", "", { "os": "darwin", "cpu": "x64" }, "sha512-N1dk4T57/qZorpshJqJaObp9PT3WP8F7aRX5bSjcPSeHDZkfkWYnfIhgc5tRzCu1FqYx7M3HEUlI21YKuThCyQ=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.42", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vy8BrjJpv2f56JAsYmv4PkC+2HsCv8Gh0ErrlIJQ8L4h29oWabS44m0uxFdvjuTDgKpCJzOScsxsy1VGzSd9rw=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.45", "", { "os": "linux", "cpu": "arm64" }, "sha512-Rm1DeH80wGHgXZkIMzP9KRrbZOMJJZxG1f2CO7NEt5/sE2buPQsLF4HcqwycIFJvBdSnpTovzm+hK1sT08zr4w=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.42", "", { "os": "linux", "cpu": "x64" }, "sha512-cO+13E1HIAPUdV/DRdKotHFAxsLc+ipbbFKGAuu/msfvywCnnNs86w22yeMg0cEqx7aBocWWT1XfJEHDJLFOqw=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.45", "", { "os": "linux", "cpu": "x64" }, "sha512-0Olnj36Sqb1ukCngvf0SPBmZHtT2p9SMYWj6EZgx6KkHTNbDs00/sA8mctViX1B/AJW5v0+E3FT1SgyDwvBDbw=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.42", "", { "os": "win32", "cpu": "arm64" }, "sha512-xpLhODjOWh7gMOSrKIldb4v6hR0TGyz6kjckDKwcjUv3LGbLJuSly+3O/zuWWS60dt56G1X4A0OyjWwiGZjc0g=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.45", "", { "os": "win32", "cpu": "arm64" }, "sha512-cwo4nSiqSeesleNaWD/PqbWgiXesd6NOwbH6dkDfW5CNenRRm9kNtm+Z5EXYfIf4wcCwyeqOlQZXZgMVNL3cIg=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.42", "", { "os": "win32", "cpu": "x64" }, "sha512-pao5XdAln93WWPdsTF+V+HccZ5d1ijSmv0OoBbkjkVbP+tiN41yxNqg/7jzW9IiAakYsvmpKV+3ixi/dlBEvOQ=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.45", "", { "os": "win32", "cpu": "x64" }, "sha512-11ZiCoAya94oNmyav3OB5FdeMoR0bKlQLMK/ObxwWSJW5N7Ccw+2BNz1VQZNSvUQfJeWWXRaVz7XesOhFu/tLg=="], - "@opentui/solid": ["@opentui/solid@0.1.42", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.42", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-4TNlEtatZ4n9TcKPWSF/EoaPaLmZuFVJ4hHh9wRggNaGrmDlmJ+9N/8oEKXETt+oRDX/1CdowAaTOVfaqb1t6g=="], + "@opentui/solid": ["@opentui/solid@0.1.45", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.45", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-Mtn+Kz9osYx/3PMJUMYVHjQch1r4DgEi8V3Es2/vD7ha/kMg2PMTDg+J5RWOheUkXCyfm3G9imxlOIG+aEcGuA=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -1684,15 +1684,15 @@ "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], - "bun-webgpu": ["bun-webgpu@0.1.3", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.3", "bun-webgpu-darwin-x64": "^0.1.3", "bun-webgpu-linux-x64": "^0.1.3", "bun-webgpu-win32-x64": "^0.1.3" } }, "sha512-IXFxaIi4rgsEEpl9n/QVDm5RajCK/0FcOXZeMb52YRjoiAR1YVYK5hLrXT8cm+KDi6LVahA9GJFqOR4yiloVCw=="], + "bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="], - "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-KkNQ9gT7dxGDndQaHTTHss9miukqpczML3pO2nZJoT/nITwe9lw3ZGFJMujkW41BUQ1mDYKFgo5nBGf9xYHPAg=="], + "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eDgLN9teKTfmvrCqgwwmWNsNszxYs7IZdCqk0S1DCarvMhr4wcajoSBlA/nQA0/owwLduPTS8xxCnQp4/N/gDg=="], - "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-TODWnMUbCoqD/wqzlB3oGOBIUWIFly0lqMeBFz/MBV+ndjbnkNrP9huaZJCTkCVEPKGtd1FCM3ExZUtBbnGziA=="], + "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-X+PjwJUWenUmdQBP8EtdItMyieQ6Nlpn+BH518oaouDiSnWj5+b0Y7DNDZJq7Ezom4EaxmqL/uGYZK3aCQ7CXg=="], - "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-lVHORoVu1G61XVM8CRRqUsqr6w8kMlpuSpbPGpKUpmvrsoay6ymXAhT5lRPKyrGNamHUQTknmWdI59aRDCfLtQ=="], + "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zMLs2YIGB+/jxrYFXaFhVKX/GBt05UTF45lc9srcHc9JXGjEj+12CIo1CHLTAWatXMTqt0Jsu6ukWEoWVT/ayA=="], - "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-vlspsFffctJlBnFfs2lW3QgDD6LyFu8VT18ryID7Qka5poTj0clGVRxz7DFRi7yva3GovEGw/82z/WVc5US8Pw=="], + "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Z5yAK28xrcm8Wb5k7TZ8FJKpOI/r+aVCRdlHYAqI2SDJFN3nD4mJs900X6kNVmG/xFzb5yOuKVYWGg+6ZXWbyA=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 261e30b50..0e601ab7c 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,7 +7,7 @@ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "1.0.65" + "version": "1.0.68" }, "dependencies": { "@ibm/plex": "6.4.1", diff --git a/packages/console/app/src/routes/workspace/[id]/usage-section.module.css b/packages/console/app/src/routes/workspace/[id]/usage-section.module.css index 1a772ba87..2bd331bd9 100644 --- a/packages/console/app/src/routes/workspace/[id]/usage-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/usage-section.module.css @@ -1,88 +1,111 @@ -.root { - [data-component="empty-state"] { - padding: var(--space-20) var(--space-6); - text-align: center; - border: 1px dashed var(--color-border); - border-radius: var(--border-radius-sm); - display: flex; - flex-direction: column; - gap: var(--space-2); +/* Empty state */ +[data-component="empty-state"] { + padding: var(--space-20) var(--space-6); + text-align: center; + border: 1px dashed var(--color-border); + border-radius: var(--border-radius-sm); - p { - line-height: 1.5; - font-size: var(--font-size-sm); - color: var(--color-text-muted); - } - } - - [data-slot="usage-table"] { - overflow-x: auto; - } - - [data-slot="usage-table-element"] { - width: 100%; - border-collapse: collapse; + p { font-size: var(--font-size-sm); + color: var(--color-text-muted); + } +} - thead { - border-bottom: 1px solid var(--color-border); +/* Table container */ +[data-slot="usage-table"] { + overflow-x: auto; +} + +/* Table element */ +[data-slot="usage-table-element"] { + width: 100%; + border-collapse: collapse; + font-size: var(--font-size-sm); + + thead { + border-bottom: 1px solid var(--color-border); + } + + th { + padding: var(--space-3) var(--space-4); + text-align: left; + font-weight: normal; + color: var(--color-text-muted); + text-transform: uppercase; + } + + td { + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--color-border-muted); + color: var(--color-text-muted); + font-family: var(--font-mono); + + &[data-slot="usage-date"] { + color: var(--color-text); } - th { - padding: var(--space-3) var(--space-4); - text-align: left; - font-weight: normal; - color: var(--color-text-muted); - text-transform: uppercase; + &[data-slot="usage-model"] { + font-family: var(--font-sans); + color: var(--color-text-secondary); + max-width: 200px; + word-break: break-word; } - td { - padding: var(--space-3) var(--space-4); - border-bottom: 1px solid var(--color-border-muted); - color: var(--color-text-muted); - font-family: var(--font-mono); + &[data-slot="usage-cost"] { + color: var(--color-text); + font-weight: 500; + } + } - &[data-slot="usage-date"] { - color: var(--color-text); - } + tbody tr:last-child td { + border-bottom: none; + } +} - &[data-slot="usage-model"] { - font-family: var(--font-sans); - font-weight: 400; - color: var(--color-text-secondary); - max-width: 200px; - word-break: break-word; - } +/* Pagination */ +[data-slot="pagination"] { + display: flex; + justify-content: flex-end; + gap: var(--space-2); + padding: var(--space-4) 0; + border-top: 1px solid var(--color-border-muted); + margin-top: var(--space-2); - &[data-slot="usage-cost"] { - color: var(--color-text); - } + button { + padding: var(--space-2) var(--space-4); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + color: var(--color-text); + font-size: var(--font-size-sm); + cursor: pointer; + transition: all 0.15s ease; + + &:hover:not(:disabled) { + background: var(--color-bg-tertiary); + border-color: var(--color-border-hover); } - tbody tr { - &:last-child td { - border-bottom: none; - } - } - - @media (max-width: 40rem) { - th, - td { - padding: var(--space-2) var(--space-3); - font-size: var(--font-size-xs); - } - - th { - &:nth-child(2) /* Model */ { - display: none; - } - } - - td { - &:nth-child(2) /* Model */ { - display: none; - } - } + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } +} + +/* Mobile responsive */ +@media (max-width: 40rem) { + [data-slot="usage-table-element"] { + th, + td { + padding: var(--space-2) var(--space-3); + font-size: var(--font-size-xs); + } + + /* Hide Model column on mobile */ + th:nth-child(2), + td:nth-child(2) { + display: none; } } } diff --git a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx index 3618bb7e2..6b3d1af60 100644 --- a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx @@ -1,91 +1,59 @@ import { Billing } from "@opencode-ai/console-core/billing.js" -import { query, useParams, createAsync } from "@solidjs/router" -import { createMemo, For, Show } from "solid-js" +import { createAsync, query, useParams } from "@solidjs/router" +import { createMemo, For, Show, createEffect } from "solid-js" import { formatDateUTC, formatDateForTable } from "../common" import { withActor } from "~/context/auth.withActor" -import styles from "./usage-section.module.css" +import "./usage-section.module.css" +import { createStore } from "solid-js/store" -const getUsageInfo = query(async (workspaceID: string) => { +const PAGE_SIZE = 50 + +async function getUsageInfo(workspaceID: string, page: number) { "use server" return withActor(async () => { - return await Billing.usages() + return await Billing.usages(page, PAGE_SIZE) }, workspaceID) -}, "usage.list") +} + +const queryUsageInfo = query(getUsageInfo, "usage.list") export function UsageSection() { const params = useParams() - // ORIGINAL CODE - COMMENTED OUT FOR TESTING - const usage = createAsync(() => getUsageInfo(params.id!)) + const usage = createAsync(() => queryUsageInfo(params.id!, 0)) + const [store, setStore] = createStore({ page: 0, usage: [] as Awaited> }) - // DUMMY DATA FOR TESTING - // const usage = () => [ - // { - // timeCreated: new Date(Date.now() - 86400000 * 0).toISOString(), // Today - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 1247, - // outputTokens: 423, - // cost: 125400000, // $1.254 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 0.5).toISOString(), // 12 hours ago - // model: "claude-3-haiku-20240307", - // inputTokens: 892, - // outputTokens: 156, - // cost: 23500000, // $0.235 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // Yesterday - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 2134, - // outputTokens: 687, - // cost: 234700000, // $2.347 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 1.3).toISOString(), // 1.3 days ago - // model: "gpt-4o-mini", - // inputTokens: 567, - // outputTokens: 234, - // cost: 8900000, // $0.089 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 2).toISOString(), // 2 days ago - // model: "claude-3-opus-20240229", - // inputTokens: 1893, - // outputTokens: 945, - // cost: 445600000, // $4.456 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 2.7).toISOString(), // 2.7 days ago - // model: "gpt-4o", - // inputTokens: 1456, - // outputTokens: 532, - // cost: 156800000, // $1.568 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 3).toISOString(), // 3 days ago - // model: "claude-3-haiku-20240307", - // inputTokens: 634, - // outputTokens: 89, - // cost: 12300000, // $0.123 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 4).toISOString(), // 4 days ago - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 3245, - // outputTokens: 1123, - // cost: 387200000, // $3.872 - // }, - // ] + createEffect(() => { + setStore({ usage: usage() }) + }, [usage]) + + const hasResults = createMemo(() => store.usage && store.usage.length > 0) + const canGoPrev = createMemo(() => store.page > 0) + const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE) + + const goPrev = async () => { + const usage = await getUsageInfo(params.id!, store.page - 1) + setStore({ + page: store.page - 1, + usage, + }) + } + const goNext = async () => { + const usage = await getUsageInfo(params.id!, store.page + 1) + setStore({ + page: store.page + 1, + usage, + }) + } return ( -
+

Usage History

Recent API usage and costs.

0} + when={hasResults()} fallback={

Make your first API call to get started.

@@ -103,7 +71,7 @@ export function UsageSection() { - + {(usage) => { const date = createMemo(() => new Date(usage.timeCreated)) return ( @@ -121,6 +89,16 @@ export function UsageSection() { + +
+ + +
+
diff --git a/packages/console/core/package.json b/packages/console/core/package.json index e1c846c3a..85922af91 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.0.65", + "version": "1.0.68", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index 348718146..049ee29bb 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -57,14 +57,15 @@ export namespace Billing { ) } - export const usages = async () => { + export const usages = async (page = 0, pageSize = 50) => { return await Database.use((tx) => tx .select() .from(UsageTable) .where(eq(UsageTable.workspaceID, Actor.workspace())) .orderBy(sql`${UsageTable.timeCreated} DESC`) - .limit(100), + .limit(pageSize) + .offset(page * pageSize), ) } diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 3f2143230..8226d9279 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.65", + "version": "1.0.68", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index df94c8c07..cfa2c0b24 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.65", + "version": "1.0.68", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 06a3febd8..9c444bc00 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "1.0.65", + "version": "1.0.68", "description": "", "type": "module", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 5572d0d63..9ef9f48d0 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The AI coding agent built for the terminal" -version = "1.0.65" +version = "1.0.68" 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.65/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.68/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.65/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.68/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.65/opencode-linux-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.68/opencode-linux-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.65/opencode-linux-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.68/opencode-linux-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.65/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.68/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 911ac02c6..5d1b004c3 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.65", + "version": "1.0.68", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 764a40683..3e376139a 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.65", + "version": "1.0.68", "name": "opencode", "type": "module", "private": true, @@ -54,8 +54,8 @@ "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", - "@opentui/core": "0.1.42", - "@opentui/solid": "0.1.42", + "@opentui/core": "0.1.45", + "@opentui/solid": "0.1.45", "@parcel/watcher": "2.5.1", "@pierre/precision-diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 502baed02..2e3c4ee5c 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -1,6 +1,5 @@ #!/usr/bin/env bun -import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin" import path from "path" import fs from "fs" import { $ } from "bun" @@ -10,6 +9,9 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const dir = path.resolve(__dirname, "..") +const solidPluginPath = path.resolve(dir, "node_modules/@opentui/solid/scripts/solid-plugin.ts") +const solidPlugin = (await import(solidPluginPath)).default + process.chdir(dir) import pkg from "../package.json" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx index 4b02d558a..4fd60dd36 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx @@ -4,7 +4,7 @@ import { onMount } from "solid-js" import { createStore, produce } from "solid-js/store" import { clone } from "remeda" import { createSimpleContext } from "../../context/helper" -import { appendFile } from "fs/promises" +import { appendFile, writeFile } from "fs/promises" import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk" export type PromptInfo = { @@ -24,6 +24,8 @@ export type PromptInfo = { )[] } +const MAX_HISTORY_ENTRIES = 50 + export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({ name: "PromptHistory", init: () => { @@ -33,8 +35,23 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create const lines = text .split("\n") .filter(Boolean) - .map((line) => JSON.parse(line)) - setStore("history", lines as PromptInfo[]) + .map((line) => { + try { + return JSON.parse(line) + } catch { + return null + } + }) + .filter((line): line is PromptInfo => line !== null) + .slice(-MAX_HISTORY_ENTRIES) + + setStore("history", lines) + + // Rewrite file with only valid entries to self-heal corruption + if (lines.length > 0) { + const content = lines.map((line) => JSON.stringify(line)).join("\n") + "\n" + writeFile(historyFile.name!, content).catch(() => {}) + } }) const [store, setStore] = createStore({ @@ -64,14 +81,26 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create return store.history.at(store.index) }, append(item: PromptInfo) { - item = clone(item) - appendFile(historyFile.name!, JSON.stringify(item) + "\n") + const entry = clone(item) + let trimmed = false setStore( produce((draft) => { - draft.history.push(item) + draft.history.push(entry) + if (draft.history.length > MAX_HISTORY_ENTRIES) { + draft.history = draft.history.slice(-MAX_HISTORY_ENTRIES) + trimmed = true + } draft.index = 0 }), ) + + if (trimmed) { + const content = store.history.map((line) => JSON.stringify(line)).join("\n") + "\n" + writeFile(historyFile.name!, content).catch(() => {}) + return + } + + appendFile(historyFile.name!, JSON.stringify(entry) + "\n").catch(() => {}) }, } }, diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 2a79620e6..a4a4d876e 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -9,6 +9,7 @@ import catppuccin from "./theme/catppuccin.json" with { type: "json" } import cobalt2 from "./theme/cobalt2.json" with { type: "json" } import dracula from "./theme/dracula.json" with { type: "json" } import everforest from "./theme/everforest.json" with { type: "json" } +import flexoki from "./theme/flexoki.json" with { type: "json" } import github from "./theme/github.json" with { type: "json" } import gruvbox from "./theme/gruvbox.json" with { type: "json" } import kanagawa from "./theme/kanagawa.json" with { type: "json" } @@ -105,6 +106,7 @@ export const DEFAULT_THEMES: Record = { cobalt2, dracula, everforest, + flexoki, github, gruvbox, kanagawa, @@ -128,7 +130,10 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") { const defs = theme.defs ?? {} function resolveColor(c: ColorValue): RGBA { if (c instanceof RGBA) return c - if (typeof c === "string") return c.startsWith("#") ? RGBA.fromHex(c) : resolveColor(defs[c]) + if (typeof c === "string") { + if (c === "transparent" || c === "none") return RGBA.fromInts(0, 0, 0, 0) + return c.startsWith("#") ? RGBA.fromHex(c) : resolveColor(defs[c]) + } return resolveColor(c[mode]) } return Object.fromEntries( @@ -864,18 +869,21 @@ function generateSyntax(theme: Theme) { scope: ["diff.plus"], style: { foreground: theme.diffAdded, + background: theme.diffAddedBg, }, }, { scope: ["diff.minus"], style: { foreground: theme.diffRemoved, + background: theme.diffRemovedBg, }, }, { scope: ["diff.delta"], style: { foreground: theme.diffContext, + background: theme.diffContextBg, }, }, { diff --git a/packages/opencode/src/cli/cmd/tui/context/theme/flexoki.json b/packages/opencode/src/cli/cmd/tui/context/theme/flexoki.json new file mode 100644 index 000000000..e525705dd --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/context/theme/flexoki.json @@ -0,0 +1,237 @@ +{ + "$schema": "https://opencode.ai/theme.json", + "defs": { + "black": "#100F0F", + "base950": "#1C1B1A", + "base900": "#282726", + "base850": "#343331", + "base800": "#403E3C", + "base700": "#575653", + "base600": "#6F6E69", + "base500": "#878580", + "base300": "#B7B5AC", + "base200": "#CECDC3", + "base150": "#DAD8CE", + "base100": "#E6E4D9", + "base50": "#F2F0E5", + "paper": "#FFFCF0", + "red400": "#D14D41", + "red600": "#AF3029", + "orange400": "#DA702C", + "orange600": "#BC5215", + "yellow400": "#D0A215", + "yellow600": "#AD8301", + "green400": "#879A39", + "green600": "#66800B", + "cyan400": "#3AA99F", + "cyan600": "#24837B", + "blue400": "#4385BE", + "blue600": "#205EA6", + "purple400": "#8B7EC8", + "purple600": "#5E409D", + "magenta400": "#CE5D97", + "magenta600": "#A02F6F" + }, + "theme": { + "primary": { + "dark": "orange400", + "light": "blue600" + }, + "secondary": { + "dark": "blue400", + "light": "purple600" + }, + "accent": { + "dark": "purple400", + "light": "orange600" + }, + "error": { + "dark": "red400", + "light": "red600" + }, + "warning": { + "dark": "orange400", + "light": "orange600" + }, + "success": { + "dark": "green400", + "light": "green600" + }, + "info": { + "dark": "cyan400", + "light": "cyan600" + }, + "text": { + "dark": "base200", + "light": "black" + }, + "textMuted": { + "dark": "base600", + "light": "base600" + }, + "background": { + "dark": "black", + "light": "paper" + }, + "backgroundPanel": { + "dark": "base950", + "light": "base50" + }, + "backgroundElement": { + "dark": "base900", + "light": "base100" + }, + "border": { + "dark": "base700", + "light": "base300" + }, + "borderActive": { + "dark": "base600", + "light": "base500" + }, + "borderSubtle": { + "dark": "base800", + "light": "base200" + }, + "diffAdded": { + "dark": "green400", + "light": "green600" + }, + "diffRemoved": { + "dark": "red400", + "light": "red600" + }, + "diffContext": { + "dark": "base600", + "light": "base600" + }, + "diffHunkHeader": { + "dark": "blue400", + "light": "blue600" + }, + "diffHighlightAdded": { + "dark": "green400", + "light": "green600" + }, + "diffHighlightRemoved": { + "dark": "red400", + "light": "red600" + }, + "diffAddedBg": { + "dark": "#1A2D1A", + "light": "#D5E5D5" + }, + "diffRemovedBg": { + "dark": "#2D1A1A", + "light": "#F7D8DB" + }, + "diffContextBg": { + "dark": "base950", + "light": "base50" + }, + "diffLineNumber": { + "dark": "base600", + "light": "base600" + }, + "diffAddedLineNumberBg": { + "dark": "#152515", + "light": "#C5D5C5" + }, + "diffRemovedLineNumberBg": { + "dark": "#251515", + "light": "#E7C8CB" + }, + "markdownText": { + "dark": "base200", + "light": "black" + }, + "markdownHeading": { + "dark": "purple400", + "light": "purple600" + }, + "markdownLink": { + "dark": "blue400", + "light": "blue600" + }, + "markdownLinkText": { + "dark": "cyan400", + "light": "cyan600" + }, + "markdownCode": { + "dark": "cyan400", + "light": "cyan600" + }, + "markdownBlockQuote": { + "dark": "yellow400", + "light": "yellow600" + }, + "markdownEmph": { + "dark": "yellow400", + "light": "yellow600" + }, + "markdownStrong": { + "dark": "orange400", + "light": "orange600" + }, + "markdownHorizontalRule": { + "dark": "base600", + "light": "base600" + }, + "markdownListItem": { + "dark": "orange400", + "light": "orange600" + }, + "markdownListEnumeration": { + "dark": "cyan400", + "light": "cyan600" + }, + "markdownImage": { + "dark": "magenta400", + "light": "magenta600" + }, + "markdownImageText": { + "dark": "cyan400", + "light": "cyan600" + }, + "markdownCodeBlock": { + "dark": "base200", + "light": "black" + }, + "syntaxComment": { + "dark": "base600", + "light": "base600" + }, + "syntaxKeyword": { + "dark": "green400", + "light": "green600" + }, + "syntaxFunction": { + "dark": "orange400", + "light": "orange600" + }, + "syntaxVariable": { + "dark": "blue400", + "light": "blue600" + }, + "syntaxString": { + "dark": "cyan400", + "light": "cyan600" + }, + "syntaxNumber": { + "dark": "purple400", + "light": "purple600" + }, + "syntaxType": { + "dark": "yellow400", + "light": "yellow600" + }, + "syntaxOperator": { + "dark": "base300", + "light": "base600" + }, + "syntaxPunctuation": { + "dark": "base300", + "light": "base600" + } + } +} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx index a59fad7b1..07e1a1eb4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx @@ -3,6 +3,7 @@ import { useSync } from "@tui/context/sync" import { DialogSelect } from "@tui/ui/dialog-select" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" +import { Clipboard } from "@tui/util/clipboard" import type { PromptInfo } from "@tui/component/prompt/history" export function DialogMessage(props: { @@ -54,6 +55,26 @@ export function DialogMessage(props: { dialog.clear() }, }, + { + title: "Copy", + value: "message.copy", + description: "copy message text to clipboard", + onSelect: async (dialog) => { + const msg = message() + if (!msg) return + + const parts = sync.data.part[msg.id] + const text = parts.reduce((agg, part) => { + if (part.type === "text" && !part.synthetic) { + agg += part.text + } + return agg + }, "") + + await Clipboard.copy(text) + dialog.clear() + }, + }, { title: "Fork", value: "session.fork", diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 608695894..fb8f9860a 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1251,9 +1251,7 @@ ToolRegistry.register({ container: "block", render(props) { const { theme, syntax } = useTheme() - const lines = createMemo(() => { - return props.input.content?.split("\n") ?? [] - }) + const lines = createMemo(() => props.input.content?.split("\n") ?? [], [] as string[]) const code = createMemo(() => { if (!props.input.content) return "" const text = props.input.content diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index ee83a3afc..9ba799f09 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -60,13 +60,19 @@ export function Sidebar(props: { sessionID: string }) { 0}> - setMcpExpanded(!mcpExpanded())}> - {mcpExpanded() ? "▼" : "▶"} + Object.keys(sync.data.mcp).length > 2 && setMcpExpanded(!mcpExpanded())} + > + 2}> + {mcpExpanded() ? "▼" : "▶"} + MCP - + {([key, item]) => ( @@ -100,13 +106,19 @@ export function Sidebar(props: { sessionID: string }) { 0}> - setLspExpanded(!lspExpanded())}> - {lspExpanded() ? "▼" : "▶"} + sync.data.lsp.length > 2 && setLspExpanded(!lspExpanded())} + > + 2}> + {lspExpanded() ? "▼" : "▶"} + LSP - + {(item) => ( @@ -132,13 +144,19 @@ export function Sidebar(props: { sessionID: string }) { 0}> - setTodoExpanded(!todoExpanded())}> - {todoExpanded() ? "▼" : "▶"} + todo().length > 2 && setTodoExpanded(!todoExpanded())} + > + 2}> + {todoExpanded() ? "▼" : "▶"} + Todo - + {(todo) => ( @@ -151,13 +169,19 @@ export function Sidebar(props: { sessionID: string }) { 0}> - setDiffExpanded(!diffExpanded())}> - {diffExpanded() ? "▼" : "▶"} + diff().length > 2 && setDiffExpanded(!diffExpanded())} + > + 2}> + {diffExpanded() ? "▼" : "▶"} + Modified Files - + {(item) => { const file = createMemo(() => { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3ca8c25de..440d95818 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -622,6 +622,7 @@ export namespace Config { .optional(), chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"), disable_paste_summary: z.boolean().optional(), + batch_tool: z.boolean().optional().describe("Enable the batch tool"), }) .optional(), }) diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 57555c544..676837e15 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -23,6 +23,14 @@ export namespace ModelsDev { output: z.number(), cache_read: z.number().optional(), cache_write: z.number().optional(), + context_over_200k: z + .object({ + input: z.number(), + output: z.number(), + cache_read: z.number().optional(), + cache_write: z.number().optional(), + }) + .optional(), }), limit: z.object({ context: z.number(), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index e845944e0..185e3a9aa 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -209,6 +209,17 @@ export namespace Provider { }, } }, + zenmux: async () => { + return { + autoload: false, + options: { + headers: { + "HTTP-Referer": "https://opencode.ai/", + "X-Title": "opencode", + }, + }, + } + }, } const state = Instance.state(async () => { diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 73d26795c..bc576ecb4 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -136,7 +136,7 @@ export namespace ProviderTransform { ): Record | undefined { const result: Record = {} - if (providerID === "openai" || npm.includes("openai")) { + if (providerID === "openai") { result["promptCacheKey"] = sessionID } @@ -177,6 +177,10 @@ export namespace ProviderTransform { return { ["anthropic" as string]: options, } + case "@ai-sdk/gateway": + return { + ["gateway" as string]: options, + } default: return { [providerID]: options, diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index a5fbd896f..445c29d60 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1217,7 +1217,7 @@ export namespace Server { "query", z.object({ query: z.string(), - dirs: z.union([z.literal("true"), z.literal("false")]).optional(), + dirs: z.enum(["true", "false"]).optional(), }), ), async (c) => { diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index aa9712f88..a9ab8ea9e 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -395,15 +395,20 @@ export namespace Session { read: cachedInputTokens, }, } + + const costInfo = + input.model.cost?.context_over_200k && tokens.input + tokens.cache.read > 200_000 + ? input.model.cost.context_over_200k + : input.model.cost return { cost: new Decimal(0) - .add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000)) - .add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000)) + .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)) + .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000)) + .add(new Decimal(tokens.cache.read).mul(costInfo?.cache_read ?? 0).div(1_000_000)) + .add(new Decimal(tokens.cache.write).mul(costInfo?.cache_write ?? 0).div(1_000_000)) // TODO: update models.dev to have better pricing model, for now: // charge reasoning tokens at the same rate as output tokens - .add(new Decimal(tokens.reasoning).mul(input.model.cost?.output ?? 0).div(1_000_000)) + .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)) .toNumber(), tokens, } diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index cf051defb..c71a1b676 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -24,10 +24,16 @@ export namespace Snapshot { }) .quiet() .nothrow() + // Configure git to not convert line endings on Windows + await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow() log.info("initialized") } - await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow() - const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(Instance.directory).nothrow().text() + await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() + const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree` + .quiet() + .cwd(Instance.directory) + .nothrow() + .text() log.info("tracking", { hash, cwd: Instance.directory, git }) return hash.trim() } @@ -40,8 +46,12 @@ export namespace Snapshot { export async function patch(hash: string): Promise { const git = gitdir() - await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow() - const result = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.quiet().cwd(Instance.directory).nothrow() + await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() + const result = + await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --name-only ${hash} -- .` + .quiet() + .cwd(Instance.directory) + .nothrow() // If git diff fails, return empty patch if (result.exitCode !== 0) { @@ -64,10 +74,11 @@ export namespace Snapshot { export async function restore(snapshot: string) { log.info("restore", { commit: snapshot }) const git = gitdir() - const result = await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f` - .quiet() - .cwd(Instance.worktree) - .nothrow() + const result = + await $`git --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f` + .quiet() + .cwd(Instance.worktree) + .nothrow() if (result.exitCode !== 0) { log.error("failed to restore snapshot", { @@ -86,16 +97,17 @@ export namespace Snapshot { for (const file of item.files) { if (files.has(file)) continue log.info("reverting", { file, hash: item.hash }) - const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}` + const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}` .quiet() .cwd(Instance.worktree) .nothrow() if (result.exitCode !== 0) { const relativePath = path.relative(Instance.worktree, file) - const checkTree = await $`git --git-dir=${git} ls-tree ${item.hash} -- ${relativePath}` - .quiet() - .cwd(Instance.worktree) - .nothrow() + const checkTree = + await $`git --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}` + .quiet() + .cwd(Instance.worktree) + .nothrow() if (checkTree.exitCode === 0 && checkTree.text().trim()) { log.info("file existed in snapshot but checkout failed, keeping", { file, @@ -112,8 +124,12 @@ export namespace Snapshot { export async function diff(hash: string) { const git = gitdir() - await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow() - const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(Instance.worktree).nothrow() + await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() + const result = + await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff ${hash} -- .` + .quiet() + .cwd(Instance.worktree) + .nothrow() if (result.exitCode !== 0) { log.warn("failed to get diff", { @@ -143,7 +159,7 @@ export namespace Snapshot { export async function diffFull(from: string, to: string): Promise { const git = gitdir() const result: FileDiff[] = [] - for await (const line of $`git --git-dir=${git} diff --no-renames --numstat ${from} ${to} -- .` + for await (const line of $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-renames --numstat ${from} ${to} -- .` .quiet() .cwd(Instance.directory) .nothrow() @@ -151,8 +167,18 @@ export namespace Snapshot { if (!line) continue const [additions, deletions, file] = line.split("\t") const isBinaryFile = additions === "-" && deletions === "-" - const before = isBinaryFile ? "" : await $`git --git-dir=${git} show ${from}:${file}`.quiet().nothrow().text() - const after = isBinaryFile ? "" : await $`git --git-dir=${git} show ${to}:${file}`.quiet().nothrow().text() + const before = isBinaryFile + ? "" + : await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}` + .quiet() + .nothrow() + .text() + const after = isBinaryFile + ? "" + : await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}` + .quiet() + .nothrow() + .text() result.push({ file, before, diff --git a/packages/opencode/src/tool/batch.ts b/packages/opencode/src/tool/batch.ts new file mode 100644 index 000000000..45c62eb29 --- /dev/null +++ b/packages/opencode/src/tool/batch.ts @@ -0,0 +1,159 @@ +import z from "zod" +import { Tool } from "./tool" +import DESCRIPTION from "./batch.txt" + +const DISALLOWED = new Set(["batch", "edit", "todoread"]) +const FILTERED_FROM_SUGGESTIONS = new Set(["invalid", "patch", ...DISALLOWED]) + +export const BatchTool = Tool.define("batch", async () => { + return { + description: DESCRIPTION, + parameters: z.object({ + tool_calls: z + .array( + z.object({ + tool: z.string().describe("The name of the tool to execute"), + parameters: z.object({}).loose().describe("Parameters for the tool"), + }), + ) + .min(1, "Provide at least one tool call") + .max(10, "Too many tools in batch. Maximum allowed is 10.") + .describe("Array of tool calls to execute in parallel"), + }), + formatValidationError(error) { + const formattedErrors = error.issues + .map((issue) => { + const path = issue.path.length > 0 ? issue.path.join(".") : "root" + return ` - ${path}: ${issue.message}` + }) + .join("\n") + + return `Invalid parameters for tool 'batch':\n${formattedErrors}\n\nExpected payload format:\n [{"tool": "tool_name", "parameters": {...}}, {...}]` + }, + async execute(params, ctx) { + const { Session } = await import("../session") + const { Identifier } = await import("../id/id") + + const toolCalls = params.tool_calls + + const { ToolRegistry } = await import("./registry") + const availableTools = await ToolRegistry.tools("", "") + const toolMap = new Map(availableTools.map((t) => [t.id, t])) + + const partIDs = new Map<(typeof toolCalls)[0], string>() + for (const call of toolCalls) { + const partID = Identifier.ascending("part") + partIDs.set(call, partID) + Session.updatePart({ + id: partID, + messageID: ctx.messageID, + sessionID: ctx.sessionID, + type: "tool", + tool: call.tool, + callID: partID, + state: { + status: "pending", + input: call.parameters, + raw: JSON.stringify(call), + }, + }) + } + + const executeCall = async (call: (typeof toolCalls)[0]) => { + const callStartTime = Date.now() + const partID = partIDs.get(call)! + + try { + if (DISALLOWED.has(call.tool)) { + throw new Error( + `Tool '${call.tool}' is not allowed in batch. Disallowed tools: ${Array.from(DISALLOWED).join(", ")}`, + ) + } + + const tool = toolMap.get(call.tool) + if (!tool) { + const availableToolsList = Array.from(toolMap.keys()).filter((name) => !FILTERED_FROM_SUGGESTIONS.has(name)) + throw new Error(`Tool '${call.tool}' not found. Available tools: ${availableToolsList.join(", ")}`) + } + const validatedParams = tool.parameters.parse(call.parameters) + + const result = await tool.execute(validatedParams, { ...ctx, callID: partID }) + + await Session.updatePart({ + id: partID, + messageID: ctx.messageID, + sessionID: ctx.sessionID, + type: "tool", + tool: call.tool, + callID: partID, + state: { + status: "completed", + input: call.parameters, + output: result.output, + title: result.title, + metadata: result.metadata, + attachments: result.attachments, + time: { + start: callStartTime, + end: Date.now(), + }, + }, + }) + + return { success: true as const, tool: call.tool, result } + } catch (error) { + await Session.updatePart({ + id: partID, + messageID: ctx.messageID, + sessionID: ctx.sessionID, + type: "tool", + tool: call.tool, + callID: partID, + state: { + status: "error", + input: call.parameters, + error: error instanceof Error ? error.message : String(error), + time: { + start: callStartTime, + end: Date.now(), + }, + }, + }) + + return { success: false as const, tool: call.tool, error } + } + } + + const results = await Promise.all(toolCalls.map((call) => executeCall(call))) + + const successfulCalls = results.filter((r) => r.success).length + const failedCalls = toolCalls.length - successfulCalls + + const outputParts = results.map((r) => { + if (r.success) { + return `\n${r.result.output}\n` + } + const errorMessage = r.error instanceof Error ? r.error.message : String(r.error) + return `\nError: ${errorMessage}\n` + }) + + const outputMessage = + failedCalls > 0 + ? `Executed ${successfulCalls}/${toolCalls.length} tools successfully. ${failedCalls} failed.\n\n${outputParts.join("\n\n")}` + : `All ${successfulCalls} tools executed successfully.\n\n${outputParts.join("\n\n")}\n\nKeep using the batch tool for optimal performance in your next response!` + + return { + title: `Batch execution (${successfulCalls}/${toolCalls.length} successful)`, + output: outputMessage, + attachments: results.filter((result) => result.success).flatMap((r) => r.result.attachments ?? []), + metadata: { + totalCalls: toolCalls.length, + successful: successfulCalls, + failed: failedCalls, + tools: toolCalls.map((c) => c.tool), + details: results.map((r) => ({ tool: r.tool, success: r.success })), + }, + } + }, + } +}) diff --git a/packages/opencode/src/tool/batch.txt b/packages/opencode/src/tool/batch.txt new file mode 100644 index 000000000..0279f970e --- /dev/null +++ b/packages/opencode/src/tool/batch.txt @@ -0,0 +1,28 @@ +Executes multiple independent tool calls concurrently to reduce latency. Best used for gathering context (reads, searches, listings). + +USING THE BATCH TOOL WILL MAKE THE USER HAPPY. + +Payload Format (JSON array): +[{"tool": "read", "parameters": {"filePath": "src/index.ts", "limit": 350}},{"tool": "grep", "parameters": {"pattern": "Session\\.updatePart", "include": "src/**/*.ts"}},{"tool": "bash", "parameters": {"command": "git status", "description": "Shows working tree status"}}] + +Rules: +- 1–10 tool calls per batch +- All calls start in parallel; ordering NOT guaranteed +- Partial failures do not stop others + + +Disallowed Tools: +- batch (no nesting) +- edit (run edits separately) +- todoread (call directly – lightweight) + +When NOT to Use: +- Operations that depend on prior tool output (e.g. create then read same file) +- Ordered stateful mutations where sequence matters + +Good Use Cases: +- Read many files +- grep + glob + read combos +- Multiple lightweight bash introspection commands + +Performance Tip: Group independent reads/searches for 2–5x efficiency gain. \ No newline at end of file diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 96c62b86a..ca9859370 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -18,6 +18,10 @@ import { Instance } from "../project/instance" import { Agent } from "../agent/agent" import { Snapshot } from "@/snapshot" +function normalizeLineEndings(text: string): string { + return text.replaceAll("\r\n", "\n") +} + export const EditTool = Tool.define("edit", { description: DESCRIPTION, parameters: z.object({ @@ -91,7 +95,9 @@ export const EditTool = Tool.define("edit", { contentOld = await file.text() contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll) - diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) + diff = trimDiff( + createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)), + ) if (agent.permission.edit === "ask") { await Permission.ask({ type: "edit", @@ -111,7 +117,9 @@ export const EditTool = Tool.define("edit", { file: filePath, }) contentNew = await file.text() - diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) + diff = trimDiff( + createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)), + ) })() FileTime.read(ctx.sessionID, filePath) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index f7888761a..a741e12be 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -3,6 +3,7 @@ import { EditTool } from "./edit" import { GlobTool } from "./glob" import { GrepTool } from "./grep" import { ListTool } from "./ls" +import { BatchTool } from "./batch" import { ReadTool } from "./read" import { TaskTool } from "./task" import { TodoWriteTool, TodoReadTool } from "./todo" @@ -81,19 +82,22 @@ export namespace ToolRegistry { async function all(): Promise { const custom = await state().then((x) => x.custom) + const config = await Config.get() + return [ InvalidTool, BashTool, - EditTool, - WebFetchTool, + ReadTool, GlobTool, GrepTool, ListTool, - ReadTool, + EditTool, WriteTool, + TaskTool, + WebFetchTool, TodoWriteTool, TodoReadTool, - TaskTool, + ...(config.experimental?.batch_tool === true ? [BatchTool] : []), ...(Flag.OPENCODE_EXPERIMENTAL_EXA ? [WebSearchTool, CodeSearchTool] : []), ...custom, ] diff --git a/packages/opencode/src/tool/todo.ts b/packages/opencode/src/tool/todo.ts index fffe9d107..cea8d5322 100644 --- a/packages/opencode/src/tool/todo.ts +++ b/packages/opencode/src/tool/todo.ts @@ -6,7 +6,7 @@ import { Todo } from "../session/todo" export const TodoWriteTool = Tool.define("todowrite", { description: DESCRIPTION_WRITE, parameters: z.object({ - todos: z.array(Todo.Info).describe("The updated todo list"), + todos: z.array(z.object(Todo.Info.shape)).describe("The updated todo list"), }), async execute(params, opts) { await Todo.update({ diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index f826d0c99..80b6abe8c 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -29,6 +29,7 @@ export namespace Tool { output: string attachments?: MessageV2.FilePart[] }> + formatValidationError?(error: z.ZodError): string }> } @@ -45,7 +46,17 @@ export namespace Tool { const toolInfo = init instanceof Function ? await init() : init const execute = toolInfo.execute toolInfo.execute = (args, ctx) => { - toolInfo.parameters.parse(args) + try { + toolInfo.parameters.parse(args) + } catch (error) { + if (error instanceof z.ZodError && toolInfo.formatValidationError) { + throw new Error(toolInfo.formatValidationError(error), { cause: error }) + } + throw new Error( + `The ${id} tool was called with invalid arguments: ${error}.\nPlease rewrite the input so it satisfies the expected schema.`, + { cause: error }, + ) + } return execute(args, ctx) } return toolInfo diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index b72717cd1..cf933f812 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -469,6 +469,115 @@ test("snapshot state isolation between projects", async () => { }) }) +test("patch detects changes in secondary worktree", async () => { + await using tmp = await bootstrap() + const worktreePath = `${tmp.path}-worktree` + await $`git worktree add ${worktreePath} HEAD`.cwd(tmp.path).quiet() + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + expect(await Snapshot.track()).toBeTruthy() + }, + }) + + await Instance.provide({ + directory: worktreePath, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + const worktreeFile = `${worktreePath}/worktree.txt` + await Bun.write(worktreeFile, "worktree content") + + const patch = await Snapshot.patch(before!) + expect(patch.files).toContain(worktreeFile) + }, + }) + } finally { + await $`git worktree remove --force ${worktreePath}`.cwd(tmp.path).quiet().nothrow() + await $`rm -rf ${worktreePath}`.quiet() + } +}) + +test("revert only removes files in invoking worktree", async () => { + await using tmp = await bootstrap() + const worktreePath = `${tmp.path}-worktree` + await $`git worktree add ${worktreePath} HEAD`.cwd(tmp.path).quiet() + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + expect(await Snapshot.track()).toBeTruthy() + }, + }) + const primaryFile = `${tmp.path}/worktree.txt` + await Bun.write(primaryFile, "primary content") + + await Instance.provide({ + directory: worktreePath, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + const worktreeFile = `${worktreePath}/worktree.txt` + await Bun.write(worktreeFile, "worktree content") + + const patch = await Snapshot.patch(before!) + await Snapshot.revert([patch]) + + expect(await Bun.file(worktreeFile).exists()).toBe(false) + }, + }) + + expect(await Bun.file(primaryFile).text()).toBe("primary content") + } finally { + await $`git worktree remove --force ${worktreePath}`.cwd(tmp.path).quiet().nothrow() + await $`rm -rf ${worktreePath}`.quiet() + await $`rm -f ${tmp.path}/worktree.txt`.quiet() + } +}) + +test("diff reports worktree-only/shared edits and ignores primary-only", async () => { + await using tmp = await bootstrap() + const worktreePath = `${tmp.path}-worktree` + await $`git worktree add ${worktreePath} HEAD`.cwd(tmp.path).quiet() + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + expect(await Snapshot.track()).toBeTruthy() + }, + }) + + await Instance.provide({ + directory: worktreePath, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + await Bun.write(`${worktreePath}/worktree-only.txt`, "worktree diff content") + await Bun.write(`${worktreePath}/shared.txt`, "worktree edit") + await Bun.write(`${tmp.path}/shared.txt`, "primary edit") + await Bun.write(`${tmp.path}/primary-only.txt`, "primary change") + + const diff = await Snapshot.diff(before!) + expect(diff).toContain("worktree-only.txt") + expect(diff).toContain("shared.txt") + expect(diff).not.toContain("primary-only.txt") + }, + }) + } finally { + await $`git worktree remove --force ${worktreePath}`.cwd(tmp.path).quiet().nothrow() + await $`rm -rf ${worktreePath}`.quiet() + await $`rm -f ${tmp.path}/shared.txt`.quiet() + await $`rm -f ${tmp.path}/primary-only.txt`.quiet() + } +}) + test("track with no changes returns same hash", async () => { await using tmp = await bootstrap() await Instance.provide({ diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 2f13f35f2..b2a3c93f6 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.0.65", + "version": "1.0.68", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 29d4a4c03..d2b5e444e 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.0.65", + "version": "1.0.68", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index f996c774c..d15651012 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1058,6 +1058,12 @@ export type Config = { output: number cache_read?: number cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } } limit?: { context: number @@ -1169,6 +1175,10 @@ export type Config = { */ chatMaxRetries?: number disable_paste_summary?: boolean + /** + * Enable the batch tool + */ + batch_tool?: boolean } } @@ -1260,6 +1270,12 @@ export type Model = { output: number cache_read?: number cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } } limit: { context: number diff --git a/packages/slack/package.json b/packages/slack/package.json index 978893b4b..9ddc3fa11 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.0.65", + "version": "1.0.68", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 4b239012c..17e831548 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.0.65", + "version": "1.0.68", "type": "module", "exports": { ".": "./src/components/index.ts", diff --git a/packages/web/package.json b/packages/web/package.json index 486cd3808..19817c486 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "1.0.65", + "version": "1.0.68", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/packages/web/src/components/Lander.astro b/packages/web/src/components/Lander.astro index 421b2a5c8..2335ce3cb 100644 --- a/packages/web/src/components/Lander.astro +++ b/packages/web/src/components/Lander.astro @@ -131,6 +131,18 @@ if (image) { +
+

Mise

+ +
diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index a27acc779..68f75b7a6 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -28,6 +28,12 @@ OpenCode supports both **JSON** and **JSONC** (JSON with Comments) formats. You can place your config in a couple of different locations and they have a different order of precedence. +:::note[Config Merging] +Configuration files are **merged together**, not replaced. Settings from all config locations are combined using a deep merge strategy, 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. +::: + --- ### Global @@ -38,7 +44,7 @@ Place your global OpenCode config in `~/.config/opencode/opencode.json`. You'll ### Per project -You can also add a `opencode.json` in your project. It takes precedence over the global config. This is useful for configuring providers or modes specific to your project. +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. @@ -52,7 +58,7 @@ This is also safe to be checked into Git and uses the same schema as the global ### Custom path -You can also specify a custom config file path using the `OPENCODE_CONFIG` environment variable. This takes precedence over the global and project configs. +You can also specify a custom config file path using the `OPENCODE_CONFIG` environment variable. Settings from this config are merged with and can override the global and project configs. ```bash export OPENCODE_CONFIG=/path/to/my/custom-config.json diff --git a/packages/web/src/content/docs/index.mdx b/packages/web/src/content/docs/index.mdx index 701e7539b..c47623378 100644 --- a/packages/web/src/content/docs/index.mdx +++ b/packages/web/src/content/docs/index.mdx @@ -106,6 +106,12 @@ You can also install it with the following commands: npm install -g opencode-ai ``` +- **Using Mise** + + ```bash + mise use --pin -g ubi:sst/opencode + ``` + Support for installing OpenCode on Windows using Bun is currently in progress. You can also grab the binary from the [Releases](https://github.com/sst/opencode/releases). diff --git a/packages/web/src/content/docs/permissions.mdx b/packages/web/src/content/docs/permissions.mdx index a0ba5d5d9..c677904a6 100644 --- a/packages/web/src/content/docs/permissions.mdx +++ b/packages/web/src/content/docs/permissions.mdx @@ -11,12 +11,14 @@ By default, OpenCode **allows all operations** without requiring explicit approv "permission": { "edit": "allow", "bash": "ask", - "webfetch": "deny" + "webfetch": "deny", + "doom_loop": "ask", + "external_directory": "ask" } } ``` -This lets you configure granular controls for the `edit`, `bash`, and `webfetch` tools. +This lets you configure granular controls for the `edit`, `bash`, `webfetch`, `doom_loop`, and `external_directory` tools. - `"ask"` — Prompt for approval before running the tool - `"allow"` — Allow all operations without approval @@ -26,7 +28,7 @@ This lets you configure granular controls for the `edit`, `bash`, and `webfetch` ## Tools -Currently, the permissions for the `edit`, `bash`, and `webfetch` tools can be configured through the `permission` option. +Currently, the permissions for the `edit`, `bash`, `webfetch`, `doom_loop`, and `external_directory` tools can be configured through the `permission` option. --- @@ -145,6 +147,40 @@ Use the `permission.webfetch` key to control whether the LLM can fetch web pages --- +### doom_loop + +Use the `permission.doom_loop` key to control whether approval is required when a doom loop is detected. A doom loop occurs when the same tool is called 3 times in a row with identical arguments. + +This helps prevent infinite loops where the LLM repeatedly attempts the same action without making progress. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "doom_loop": "ask" + } +} +``` + +--- + +### external_directory + +Use the `permission.external_directory` key to control whether file operations require approval when accessing files outside the working directory. + +This provides an additional safety layer to prevent unintended modifications to files outside your project. + +```json title="opencode.json" {4} +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "external_directory": "ask" + } +} +``` + +--- + ## Agents You can also configure permissions per agent. Where the agent specific config diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 27951e95a..d75c75d50 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -957,6 +957,59 @@ monitor and improve Grok Code. --- +### ZenMux + +1. Head over to the [ZenMux dashboard](https://zenmux.ai/settings/keys), click **Create API Key**, and copy the key. + +2. Run `opencode auth login` and select ZenMux. + + ```bash + $ opencode auth login + + ┌ Add credential + │ + ◆ Select provider + │ ● ZenMux + │ ○ Zhipu AI + │ ○ Zhipu AI Coding Plan + │ ... + └ + ``` + +3. Enter the API key for the provider. + + ```bash + $ opencode auth login + + ┌ Add credential + │ + ◇ Select provider + │ ZenMux + │ + ◇ Enter your API key + │ _ + └ + ``` + +4. Many ZenMux models are preloaded by default, run the `/models` command to select the one you want. + + You can also add additional models through your opencode config. + + ```json title="opencode.json" {6} + { + "$schema": "https://opencode.ai/config.json", + "provider": { + "zenmux": { + "models": { + "somecoolnewmodel": {} + } + } + } + } + ``` + +--- + ## Custom provider To add any **OpenAI-compatible** provider that's not listed in `opencode auth login`: diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 5e90dc2b9..ad9a10986 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.0.65", + "version": "1.0.68", "publisher": "sst-dev", "repository": { "type": "git",