Просмотр исходного кода

Merge remote-tracking branch 'public/dev' into ide-plugin

# Conflicts:
#	nix/hashes.json
paviko 4 месяцев назад
Родитель
Сommit
3fe6e3b0a1
51 измененных файлов с 603 добавлено и 327 удалено
  1. 16 8
      .github/workflows/auto-label-tui.yml
  2. 1 0
      STATS.md
  3. 34 27
      bun.lock
  4. 1 1
      nix/hashes.json
  5. 1 1
      package.json
  6. 1 1
      packages/console/app/package.json
  7. 8 0
      packages/console/app/src/component/icon.tsx
  8. 2 2
      packages/console/app/src/config.ts
  9. 24 2
      packages/console/app/src/routes/workspace/[id]/model-section.tsx
  10. 1 1
      packages/console/core/package.json
  11. 1 1
      packages/console/function/package.json
  12. 1 1
      packages/console/mail/package.json
  13. 1 1
      packages/desktop/package.json
  14. 20 0
      packages/desktop/src/hooks/create-session-seen.ts
  15. 7 4
      packages/desktop/src/pages/session.tsx
  16. 6 6
      packages/extensions/zed/extension.toml
  17. 1 1
      packages/function/package.json
  18. 4 3
      packages/opencode/package.json
  19. 9 0
      packages/opencode/parsers-config.ts
  20. 28 1
      packages/opencode/script/publish.ts
  21. 42 10
      packages/opencode/src/bun/index.ts
  22. 11 31
      packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
  23. 5 1
      packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
  24. 68 62
      packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
  25. 11 40
      packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
  26. 3 1
      packages/opencode/src/cli/cmd/tui/ui/toast.tsx
  27. 88 0
      packages/opencode/src/lsp/server.ts
  28. 2 1
      packages/opencode/src/mcp/index.ts
  29. 16 12
      packages/opencode/src/provider/provider.ts
  30. 21 1
      packages/opencode/src/provider/transform.ts
  31. 25 17
      packages/opencode/src/session/index.ts
  32. 13 15
      packages/opencode/src/session/prompt.ts
  33. 28 5
      packages/opencode/src/tool/bash.ts
  34. 1 1
      packages/opencode/src/tool/bash.txt
  35. 1 1
      packages/opencode/src/tool/edit.ts
  36. 1 1
      packages/opencode/src/tool/patch.ts
  37. 1 1
      packages/opencode/src/tool/read.ts
  38. 1 1
      packages/opencode/src/tool/write.ts
  39. 19 16
      packages/opencode/test/tool/bash.test.ts
  40. 1 1
      packages/plugin/package.json
  41. 1 1
      packages/sdk/js/package.json
  42. 4 0
      packages/sdk/js/src/gen/types.gen.ts
  43. 1 1
      packages/slack/package.json
  44. 1 1
      packages/ui/package.json
  45. 12 6
      packages/ui/src/components/markdown.css
  46. 2 0
      packages/ui/src/components/message-part.css
  47. 1 1
      packages/util/package.json
  48. 1 1
      packages/web/package.json
  49. 34 19
      packages/web/src/content/docs/lsp.mdx
  50. 20 17
      packages/web/src/content/docs/zen.mdx
  51. 1 1
      sdks/vscode/package.json

+ 16 - 8
.github/workflows/auto-label-tui.yml

@@ -28,14 +28,14 @@ jobs:
             const versionPattern = /[v]?1\.0\./i;
             const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
 
+            // Check for "nix" keyword
+            const nixPattern = /\bnix\b/i;
+            const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
+
+            const labels = [];
+
             if (isWebRelated) {
-              // Add web label
-              await github.rest.issues.addLabels({
-                owner: context.repo.owner,
-                repo: context.repo.repo,
-                issue_number: issue.number,
-                labels: ['web']
-              });
+              labels.push('web');
               
               // Assign to adamdotdevin
               await github.rest.issues.addAssignees({
@@ -46,10 +46,18 @@ jobs:
               });
             } else if (isVersionRelated) {
               // Only add opentui if NOT web-related
+              labels.push('opentui');
+            }
+
+            if (isNixRelated) {
+              labels.push('nix');
+            }
+
+            if (labels.length > 0) {
               await github.rest.issues.addLabels({
                 owner: context.repo.owner,
                 repo: context.repo.repo,
                 issue_number: issue.number,
-                labels: ['opentui']
+                labels: labels
               });
             }

+ 1 - 0
STATS.md

@@ -144,3 +144,4 @@
 | 2025-11-16 | 771,069 (+5,114)  | 716,596 (+3,726)  | 1,487,665 (+8,840)  |
 | 2025-11-17 | 780,161 (+9,092)  | 723,339 (+6,743)  | 1,503,500 (+15,835) |
 | 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205)  | 1,524,107 (+20,607) |
+| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |

+ 34 - 27
bun.lock

@@ -43,7 +43,7 @@
     },
     "packages/console/core": {
       "name": "@opencode-ai/console-core",
-      "version": "1.0.78",
+      "version": "1.0.80",
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "@jsx-email/render": "1.1.1",
@@ -70,7 +70,7 @@
     },
     "packages/console/function": {
       "name": "@opencode-ai/console-function",
-      "version": "1.0.78",
+      "version": "1.0.80",
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
@@ -94,7 +94,7 @@
     },
     "packages/console/mail": {
       "name": "@opencode-ai/console-mail",
-      "version": "1.0.78",
+      "version": "1.0.80",
       "dependencies": {
         "@jsx-email/all": "2.2.3",
         "@jsx-email/cli": "1.4.3",
@@ -118,7 +118,7 @@
     },
     "packages/desktop": {
       "name": "@opencode-ai/desktop",
-      "version": "1.0.78",
+      "version": "1.0.80",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -158,7 +158,7 @@
     },
     "packages/function": {
       "name": "@opencode-ai/function",
-      "version": "1.0.78",
+      "version": "1.0.80",
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "22.0.0",
@@ -174,7 +174,7 @@
     },
     "packages/opencode": {
       "name": "opencode",
-      "version": "1.0.78",
+      "version": "1.0.80",
       "bin": {
         "opencode": "./bin/opencode",
       },
@@ -182,6 +182,7 @@
         "@actions/core": "1.11.1",
         "@actions/github": "6.0.1",
         "@agentclientprotocol/sdk": "0.5.1",
+        "@ai-sdk/mcp": "0.0.8",
         "@clack/prompts": "1.0.0-alpha.1",
         "@hono/standard-validator": "0.1.5",
         "@hono/zod-validator": "catalog:",
@@ -193,8 +194,8 @@
         "@opencode-ai/plugin": "workspace:*",
         "@opencode-ai/script": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
-        "@opentui/core": "0.1.46",
-        "@opentui/solid": "0.1.46",
+        "@opentui/core": "0.1.47",
+        "@opentui/solid": "0.1.47",
         "@parcel/watcher": "2.5.1",
         "@pierre/precision-diffs": "catalog:",
         "@solid-primitives/event-bus": "1.1.2",
@@ -289,7 +290,7 @@
     },
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
-      "version": "1.0.78",
+      "version": "1.0.80",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "zod": "catalog:",
@@ -309,7 +310,7 @@
     },
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
-      "version": "1.0.78",
+      "version": "1.0.80",
       "devDependencies": {
         "@hey-api/openapi-ts": "0.81.0",
         "@tsconfig/node22": "catalog:",
@@ -320,7 +321,7 @@
     },
     "packages/slack": {
       "name": "@opencode-ai/slack",
-      "version": "1.0.78",
+      "version": "1.0.80",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@slack/bolt": "^3.17.1",
@@ -333,7 +334,7 @@
     },
     "packages/ui": {
       "name": "@opencode-ai/ui",
-      "version": "1.0.78",
+      "version": "1.0.80",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -363,7 +364,7 @@
     },
     "packages/util": {
       "name": "@opencode-ai/util",
-      "version": "0.0.0",
+      "version": "1.0.80",
       "dependencies": {
         "zod": "catalog:",
       },
@@ -373,7 +374,7 @@
     },
     "packages/web": {
       "name": "@opencode-ai/web",
-      "version": "1.0.78",
+      "version": "1.0.80",
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
@@ -427,7 +428,7 @@
     "@types/bun": "1.3.0",
     "@types/node": "22.13.9",
     "@typescript/native-preview": "7.0.0-dev.20251014.1",
-    "ai": "5.0.8",
+    "ai": "5.0.97",
     "diff": "8.0.2",
     "fuzzysort": "3.1.0",
     "hono": "4.7.10",
@@ -462,12 +463,14 @@
 
     "@ai-sdk/anthropic": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
 
-    "@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.4", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-1roLdgMbFU3Nr4MC97/te7w6OqxsWBkDUkpbCcvxF3jz/ku91WVaJldn/PKU8feMKNyI5W9wnqhbjb1BqbExOQ=="],
+    "@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/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.7" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-dnVIgSz1DZD/0gVau6ifYN3HZFN15HZwC9VjevTFfvrfSfbEvpXj5x/k/zk/0XuQrlQ5g8JiwJtxc9bx24x2xw=="],
 
     "@ai-sdk/google-vertex": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.9", "@ai-sdk/google": "2.0.11", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.7", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-tStlnOCRGRqKKJSCOtXhijX4r9kYVK2v+Vs7miJnfvr3sZfO8nRS0xnNhfgu17xuNi5LMMufeCYURTz4lKxzUQ=="],
 
+    "@ai-sdk/mcp": ["@ai-sdk/[email protected]", "", { "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=="],
+
     "@ai-sdk/openai": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="],
 
     "@ai-sdk/openai-compatible": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="],
@@ -1094,21 +1097,21 @@
 
     "@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
-    "@opentui/core": ["@opentui/[email protected]6", "", { "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.46", "@opentui/core-darwin-x64": "0.1.46", "@opentui/core-linux-arm64": "0.1.46", "@opentui/core-linux-x64": "0.1.46", "@opentui/core-win32-arm64": "0.1.46", "@opentui/core-win32-x64": "0.1.46", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-9682jrr65vYP0hPHfrZRK3xymlCSLVBMrRKNtclFasDi6bRvACUrtziFOIIyMIvPHRJCFWPbtz0MppARmN4zvQ=="],
+    "@opentui/core": ["@opentui/[email protected]7", "", { "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.47", "@opentui/core-darwin-x64": "0.1.47", "@opentui/core-linux-arm64": "0.1.47", "@opentui/core-linux-x64": "0.1.47", "@opentui/core-win32-arm64": "0.1.47", "@opentui/core-win32-x64": "0.1.47", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-gKcYX9EJ/e5VLEwBH2kalDr5xoI9MEanzQV7uV3Sb2Z9+ndwEUShKKna3odN8g4E20c4sX2VpwmB9hhl3Tsd9w=="],
 
-    "@opentui/core-darwin-arm64": ["@opentui/[email protected]6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sp/uFS8J/1yVfhgkTJ43OZfy64hv1/9sdT+oC5yb8XTNPI3QGtg6ixjr3nRoD/Lkxuj2i5SJ30RZufqH6rkCpA=="],
+    "@opentui/core-darwin-arm64": ["@opentui/[email protected]7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0/u4VkJJPvW24cZzMaKf6Dm+VzeO1a94l6NV3AQ1Wb+pPTEyOmNWkRvj03ZrRLMCyQduaFVtlnor8DVCk6OHuQ=="],
 
-    "@opentui/core-darwin-x64": ["@opentui/[email protected]6", "", { "os": "darwin", "cpu": "x64" }, "sha512-JtxEruRyLQRK8ByPmBm1nYYSvnX6mXNC+mngvd5RDiCzLzkM6qVBQBd/m3Hxp2/s6MO5Z2+iVBzZ8XFH5T4IZw=="],
+    "@opentui/core-darwin-x64": ["@opentui/[email protected]7", "", { "os": "darwin", "cpu": "x64" }, "sha512-y1+c/e+IaZAj5N02GnD+oaubbb5JiW5eKgF0h58kw73iXDMfynuoGOpREz58i1rUFYOMYJGdrSjEHtXk2pD2XA=="],
 
-    "@opentui/core-linux-arm64": ["@opentui/[email protected]6", "", { "os": "linux", "cpu": "arm64" }, "sha512-pN8nR4CwBlkZ5uh5KvoytiXXav2GhkP9cB2d3gPe49c7MBz2XrjGexgb47xjaq0hAVbytv9XUifqdPTcFQdPaQ=="],
+    "@opentui/core-linux-arm64": ["@opentui/[email protected]7", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZESHmqILtfb6FFEfi40JGKl8z0+LhOSoHgfOK1PPyuyRT9Mk8uXeQgPMF5W6Ac0pp4w+uWVC4TrFjijCCSiaUQ=="],
 
-    "@opentui/core-linux-x64": ["@opentui/[email protected]6", "", { "os": "linux", "cpu": "x64" }, "sha512-oH4/FEYZYce9qMQVqGl4+Btw+Mfsf6ybpWIIJUJjXMWWZlAgsTMAWM8m195Oe6WstfFLF+nRH7NUcm/YOsCHnw=="],
+    "@opentui/core-linux-x64": ["@opentui/[email protected]7", "", { "os": "linux", "cpu": "x64" }, "sha512-qfvy1qshgnZMcAHQ3MS093IBjxM2pPx+kEnW7icsyud60zoJgoUugdN2kjgJiIJiYX3f3PgE68J6CVW2MCtYfQ=="],
 
-    "@opentui/core-win32-arm64": ["@opentui/[email protected]6", "", { "os": "win32", "cpu": "arm64" }, "sha512-C/rTBJ9bzBcZJRCLIxi9Ka/DANe2SaHtryotseWPk9RDydw7LTHGoi3VtRW0RFijQGqmvFg+31MeNhvY1YZ65Q=="],
+    "@opentui/core-win32-arm64": ["@opentui/[email protected]7", "", { "os": "win32", "cpu": "arm64" }, "sha512-f6OoPnaz303H6fudi8blS+iEcJtlFlcqdBoWnWnJQfN9rLmajW3Yf7RfpNOoLUlDcwxQLyTL/5EHwbcG8D4r7A=="],
 
-    "@opentui/core-win32-x64": ["@opentui/[email protected]6", "", { "os": "win32", "cpu": "x64" }, "sha512-d2DXSlA93LbSriX+pDDZ5sMwkcW1+eVoeykxeW4UParSb4/3ceBCD4aSARaZ6yoq0rR1IWOdgKdiihZH4mwdJQ=="],
+    "@opentui/core-win32-x64": ["@opentui/[email protected]7", "", { "os": "win32", "cpu": "x64" }, "sha512-lQnJg7FucyyTbN/ybTj5FZ7S8OAfT5KxXDR5l9Sla7R5MIDY6nBXYM3GWeF81jzDd4K4Z/0hxNFtWSopEXRFYg=="],
 
-    "@opentui/solid": ["@opentui/[email protected]6", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.46", "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-t+LDXS+FBT+fjQHOxuL44Bx1jGuXuLouyB25BZuypQKKT8sOhi8rJ5+Q4UwH5lrI4OoRBXmzrgsWj7DD58sHDw=="],
+    "@opentui/solid": ["@opentui/[email protected]7", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.47", "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-azN2sf8X/6HiLkz8ip2lcY532ApNEkl+BHd+wml/HdwdgLE7nthgA6x8Pgvi7f4qkRmpeYATU+danIzB6K6B8A=="],
 
     "@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
 
@@ -1650,6 +1653,8 @@
 
     "@vercel/nft": ["@vercel/[email protected]", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-UEq+eF0ocEf9WQCV1gktxKhha36KDs7jln5qii6UpPf5clMqDc0p3E7d9l2Smx0i9Pm1qpq4S4lLfNl97bbv6w=="],
 
+    "@vercel/oidc": ["@vercel/[email protected]", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="],
+
     "@vinxi/listhen": ["@vinxi/[email protected]", "", { "dependencies": { "@parcel/watcher": "^2.3.0", "@parcel/watcher-wasm": "2.3.0", "citty": "^0.1.5", "clipboardy": "^4.0.0", "consola": "^3.2.3", "defu": "^6.1.4", "get-port-please": "^3.1.2", "h3": "^1.10.0", "http-shutdown": "^1.2.2", "jiti": "^1.21.0", "mlly": "^1.5.0", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", "ufo": "^1.3.2", "untun": "^0.1.3", "uqr": "^0.1.2" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-WSN1z931BtasZJlgPp704zJFnQFRg7yzSjkm3MzAWQYe4uXFXlFr1hc5Ac2zae5/HDOz5x1/zDM5Cb54vTCnWw=="],
 
     "@vinxi/plugin-directives": ["@vinxi/[email protected]", "", { "dependencies": { "@babel/parser": "^7.23.5", "acorn": "^8.10.0", "acorn-jsx": "^5.3.2", "acorn-loose": "^8.3.0", "acorn-typescript": "^1.4.3", "astring": "^1.8.6", "magicast": "^0.2.10", "recast": "^0.23.4", "tslib": "^2.6.2" }, "peerDependencies": { "vinxi": "^0.5.5" } }, "sha512-pH/KIVBvBt7z7cXrUH/9uaqcdxjegFC7+zvkZkdOyWzs+kQD5KPf3cl8kC+5ayzXHT+OMlhGhyitytqN3cGmHg=="],
@@ -1684,7 +1689,7 @@
 
     "agentkeepalive": ["[email protected]", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
 
-    "ai": ["[email protected].8", "", { "dependencies": { "@ai-sdk/gateway": "1.0.4", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-qbnhj046UvG30V1S5WhjBn+RBGEAmi8PSZWqMhRsE3EPxvO5BcePXTZFA23e9MYyWS9zr4Vm8Mv3wQXwLmtIBw=="],
+    "ai": ["[email protected].97", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="],
 
     "ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
 
@@ -3862,7 +3867,7 @@
 
     "@ai-sdk/amazon-bedrock/zod": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
 
-    "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="],
+    "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]7", "", { "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/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA=="],
 
@@ -3870,6 +3875,8 @@
 
     "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA=="],
 
+    "@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "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=="],
+
     "@astrojs/cloudflare/vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
 
     "@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/[email protected]", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
@@ -4144,7 +4151,7 @@
 
     "accepts/mime-types": ["[email protected]", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
 
-    "ai/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="],
+    "ai/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]7", "", { "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=="],
 
     "ansi-align/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 

+ 1 - 1
nix/hashes.json

@@ -1,3 +1,3 @@
 {
-  "nodeModules": "sha256-8f5VTGkHFxHKVCKE2MCrrALy+3J+/4dknyzkgBOLNk8="
+  "nodeModules": "sha256-xqiDrKpODha+cfU6UpXLEUcApZ1xEkjRpqzFVJmq1uA="
 }

+ 1 - 1
package.json

@@ -33,7 +33,7 @@
       "@solidjs/meta": "0.29.4",
       "@tailwindcss/vite": "4.1.11",
       "diff": "8.0.2",
-      "ai": "5.0.8",
+      "ai": "5.0.97",
       "hono": "4.7.10",
       "fuzzysort": "3.1.0",
       "luxon": "3.6.1",

+ 1 - 1
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.78"
+    "version": "1.0.80"
   },
   "dependencies": {
     "@ibm/plex": "6.4.1",

+ 8 - 0
packages/console/app/src/component/icon.tsx

@@ -202,6 +202,14 @@ export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
   )
 }
 
+export function IconGoogle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+  return (
+    <svg {...props} viewBox="0 0 50 50" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
+      <path d="M49.04,24.001l-1.082-0.043h-0.001C36.134,23.492,26.508,13.866,26.042,2.043L25.999,0.96C25.978,0.424,25.537,0,25,0	s-0.978,0.424-0.999,0.96l-0.043,1.083C23.492,13.866,13.866,23.492,2.042,23.958L0.96,24.001C0.424,24.022,0,24.463,0,25	c0,0.537,0.424,0.978,0.961,0.999l1.082,0.042c11.823,0.467,21.449,10.093,21.915,21.916l0.043,1.083C24.022,49.576,24.463,50,25,50	s0.978-0.424,0.999-0.96l0.043-1.083c0.466-11.823,10.092-21.449,21.915-21.916l1.082-0.042C49.576,25.978,50,25.537,50,25	C50,24.463,49.576,24.022,49.04,24.001z"></path>
+    </svg>
+  )
+}
+
 export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
   return (
     <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">

+ 2 - 2
packages/console/app/src/config.ts

@@ -22,8 +22,8 @@ export const config = {
 
   // Static stats (used on landing page)
   stats: {
-    contributors: "250",
-    commits: "3,500",
+    contributors: "300",
+    commits: "4,000",
     monthlyUsers: "300,000",
   },
 } as const

+ 24 - 2
packages/console/app/src/routes/workspace/[id]/model-section.tsx

@@ -5,11 +5,21 @@ import { withActor } from "~/context/auth.withActor"
 import { ZenData } from "@opencode-ai/console-core/model.js"
 import styles from "./model-section.module.css"
 import { querySessionInfo } from "../common"
-import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon"
+import {
+  IconAlibaba,
+  IconAnthropic,
+  IconGoogle,
+  IconMoonshotAI,
+  IconOpenAI,
+  IconStealth,
+  IconXai,
+  IconZai,
+} from "~/component/icon"
 
 const getModelLab = (modelId: string) => {
   if (modelId.startsWith("claude")) return "Anthropic"
   if (modelId.startsWith("gpt")) return "OpenAI"
+  if (modelId.startsWith("gemini")) return "Google"
   if (modelId.startsWith("kimi")) return "Moonshot AI"
   if (modelId.startsWith("glm")) return "Z.ai"
   if (modelId.startsWith("qwen")) return "Alibaba"
@@ -24,7 +34,17 @@ const getModelsInfo = query(async (workspaceID: string) => {
       all: Object.entries(ZenData.list().models)
         .filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
         .filter(([id, _model]) => !id.startsWith("alpha-"))
-        .sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name))
+        .sort(([idA, modelA], [idB, modelB]) => {
+          const priority = ["big-pickle", "grok", "claude", "gpt", "gemini"]
+          const getPriority = (id: string) => {
+            const index = priority.findIndex((p) => id.startsWith(p))
+            return index === -1 ? Infinity : index
+          }
+          const pA = getPriority(idA)
+          const pB = getPriority(idB)
+          if (pA !== pB) return pA - pB
+          return modelA.name.localeCompare(modelB.name)
+        })
         .map(([id, model]) => ({ id, name: model.name })),
       disabled: await Model.listDisabled(),
     }
@@ -96,6 +116,8 @@ export function ModelSection() {
                                   return <IconOpenAI width={16} height={16} />
                                 case "Anthropic":
                                   return <IconAnthropic width={16} height={16} />
+                                case "Google":
+                                  return <IconGoogle width={16} height={16} />
                                 case "Moonshot AI":
                                   return <IconMoonshotAI width={16} height={16} />
                                 case "Z.ai":

+ 1 - 1
packages/console/core/package.json

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

+ 1 - 1
packages/console/function/package.json

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

+ 1 - 1
packages/console/mail/package.json

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

+ 1 - 1
packages/desktop/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/desktop",
-  "version": "1.0.78",
+  "version": "1.0.80",
   "description": "",
   "type": "module",
   "scripts": {

+ 20 - 0
packages/desktop/src/hooks/create-session-seen.ts

@@ -0,0 +1,20 @@
+import { createSignal, onCleanup, onMount } from "solid-js"
+import { isServer } from "solid-js/web"
+
+export function createSessionSeen(key: string, delay = 1000) {
+  // 1. Initialize state based on storage (default to true on server to avoid flash)
+  const storageKey = `app:seen:${key}`
+  const [hasSeen] = createSignal(!isServer && sessionStorage.getItem(storageKey) === "true")
+
+  onMount(() => {
+    // 2. If we haven't seen it, mark it as seen for NEXT time
+    if (!hasSeen()) {
+      const timer = setTimeout(() => {
+        sessionStorage.setItem(storageKey, "true")
+      }, delay)
+      onCleanup(() => clearTimeout(timer))
+    }
+  })
+
+  return hasSeen
+}

+ 7 - 4
packages/desktop/src/pages/session.tsx

@@ -52,6 +52,7 @@ import { useSession } from "@/context/session"
 import { StickyAccordionHeader } from "@/components/sticky-accordion-header"
 import { SessionReview } from "@/components/session-review"
 import { useLayout } from "@/context/layout"
+import { createSessionSeen } from "@/hooks/create-session-seen"
 
 export default function Page() {
   const layout = useLayout()
@@ -451,7 +452,9 @@ export default function Page() {
                         <For each={session.messages.user()}>
                           {(message) => {
                             const isActive = createMemo(() => session.messages.active()?.id === message.id)
-                            const [titled, setTitled] = createSignal(!!message.summary?.title)
+                            const titleSeen = createSessionSeen(`message-title-${message.id}`)
+                            const contentSeen = createSessionSeen(`message-content-${message.id}`)
+                            const [titled, setTitled] = createSignal(titleSeen())
                             const assistantMessages = createMemo(() => {
                               if (!session.id) return []
                               return sync.data.message[session.id]?.filter(
@@ -474,8 +477,9 @@ export default function Page() {
 
                             // allowing time for the animations to finish
                             createEffect(() => {
+                              if (titleSeen()) return
                               const title = message.summary?.title
-                              setTimeout(() => setTitled(!!title), 10_000)
+                              if (title) setTimeout(() => setTitled(true), 10_000)
                             })
                             createEffect(() => {
                               const completed = !working()
@@ -523,8 +527,7 @@ export default function Page() {
                                             <Markdown
                                               classList={{
                                                 "text-14-regular": !!message.summary?.diffs?.length,
-                                                "[&>*]:fade-up-text":
-                                                  !message.summary?.diffs?.length && !initialCompleted,
+                                                "[&>*]:fade-up-text": !message.summary?.diffs?.length && !contentSeen(),
                                               }}
                                               text={summary()}
                                             />

+ 6 - 6
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.78"
+version = "1.0.80"
 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.78/opencode-darwin-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.80/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.78/opencode-darwin-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.80/opencode-darwin-x64.zip"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.78/opencode-linux-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.80/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.78/opencode-linux-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.80/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.78/opencode-windows-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.80/opencode-windows-x64.zip"
 cmd = "./opencode.exe"
 args = ["acp"]

+ 1 - 1
packages/function/package.json

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

+ 4 - 3
packages/opencode/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "1.0.78",
+  "version": "1.0.80",
   "name": "opencode",
   "type": "module",
   "private": true,
@@ -47,6 +47,7 @@
     "@actions/core": "1.11.1",
     "@actions/github": "6.0.1",
     "@agentclientprotocol/sdk": "0.5.1",
+    "@ai-sdk/mcp": "0.0.8",
     "@clack/prompts": "1.0.0-alpha.1",
     "@hono/standard-validator": "0.1.5",
     "@hono/zod-validator": "catalog:",
@@ -58,8 +59,8 @@
     "@opencode-ai/plugin": "workspace:*",
     "@opencode-ai/script": "workspace:*",
     "@opencode-ai/sdk": "workspace:*",
-    "@opentui/core": "0.1.46",
-    "@opentui/solid": "0.1.46",
+    "@opentui/core": "0.1.47",
+    "@opentui/solid": "0.1.47",
     "@parcel/watcher": "2.5.1",
     "@pierre/precision-diffs": "catalog:",
     "@solid-primitives/event-bus": "1.1.2",

+ 9 - 0
packages/opencode/parsers-config.ts

@@ -167,6 +167,15 @@ export default {
         ],
       },
     },
+    {
+      filetype: "yaml",
+      wasm: "https://github.com/tree-sitter-grammars/tree-sitter-yaml/releases/download/v0.7.2/tree-sitter-yaml.wasm",
+      queries: {
+        highlights: [
+          "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/yaml/highlights.scm",
+        ],
+      },
+    },
     {
       filetype: "haskell",
       wasm: "https://github.com/tree-sitter/tree-sitter-haskell/releases/download/v0.23.1/tree-sitter-haskell.wasm",

+ 28 - 1
packages/opencode/script/publish.ts

@@ -131,7 +131,34 @@ if (!Script.preview) {
     "",
     "package() {",
     `  cd "opencode-\${pkgver}/packages/opencode"`,
-    '  install -Dm755 $(find dist/*/bin/opencode) "${pkgdir}/usr/bin/opencode"',
+    '  mkdir -p "${pkgdir}/usr/bin"',
+    '  arch="x64"',
+    '  case "$CARCH" in',
+    '    x86_64) arch="x64" ;;',
+    '    aarch64) arch="arm64" ;;',
+    '    *) printf "unsupported architecture: %s\\n" "$CARCH" >&2 ; return 1 ;;',
+    "  esac",
+    '  libc=""',
+    "  if command -v ldd >/dev/null 2>&1; then",
+    "    if ldd --version 2>&1 | grep -qi musl; then",
+    '      libc="-musl"',
+    "    fi",
+    "  fi",
+    '  if [ -z "$libc" ] && ls /lib/ld-musl-* >/dev/null 2>&1; then',
+    '    libc="-musl"',
+    "  fi",
+    '  base=""',
+    '  if [ "$arch" = "x64" ]; then',
+    "    if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then",
+    '      base="-baseline"',
+    "    fi",
+    "  fi",
+    '  bin="dist/opencode-linux-${arch}${base}${libc}/bin/opencode"',
+    '  if [ ! -f "$bin" ]; then',
+    '    printf "unable to find binary for %s%s%s\\n" "$arch" "$base" "$libc" >&2',
+    "    return 1",
+    "  fi",
+    '  install -Dm755 "$bin" "${pkgdir}/usr/bin/opencode"',
     "}",
     "",
   ].join("\n")

+ 42 - 10
packages/opencode/src/bun/index.ts

@@ -79,16 +79,48 @@ export namespace BunProc {
       version,
     })
 
-    await BunProc.run(args, {
-      cwd: Global.Path.cache,
-    }).catch((e) => {
-      throw new InstallFailedError(
-        { pkg, version },
-        {
-          cause: e,
-        },
-      )
-    })
+    const total = 3
+    const wait = 500
+
+    const runInstall = async (count: number = 1): Promise<void> => {
+      log.info("bun install attempt", {
+        pkg,
+        version,
+        attempt: count,
+        total,
+      })
+      await BunProc.run(args, {
+        cwd: Global.Path.cache,
+      }).catch(async (error) => {
+        log.warn("bun install failed", {
+          pkg,
+          version,
+          attempt: count,
+          total,
+          error,
+        })
+        if (count >= total) {
+          throw new InstallFailedError(
+            { pkg, version },
+            {
+              cause: error,
+            },
+          )
+        }
+        const delay = wait * count
+        log.info("bun install retrying", {
+          pkg,
+          version,
+          next: count + 1,
+          delay,
+        })
+        await Bun.sleep(delay)
+        return runInstall(count + 1)
+      })
+    }
+
+    await runInstall()
+
     parsed.dependencies[pkg] = version
     await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
     return mod

+ 11 - 31
packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx

@@ -1,39 +1,21 @@
 import { createMemo } from "solid-js"
 import { useLocal } from "@tui/context/local"
-import { useSync } from "@tui/context/sync"
 import { DialogSelect } from "@tui/ui/dialog-select"
 import { useDialog } from "@tui/ui/dialog"
-import { useTheme } from "@tui/context/theme"
 
 export function DialogAgent() {
   const local = useLocal()
-  const sync = useSync()
   const dialog = useDialog()
-  const { theme } = useTheme()
 
-  const options = createMemo(() => {
-    const allAgents = sync.data.agent
-    const primaryAgents = allAgents.filter((x) => x.mode !== "subagent")
-    const subagents = allAgents.filter((x) => x.mode === "subagent")
-
-    const primaryOptions = primaryAgents.map((item) => ({
-      value: item.name,
-      title: item.name,
-      description: item.builtIn ? "native" : item.description,
-      category: "Primary Agents",
-    }))
-
-    const subagentOptions = subagents.map((item) => ({
-      value: item.name,
-      title: item.name,
-      description: item.builtIn ? "native" : item.description,
-      category: "Subagents (non-selectable)",
-      disabled: true,
-      bg: theme.backgroundPanel,
-    }))
-
-    return [...primaryOptions, ...subagentOptions]
-  })
+  const options = createMemo(() =>
+    local.agent.list().map((item) => {
+      return {
+        value: item.name,
+        title: item.name,
+        description: item.builtIn ? "native" : item.description,
+      }
+    }),
+  )
 
   return (
     <DialogSelect
@@ -41,10 +23,8 @@ export function DialogAgent() {
       current={local.agent.current().name}
       options={options()}
       onSelect={(option) => {
-        if (!option.disabled) {
-          local.agent.set(option.value)
-          dialog.clear()
-        }
+        local.agent.set(option.value)
+        dialog.clear()
       }}
     />
   )

+ 5 - 1
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -9,7 +9,7 @@ import {
   fg,
   type KeyBinding,
 } from "@opentui/core"
-import { createEffect, createMemo, Match, Switch, type JSX, onMount, batch } from "solid-js"
+import { createEffect, createMemo, Match, Switch, type JSX, onMount } from "solid-js"
 import { useLocal } from "@tui/context/local"
 import { useTheme } from "@tui/context/theme"
 import { SplitBorder } from "@tui/component/border"
@@ -425,6 +425,10 @@ export function Prompt(props: PromptProps) {
         },
         body: {
           agent: local.agent.current().name,
+          model: {
+            providerID: local.model.current().providerID,
+            modelID: local.model.current().modelID,
+          },
           command: inputText,
         },
       })

+ 68 - 62
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

@@ -54,6 +54,7 @@ import { DialogMessage } from "./dialog-message"
 import type { PromptInfo } from "../../component/prompt/history"
 import { iife } from "@/util/iife"
 import { DialogConfirm } from "@tui/ui/dialog-confirm"
+import { DialogPrompt } from "@tui/ui/dialog-prompt"
 import { DialogTimeline } from "./dialog-timeline"
 import { DialogSessionRename } from "../../component/dialog-session-rename"
 import { Sidebar } from "./sidebar"
@@ -105,11 +106,6 @@ export function Session() {
     return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
   })
 
-  const lastUserMessage = createMemo(() => {
-    const p = pending()
-    return messages().findLast((x) => x.role === "user" && (!p || x.id < p)) as UserMessage
-  })
-
   const dimensions = useTerminalDimensions()
   const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto"))
   const [conceal, setConceal] = createSignal(true)
@@ -583,12 +579,19 @@ export function Session() {
             transcript += `---\n\n`
           }
 
-          // Save to file in data directory
-          const exportDir = path.join(Global.Path.data, "exports")
-          await fs.mkdir(exportDir, { recursive: true })
+          // Prompt for optional filename
+          const customFilename = await DialogPrompt.show(
+            dialog,
+            "Export filename",
+            `session-${sessionData.id.slice(0, 8)}.md`,
+          )
 
-          const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
-          const filename = `session-${sessionData.id.slice(0, 8)}-${timestamp}.md`
+          // Cancel if user pressed escape
+          if (customFilename === null) return
+
+          // Save to file in current working directory
+          const exportDir = process.cwd()
+          const filename = customFilename.trim()
           const filepath = path.join(exportDir, filename)
 
           await Bun.write(filepath, transcript)
@@ -982,59 +985,62 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
           <text fg={theme.textMuted}>{props.message.error?.data.message}</text>
         </box>
       </Show>
-      <Show when={props.last && status().type !== "idle"}>
-        <box paddingLeft={3} flexDirection="row" gap={1} marginTop={1}>
-          <text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
-          <Shimmer text={props.message.modelID} color={theme.text} />
-          {(() => {
-            const retry = createMemo(() => {
-              const s = status()
-              if (s.type !== "retry") return
-              return s
-            })
-            const message = createMemo(() => {
-              const r = retry()
-              if (!r) return
-              if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
-                return "gemini 3 way too hot right now"
-              if (r.message.length > 50) return r.message.slice(0, 50) + "..."
-              return r.message
-            })
-            const [seconds, setSeconds] = createSignal(0)
-            onMount(() => {
-              const timer = setInterval(() => {
-                const next = retry()?.next
-                if (next) setSeconds(Math.round((next - Date.now()) / 1000))
-              }, 1000)
-
-              onCleanup(() => {
-                clearInterval(timer)
+      <Switch>
+        <Match when={props.last && status().type !== "idle"}>
+          <box paddingLeft={3} flexDirection="row" gap={1} marginTop={1}>
+            <text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
+            <Shimmer text={props.message.modelID} color={theme.text} />
+            {(() => {
+              const retry = createMemo(() => {
+                const s = status()
+                if (s.type !== "retry") return
+                return s
               })
-            })
-            return (
-              <Show when={retry()}>
-                <text fg={theme.error}>
-                  {message()} [retrying {seconds() > 0 ? `in ${seconds()}s ` : ""}
-                  attempt #{retry()!.attempt}]
-                </text>
-              </Show>
-            )
-          })()}
-        </box>
-      </Show>
-      <Show
-        when={
-          props.message.time.completed &&
-          props.parts.some((item) => item.type === "step-finish" && item.reason !== "tool-calls")
-        }
-      >
-        <box paddingLeft={3}>
-          <text marginTop={1}>
-            <span style={{ fg: local.agent.color(props.message.mode) }}>{Locale.titlecase(props.message.mode)}</span>{" "}
-            <span style={{ fg: theme.textMuted }}>{props.message.modelID}</span>
-          </text>
-        </box>
-      </Show>
+              const message = createMemo(() => {
+                const r = retry()
+                if (!r) return
+                if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
+                  return "gemini 3 way too hot right now"
+                if (r.message.length > 50) return r.message.slice(0, 50) + "..."
+                return r.message
+              })
+              const [seconds, setSeconds] = createSignal(0)
+              onMount(() => {
+                const timer = setInterval(() => {
+                  const next = retry()?.next
+                  if (next) setSeconds(Math.round((next - Date.now()) / 1000))
+                }, 1000)
+
+                onCleanup(() => {
+                  clearInterval(timer)
+                })
+              })
+              return (
+                <Show when={retry()}>
+                  <text fg={theme.error}>
+                    {message()} [retrying {seconds() > 0 ? `in ${seconds()}s ` : ""}
+                    attempt #{retry()!.attempt}]
+                  </text>
+                </Show>
+              )
+            })()}
+          </box>
+        </Match>
+        <Match
+          when={
+            (props.message.time.completed &&
+              props.parts.some((item) => item.type === "step-finish" && item.reason !== "tool-calls")) ||
+            props.last
+          }
+        >
+          <box paddingLeft={3}>
+            <text marginTop={1}>
+              <span style={{ fg: local.agent.color(props.message.mode) }}>{Locale.titlecase(props.message.mode)}</span>{" "}
+              <span style={{ fg: theme.textMuted }}>{props.message.modelID}</span>
+            </text>
+          </box>
+        </Match>
+      </Switch>
     </>
   )
 }

+ 11 - 40
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx

@@ -54,8 +54,10 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
 
   const filtered = createMemo(() => {
     const needle = store.filter.toLowerCase()
-    const result = pipe(props.options, (x) =>
-      !needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj),
+    const result = pipe(
+      props.options,
+      filter((x) => x.disabled !== true),
+      (x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)),
     )
     return result
   })
@@ -94,16 +96,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
     let next = store.selected + direction
     if (next < 0) next = flat().length - 1
     if (next >= flat().length) next = 0
-
-    // Skip disabled options when flipping through agents
-    let attempts = 0
-    while (flat()[next]?.disabled && attempts < flat().length) {
-      next = next + direction
-      if (next < 0) next = flat().length - 1
-      if (next >= flat().length) next = 0
-      attempts++
-    }
-
     moveTo(next)
   }
 
@@ -134,7 +126,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
     if (evt.name === "pagedown") move(10)
     if (evt.name === "return") {
       const option = selected()
-      if (option && !option.disabled) {
+      if (option) {
         // evt.preventDefault()
         if (option.onSelect) option.onSelect(dialog)
         props.onSelect?.(option)
@@ -144,7 +136,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
     for (const item of props.keybind ?? []) {
       if (Keybind.match(item.keybind, keybind.parse(evt))) {
         const s = selected()
-        if (s && !s.disabled) {
+        if (s) {
           evt.preventDefault()
           item.onTrigger(s)
         }
@@ -216,19 +208,15 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
                       id={JSON.stringify(option.value)}
                       flexDirection="row"
                       onMouseUp={() => {
-                        if (!option.disabled) {
-                          option.onSelect?.(dialog)
-                          props.onSelect?.(option)
-                        }
+                        option.onSelect?.(dialog)
+                        props.onSelect?.(option)
                       }}
                       onMouseOver={() => {
                         const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
                         if (index === -1) return
                         moveTo(index)
                       }}
-                      backgroundColor={
-                        active() && !option.disabled ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)
-                      }
+                      backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
                       paddingLeft={1}
                       paddingRight={1}
                       gap={1}
@@ -239,7 +227,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
                         description={option.description !== category ? option.description : undefined}
                         active={active()}
                         current={isDeepEqual(option.value, props.current)}
-                        disabled={option.disabled}
                       />
                     </box>
                   )
@@ -269,22 +256,13 @@ function Option(props: {
   active?: boolean
   current?: boolean
   footer?: JSX.Element | string
-  disabled?: boolean
   onMouseOver?: () => void
 }) {
   const { theme } = useTheme()
 
-  const textColor = props.disabled
-    ? theme.textMuted
-    : props.active
-      ? theme.background
-      : props.current
-        ? theme.primary
-        : theme.text
-
   return (
     <>
-      <Show when={props.current && !props.disabled}>
+      <Show when={props.current}>
         <text
           flexShrink={0}
           fg={props.active ? theme.background : props.current ? theme.primary : theme.text}
@@ -293,17 +271,10 @@ function Option(props: {
         </text>
       </Show>
-      <Show when={props.disabled}>
-        <text flexShrink={0} fg={theme.textMuted} marginRight={0.5}>
-          ○
-        </text>
-      </Show>
       <text
         flexGrow={1}
         fg={props.active ? theme.background : props.current ? theme.primary : theme.text}
-        attributes={
-          props.active && !props.disabled ? TextAttributes.BOLD : props.disabled ? TextAttributes.DIM : undefined
-        }
+        attributes={props.active ? TextAttributes.BOLD : undefined}
         overflow="hidden"
         wrapMode="none"
       >

+ 3 - 1
packages/opencode/src/cli/cmd/tui/ui/toast.tsx

@@ -38,7 +38,9 @@ export function Toast() {
               {current().title}
             </text>
           </Show>
-          <text fg={theme.text}>{current().message}</text>
+          <text fg={theme.text} wrapMode="word" width="100%">
+            {current().message}
+          </text>
         </box>
       )}
     </Show>

+ 88 - 0
packages/opencode/src/lsp/server.ts

@@ -945,6 +945,54 @@ export namespace LSPServer {
     },
   }
 
+  export const YamlLS: Info = {
+    id: "yaml-ls",
+    extensions: [".yaml", ".yml"],
+    root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
+    async spawn(root) {
+      let binary = Bun.which("yaml-language-server")
+      const args: string[] = []
+      if (!binary) {
+        const js = path.join(
+          Global.Path.bin,
+          "node_modules",
+          "yaml-language-server",
+          "out",
+          "server",
+          "src",
+          "server.js",
+        )
+        const exists = await Bun.file(js).exists()
+        if (!exists) {
+          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+          await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
+            cwd: Global.Path.bin,
+            env: {
+              ...process.env,
+              BUN_BE_BUN: "1",
+            },
+            stdout: "pipe",
+            stderr: "pipe",
+            stdin: "pipe",
+          }).exited
+        }
+        binary = BunProc.which()
+        args.push("run", js)
+      }
+      args.push("--stdio")
+      const proc = spawn(binary, args, {
+        cwd: root,
+        env: {
+          ...process.env,
+          BUN_BE_BUN: "1",
+        },
+      })
+      return {
+        process: proc,
+      }
+    },
+  }
+
   export const LuaLS: Info = {
     id: "lua-ls",
     root: NearestRoot([
@@ -1077,4 +1125,44 @@ export namespace LSPServer {
       }
     },
   }
+
+  export const PHPIntelephense: Info = {
+    id: "php intelephense",
+    extensions: [".php"],
+    root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
+    async spawn(root) {
+      let binary = Bun.which("intelephense")
+      const args: string[] = []
+      if (!binary) {
+        const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
+        if (!(await Bun.file(js).exists())) {
+          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+          await Bun.spawn([BunProc.which(), "install", "intelephense"], {
+            cwd: Global.Path.bin,
+            env: {
+              ...process.env,
+              BUN_BE_BUN: "1",
+            },
+            stdout: "pipe",
+            stderr: "pipe",
+            stdin: "pipe",
+          }).exited
+        }
+        binary = BunProc.which()
+        args.push("run", js)
+      }
+      args.push("--stdio")
+      const proc = spawn(binary, args, {
+        cwd: root,
+        env: {
+          ...process.env,
+          BUN_BE_BUN: "1",
+        },
+      })
+      return {
+        process: proc,
+        initialization: {},
+      }
+    },
+  }
 }

+ 2 - 1
packages/opencode/src/mcp/index.ts

@@ -1,4 +1,5 @@
-import { experimental_createMCPClient, type Tool } from "ai"
+import { type Tool } from "ai"
+import { experimental_createMCPClient } from "@ai-sdk/mcp"
 import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
 import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
 import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"

+ 16 - 12
packages/opencode/src/provider/provider.ts

@@ -494,6 +494,7 @@ export namespace Provider {
       if (pkg.includes("@ai-sdk/openai-compatible") && options["includeUsage"] === undefined) {
         options["includeUsage"] = true
       }
+
       const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }))
       const existing = s.sdk.get(key)
       if (existing) return existing
@@ -515,26 +516,29 @@ export namespace Provider {
       const modPath =
         provider.id === "google-vertex-anthropic" ? `${installedPath}/dist/anthropic/index.mjs` : installedPath
       const mod = await import(modPath)
-      if (options["timeout"] !== undefined && options["timeout"] !== null) {
+
+      const customFetch = options["fetch"]
+
+      options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
         // Preserve custom fetch if it exists, wrap it with timeout logic
-        const customFetch = options["fetch"]
-        options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
-          const { signal, ...rest } = init ?? {}
+        const fetchFn = customFetch ?? fetch
+        const opts = init ?? {}
 
+        if (options["timeout"] !== undefined && options["timeout"] !== null) {
           const signals: AbortSignal[] = []
-          if (signal) signals.push(signal)
+          if (opts.signal) signals.push(opts.signal)
           if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
 
           const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
 
-          const fetchFn = customFetch ?? fetch
-          return fetchFn(input, {
-            ...rest,
-            signal: combined,
-            // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
-            timeout: false,
-          })
+          opts.signal = combined
         }
+
+        return fetchFn(input, {
+          ...opts,
+          // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
+          timeout: false,
+        })
       }
       const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
       const loaded = fn({

+ 21 - 1
packages/opencode/src/provider/transform.ts

@@ -24,7 +24,6 @@ export namespace ProviderTransform {
       const result: ModelMessage[] = []
       for (let i = 0; i < msgs.length; i++) {
         const msg = msgs[i]
-        const prevMsg = msgs[i - 1]
         const nextMsg = msgs[i + 1]
 
         if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
@@ -137,10 +136,23 @@ export namespace ProviderTransform {
   ): Record<string, any> | undefined {
     const result: Record<string, any> = {}
 
+    // switch to providerID later, for now use this
+    if (npm === "@openrouter/ai-sdk-provider") {
+      result["usage"] = {
+        include: true,
+      }
+    }
+
     if (providerID === "openai") {
       result["promptCacheKey"] = sessionID
     }
 
+    if (providerID === "google") {
+      result["thinkingConfig"] = {
+        includeThoughts: true,
+      }
+    }
+
     if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
       if (modelID.includes("codex")) {
         result["store"] = false
@@ -178,10 +190,18 @@ export namespace ProviderTransform {
         return {
           ["anthropic" as string]: options,
         }
+      case "@ai-sdk/google":
+        return {
+          ["google" as string]: options,
+        }
       case "@ai-sdk/gateway":
         return {
           ["gateway" as string]: options,
         }
+      case "@openrouter/ai-sdk-provider":
+        return {
+          ["openrouter" as string]: options,
+        }
       default:
         return {
           [providerID]: options,

+ 25 - 17
packages/opencode/src/session/index.ts

@@ -382,17 +382,23 @@ export namespace Session {
       const adjustedInputTokens = excludesCachedTokens
         ? (input.usage.inputTokens ?? 0)
         : (input.usage.inputTokens ?? 0) - cachedInputTokens
+      const safe = (value: number) => {
+        if (!Number.isFinite(value)) return 0
+        return value
+      }
 
       const tokens = {
-        input: adjustedInputTokens,
-        output: input.usage.outputTokens ?? 0,
-        reasoning: input.usage?.reasoningTokens ?? 0,
+        input: safe(adjustedInputTokens),
+        output: safe(input.usage.outputTokens ?? 0),
+        reasoning: safe(input.usage?.reasoningTokens ?? 0),
         cache: {
-          write: (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
-            // @ts-expect-error
-            input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
-            0) as number,
-          read: cachedInputTokens,
+          write: safe(
+            (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
+              // @ts-expect-error
+              input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
+              0) as number,
+          ),
+          read: safe(cachedInputTokens),
         },
       }
 
@@ -401,15 +407,17 @@ export namespace Session {
           ? input.model.cost.context_over_200k
           : input.model.cost
       return {
-        cost: new Decimal(0)
-          .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(costInfo?.output ?? 0).div(1_000_000))
-          .toNumber(),
+        cost: safe(
+          new Decimal(0)
+            .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(costInfo?.output ?? 0).div(1_000_000))
+            .toNumber(),
+        ),
         tokens,
       }
     },

+ 13 - 15
packages/opencode/src/session/prompt.ts

@@ -600,12 +600,9 @@ export namespace SessionPrompt {
     throw new Error("Impossible")
   })
 
-  async function resolveModel(input: { model: PromptInput["model"]; agent: Agent.Info }) {
-    if (input.model) {
-      return input.model
-    }
-    if (input.agent.model) {
-      return input.agent.model
+  async function lastModel(sessionID: string) {
+    for await (const item of MessageV2.stream(sessionID)) {
+      if (item.info.role === "user" && item.info.model) return item.info.model
     }
     return Provider.defaultModel()
   }
@@ -794,10 +791,7 @@ export namespace SessionPrompt {
       tools: input.tools,
       system: input.system,
       agent: agent.name,
-      model: await resolveModel({
-        model: input.model,
-        agent,
-      }),
+      model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
     }
 
     const parts = await Promise.all(
@@ -1091,6 +1085,12 @@ export namespace SessionPrompt {
   export const ShellInput = z.object({
     sessionID: Identifier.schema("session"),
     agent: z.string(),
+    model: z
+      .object({
+        providerID: z.string(),
+        modelID: z.string(),
+      })
+      .optional(),
     command: z.string(),
   })
   export type ShellInput = z.infer<typeof ShellInput>
@@ -1100,7 +1100,7 @@ export namespace SessionPrompt {
       SessionRevert.cleanup(session)
     }
     const agent = await Agent.get(input.agent)
-    const model = await resolveModel({ agent, model: undefined })
+    const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
     const userMsg: MessageV2.User = {
       id: Identifier.ascending("message"),
       sessionID: input.sessionID,
@@ -1338,10 +1338,8 @@ export namespace SessionPrompt {
           return cmdAgent.model
         }
       }
-      if (input.model) {
-        return Provider.parseModel(input.model)
-      }
-      return await Provider.defaultModel()
+      if (input.model) return Provider.parseModel(input.model)
+      return await lastModel(input.sessionID)
     })()
     const agent = await Agent.get(agentName)
 

+ 28 - 5
packages/opencode/src/tool/bash.ts

@@ -12,6 +12,7 @@ import { Filesystem } from "@/util/filesystem"
 import { Wildcard } from "@/util/wildcard"
 import { Permission } from "@/permission"
 import { fileURLToPath } from "url"
+import path from "path"
 
 const MAX_OUTPUT_LENGTH = 30_000
 const DEFAULT_TIMEOUT = 1 * 60 * 1000
@@ -68,7 +69,8 @@ export const BashTool = Tool.define("bash", {
     if (!tree) {
       throw new Error("Failed to parse command")
     }
-    const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
+    const agent = await Agent.get(ctx.agent)
+    const permissions = agent.permission.bash
 
     const askPatterns = new Set<string>()
     for (const node of tree.rootNode.descendantsOfType("command")) {
@@ -107,9 +109,30 @@ export const BashTool = Tool.define("bash", {
                 : resolved
 
             if (!Filesystem.contains(Instance.directory, normalized)) {
-              throw new Error(
-                `This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
-              )
+              const parentDir = path.dirname(normalized)
+              if (agent.permission.external_directory === "ask") {
+                await Permission.ask({
+                  type: "external_directory",
+                  pattern: [parentDir, path.join(parentDir, "*")],
+                  sessionID: ctx.sessionID,
+                  messageID: ctx.messageID,
+                  callID: ctx.callID,
+                  title: `This command references paths outside of ${Instance.directory}`,
+                  metadata: {
+                    command: params.command,
+                  },
+                })
+              } else if (agent.permission.external_directory === "deny") {
+                throw new Permission.RejectedError(
+                  ctx.sessionID,
+                  "external_directory",
+                  ctx.callID,
+                  {
+                    command: params.command,
+                  },
+                  `This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
+                )
+              }
             }
           }
         }
@@ -271,7 +294,7 @@ export const BashTool = Tool.define("bash", {
     }
 
     return {
-      title: params.command,
+      title: params.description,
       metadata: {
         output,
         exit: proc.exitCode,

+ 1 - 1
packages/opencode/src/tool/bash.txt

@@ -35,7 +35,7 @@ Usage notes:
 
 # Committing changes with git
 
-When the user asks you to create a new git commit, follow these steps carefully:
+If and only if the user asks you to create a new git commit, follow these steps carefully:
 
 1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following bash commands in parallel, each using the Bash tool:
    - Run a git status command to see all untracked files.

+ 1 - 1
packages/opencode/src/tool/edit.ts

@@ -47,7 +47,7 @@ export const EditTool = Tool.define("edit", {
       if (agent.permission.external_directory === "ask") {
         await Permission.ask({
           type: "external_directory",
-          pattern: parentDir,
+          pattern: [parentDir, path.join(parentDir, "*")],
           sessionID: ctx.sessionID,
           messageID: ctx.messageID,
           callID: ctx.callID,

+ 1 - 1
packages/opencode/src/tool/patch.ts

@@ -58,7 +58,7 @@ export const PatchTool = Tool.define("patch", {
         if (agent.permission.external_directory === "ask") {
           await Permission.ask({
             type: "external_directory",
-            pattern: parentDir,
+            pattern: [parentDir, path.join(parentDir, "*")],
             sessionID: ctx.sessionID,
             messageID: ctx.messageID,
             callID: ctx.callID,

+ 1 - 1
packages/opencode/src/tool/read.ts

@@ -36,7 +36,7 @@ export const ReadTool = Tool.define("read", {
       if (agent.permission.external_directory === "ask") {
         await Permission.ask({
           type: "external_directory",
-          pattern: parentDir,
+          pattern: [parentDir, path.join(parentDir, "*")],
           sessionID: ctx.sessionID,
           messageID: ctx.messageID,
           callID: ctx.callID,

+ 1 - 1
packages/opencode/src/tool/write.ts

@@ -26,7 +26,7 @@ export const WriteTool = Tool.define("write", {
       if (agent.permission.external_directory === "ask") {
         await Permission.ask({
           type: "external_directory",
-          pattern: parentDir,
+          pattern: [parentDir, path.join(parentDir, "*")],
           sessionID: ctx.sessionID,
           messageID: ctx.messageID,
           callID: ctx.callID,

+ 19 - 16
packages/opencode/test/tool/bash.test.ts

@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
 import path from "path"
 import { BashTool } from "../../src/tool/bash"
 import { Instance } from "../../src/project/instance"
+import { Permission } from "../../src/permission"
 
 const ctx = {
   sessionID: "test",
@@ -33,20 +34,22 @@ describe("tool.bash", () => {
     })
   })
 
-  test("cd ../ should fail outside of project root", async () => {
-    await Instance.provide({
-      directory: projectRoot,
-      fn: async () => {
-        expect(
-          bash.execute(
-            {
-              command: "cd ../",
-              description: "Try to cd to parent directory",
-            },
-            ctx,
-          ),
-        ).rejects.toThrow("This command references paths outside of")
-      },
-    })
-  })
+  // TODO: better test
+  // test("cd ../ should ask for permission for external directory", async () => {
+  //   await Instance.provide({
+  //     directory: projectRoot,
+  //     fn: async () => {
+  //       bash.execute(
+  //         {
+  //           command: "cd ../",
+  //           description: "Try to cd to parent directory",
+  //         },
+  //         ctx,
+  //       )
+  //       // Give time for permission to be asked
+  //       await new Promise((resolve) => setTimeout(resolve, 1000))
+  //       expect(Permission.pending()[ctx.sessionID]).toBeDefined()
+  //     },
+  //   })
+  // })
 })

+ 1 - 1
packages/plugin/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/plugin",
-  "version": "1.0.78",
+  "version": "1.0.80",
   "type": "module",
   "scripts": {
     "typecheck": "tsgo --noEmit",

+ 1 - 1
packages/sdk/js/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/sdk",
-  "version": "1.0.78",
+  "version": "1.0.80",
   "type": "module",
   "scripts": {
     "typecheck": "tsgo --noEmit",

+ 4 - 0
packages/sdk/js/src/gen/types.gen.ts

@@ -2294,6 +2294,10 @@ export type SessionCommandResponse = SessionCommandResponses[keyof SessionComman
 export type SessionShellData = {
   body?: {
     agent: string
+    model?: {
+      providerID: string
+      modelID: string
+    }
     command: string
   }
   path: {

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/slack",
-  "version": "1.0.78",
+  "version": "1.0.80",
   "type": "module",
   "scripts": {
     "dev": "bun run src/index.ts",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/ui",
-  "version": "1.0.78",
+  "version": "1.0.80",
   "type": "module",
   "exports": {
     ".": "./src/components/index.ts",

+ 12 - 6
packages/ui/src/components/markdown.css

@@ -1,8 +1,7 @@
 [data-component="markdown"] {
   min-width: 0;
   max-width: 100%;
-  overflow: auto;
-  scrollbar-width: none;
+  overflow: hidden;
   color: var(--text-base);
   text-wrap: pretty;
 
@@ -14,10 +13,6 @@
   line-height: var(--line-height-large); /* 166.667% */
   letter-spacing: var(--letter-spacing-normal);
 
-  &::-webkit-scrollbar {
-    display: none;
-  }
-
   h1,
   h2,
   h3 {
@@ -41,4 +36,15 @@
     margin-bottom: 16px;
     border-color: var(--border-weaker-base);
   }
+
+  pre {
+    margin-top: 2rem;
+    margin-bottom: 2rem;
+    overflow: auto;
+
+    scrollbar-width: none;
+    &::-webkit-scrollbar {
+      display: none;
+    }
+  }
 }

+ 2 - 0
packages/ui/src/components/message-part.css

@@ -22,6 +22,8 @@
 }
 
 [data-component="text-part"] {
+  width: 100%;
+
   [data-component="markdown"] {
     margin-top: 32px;
   }

+ 1 - 1
packages/util/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/util",
-  "version": "0.0.0",
+  "version": "1.0.80",
   "private": true,
   "type": "module",
   "exports": {

+ 1 - 1
packages/web/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@opencode-ai/web",
   "type": "module",
-  "version": "1.0.78",
+  "version": "1.0.80",
   "scripts": {
     "dev": "astro dev",
     "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

+ 34 - 19
packages/web/src/content/docs/lsp.mdx

@@ -11,25 +11,27 @@ OpenCode integrates with your Language Server Protocol (LSP) to help the LLM int
 
 OpenCode comes with several built-in LSP servers for popular languages:
 
-| LSP Server    | Extensions                                           | Requirements                                                 |
-| ------------- | ---------------------------------------------------- | ------------------------------------------------------------ |
-| typescript    | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts         | `typescript` dependency in project                           |
-| deno          | .ts, .tsx, .js, .jsx, .mjs                           | `deno` command available (auto-detects deno.json/deno.jsonc) |
-| eslint        | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue   | `eslint` dependency in project                               |
-| gopls         | .go                                                  | `go` command available                                       |
-| ruby-lsp      | .rb, .rake, .gemspec, .ru                            | `ruby` and `gem` commands available                          |
-| pyright       | .py, .pyi                                            | `pyright` dependency installed                               |
-| elixir-ls     | .ex, .exs                                            | `elixir` command available                                   |
-| zls           | .zig, .zon                                           | `zig` command available                                      |
-| csharp        | .cs                                                  | `.NET SDK` installed                                         |
-| vue           | .vue                                                 | Auto-installs for Vue projects                               |
-| rust          | .rs                                                  | `rust-analyzer` command available                            |
-| clangd        | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects                             |
-| svelte        | .svelte                                              | Auto-installs for Svelte projects                            |
-| astro         | .astro                                               | Auto-installs for Astro projects                             |
-| jdtls         | .java                                                | `Java SDK (version 21+)` installed                           |
-| lua-ls        | .lua                                                 | Auto-installs for Lua projects                               |
-| sourcekit-lsp | .swift, .objc, .objcpp                               | `swift` installed (`xcode` on macOS)                         |
+| LSP Server       | Extensions                                           | Requirements                                                 |
+| ---------------- | ---------------------------------------------------- | ------------------------------------------------------------ |
+| typescript       | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts         | `typescript` dependency in project                           |
+| deno             | .ts, .tsx, .js, .jsx, .mjs                           | `deno` command available (auto-detects deno.json/deno.jsonc) |
+| eslint           | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue   | `eslint` dependency in project                               |
+| gopls            | .go                                                  | `go` command available                                       |
+| ruby-lsp         | .rb, .rake, .gemspec, .ru                            | `ruby` and `gem` commands available                          |
+| pyright          | .py, .pyi                                            | `pyright` dependency installed                               |
+| elixir-ls        | .ex, .exs                                            | `elixir` command available                                   |
+| zls              | .zig, .zon                                           | `zig` command available                                      |
+| csharp           | .cs                                                  | `.NET SDK` installed                                         |
+| vue              | .vue                                                 | Auto-installs for Vue projects                               |
+| rust             | .rs                                                  | `rust-analyzer` command available                            |
+| clangd           | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects                             |
+| svelte           | .svelte                                              | Auto-installs for Svelte projects                            |
+| astro            | .astro                                               | Auto-installs for Astro projects                             |
+| yaml-ls          | .yaml, .yml                                          | Auto-installs Red Hat yaml-language-server                   |
+| jdtls            | .java                                                | `Java SDK (version 21+)` installed                           |
+| 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                               |
 
 LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
 
@@ -105,3 +107,16 @@ You can add custom LSP servers by specifying the command and file extensions:
   }
 }
 ```
+
+---
+
+## Additional Information
+
+### PHP Intelephense
+
+PHP Intelephense offers premium features through a license key. Uou can provide a license key by placing (only) the key in a text file at:
+
+- On macOS/Linux: `$HOME/intelephense/licence.txt`
+- On Windows: `%USERPROFILE%/intelephense/licence.txt`
+
+The file should contain only the license key with no additional content.

+ 20 - 17
packages/web/src/content/docs/zen.mdx

@@ -62,23 +62,24 @@ You are charged per request and you can add credits to your account.
 
 You can also access our models through the following API endpoints.
 
-| Model             | Model ID          | Endpoint                                      | AI SDK Package              |
-| ----------------- | ----------------- | --------------------------------------------- | --------------------------- |
-| GPT 5.1           | gpt-5.1           | `https://opencode.ai/zen/v1/responses`        | `@ai-sdk/openai`            |
-| GPT 5.1 Codex     | gpt-5.1-codex     | `https://opencode.ai/zen/v1/responses`        | `@ai-sdk/openai`            |
-| GPT 5             | gpt-5             | `https://opencode.ai/zen/v1/responses`        | `@ai-sdk/openai`            |
-| GPT 5 Codex       | gpt-5-codex       | `https://opencode.ai/zen/v1/responses`        | `@ai-sdk/openai`            |
-| GPT 5 Nano        | gpt-5-nano        | `https://opencode.ai/zen/v1/responses`        | `@ai-sdk/openai`            |
-| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages`         | `@ai-sdk/anthropic`         |
-| Claude Sonnet 4   | claude-sonnet-4   | `https://opencode.ai/zen/v1/messages`         | `@ai-sdk/anthropic`         |
-| Claude Haiku 4.5  | claude-haiku-4-5  | `https://opencode.ai/zen/v1/messages`         | `@ai-sdk/anthropic`         |
-| Claude Haiku 3.5  | claude-3-5-haiku  | `https://opencode.ai/zen/v1/messages`         | `@ai-sdk/anthropic`         |
-| Claude Opus 4.1   | claude-opus-4-1   | `https://opencode.ai/zen/v1/messages`         | `@ai-sdk/anthropic`         |
-| GLM 4.6           | glm-4.6           | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
-| Kimi K2           | kimi-k2           | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
-| Qwen3 Coder 480B  | qwen3-coder       | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
-| Grok Code Fast 1  | grok-code         | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
-| Big Pickle        | big-pickle        | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
+| Model             | Model ID          | Endpoint                                         | AI SDK Package              |
+| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- |
+| GPT 5.1           | gpt-5.1           | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
+| GPT 5.1 Codex     | gpt-5.1-codex     | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
+| GPT 5             | gpt-5             | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
+| GPT 5 Codex       | gpt-5-codex       | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
+| GPT 5 Nano        | gpt-5-nano        | `https://opencode.ai/zen/v1/responses`           | `@ai-sdk/openai`            |
+| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages`            | `@ai-sdk/anthropic`         |
+| Claude Sonnet 4   | claude-sonnet-4   | `https://opencode.ai/zen/v1/messages`            | `@ai-sdk/anthropic`         |
+| Claude Haiku 4.5  | claude-haiku-4-5  | `https://opencode.ai/zen/v1/messages`            | `@ai-sdk/anthropic`         |
+| Claude Haiku 3.5  | claude-3-5-haiku  | `https://opencode.ai/zen/v1/messages`            | `@ai-sdk/anthropic`         |
+| Claude Opus 4.1   | claude-opus-4-1   | `https://opencode.ai/zen/v1/messages`            | `@ai-sdk/anthropic`         |
+| Gemini 3 Pro      | gemini-3-pro      | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google`            |
+| GLM 4.6           | glm-4.6           | `https://opencode.ai/zen/v1/chat/completions`    | `@ai-sdk/openai-compatible` |
+| Kimi K2           | kimi-k2           | `https://opencode.ai/zen/v1/chat/completions`    | `@ai-sdk/openai-compatible` |
+| Qwen3 Coder 480B  | qwen3-coder       | `https://opencode.ai/zen/v1/chat/completions`    | `@ai-sdk/openai-compatible` |
+| Grok Code Fast 1  | grok-code         | `https://opencode.ai/zen/v1/chat/completions`    | `@ai-sdk/openai-compatible` |
+| Big Pickle        | big-pickle        | `https://opencode.ai/zen/v1/chat/completions`    | `@ai-sdk/openai-compatible` |
 
 The [model id](/docs/config/#models) in your OpenCode config
 uses the format `opencode/<model-id>`. For example, for GPT 5.1 Codex, you would
@@ -130,6 +131,8 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
 | Claude Haiku 4.5                  | $1.00  | $5.00  | $0.10       | $1.25        |
 | Claude Haiku 3.5                  | $0.80  | $4.00  | $0.08       | $1.00        |
 | Claude Opus 4.1                   | $15.00 | $75.00 | $1.50       | $18.75       |
+| Gemini 3 Pro (≤ 200K tokens)      | $2.00  | $12.00 | $0.20       | -            |
+| Gemini 3 Pro (> 200K tokens)      | $4.00  | $18.00 | $0.40       | -            |
 | GPT 5.1                           | $1.25  | $10.00 | $0.125      | -            |
 | GPT 5.1 Codex                     | $1.25  | $10.00 | $0.125      | -            |
 | GPT 5                             | $1.25  | $10.00 | $0.125      | -            |

+ 1 - 1
sdks/vscode/package.json

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