diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml
index f7d7f6081..ebf146395 100644
--- a/.github/workflows/opencode.yml
+++ b/.github/workflows/opencode.yml
@@ -30,4 +30,4 @@ jobs:
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
- model: opencode/glm-4.6
+ model: opencode/claude-haiku-4-5
diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml
index f8bbb78c6..bd98de164 100644
--- a/.github/workflows/snapshot.yml
+++ b/.github/workflows/snapshot.yml
@@ -1,11 +1,13 @@
name: snapshot
on:
+ workflow_dispatch:
push:
branches:
- dev
- test-bedrock
- v0
+ - otui-diffs
concurrency: ${{ github.workflow }}-${{ github.ref }}
diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml
index 38dba2e35..1f1ca0e80 100644
--- a/.github/workflows/update-nix-hashes.yml
+++ b/.github/workflows/update-nix-hashes.yml
@@ -18,6 +18,7 @@ on:
jobs:
update:
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
env:
SYSTEM: x86_64-linux
@@ -29,6 +30,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ github.head_ref || github.ref_name }}
+ repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
- name: Setup Nix
uses: DeterminateSystems/nix-installer-action@v20
diff --git a/.husky/pre-push b/.husky/pre-push
index b26017ee9..2fd039d56 100755
--- a/.husky/pre-push
+++ b/.husky/pre-push
@@ -1,2 +1,9 @@
#!/bin/sh
+# Check if bun version matches package.json
+EXPECTED_VERSION=$(grep '"packageManager"' package.json | sed 's/.*"bun@\([^"]*\)".*/\1/')
+CURRENT_VERSION=$(bun --version)
+if [ "$CURRENT_VERSION" != "$EXPECTED_VERSION" ]; then
+ echo "Error: Bun version $CURRENT_VERSION does not match expected version $EXPECTED_VERSION from package.json"
+ exit 1
+fi
bun typecheck
diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc
index 369832f9f..ce4a6658b 100644
--- a/.opencode/opencode.jsonc
+++ b/.opencode/opencode.jsonc
@@ -16,5 +16,12 @@
"type": "remote",
"url": "https://mcp.exa.ai/mcp",
},
+ "morph": {
+ "type": "local",
+ "command": ["bunx", "@morphllm/morphmcp"],
+ "environment": {
+ "ENABLED_TOOLS": "warp_grep",
+ },
+ },
},
}
diff --git a/STATS.md b/STATS.md
index b58a8f1c5..d9f348f91 100644
--- a/STATS.md
+++ b/STATS.md
@@ -152,3 +152,6 @@
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
+| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
+| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
+| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
diff --git a/bun.lock b/bun.lock
index 9ea4c7de3..4f49abe70 100644
--- a/bun.lock
+++ b/bun.lock
@@ -20,7 +20,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -48,7 +48,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -75,7 +75,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -99,7 +99,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -123,7 +123,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -164,7 +164,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -192,7 +192,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -208,7 +208,7 @@
},
"packages/opencode": {
"name": "opencode",
- "version": "1.0.119",
+ "version": "1.0.120",
"bin": {
"opencode": "./bin/opencode",
},
@@ -217,10 +217,10 @@
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/amazon-bedrock": "3.0.57",
- "@ai-sdk/anthropic": "2.0.45",
+ "@ai-sdk/anthropic": "2.0.50",
"@ai-sdk/azure": "2.0.73",
- "@ai-sdk/google": "2.0.42",
- "@ai-sdk/google-vertex": "3.0.74",
+ "@ai-sdk/google": "2.0.44",
+ "@ai-sdk/google-vertex": "3.0.81",
"@ai-sdk/mcp": "0.0.8",
"@ai-sdk/openai": "2.0.71",
"@ai-sdk/openai-compatible": "1.0.27",
@@ -235,9 +235,9 @@
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
- "@openrouter/ai-sdk-provider": "1.2.5",
- "@opentui/core": "0.1.50",
- "@opentui/solid": "0.1.50",
+ "@openrouter/ai-sdk-provider": "1.2.8",
+ "@opentui/core": "0.1.51",
+ "@opentui/solid": "0.1.51",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -256,7 +256,7 @@
"jsonc-parser": "3.3.1",
"minimatch": "10.0.3",
"open": "10.1.2",
- "opentui-spinner": "0.0.5",
+ "opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
"remeda": "catalog:",
"solid-js": "catalog:",
@@ -295,7 +295,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -315,7 +315,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
- "version": "1.0.119",
+ "version": "1.0.120",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -326,7 +326,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -339,7 +339,7 @@
},
"packages/tauri": {
"name": "@opencode-ai/tauri",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
@@ -352,7 +352,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -384,7 +384,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"zod": "catalog:",
},
@@ -394,7 +394,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
- "version": "1.0.119",
+ "version": "1.0.120",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -491,9 +491,9 @@
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W+cB1sOWvPcz9qiIsNtD+HxUrBUva2vWv2K1EFukuImX+HA0uZx3EyyOjhYQ9gtf/teqEG80M6OvJ7xx/VLV2A=="],
- "@ai-sdk/google": ["@ai-sdk/google@2.0.42", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Jdn+3TZm4iIt62CUjjUoIOshqFIXyzNmUDfkSVV4FcjlSo5+AuhzI1KC7QiNHlqPNejzR6NLIqGJx96VAES34g=="],
+ "@ai-sdk/google": ["@ai-sdk/google@2.0.44", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-c5dck36FjqiVoeeMJQLTEmUheoURcGTU/nBT6iJu8/nZiKFT/y8pD85KMDRB7RerRYaaQOtslR2d6/5PditiRw=="],
- "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.74", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.45", "@ai-sdk/google": "2.0.42", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W0375p41RQOheAmy7iJGtuJWQWX/aKkO4sJHf6eIYa3bkz93Cbo1aRG1X7ocyMusLZ3dIaW7x6X9WHD8IHkNfg=="],
+ "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.81", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.50", "@ai-sdk/google": "2.0.44", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yrl5Ug0Mqwo9ya45oxczgy2RWgpEA/XQQCSFYP+3NZMQ4yA3Iim1vkOjVCsGaZZ8rjVk395abi1ZMZV0/6rqVA=="],
"@ai-sdk/mcp": ["@ai-sdk/mcp@0.0.8", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9y9GuGcZ9/+pMIHfpOCJgZVp+AZMv6TkjX2NVT17SQZvTF2N8LXuCXyoUPyi1PxIxzxl0n463LxxaB2O6olC+Q=="],
@@ -1079,27 +1079,27 @@
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
- "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.2.5", "", { "dependencies": { "@openrouter/sdk": "^0.1.8" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-NrvJFPvdEUo6DYUQIVWPGfhafuZ2PAIX7+CUMKGknv8TcTNVo0TyP1y5SU7Bgjf/Wup9/74UFKUB07icOhVZjQ=="],
+ "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.2.8", "", { "dependencies": { "@openrouter/sdk": "^0.1.8" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-pQT8AzZBKg9f4bkt4doF486ZlhK0XjKkevrLkiqYgfh1Jplovieu28nK4Y+xy3sF18/mxjqh9/2y6jh01qzLrA=="],
"@openrouter/sdk": ["@openrouter/sdk@0.1.27", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
- "@opentui/core": ["@opentui/core@0.1.50", "", { "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.50", "@opentui/core-darwin-x64": "0.1.50", "@opentui/core-linux-arm64": "0.1.50", "@opentui/core-linux-x64": "0.1.50", "@opentui/core-win32-arm64": "0.1.50", "@opentui/core-win32-x64": "0.1.50", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-QhjwT2f8AIQj0gbL/WQ2M93sl2/qp9+Kqxyh4dOhp8z3qnTc5D7J105VrMyeWZW7/P27ubgbFAqqWXrZ4FsuLw=="],
+ "@opentui/core": ["@opentui/core@0.1.51", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.51", "@opentui/core-darwin-x64": "0.1.51", "@opentui/core-linux-arm64": "0.1.51", "@opentui/core-linux-x64": "0.1.51", "@opentui/core-win32-arm64": "0.1.51", "@opentui/core-win32-x64": "0.1.51", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-9w9vg2nYC4eTKdh5en7WpBB44Nrib3uMtcPNXr2JxftjzDXU5Qmcv3vbKbxHqgzgk7FYm2Z9OFAOk7innbnplA=="],
- "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.50", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FKqTDOsZl9TXF7KN2SdZKoRHQNvqKSY27AG3jhKCoiyLGdaNCAsaeBWqAmpnL4E4kMkV3aiQSCrKTrYsaevvOg=="],
+ "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.51", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5EicEs5JQiMmr3rzKdGfHnsRXJ7cv/pxz0/C2Xcg+jKMSUmUvS7LE3Mi3HBY05GjZyB/EZ3bYyHqXB4nanYTNg=="],
- "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.50", "", { "os": "darwin", "cpu": "x64" }, "sha512-GczVNqqpM/HtsgeBB08K6zL1B7oc6Y5G2cMklo06LrYRdDkFdDtY5fNNnJR2/psZWzTrI3M+sLnKWgUGD5CxUQ=="],
+ "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.51", "", { "os": "darwin", "cpu": "x64" }, "sha512-mG9WlKdv0yWCIldFEAzgnm1NCyOtfiVG8zVZLiGaLhsL7Jii+f4RpOmlvFlb2sSSTxjC3HxRzWPC7WBSzk4txA=="],
- "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-+CKMhweEXH0tLGM6qqaqk6DyCEmwrTVubTtez/pSM3GgcROSXIBui9TEZpIlPgSCVmjbotGS6eSIg4oU+p9o7w=="],
+ "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.51", "", { "os": "linux", "cpu": "arm64" }, "sha512-CNUx8nvkKRCsoLg/z7W8tD0hBFEUE9yEpqgyACc/ODdaaRLrUPQkBgUYWM/XGV9OnoqnpqLSKf95+DoUaTZ0lA=="],
- "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.50", "", { "os": "linux", "cpu": "x64" }, "sha512-yv5KWiMohAK9bsi1gth9DDZDpoJA1EDHexjhThsPT8EH82g13T088dnJZuJWUE9dr1OwTCQG8DyorNxX3ViEGQ=="],
+ "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.51", "", { "os": "linux", "cpu": "x64" }, "sha512-GpZ8vqX2dPyvBKvCRkBJp5x7zsmbixNCsCfzSVG9VIAVyT6qhylT2wrTOTGJLzDfc12ugdeL/1WUIR80YvwARA=="],
- "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.50", "", { "os": "win32", "cpu": "arm64" }, "sha512-6/6pURTRNTLFKF8IhYVi7U+T/HGMeURav9LIYw7yfcOibd0kLMthmemhS0Lzyk5dmtp0T4V4NmRmtlq/fIzyjQ=="],
+ "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.51", "", { "os": "win32", "cpu": "arm64" }, "sha512-eIc13B9dmoJ2x0EEsd4JWKCLzgVPBv+6Gn6vbjaSL9T/+14XRKMQuEdnQmU6+/KruAumfK+qSXlFdmjhFKrDOA=="],
- "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.50", "", { "os": "win32", "cpu": "x64" }, "sha512-EME8GBFq9uCLbH5js8fH7/xY4ZtLIZlt3bkYKT6lPiCNdaf/6ebg+F/ObPXFkJrc8VeV1ql2bXhQ6RLi7izvAA=="],
+ "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.51", "", { "os": "win32", "cpu": "x64" }, "sha512-ytpWgA3oNLehI5s9pxLAtg5kVO7Npq6onGPTZUikM5LlH71bX3Bm4PP8qLkD0AG9zDiO7ndp93ZFuT9yGqKPiQ=="],
- "@opentui/solid": ["@opentui/solid@0.1.50", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.50", "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-q778kp/eksh8UOPSQO2h8h9CGGDqepTf9u2WYTS2HYHRAI2SRtUWpN9L7Euyt3BtG9L/wpsIOHK/ufPhQH1X6A=="],
+ "@opentui/solid": ["@opentui/solid@0.1.51", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.51", "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-CBohbgFjUVG2P6/iAN52OCGaK0s5Wc2VWKyNrs7Fd9mFDMrC/IfrGAeaDeJXdQ8T0YhZ0PiUVijzFILCi2yOUw=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -2969,7 +2969,7 @@
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
- "opentui-spinner": ["opentui-spinner@0.0.5", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-abSWzWA7eyuD0PjerAWbBznLmOQn+8xRDaLGCVIs4ctETi2laNFr5KwicYnPXsHZpPc2neV7WtQm+diCEfOhLA=="],
+ "opentui-spinner": ["opentui-spinner@0.0.6", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="],
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
@@ -3743,11 +3743,11 @@
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
- "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
+ "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
- "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.45", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ipv62vavDCmrV/oE/lXehL9FzwQuZOnnlhPEftWizx464Wb6lvnBTJx8uhmEYruFSzOWTI95Z33ncZ4tA8E6RQ=="],
+ "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
- "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
+ "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
"@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
@@ -4073,7 +4073,7 @@
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
- "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.45", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ipv62vavDCmrV/oE/lXehL9FzwQuZOnnlhPEftWizx464Wb6lvnBTJx8uhmEYruFSzOWTI95Z33ncZ4tA8E6RQ=="],
+ "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
@@ -4619,7 +4619,7 @@
"jsonwebtoken/jws/jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
- "opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
+ "opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
diff --git a/flake.lock b/flake.lock
index b0749bea4..33aae3812 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1764138170,
- "narHash": "sha256-2bCmfCUZyi2yj9FFXYKwsDiaZmizN75cLhI/eWmf3tk=",
+ "lastModified": 1764290847,
+ "narHash": "sha256-VwPgoDgnd628GdE3KyLqTyPF1WWh0VwT5UoKygoi8sg=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "bb813de6d2241bcb1b5af2d3059f560c66329967",
+ "rev": "cd5fedfc384cb98d9fd3827b55f4522f49efda42",
"type": "github"
},
"original": {
diff --git a/nix/hashes.json b/nix/hashes.json
index 1f11430f2..1a2d21588 100644
--- a/nix/hashes.json
+++ b/nix/hashes.json
@@ -1,3 +1,3 @@
{
- "nodeModules": "sha256-dTGBX5mde/hQP36MSFwq3G81OdwpcYRl8bcjLpesbPw="
+ "nodeModules": "sha256-4OrnnZy44edgShLMUrYFsO+07gAnL//pGKO/ehWu5P4="
}
diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index 48d82495b..2b2b02135 100644
--- a/packages/console/app/package.json
+++ b/packages/console/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
- "version": "1.0.119",
+ "version": "1.0.120",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
diff --git a/packages/console/app/src/routes/api/enterprise.ts b/packages/console/app/src/routes/api/enterprise.ts
index e33737d57..6776a7b3c 100644
--- a/packages/console/app/src/routes/api/enterprise.ts
+++ b/packages/console/app/src/routes/api/enterprise.ts
@@ -36,6 +36,7 @@ ${body.email}`.trim()
to: "contact@anoma.ly",
subject: `Enterprise Inquiry from ${body.name}`,
body: emailContent,
+ replyTo: body.email,
})
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
diff --git a/packages/console/core/package.json b/packages/console/core/package.json
index a77d9dac2..122f36bbd 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.119",
+ "version": "1.0.120",
"private": true,
"type": "module",
"dependencies": {
diff --git a/packages/console/core/src/aws.ts b/packages/console/core/src/aws.ts
index e87ada6ef..a4c151086 100644
--- a/packages/console/core/src/aws.ts
+++ b/packages/console/core/src/aws.ts
@@ -22,6 +22,7 @@ export namespace AWS {
to: z.string(),
subject: z.string(),
body: z.string(),
+ replyTo: z.string().optional(),
}),
async (input) => {
const res = await createClient().fetch("https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", {
@@ -35,6 +36,7 @@ export namespace AWS {
Destination: {
ToAddresses: [input.to],
},
+ ...(input.replyTo && { ReplyToAddresses: [input.replyTo] }),
Content: {
Simple: {
Subject: {
diff --git a/packages/console/function/package.json b/packages/console/function/package.json
index a3561770f..0dcce2034 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.119",
+ "version": "1.0.120",
"$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 c2583b8ff..da8744452 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.119",
+ "version": "1.0.120",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
diff --git a/packages/desktop/index.html b/packages/desktop/index.html
index 57e10defa..0ac3d566d 100644
--- a/packages/desktop/index.html
+++ b/packages/desktop/index.html
@@ -9,7 +9,8 @@
-
+
+
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 5bdd0c987..ed919b470 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
- "version": "1.0.119",
+ "version": "1.0.120",
"description": "",
"type": "module",
"scripts": {
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx
index 60f9e9ef5..281b6765a 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/packages/desktop/src/pages/session.tsx
@@ -12,7 +12,7 @@ import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Code } from "@opencode-ai/ui/code"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
-import { MessageNav } from "@opencode-ai/ui/message-nav"
+import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
import { SessionReview } from "@opencode-ai/ui/session-review"
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
import {
@@ -333,43 +333,35 @@ export default function Page() {
flex: layout.review.state() === "pane",
}}
>
-
+
- 1}>
- <>
-
-
- >
-
+
-
+
New session
@@ -390,12 +382,14 @@ export default function Page() {
-
-
{
- inputRef = el
- }}
- />
+
+
+
{
+ inputRef = el
+ }}
+ />
+
@@ -498,7 +492,7 @@ export default function Page() {
-
+
{
inputRef = el
diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json
index 6a06363c6..9c453027d 100644
--- a/packages/enterprise/package.json
+++ b/packages/enterprise/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
- "version": "1.0.119",
+ "version": "1.0.120",
"private": true,
"type": "module",
"scripts": {
diff --git a/packages/enterprise/src/entry-server.tsx b/packages/enterprise/src/entry-server.tsx
index df095023a..68f4325c8 100644
--- a/packages/enterprise/src/entry-server.tsx
+++ b/packages/enterprise/src/entry-server.tsx
@@ -9,6 +9,8 @@ export default createHandler(() => (
OpenCode
+
+
{assets}
diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx
index 271fb290a..ffe7f533a 100644
--- a/packages/enterprise/src/routes/share/[shareID].tsx
+++ b/packages/enterprise/src/routes/share/[shareID].tsx
@@ -12,7 +12,7 @@ import { iife } from "@opencode-ai/util/iife"
import { Binary } from "@opencode-ai/util/binary"
import { NamedError } from "@opencode-ai/util/error"
import { DateTime } from "luxon"
-import { MessageNav } from "@opencode-ai/ui/message-nav"
+import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
import { createStore } from "solid-js/store"
import z from "zod"
import NotFound from "../[...404]"
@@ -171,7 +171,7 @@ export default function () {
})
const title = () => (
-
+
@@ -215,7 +215,6 @@ export default function () {
)
const wide = createMemo(() => diffs().length === 0)
- const columnPadding = () => (wide() ? "px-6" : "px-21 @4xl:px-6")
return (
@@ -243,56 +242,39 @@ export default function () {
-
+
-
{title()}
+
+ {title()}
+
-
1}>
- <>
-
-
-
-
-
-
- >
-
+
-
+
@@ -313,7 +295,7 @@ export default function () {
0}>
-
+
Session
@@ -344,7 +326,9 @@ export default function () {
- {turns()}
+
+ {turns()}
+
diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml
index 7ceea03bb..fe9c80dc6 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.119"
+version = "1.0.120"
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.119/opencode-darwin-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.120/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.119/opencode-darwin-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.120/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.119/opencode-linux-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.120/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.119/opencode-linux-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.120/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.119/opencode-windows-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.120/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
diff --git a/packages/function/package.json b/packages/function/package.json
index 516b0b3f7..4b70b2801 100644
--- a/packages/function/package.json
+++ b/packages/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
- "version": "1.0.119",
+ "version": "1.0.120",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index e0909c194..d7ff9b2e5 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.119",
+ "version": "1.0.120",
"name": "opencode",
"type": "module",
"private": true,
@@ -43,10 +43,10 @@
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/amazon-bedrock": "3.0.57",
- "@ai-sdk/anthropic": "2.0.45",
+ "@ai-sdk/anthropic": "2.0.50",
"@ai-sdk/azure": "2.0.73",
- "@ai-sdk/google": "2.0.42",
- "@ai-sdk/google-vertex": "3.0.74",
+ "@ai-sdk/google": "2.0.44",
+ "@ai-sdk/google-vertex": "3.0.81",
"@ai-sdk/mcp": "0.0.8",
"@ai-sdk/openai": "2.0.71",
"@ai-sdk/openai-compatible": "1.0.27",
@@ -61,9 +61,9 @@
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
- "@openrouter/ai-sdk-provider": "1.2.5",
- "@opentui/core": "0.1.50",
- "@opentui/solid": "0.1.50",
+ "@openrouter/ai-sdk-provider": "1.2.8",
+ "@opentui/core": "0.1.51",
+ "@opentui/solid": "0.1.51",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -82,7 +82,7 @@
"jsonc-parser": "3.3.1",
"minimatch": "10.0.3",
"open": "10.1.2",
- "opentui-spinner": "0.0.5",
+ "opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
"remeda": "catalog:",
"solid-js": "catalog:",
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index 740f67b7e..234b99b3e 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -114,6 +114,41 @@ export namespace Agent {
mode: "subagent",
builtIn: true,
},
+ explore: {
+ name: "explore",
+ tools: {
+ todoread: false,
+ todowrite: false,
+ edit: false,
+ write: false,
+ ...defaultTools,
+ },
+ description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
+ prompt: [
+ `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.`,
+ ``,
+ `Your strengths:`,
+ `- Rapidly finding files using glob patterns`,
+ `- Searching code and text with powerful regex patterns`,
+ `- Reading and analyzing file contents`,
+ ``,
+ `Guidelines:`,
+ `- Use Glob for broad file pattern matching`,
+ `- Use Grep for searching file contents with regex`,
+ `- Use Read when you know the specific file path you need to read`,
+ `- Use Bash for file operations like copying, moving, or listing directory contents`,
+ `- Adapt your search approach based on the thoroughness level specified by the caller`,
+ `- Return file paths as absolute paths in your final response`,
+ `- For clear communication, avoid using emojis`,
+ `- Do not create any files, or run bash commands that modify the user's system state in any way`,
+ ``,
+ `Complete the user's search request efficiently and report your findings clearly.`,
+ ].join("\n"),
+ options: {},
+ permission: agentPermission,
+ mode: "subagent",
+ builtIn: true,
+ },
build: {
name: "build",
tools: { ...defaultTools },
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
index ba1dc70b2..4aaac6123 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
@@ -6,6 +6,7 @@ import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
import { useDialog } from "@tui/ui/dialog"
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
import { Keybind } from "@/util/keybind"
+import { iife } from "@/util/iife"
export function DialogModel() {
const local = useLocal()
@@ -21,73 +22,48 @@ export function DialogModel() {
const options = createMemo(() => {
const query = ref()?.filter
- const favorites = local.model.favorite()
+ const favorites = connected() ? local.model.favorite() : []
const recents = local.model.recent()
- const currentModel = local.model.current()
- const orderedRecents = currentModel
- ? [
- currentModel,
- ...recents.filter(
- (item) => item.providerID !== currentModel.providerID || item.modelID !== currentModel.modelID,
- ),
- ]
- : recents
-
- const isCurrent = (item: { providerID: string; modelID: string }) =>
- currentModel && item.providerID === currentModel.providerID && item.modelID === currentModel.modelID
-
- const currentIsFavorite = currentModel && favorites.some((fav) => isCurrent(fav))
-
- const recentList = orderedRecents
+ const recentList = recents
.filter((item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID))
.slice(0, 5)
- const orderedFavorites = currentModel
- ? [...favorites.filter((item) => isCurrent(item)), ...favorites.filter((item) => !isCurrent(item))]
- : favorites
-
- const orderedRecentList =
- currentModel && !currentIsFavorite
- ? [...recentList.filter((item) => isCurrent(item)), ...recentList.filter((item) => !isCurrent(item))]
- : recentList
-
- const favoriteOptions =
- !query && favorites.length > 0
- ? orderedFavorites.flatMap((item) => {
- const provider = sync.data.provider.find((x) => x.id === item.providerID)
- if (!provider) return []
- const model = provider.models[item.modelID]
- if (!model) return []
- return [
- {
- key: item,
- value: {
- providerID: provider.id,
- modelID: model.id,
- },
- title: model.name ?? item.modelID,
- description: `${provider.name} ★`,
- category: "Favorites",
- disabled: provider.id === "opencode" && model.id.includes("-nano"),
- footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
- onSelect: () => {
- dialog.clear()
- local.model.set(
- {
- providerID: provider.id,
- modelID: model.id,
- },
- { recent: true },
- )
- },
+ const favoriteOptions = !query
+ ? favorites.flatMap((item) => {
+ const provider = sync.data.provider.find((x) => x.id === item.providerID)
+ if (!provider) return []
+ const model = provider.models[item.modelID]
+ if (!model) return []
+ return [
+ {
+ key: item,
+ value: {
+ providerID: provider.id,
+ modelID: model.id,
},
- ]
- })
- : []
+ title: model.name ?? item.modelID,
+ description: provider.name,
+ category: "Favorites",
+ disabled: provider.id === "opencode" && model.id.includes("-nano"),
+ footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
+ onSelect: () => {
+ dialog.clear()
+ local.model.set(
+ {
+ providerID: provider.id,
+ modelID: model.id,
+ },
+ { recent: true },
+ )
+ },
+ },
+ ]
+ })
+ : []
const recentOptions = !query
- ? orderedRecentList.flatMap((item) => {
+ ? recentList.flatMap((item) => {
const provider = sync.data.provider.find((x) => x.id === item.providerID)
if (!provider) return []
const model = provider.models[item.modelID]
@@ -137,13 +113,14 @@ export function DialogModel() {
providerID: provider.id,
modelID: model,
}
- const favorite = favorites.some(
- (item) => item.providerID === value.providerID && item.modelID === value.modelID,
- )
return {
value,
title: info.name ?? model,
- description: connected() ? `${provider.name}${favorite ? " ★" : ""}` : undefined,
+ description: favorites.some(
+ (item) => item.providerID === value.providerID && item.modelID === value.modelID,
+ )
+ ? "(Favorite)"
+ : undefined,
category: connected() ? provider.name : undefined,
disabled: provider.id === "opencode" && model.includes("-nano"),
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
@@ -165,10 +142,10 @@ export function DialogModel() {
const inFavorites = favorites.some(
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
)
- const inRecents = orderedRecents.some(
+ if (inFavorites) return false
+ const inRecents = recents.some(
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
)
- if (inFavorites) return false
if (inRecents) return false
return true
}),
@@ -196,7 +173,7 @@ export function DialogModel() {
keybind={[
{
keybind: { ctrl: true, name: "a", meta: false, shift: false, leader: false },
- title: connected() ? "Connect provider" : "More providers",
+ title: connected() ? "Connect provider" : "View all providers",
onTrigger() {
dialog.replace(() =>
)
},
@@ -204,6 +181,7 @@ export function DialogModel() {
{
keybind: Keybind.parse("ctrl+f")[0],
title: "Favorite",
+ disabled: !connected(),
onTrigger: (option) => {
local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
},
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
index 30a8bb2fc..8ba7845f2 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
@@ -26,13 +26,15 @@ export function createDialogProviderOptions() {
const options = createMemo(() => {
return pipe(
sync.data.provider_next.all,
+ sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
map((provider) => ({
title: provider.name,
value: provider.id,
- footer: {
- opencode: "Recommended",
- anthropic: "Claude Max or API key",
+ description: {
+ opencode: "(Recommended)",
+ anthropic: "(Claude Max or API key)",
}[provider.id],
+ category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
async onSelect() {
const methods = sync.data.provider_auth[provider.id] ?? [
{
@@ -85,7 +87,6 @@ export function createDialogProviderOptions() {
}
},
})),
- sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99),
)
})
return options
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
index 4232f3ae8..f74a176ec 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
@@ -239,7 +239,7 @@ export function Autocomplete(props: {
},
{
display: "/thinking",
- description: "toggle thinking blocks",
+ description: "toggle thinking visibility",
onSelect: () => command.trigger("session.toggle.thinking"),
},
)
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index 06e9a49e6..ae11fcc23 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -809,7 +809,8 @@ export function Prompt(props: PromptProps) {
borderColor={highlight()}
customBorderChars={{
...EmptyBorder,
- vertical: "╹",
+ // when the background is transparent, don't draw the vertical line
+ vertical: theme.background.a != 0 ? "╹" : " ",
}}
>
50) return r.message.slice(0, 50) + "..."
return r.message
})
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 fdbcb34f9..b6e363c86 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -81,6 +81,7 @@ const context = createContext<{
conceal: () => boolean
showThinking: () => boolean
showTimestamps: () => boolean
+ diffWrapMode: () => "word" | "none"
sync: ReturnType
}>()
@@ -111,8 +112,9 @@ export function Session() {
const dimensions = useTerminalDimensions()
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto"))
const [conceal, setConceal] = createSignal(true)
- const [showThinking, setShowThinking] = createSignal(true)
+ const [showThinking, setShowThinking] = createSignal(kv.get("thinking_visibility", true))
const [showTimestamps, setShowTimestamps] = createSignal(kv.get("timestamps", "hide") === "show")
+ const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
const wide = createMemo(() => dimensions().width > 120)
const sidebarVisible = createMemo(() => {
@@ -430,11 +432,24 @@ export function Session() {
},
},
{
- title: "Toggle thinking blocks",
+ title: showThinking() ? "Hide thinking" : "Show thinking",
value: "session.toggle.thinking",
category: "Session",
onSelect: (dialog) => {
- setShowThinking((prev) => !prev)
+ setShowThinking((prev) => {
+ const next = !prev
+ kv.set("thinking_visibility", next)
+ return next
+ })
+ dialog.clear()
+ },
+ },
+ {
+ title: "Toggle diff wrapping",
+ value: "session.toggle.diffwrap",
+ category: "Session",
+ onSelect: (dialog) => {
+ setDiffWrapMode((prev) => (prev === "word" ? "none" : "word"))
dialog.clear()
},
},
@@ -739,6 +754,7 @@ export function Session() {
conceal,
showThinking,
showTimestamps,
+ diffWrapMode,
sync,
}}
>
@@ -1298,21 +1314,9 @@ ToolRegistry.register({
container: "block",
render(props) {
const { theme, syntax } = useTheme()
- const lines = createMemo(
- () => (typeof props.input.content === "string" ? props.input.content.split("\n") : []),
- [] as string[],
- )
const code = createMemo(() => {
if (!props.input.content) return ""
- const text = props.input.content
- return text
- })
-
- const numbers = createMemo(() => {
- const pad = lines().length.toString().length
- return lines()
- .map((_, index) => index + 1)
- .map((x) => x.toString().padStart(pad, " "))
+ return props.input.content
})
const diagnostics = createMemo(() => props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? [])
@@ -1322,14 +1326,9 @@ ToolRegistry.register({
Wrote {props.input.filePath}
-
-
- {(value) => {value}}
-
-
-
-
-
+
+
+
{(diagnostic) => (
@@ -1401,15 +1400,15 @@ ToolRegistry.register({
return (
<>
-
- Task [{props.input.subagent_type ?? "unknown"}] {props.input.description}
+
+ {Locale.titlecase(props.input.subagent_type ?? "unknown")} Task "{props.input.description}"
{(task) => (
- ∟ {task.tool} {task.state.status === "completed" ? task.state.title : ""}
+ ∟ {Locale.titlecase(task.tool)} {task.state.status === "completed" ? task.state.title : ""}
)}
@@ -1471,84 +1470,17 @@ ToolRegistry.register({
const ctx = use()
const { theme, syntax } = useTheme()
- const style = createMemo(() => {
+ const view = createMemo(() => {
const diffStyle = ctx.sync.data.config.tui?.diff_style
- if (diffStyle === "stacked") return "stacked"
+ if (diffStyle === "stacked") return "unified"
// Default to "auto" behavior
- return ctx.width > 120 ? "split" : "stacked"
- })
-
- const diff = createMemo(() => {
- const diff = props.metadata.diff ?? props.permission["diff"]
- if (!diff) return null
-
- try {
- const patches = parsePatch(diff)
- if (patches.length === 0) return null
-
- const patch = patches[0]
- const oldLines: string[] = []
- const newLines: string[] = []
-
- for (const hunk of patch.hunks) {
- let i = 0
- while (i < hunk.lines.length) {
- const line = hunk.lines[i]
-
- if (line.startsWith("-")) {
- const removedLines: string[] = []
- while (i < hunk.lines.length && hunk.lines[i].startsWith("-")) {
- removedLines.push("- " + hunk.lines[i].slice(1))
- i++
- }
-
- const addedLines: string[] = []
- while (i < hunk.lines.length && hunk.lines[i].startsWith("+")) {
- addedLines.push("+ " + hunk.lines[i].slice(1))
- i++
- }
-
- const maxLen = Math.max(removedLines.length, addedLines.length)
- for (let j = 0; j < maxLen; j++) {
- oldLines.push(removedLines[j] ?? "")
- newLines.push(addedLines[j] ?? "")
- }
- } else if (line.startsWith("+")) {
- const addedLines: string[] = []
- while (i < hunk.lines.length && hunk.lines[i].startsWith("+")) {
- addedLines.push("+ " + hunk.lines[i].slice(1))
- i++
- }
-
- for (const added of addedLines) {
- oldLines.push("")
- newLines.push(added)
- }
- } else {
- oldLines.push(" " + line.slice(1))
- newLines.push(" " + line.slice(1))
- i++
- }
- }
- }
-
- return {
- oldContent: oldLines.join("\n"),
- newContent: newLines.join("\n"),
- }
- } catch (error) {
- return null
- }
- })
-
- const code = createMemo(() => {
- if (!props.metadata.diff) return ""
- const text = props.metadata.diff.split("\n").slice(5).join("\n")
- return text.trim()
+ return ctx.width > 120 ? "split" : "unified"
})
const ft = createMemo(() => filetype(props.input.filePath))
+ const diffContent = createMemo(() => props.metadata.diff ?? props.permission["diff"])
+
const diagnostics = createMemo(() => {
const arr = props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? []
return arr.filter((x) => x.severity === 1).slice(0, 3)
@@ -1562,26 +1494,28 @@ ToolRegistry.register({
replaceAll: props.input.replaceAll,
})}
-
-
- {props.permission["diff"]?.trim()}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
index 83f8e27fc..9ae370658 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
@@ -55,9 +55,6 @@ export function DialogPrompt(props: DialogPromptProps) {
enter submit
-
- esc cancel
-
)
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
index b8d2a5b14..f6d79946c 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
@@ -13,6 +13,7 @@ import { Locale } from "@/util/locale"
export interface DialogSelectProps
{
title: string
+ placeholder?: string
options: DialogSelectOption[]
ref?: (ref: DialogSelectRef) => void
onMove?: (option: DialogSelectOption) => void
@@ -21,6 +22,7 @@ export interface DialogSelectProps {
keybind?: {
keybind: Keybind.Info
title: string
+ disabled?: boolean
onTrigger: (option: DialogSelectOption) => void
}[]
current?: T
@@ -150,6 +152,7 @@ export function DialogSelect(props: DialogSelectProps) {
}
for (const item of props.keybind ?? []) {
+ if (item.disabled) continue
if (Keybind.match(item.keybind, keybind.parse(evt))) {
const s = selected()
if (s) {
@@ -171,8 +174,10 @@ export function DialogSelect(props: DialogSelectProps) {
}
props.ref?.(ref)
+ const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled) ?? [])
+
return (
-
+
@@ -195,7 +200,7 @@ export function DialogSelect(props: DialogSelectProps) {
input = r
setTimeout(() => input.focus(), 1)
}}
- placeholder="Enter search term"
+ placeholder={props.placeholder ?? "Search"}
/>
@@ -253,18 +258,20 @@ export function DialogSelect(props: DialogSelectProps) {
)}
-
-
- {(item) => (
-
-
- {item.title}{" "}
-
- {Keybind.toString(item.keybind)}
-
- )}
-
-
+ }>
+
+
+ {(item) => (
+
+
+ {item.title}{" "}
+
+ {Keybind.toString(item.keybind)}
+
+ )}
+
+
+
)
}
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 383a47566..a49612090 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -527,6 +527,7 @@ export namespace Config {
plan: Agent.optional(),
build: Agent.optional(),
general: Agent.optional(),
+ explore: Agent.optional(),
})
.catchall(Agent)
.optional()
diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts
index d1bff181f..404898080 100644
--- a/packages/opencode/src/format/formatter.ts
+++ b/packages/opencode/src/format/formatter.ts
@@ -246,3 +246,12 @@ export const htmlbeautifier: Info = {
return Bun.which("htmlbeautifier") !== null
},
}
+
+export const dart: Info = {
+ name: "dart",
+ command: ["dart", "format", "$FILE"],
+ extensions: [".dart"],
+ async enabled() {
+ return Bun.which("dart") !== null
+ },
+}
diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts
index 0bc229e97..ce2fbfa69 100644
--- a/packages/opencode/src/lsp/server.ts
+++ b/packages/opencode/src/lsp/server.ts
@@ -1166,4 +1166,22 @@ export namespace LSPServer {
}
},
}
+
+ export const Dart: Info = {
+ id: "dart",
+ extensions: [".dart"],
+ root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
+ async spawn(root) {
+ const dart = Bun.which("dart")
+ if (!dart) {
+ log.info("dart not found, please install dart first")
+ return
+ }
+ return {
+ process: spawn(dart, ["language-server", "--lsp"], {
+ cwd: root,
+ }),
+ }
+ },
+ }
}
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index a4c406c0f..ded0b9b19 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -677,6 +677,21 @@ export namespace Provider {
}
}
+ export async function closest(providerID: string, query: string[]) {
+ const s = await state()
+ const provider = s.providers[providerID]
+ if (!provider) return undefined
+ for (const item of query) {
+ for (const modelID of Object.keys(provider.info.models)) {
+ if (modelID.includes(item))
+ return {
+ providerID,
+ modelID,
+ }
+ }
+ }
+ }
+
export async function getSmallModel(providerID: string) {
const cfg = await Config.get()
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 1a9b08d12..718e90921 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -666,7 +666,7 @@ export namespace MessageV2 {
}
}
- return convertToModelMessages(result)
+ return convertToModelMessages(result.filter((msg) => msg.parts.length > 0))
}
export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts
index 5bd833c0f..2f2ba4e94 100644
--- a/packages/opencode/src/session/processor.ts
+++ b/packages/opencode/src/session/processor.ts
@@ -333,7 +333,7 @@ export namespace SessionProcessor {
error: e,
})
const error = MessageV2.fromError(e, { providerID: input.providerID })
- if ((error?.name === "APIError" && error.data.isRetryable) || error.data.message.includes("Overloaded")) {
+ if (error?.name === "APIError" && error.data.isRetryable) {
attempt++
const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined)
SessionStatus.set(input.sessionID, {
diff --git a/packages/plugin/package.json b/packages/plugin/package.json
index d4875c3a3..4efa3f44e 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.119",
+ "version": "1.0.120",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -24,4 +24,4 @@
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
-}
\ No newline at end of file
+}
diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json
index 1b9dc0fcf..9287d52fa 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.119",
+ "version": "1.0.120",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -26,4 +26,4 @@
"publishConfig": {
"directory": "dist"
}
-}
\ No newline at end of file
+}
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index bf23f77ec..70fceedbc 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/packages/sdk/js/src/gen/types.gen.ts
@@ -1073,6 +1073,7 @@ export type Config = {
plan?: AgentConfig
build?: AgentConfig
general?: AgentConfig
+ explore?: AgentConfig
[key: string]: AgentConfig | undefined
}
/**
diff --git a/packages/slack/package.json b/packages/slack/package.json
index 86a59c34a..3b8d25258 100644
--- a/packages/slack/package.json
+++ b/packages/slack/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
- "version": "1.0.119",
+ "version": "1.0.120",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
diff --git a/packages/tauri/package.json b/packages/tauri/package.json
index 0ac85eb68..6459963cb 100644
--- a/packages/tauri/package.json
+++ b/packages/tauri/package.json
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/tauri",
"private": true,
- "version": "1.0.119",
+ "version": "1.0.120",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 7dd60eb90..49d68a3b3 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
- "version": "1.0.119",
+ "version": "1.0.120",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",
diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx
index c895a6e29..bd2134515 100644
--- a/packages/ui/src/components/diff.tsx
+++ b/packages/ui/src/components/diff.tsx
@@ -46,7 +46,7 @@ export function Diff(props: DiffProps) {
})
onMount(() => {
- if (isServer) return
+ if (isServer || !props.preloadedDiff) return
fileDiffInstance = new FileDiff({
...createDefaultOptions(props.diffStyle),
...others,
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 40740fa1f..1c2ba97f3 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -17,7 +17,8 @@ import { Diff } from "./diff"
import { DiffChanges } from "./diff-changes"
import { Markdown } from "./markdown"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import { sanitize, sanitizePart } from "@opencode-ai/util/sanitize"
+import { sanitizePart } from "@opencode-ai/util/sanitize"
+import { unwrap } from "solid-js/store"
export interface MessageProps {
message: MessageType
@@ -83,15 +84,10 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
export function Part(props: MessagePartProps) {
const component = createMemo(() => PART_MAPPING[props.part.type])
+ const part = createMemo(() => sanitizePart(unwrap(props.part), props.sanitize))
return (
-
+
)
}
@@ -102,7 +98,6 @@ export interface ToolProps {
tool: string
output?: string
hideDetails?: boolean
- sanitize?: RegExp
}
export type ToolComponent = Component
@@ -170,7 +165,6 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
metadata={metadata}
output={part.state.status === "completed" ? part.state.output : undefined}
hideDetails={props.hideDetails}
- sanitize={props.sanitize}
/>
@@ -182,7 +176,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
PART_MAPPING["text"] = function TextPartDisplay(props) {
const part = props.part as TextPart
- const sanitized = createMemo(() => (props.sanitize ? (sanitizePart(part, props.sanitize) as TextPart) : part))
+ const sanitized = createMemo(() => (props.sanitize ? (sanitizePart(unwrap(part), props.sanitize) as TextPart) : part))
return (
@@ -211,7 +205,7 @@ ToolRegistry.register({
icon="glasses"
trigger={{
title: "Read",
- subtitle: props.input.filePath ? getFilename(sanitize(props.input.filePath, props.sanitize)) : "",
+ subtitle: props.input.filePath ? getFilename(props.input.filePath) : "",
}}
/>
)
@@ -222,12 +216,9 @@ ToolRegistry.register({
name: "list",
render(props) {
return (
-
+
- {sanitize(props.output, props.sanitize)}
+ {props.output}
)
@@ -335,7 +326,7 @@ ToolRegistry.register({
>
@@ -355,13 +346,9 @@ ToolRegistry.register({
Edit
-
- {getDirectory(sanitize(props.input.filePath!, props.sanitize))}
-
+ {getDirectory(props.input.filePath!)}
-
- {getFilename(sanitize(props.input.filePath ?? "", props.sanitize))}
-
+ {getFilename(props.input.filePath ?? "")}
@@ -376,11 +363,11 @@ ToolRegistry.register({
diff --git a/packages/ui/src/components/pierre.ts b/packages/ui/src/components/pierre.ts
index ef01318de..5821697c7 100644
--- a/packages/ui/src/components/pierre.ts
+++ b/packages/ui/src/components/pierre.ts
@@ -6,7 +6,7 @@ export function createDefaultOptions
(style: FileDiffOptions["diffStyle"])
themeType: "system",
disableLineNumbers: false,
overflow: "wrap",
- diffStyle: style,
+ diffStyle: style ?? "unified",
diffIndicators: "bars",
disableBackground: false,
expansionLineCount: 20,
diff --git a/packages/ui/src/components/session-message-rail.css b/packages/ui/src/components/session-message-rail.css
new file mode 100644
index 000000000..9d302e10e
--- /dev/null
+++ b/packages/ui/src/components/session-message-rail.css
@@ -0,0 +1,42 @@
+[data-component="session-message-rail"] {
+ display: contents;
+}
+
+[data-slot="session-message-rail-compact"],
+[data-slot="session-message-rail-full"] {
+ position: absolute;
+ left: 1.5rem;
+ margin-top: 0.625rem;
+}
+
+[data-slot="session-message-rail-compact"] {
+ display: flex;
+}
+
+[data-slot="session-message-rail-full"] {
+ display: none;
+}
+
+@media (min-width: 72rem) {
+ [data-slot="session-message-rail-compact"] {
+ display: none;
+ }
+
+ [data-slot="session-message-rail-full"] {
+ display: flex;
+ }
+}
+
+[data-component="session-message-rail"] [data-slot="session-message-rail-full"] {
+ transform: none;
+}
+
+[data-component="session-message-rail"][data-wide] [data-slot="session-message-rail-full"] {
+ margin-top: 0.125rem;
+ left: calc(((100% - min(100%, 36.5rem)) / 2) - 1.5rem);
+ transform: translateX(-100%);
+}
+
+[data-component="session-message-rail"]:not([data-wide]) [data-slot="session-message-rail-full"] {
+ margin-top: 0.625rem;
+}
diff --git a/packages/ui/src/components/session-message-rail.tsx b/packages/ui/src/components/session-message-rail.tsx
new file mode 100644
index 000000000..29d9ac81e
--- /dev/null
+++ b/packages/ui/src/components/session-message-rail.tsx
@@ -0,0 +1,57 @@
+import { UserMessage } from "@opencode-ai/sdk"
+import { ComponentProps, Show, splitProps } from "solid-js"
+import { MessageNav } from "./message-nav"
+import "./session-message-rail.css"
+
+export interface SessionMessageRailProps extends ComponentProps<"div"> {
+ messages: UserMessage[]
+ current?: UserMessage
+ working?: boolean
+ wide?: boolean
+ onMessageSelect: (message: UserMessage) => void
+}
+
+export function SessionMessageRail(props: SessionMessageRailProps) {
+ const [local, others] = splitProps(props, [
+ "messages",
+ "current",
+ "working",
+ "wide",
+ "onMessageSelect",
+ "class",
+ "classList",
+ ])
+
+ return (
+ 1}>
+
+
+ )
+}
diff --git a/packages/util/package.json b/packages/util/package.json
index fe060c21a..62b8b3176 100644
--- a/packages/util/package.json
+++ b/packages/util/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
- "version": "1.0.119",
+ "version": "1.0.120",
"private": true,
"type": "module",
"exports": {
diff --git a/packages/util/src/sanitize.ts b/packages/util/src/sanitize.ts
index 270b618ae..38ad2b290 100644
--- a/packages/util/src/sanitize.ts
+++ b/packages/util/src/sanitize.ts
@@ -2,7 +2,7 @@ import type { Part } from "@opencode-ai/sdk/client"
export const sanitize = (text: string | undefined, remove?: RegExp) => (remove ? text?.replace(remove, "") : text) ?? ""
-export const sanitizePart = (part: Part, remove: RegExp) => {
+export const sanitizePart = (part: Part, remove: RegExp | undefined) => {
if (part.type === "text") {
part.text = sanitize(part.text, remove)
} else if (part.type === "reasoning") {
diff --git a/packages/web/package.json b/packages/web/package.json
index 33c374ec6..b006df008 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
- "version": "1.0.119",
+ "version": "1.0.120",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
diff --git a/packages/web/src/content/docs/formatters.mdx b/packages/web/src/content/docs/formatters.mdx
index 9fc41a53d..2af4207ad 100644
--- a/packages/web/src/content/docs/formatters.mdx
+++ b/packages/web/src/content/docs/formatters.mdx
@@ -26,6 +26,7 @@ OpenCode comes with several built-in formatters for popular languages and framew
| standardrb | .rb, .rake, .gemspec, .ru | `standardrb` command available |
| htmlbeautifier | .erb, .html.erb | `htmlbeautifier` command available |
| air | .R | `air` command available |
+| dart | .dart | `dart` command available |
So if your project has `prettier` in your `package.json`, OpenCode will automatically use it.
@@ -69,7 +70,16 @@ Let's look at some examples.
### Disabling formatters
-To disable a specific formatter, set `disabled` to `true`:
+To disable **all** formatters globally, set `formatter` to `false`:
+
+```json title="opencode.json" {3}
+{
+ "$schema": "https://opencode.ai/config.json",
+ "formatter": false
+}
+```
+
+To disable a **specific** formatter, set `disabled` to `true`:
```json title="opencode.json" {5}
{
diff --git a/packages/web/src/content/docs/lsp.mdx b/packages/web/src/content/docs/lsp.mdx
index 5c12f03f6..8f35cf106 100644
--- a/packages/web/src/content/docs/lsp.mdx
+++ b/packages/web/src/content/docs/lsp.mdx
@@ -32,6 +32,7 @@ OpenCode comes with several built-in LSP servers for popular languages:
| lua-ls | .lua | Auto-installs for Lua projects |
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |
| php intelephense | .php | Auto-installs for PHP projects |
+| dart | .dart | `dart` command available |
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
@@ -77,7 +78,16 @@ Let's look at some examples.
### Disabling LSP servers
-To disable a specific LSP server, set `disabled` to `true`:
+To disable **all** LSP servers globally, set `lsp` to `false`:
+
+```json title="opencode.json" {3}
+{
+ "$schema": "https://opencode.ai/config.json",
+ "lsp": false
+}
+```
+
+To disable a **specific** LSP server, set `disabled` to `true`:
```json title="opencode.json" {5}
{
diff --git a/packages/web/src/content/docs/tools.mdx b/packages/web/src/content/docs/tools.mdx
index 11761d4c8..78b68bef3 100644
--- a/packages/web/src/content/docs/tools.mdx
+++ b/packages/web/src/content/docs/tools.mdx
@@ -228,6 +228,10 @@ Apply patches to files.
This tool applies patch files to your codebase. Useful for applying diffs and patches from various sources.
+:::tip
+This tool is disabled for subagents by default, but you can enable it manually. [Learn more](/docs/agents/#tools)
+:::
+
---
### todowrite
@@ -245,6 +249,10 @@ Manage todo lists during coding sessions.
Creates and updates task lists to track progress during complex operations. The LLM uses this to organize multi-step tasks.
+:::tip
+This tool is disabled for subagents by default, but you can enable it manually. [Learn more](/docs/agents/#tools)
+:::
+
---
### todoread
diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx
index f672662ed..5632bcca4 100644
--- a/packages/web/src/content/docs/zen.mdx
+++ b/packages/web/src/content/docs/zen.mdx
@@ -108,8 +108,8 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
| Big Pickle | Free | Free | Free | - |
| Grok Code Fast 1 | Free | Free | Free | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
-| Kimi K2 | $0.45 | $2.50 | - | - |
-| Kimi K2 Thinking | $0.60 | $2.50 | - | - |
+| Kimi K2 | $0.40 | $2.50 | - | - |
+| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
diff --git a/script/publish.ts b/script/publish.ts
index 364a1d217..f29b29cfc 100755
--- a/script/publish.ts
+++ b/script/publish.ts
@@ -73,6 +73,44 @@ if (!Script.preview) {
console.log(notes.join("\n"))
console.log("-----------------------------")
opencode.server.close()
+
+ // Get contributors
+ const team = [
+ "actions-user",
+ "opencode",
+ "rekram1-node",
+ "thdxr",
+ "kommander",
+ "jayair",
+ "fwang",
+ "adamdotdevin",
+ "opencode-agent[bot]",
+ ]
+ const compare =
+ await $`gh api "/repos/sst/opencode/compare/v${previous}...HEAD" --jq '.commits[] | {login: .author.login, message: .commit.message}'`.text()
+ const contributors = new Map()
+
+ for (const line of compare.split("\n").filter(Boolean)) {
+ const { login, message } = JSON.parse(line) as { login: string | null; message: string }
+ const title = message.split("\n")[0] ?? ""
+ if (title.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue
+
+ if (login && !team.includes(login)) {
+ if (!contributors.has(login)) contributors.set(login, [])
+ contributors.get(login)?.push(title)
+ }
+ }
+
+ if (contributors.size > 0) {
+ notes.push("")
+ notes.push(`**Thank you to ${contributors.size} community contributor${contributors.size > 1 ? "s" : ""}:**`)
+ for (const [username, userCommits] of contributors) {
+ notes.push(`- @${username}:`)
+ for (const commit of userCommits) {
+ notes.push(` - ${commit}`)
+ }
+ }
+ }
}
const pkgjsons = await Array.fromAsync(
diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json
index d3294b1cb..eec8115d9 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.119",
+ "version": "1.0.120",
"publisher": "sst-dev",
"repository": {
"type": "git",