Explorar el Código

Merge branch 'dev' of https://github.com/sst/opencode into dev

David Hill hace 2 meses
padre
commit
99158e736b
Se han modificado 100 ficheros con 1780 adiciones y 575 borrados
  1. 1 1
      .github/workflows/publish.yml
  2. 21 15
      bun.lock
  3. 3 3
      flake.lock
  4. 19 0
      github/action.yml
  5. 1 0
      infra/console.ts
  6. 1 1
      nix/hashes.json
  7. 1 1
      packages/console/app/package.json
  8. 1 2
      packages/console/app/src/component/header.tsx
  9. 20 26
      packages/console/app/src/routes/download/index.tsx
  10. 17 10
      packages/console/app/src/routes/index.tsx
  11. 5 2
      packages/console/app/src/routes/workspace/[id]/model-section.tsx
  12. 10 5
      packages/console/app/src/routes/zen/util/handler.ts
  13. 12 6
      packages/console/app/src/routes/zen/util/trialLimiter.ts
  14. 1 1
      packages/console/core/package.json
  15. 4 1
      packages/console/core/script/promote-models.ts
  16. 4 1
      packages/console/core/script/pull-models.ts
  17. 8 3
      packages/console/core/script/update-models.ts
  18. 17 8
      packages/console/core/src/model.ts
  19. 8 4
      packages/console/core/sst-env.d.ts
  20. 1 1
      packages/console/function/package.json
  21. 8 4
      packages/console/function/sst-env.d.ts
  22. 1 1
      packages/console/mail/package.json
  23. 8 4
      packages/console/resource/sst-env.d.ts
  24. 1 1
      packages/desktop/package.json
  25. 17 0
      packages/desktop/src/components/link.tsx
  26. 89 58
      packages/desktop/src/components/prompt-input.tsx
  27. 53 34
      packages/desktop/src/context/layout.tsx
  28. 13 15
      packages/desktop/src/context/local.tsx
  29. 2 2
      packages/desktop/src/context/session.tsx
  30. 7 5
      packages/desktop/src/hooks/use-providers.ts
  31. 366 167
      packages/desktop/src/pages/layout.tsx
  32. 0 1
      packages/desktop/src/pages/session.tsx
  33. 1 1
      packages/enterprise/package.json
  34. 5 2
      packages/enterprise/src/routes/share/[shareID].tsx
  35. 8 4
      packages/enterprise/sst-env.d.ts
  36. 6 6
      packages/extensions/zed/extension.toml
  37. 1 1
      packages/function/package.json
  38. 8 4
      packages/function/sst-env.d.ts
  39. 1 1
      packages/opencode/package.json
  40. 238 2
      packages/opencode/src/acp/agent.ts
  41. 31 0
      packages/opencode/src/acp/session.ts
  42. 1 0
      packages/opencode/src/cli/cmd/debug/lsp.ts
  43. 34 14
      packages/opencode/src/cli/cmd/tui/app.tsx
  44. 4 2
      packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
  45. 6 0
      packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
  46. 1 1
      packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
  47. 27 5
      packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
  48. 10 2
      packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx
  49. 1 0
      packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
  50. 5 3
      packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
  51. 3 1
      packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx
  52. 3 1
      packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx
  53. 3 1
      packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx
  54. 3 1
      packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
  55. 9 1
      packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
  56. 2 1
      packages/opencode/src/config/config.ts
  57. 1 0
      packages/opencode/src/flag/flag.ts
  58. 2 1
      packages/opencode/src/installation/index.ts
  59. 7 6
      packages/opencode/src/lsp/client.ts
  60. 2 1
      packages/opencode/src/lsp/index.ts
  61. 4 1
      packages/opencode/src/provider/transform.ts
  62. 5 3
      packages/opencode/src/server/server.ts
  63. 25 17
      packages/opencode/src/session/prompt.ts
  64. 2 2
      packages/opencode/src/tool/bash.ts
  65. 2 0
      packages/opencode/src/tool/bash.txt
  66. 0 1
      packages/opencode/src/tool/webfetch.txt
  67. 4 1
      packages/opencode/src/util/log.ts
  68. 1 1
      packages/plugin/package.json
  69. 10 0
      packages/plugin/src/index.ts
  70. 1 1
      packages/sdk/js/package.json
  71. 1 1
      packages/sdk/js/src/v2/gen/types.gen.ts
  72. 1 2
      packages/sdk/openapi.json
  73. 1 1
      packages/slack/package.json
  74. 3 1
      packages/tauri/package.json
  75. 45 0
      packages/tauri/src-tauri/Cargo.lock
  76. 3 1
      packages/tauri/src-tauri/Cargo.toml
  77. 3 1
      packages/tauri/src-tauri/capabilities/default.json
  78. 10 2
      packages/tauri/src-tauri/src/lib.rs
  79. 1 1
      packages/tauri/src-tauri/tauri.conf.json
  80. 1 1
      packages/ui/package.json
  81. 2 1
      packages/ui/src/components/avatar.css
  82. 12 1
      packages/ui/src/components/avatar.tsx
  83. 11 0
      packages/ui/src/components/dialog.css
  84. 3 0
      packages/ui/src/components/icon.tsx
  85. 3 4
      packages/ui/src/components/list.css
  86. 4 17
      packages/ui/src/components/message-nav.tsx
  87. 2 2
      packages/ui/src/components/select-dialog.tsx
  88. 1 12
      packages/ui/src/components/session-message-rail.tsx
  89. 2 2
      packages/ui/src/components/session-turn.tsx
  90. 44 18
      packages/ui/src/components/text-field.css
  91. 35 7
      packages/ui/src/components/text-field.tsx
  92. 203 0
      packages/ui/src/components/toast.css
  93. 160 0
      packages/ui/src/components/toast.tsx
  94. 1 0
      packages/ui/src/components/tooltip.css
  95. 2 2
      packages/ui/src/hooks/use-filtered-list.tsx
  96. 2 1
      packages/ui/src/styles/index.css
  97. 1 1
      packages/util/package.json
  98. 1 1
      packages/web/package.json
  99. 7 5
      packages/web/src/content/docs/ecosystem.mdx
  100. 18 18
      packages/web/src/content/docs/github.mdx

+ 1 - 1
.github/workflows/publish.yml

@@ -184,4 +184,4 @@ jobs:
           updaterJsonPreferNsis: true
           updaterJsonPreferNsis: true
           releaseId: ${{ needs.publish.outputs.releaseId }}
           releaseId: ${{ needs.publish.outputs.releaseId }}
           tagName: ${{ needs.publish.outputs.tagName }}
           tagName: ${{ needs.publish.outputs.tagName }}
-          assetName: opencode-desktop-[platform]-[arch][ext]
+          releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]

+ 21 - 15
bun.lock

@@ -20,7 +20,7 @@
     },
     },
     "packages/console/app": {
     "packages/console/app": {
       "name": "@opencode-ai/console-app",
       "name": "@opencode-ai/console-app",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@cloudflare/vite-plugin": "1.15.2",
         "@cloudflare/vite-plugin": "1.15.2",
         "@ibm/plex": "6.4.1",
         "@ibm/plex": "6.4.1",
@@ -48,7 +48,7 @@
     },
     },
     "packages/console/core": {
     "packages/console/core": {
       "name": "@opencode-ai/console-core",
       "name": "@opencode-ai/console-core",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "@aws-sdk/client-sts": "3.782.0",
         "@jsx-email/render": "1.1.1",
         "@jsx-email/render": "1.1.1",
@@ -75,7 +75,7 @@
     },
     },
     "packages/console/function": {
     "packages/console/function": {
       "name": "@opencode-ai/console-function",
       "name": "@opencode-ai/console-function",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
         "@ai-sdk/openai": "2.0.2",
@@ -99,7 +99,7 @@
     },
     },
     "packages/console/mail": {
     "packages/console/mail": {
       "name": "@opencode-ai/console-mail",
       "name": "@opencode-ai/console-mail",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@jsx-email/all": "2.2.3",
         "@jsx-email/all": "2.2.3",
         "@jsx-email/cli": "1.4.3",
         "@jsx-email/cli": "1.4.3",
@@ -123,7 +123,7 @@
     },
     },
     "packages/desktop": {
     "packages/desktop": {
       "name": "@opencode-ai/desktop",
       "name": "@opencode-ai/desktop",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
@@ -168,7 +168,7 @@
     },
     },
     "packages/enterprise": {
     "packages/enterprise": {
       "name": "@opencode-ai/enterprise",
       "name": "@opencode-ai/enterprise",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@opencode-ai/ui": "workspace:*",
         "@opencode-ai/ui": "workspace:*",
         "@opencode-ai/util": "workspace:*",
         "@opencode-ai/util": "workspace:*",
@@ -197,7 +197,7 @@
     },
     },
     "packages/function": {
     "packages/function": {
       "name": "@opencode-ai/function",
       "name": "@opencode-ai/function",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "22.0.0",
         "@octokit/rest": "22.0.0",
@@ -213,7 +213,7 @@
     },
     },
     "packages/opencode": {
     "packages/opencode": {
       "name": "opencode",
       "name": "opencode",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "bin": {
       "bin": {
         "opencode": "./bin/opencode",
         "opencode": "./bin/opencode",
       },
       },
@@ -305,7 +305,7 @@
     },
     },
     "packages/plugin": {
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
       "name": "@opencode-ai/plugin",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "zod": "catalog:",
         "zod": "catalog:",
@@ -325,7 +325,7 @@
     },
     },
     "packages/sdk/js": {
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
       "name": "@opencode-ai/sdk",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "devDependencies": {
       "devDependencies": {
         "@hey-api/openapi-ts": "0.88.1",
         "@hey-api/openapi-ts": "0.88.1",
         "@tsconfig/node22": "catalog:",
         "@tsconfig/node22": "catalog:",
@@ -336,7 +336,7 @@
     },
     },
     "packages/slack": {
     "packages/slack": {
       "name": "@opencode-ai/slack",
       "name": "@opencode-ai/slack",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "@slack/bolt": "^3.17.1",
         "@slack/bolt": "^3.17.1",
@@ -349,7 +349,7 @@
     },
     },
     "packages/tauri": {
     "packages/tauri": {
       "name": "@opencode-ai/tauri",
       "name": "@opencode-ai/tauri",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@opencode-ai/desktop": "workspace:*",
         "@opencode-ai/desktop": "workspace:*",
         "@tauri-apps/api": "^2",
         "@tauri-apps/api": "^2",
@@ -357,7 +357,9 @@
         "@tauri-apps/plugin-opener": "^2",
         "@tauri-apps/plugin-opener": "^2",
         "@tauri-apps/plugin-process": "~2",
         "@tauri-apps/plugin-process": "~2",
         "@tauri-apps/plugin-shell": "~2",
         "@tauri-apps/plugin-shell": "~2",
+        "@tauri-apps/plugin-store": "~2",
         "@tauri-apps/plugin-updater": "~2",
         "@tauri-apps/plugin-updater": "~2",
+        "@tauri-apps/plugin-window-state": "~2",
         "solid-js": "catalog:",
         "solid-js": "catalog:",
       },
       },
       "devDependencies": {
       "devDependencies": {
@@ -371,7 +373,7 @@
     },
     },
     "packages/ui": {
     "packages/ui": {
       "name": "@opencode-ai/ui",
       "name": "@opencode-ai/ui",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
@@ -403,7 +405,7 @@
     },
     },
     "packages/util": {
     "packages/util": {
       "name": "@opencode-ai/util",
       "name": "@opencode-ai/util",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "zod": "catalog:",
         "zod": "catalog:",
       },
       },
@@ -414,7 +416,7 @@
     },
     },
     "packages/web": {
     "packages/web": {
       "name": "@opencode-ai/web",
       "name": "@opencode-ai/web",
-      "version": "1.0.149",
+      "version": "1.0.150",
       "dependencies": {
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
         "@astrojs/markdown-remark": "6.3.1",
@@ -1662,8 +1664,12 @@
 
 
     "@tauri-apps/plugin-shell": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="],
     "@tauri-apps/plugin-shell": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="],
 
 
+    "@tauri-apps/plugin-store": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ckGSEzZ5Ii4Hf2D5x25Oqnm2Zf9MfDWAzR+volY0z/OOBz6aucPKEY0F649JvQ0Vupku6UJo7ugpGRDOFOunkA=="],
+
     "@tauri-apps/plugin-updater": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg=="],
     "@tauri-apps/plugin-updater": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg=="],
 
 
+    "@tauri-apps/plugin-window-state": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw=="],
+
     "@thisbeyond/solid-dnd": ["@thisbeyond/[email protected]", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="],
     "@thisbeyond/solid-dnd": ["@thisbeyond/[email protected]", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="],
 
 
     "@tokenizer/token": ["@tokenizer/[email protected]", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
     "@tokenizer/token": ["@tokenizer/[email protected]", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],

+ 3 - 3
flake.lock

@@ -2,11 +2,11 @@
   "nodes": {
   "nodes": {
     "nixpkgs": {
     "nixpkgs": {
       "locked": {
       "locked": {
-        "lastModified": 1765270179,
-        "narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=",
+        "lastModified": 1765425892,
+        "narHash": "sha256-jlQpSkg2sK6IJVzTQBDyRxQZgKADC2HKMRfGCSgNMHo=",
         "owner": "NixOS",
         "owner": "NixOS",
         "repo": "nixpkgs",
         "repo": "nixpkgs",
-        "rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9",
+        "rev": "5d6bdbddb4695a62f0d00a3620b37a15275a5093",
         "type": "github"
         "type": "github"
       },
       },
       "original": {
       "original": {

+ 19 - 0
github/action.yml

@@ -20,10 +20,29 @@ inputs:
 runs:
 runs:
   using: "composite"
   using: "composite"
   steps:
   steps:
+    - name: Get opencode version
+      id: version
+      shell: bash
+      run: |
+        VERSION=$(curl -sf https://api.github.com/repos/sst/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
+        echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT
+
+    - name: Cache opencode
+      id: cache
+      uses: actions/cache@v4
+      with:
+        path: ~/.opencode/bin
+        key: opencode-${{ runner.os }}-${{ runner.arch }}-${{ steps.version.outputs.version }}
+
     - name: Install opencode
     - name: Install opencode
+      if: steps.cache.outputs.cache-hit != 'true'
       shell: bash
       shell: bash
       run: curl -fsSL https://opencode.ai/install | bash
       run: curl -fsSL https://opencode.ai/install | bash
 
 
+    - name: Add opencode to PATH
+      shell: bash
+      run: echo "$HOME/.opencode/bin" >> $GITHUB_PATH
+
     - name: Run opencode
     - name: Run opencode
       shell: bash
       shell: bash
       id: run_opencode
       id: run_opencode

+ 1 - 0
infra/console.ts

@@ -102,6 +102,7 @@ const ZEN_MODELS = [
   new sst.Secret("ZEN_MODELS2"),
   new sst.Secret("ZEN_MODELS2"),
   new sst.Secret("ZEN_MODELS3"),
   new sst.Secret("ZEN_MODELS3"),
   new sst.Secret("ZEN_MODELS4"),
   new sst.Secret("ZEN_MODELS4"),
+  new sst.Secret("ZEN_MODELS5"),
 ]
 ]
 const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
 const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
 const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
 const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {

+ 1 - 1
nix/hashes.json

@@ -1,3 +1,3 @@
 {
 {
-  "nodeModules": "sha256-3GaqUwomnIUW8MqUi1jDVPHQ/C5Z+D9wMR//tAGxvSQ="
+  "nodeModules": "sha256-b6AEbARiEcI/Pu1g0LbRfH1Oo5rClncW44Ug0d4oP0w="
 }
 }

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@opencode-ai/console-app",
   "name": "@opencode-ai/console-app",
-  "version": "1.0.149",
+  "version": "1.0.150",
   "type": "module",
   "type": "module",
   "scripts": {
   "scripts": {
     "typecheck": "tsgo --noEmit",
     "typecheck": "tsgo --noEmit",

+ 1 - 2
packages/console/app/src/component/header.tsx

@@ -169,7 +169,6 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
               </Match>
               </Match>
             </Switch>
             </Switch>
           </li>
           </li>
-
         </ul>
         </ul>
       </nav>
       </nav>
       <nav data-component="nav-mobile">
       <nav data-component="nav-mobile">
@@ -181,7 +180,7 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
           class="nav-toggle"
           class="nav-toggle"
           onClick={() => setStore("mobileMenuOpen", !store.mobileMenuOpen)}
           onClick={() => setStore("mobileMenuOpen", !store.mobileMenuOpen)}
         >
         >
-        <span class="sr-only">Open menu</span>
+          <span class="sr-only">Open menu</span>
           <Switch>
           <Switch>
             <Match when={store.mobileMenuOpen}>
             <Match when={store.mobileMenuOpen}>
               <svg
               <svg

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 20 - 26
packages/console/app/src/routes/download/index.tsx


+ 17 - 10
packages/console/app/src/routes/index.tsx

@@ -52,8 +52,6 @@ export default function Home() {
 
 
         <div data-component="content">
         <div data-component="content">
           <section data-component="hero">
           <section data-component="hero">
-            
-
             <div data-slot="hero-copy">
             <div data-slot="hero-copy">
               {/*<a data-slot="releases"*/}
               {/*<a data-slot="releases"*/}
               {/*   href={release()?.url ?? `${config.github.repoUrl}/releases`}*/}
               {/*   href={release()?.url ?? `${config.github.repoUrl}/releases`}*/}
@@ -654,13 +652,21 @@ export default function Home() {
               </li>
               </li>
               <li>
               <li>
                 <Faq question="Do I need extra AI subscriptions to use OpenCode?">
                 <Faq question="Do I need extra AI subscriptions to use OpenCode?">
-                  Not necessarily, but probably. You’ll need an AI subscription if you want to connect OpenCode to a
-                  paid provider, although you can work with{" "}
+                  Not necessarily, OpenCode comes with a set of free models that you can use without creating an
+                  account. Aside from these, you can use any of the popular coding models by creating a{" "}
+                  <A href="/zen">Zen</A> account. While we encourage users to use Zen, OpenCode also works with all
+                  popular providers such as OpenAI, Anthropic, xAI etc. You can even connect your{" "}
                   <a href="/docs/providers/#lm-studio" target="_blank">
                   <a href="/docs/providers/#lm-studio" target="_blank">
                     local models
                     local models
-                  </a>{" "}
-                  for free. While we encourage users to use <A href="/zen">Zen</A>, OpenCode works with all popular
-                  providers such as OpenAI, Anthropic, xAI etc.
+                  </a>
+                  .
+                </Faq>
+              </li>
+              <li>
+                <Faq question="Can I use my existing AI subscriptions with OpenCode?">
+                  Yes, OpenCode supports subscription plans from all major providers. You can use your Claude Pro/Max,
+                  ChatGPT Plus/Pro, or GitHub Copilot subscriptions. <a href="/docs/providers/#directory">Learn more</a>
+                  .
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
@@ -670,13 +676,14 @@ export default function Home() {
               </li>
               </li>
               <li>
               <li>
                 <Faq question="How much does OpenCode cost?">
                 <Faq question="How much does OpenCode cost?">
-                  OpenCode is 100% free to use. Any additional costs will come from your subscription to a model
-                  provider. While OpenCode works with any model provider, we recommend using <A href="/zen">Zen</A>.
+                  OpenCode is 100% free to use. It also comes with a set of free models. There might be additional costs
+                  if you connect any other provider.
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question="What about data and privacy?">
                 <Faq question="What about data and privacy?">
-                  Your data and information is only stored when you create sharable links in OpenCode. Learn more about{" "}
+                  Your data and information is only stored when you use our free models or create sharable links. Learn
+                  more about <a href="/docs/zen/#privacy">our models</a> and{" "}
                   <a href="/docs/share/#privacy">share pages</a>.
                   <a href="/docs/share/#privacy">share pages</a>.
                 </Faq>
                 </Faq>
               </li>
               </li>

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

@@ -43,9 +43,12 @@ const getModelsInfo = query(async (workspaceID: string) => {
           const pA = getPriority(idA)
           const pA = getPriority(idA)
           const pB = getPriority(idB)
           const pB = getPriority(idB)
           if (pA !== pB) return pA - pB
           if (pA !== pB) return pA - pB
-          return modelA.name.localeCompare(modelB.name)
+
+          const modelAName = Array.isArray(modelA) ? modelA[0].name : modelA.name
+          const modelBName = Array.isArray(modelB) ? modelB[0].name : modelB.name
+          return modelAName.localeCompare(modelBName)
         })
         })
-        .map(([id, model]) => ({ id, name: model.name })),
+        .map(([id, model]) => ({ id, name: Array.isArray(model) ? model[0].name : model.name })),
       disabled: await Model.listDisabled(),
       disabled: await Model.listDisabled(),
     }
     }
   }, workspaceID)
   }, workspaceID)

+ 10 - 5
packages/console/app/src/routes/zen/util/handler.ts

@@ -57,15 +57,17 @@ export async function handler(
     const sessionId = input.request.headers.get("x-opencode-session") ?? ""
     const sessionId = input.request.headers.get("x-opencode-session") ?? ""
     const requestId = input.request.headers.get("x-opencode-request") ?? ""
     const requestId = input.request.headers.get("x-opencode-request") ?? ""
     const projectId = input.request.headers.get("x-opencode-project") ?? ""
     const projectId = input.request.headers.get("x-opencode-project") ?? ""
+    const ocClient = input.request.headers.get("x-opencode-client") ?? ""
     logger.metric({
     logger.metric({
       is_tream: isStream,
       is_tream: isStream,
       session: sessionId,
       session: sessionId,
       request: requestId,
       request: requestId,
+      client: ocClient,
     })
     })
     const zenData = ZenData.list()
     const zenData = ZenData.list()
     const modelInfo = validateModel(zenData, model)
     const modelInfo = validateModel(zenData, model)
     const dataDumper = createDataDumper(sessionId, requestId, projectId)
     const dataDumper = createDataDumper(sessionId, requestId, projectId)
-    const trialLimiter = createTrialLimiter(modelInfo.trial?.limit, ip)
+    const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
     const isTrial = await trialLimiter?.isTrial()
     const isTrial = await trialLimiter?.isTrial()
     const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
     const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
     await rateLimiter?.check()
     await rateLimiter?.check()
@@ -286,11 +288,14 @@ export async function handler(
   }
   }
 
 
   function validateModel(zenData: ZenData, reqModel: string) {
   function validateModel(zenData: ZenData, reqModel: string) {
-    if (!(reqModel in zenData.models)) {
-      throw new ModelError(`Model ${reqModel} not supported`)
-    }
+    if (!(reqModel in zenData.models)) throw new ModelError(`Model ${reqModel} not supported`)
+
     const modelId = reqModel as keyof typeof zenData.models
     const modelId = reqModel as keyof typeof zenData.models
-    const modelData = zenData.models[modelId]
+    const modelData = Array.isArray(zenData.models[modelId])
+      ? zenData.models[modelId].find((model) => opts.format === model.formatFilter)
+      : zenData.models[modelId]
+
+    if (!modelData) throw new ModelError(`Model ${reqModel} not supported for format ${opts.format}`)
 
 
     logger.metric({ model: modelId })
     logger.metric({ model: modelId })
 
 

+ 12 - 6
packages/console/app/src/routes/zen/util/trialLimiter.ts

@@ -1,12 +1,18 @@
 import { Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
 import { Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
 import { IpTable } from "@opencode-ai/console-core/schema/ip.sql.js"
 import { IpTable } from "@opencode-ai/console-core/schema/ip.sql.js"
 import { UsageInfo } from "./provider/provider"
 import { UsageInfo } from "./provider/provider"
+import { ZenData } from "@opencode-ai/console-core/model.js"
 
 
-export function createTrialLimiter(limit: number | undefined, ip: string) {
-  if (!limit) return
+export function createTrialLimiter(trial: ZenData.Trial | undefined, ip: string, client: string) {
+  if (!trial) return
   if (!ip) return
   if (!ip) return
 
 
-  let trial: boolean
+  const limit =
+    trial.limits.find((limit) => limit.client === client)?.limit ??
+    trial.limits.find((limit) => limit.client === undefined)?.limit
+  if (!limit) return
+
+  let _isTrial: boolean
 
 
   return {
   return {
     isTrial: async () => {
     isTrial: async () => {
@@ -20,11 +26,11 @@ export function createTrialLimiter(limit: number | undefined, ip: string) {
           .then((rows) => rows[0]),
           .then((rows) => rows[0]),
       )
       )
 
 
-      trial = (data?.usage ?? 0) < limit
-      return trial
+      _isTrial = (data?.usage ?? 0) < limit
+      return _isTrial
     },
     },
     track: async (usageInfo: UsageInfo) => {
     track: async (usageInfo: UsageInfo) => {
-      if (!trial) return
+      if (!_isTrial) return
       const usage =
       const usage =
         usageInfo.inputTokens +
         usageInfo.inputTokens +
         usageInfo.outputTokens +
         usageInfo.outputTokens +

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

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

+ 4 - 1
packages/console/core/script/promote-models.ts

@@ -16,16 +16,19 @@ const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[
 const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
 const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
 const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
 const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
 const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
 const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
+const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
 if (!value1) throw new Error("ZEN_MODELS1 not found")
 if (!value1) throw new Error("ZEN_MODELS1 not found")
 if (!value2) throw new Error("ZEN_MODELS2 not found")
 if (!value2) throw new Error("ZEN_MODELS2 not found")
 if (!value3) throw new Error("ZEN_MODELS3 not found")
 if (!value3) throw new Error("ZEN_MODELS3 not found")
 if (!value4) throw new Error("ZEN_MODELS4 not found")
 if (!value4) throw new Error("ZEN_MODELS4 not found")
+if (!value5) throw new Error("ZEN_MODELS5 not found")
 
 
 // validate value
 // validate value
-ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
+ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5))
 
 
 // update the secret
 // update the secret
 await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
 await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
 await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
 await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
 await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
 await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
 await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
 await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
+await $`bun sst secret set ZEN_MODELS5 ${value5} --stage ${stage}`

+ 4 - 1
packages/console/core/script/pull-models.ts

@@ -16,16 +16,19 @@ const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[
 const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
 const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
 const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
 const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
 const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
 const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
+const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
 if (!value1) throw new Error("ZEN_MODELS1 not found")
 if (!value1) throw new Error("ZEN_MODELS1 not found")
 if (!value2) throw new Error("ZEN_MODELS2 not found")
 if (!value2) throw new Error("ZEN_MODELS2 not found")
 if (!value3) throw new Error("ZEN_MODELS3 not found")
 if (!value3) throw new Error("ZEN_MODELS3 not found")
 if (!value4) throw new Error("ZEN_MODELS4 not found")
 if (!value4) throw new Error("ZEN_MODELS4 not found")
+if (!value5) throw new Error("ZEN_MODELS5 not found")
 
 
 // validate value
 // validate value
-ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
+ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5))
 
 
 // update the secret
 // update the secret
 await $`bun sst secret set ZEN_MODELS1 ${value1}`
 await $`bun sst secret set ZEN_MODELS1 ${value1}`
 await $`bun sst secret set ZEN_MODELS2 ${value2}`
 await $`bun sst secret set ZEN_MODELS2 ${value2}`
 await $`bun sst secret set ZEN_MODELS3 ${value3}`
 await $`bun sst secret set ZEN_MODELS3 ${value3}`
 await $`bun sst secret set ZEN_MODELS4 ${value4}`
 await $`bun sst secret set ZEN_MODELS4 ${value4}`
+await $`bun sst secret set ZEN_MODELS5 ${value5}`

+ 8 - 3
packages/console/core/script/update-models.ts

@@ -14,15 +14,17 @@ const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=
 const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
 const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
 const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
 const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
 const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
 const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
+const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
 if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
 if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
 if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
 if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
 if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
 if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
 if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
 if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
+if (!oldValue5) throw new Error("ZEN_MODELS5 not found")
 
 
 // store the prettified json to a temp file
 // store the prettified json to a temp file
 const filename = `models-${Date.now()}.json`
 const filename = `models-${Date.now()}.json`
 const tempFile = Bun.file(path.join(os.tmpdir(), filename))
 const tempFile = Bun.file(path.join(os.tmpdir(), filename))
-await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4), null, 2))
+await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5), null, 2))
 console.log("tempFile", tempFile.name)
 console.log("tempFile", tempFile.name)
 
 
 // open temp file in vim and read the file on close
 // open temp file in vim and read the file on close
@@ -31,12 +33,15 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
 ZenData.validate(JSON.parse(newValue))
 ZenData.validate(JSON.parse(newValue))
 
 
 // update the secret
 // update the secret
-const chunk = Math.ceil(newValue.length / 4)
+const chunk = Math.ceil(newValue.length / 5)
 const newValue1 = newValue.slice(0, chunk)
 const newValue1 = newValue.slice(0, chunk)
 const newValue2 = newValue.slice(chunk, chunk * 2)
 const newValue2 = newValue.slice(chunk, chunk * 2)
 const newValue3 = newValue.slice(chunk * 2, chunk * 3)
 const newValue3 = newValue.slice(chunk * 2, chunk * 3)
-const newValue4 = newValue.slice(chunk * 3)
+const newValue4 = newValue.slice(chunk * 3, chunk * 4)
+const newValue5 = newValue.slice(chunk * 4)
+
 await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
 await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
 await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
 await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
 await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
 await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
 await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
 await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
+await $`bun sst secret set ZEN_MODELS5 ${newValue5}`

+ 17 - 8
packages/console/core/src/model.ts

@@ -9,7 +9,17 @@ import { Resource } from "@opencode-ai/console-resource"
 
 
 export namespace ZenData {
 export namespace ZenData {
   const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"])
   const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"])
+  const TrialSchema = z.object({
+    provider: z.string(),
+    limits: z.array(
+      z.object({
+        limit: z.number(),
+        client: z.enum(["cli", "desktop"]).optional(),
+      }),
+    ),
+  })
   export type Format = z.infer<typeof FormatSchema>
   export type Format = z.infer<typeof FormatSchema>
+  export type Trial = z.infer<typeof TrialSchema>
 
 
   const ModelCostSchema = z.object({
   const ModelCostSchema = z.object({
     input: z.number(),
     input: z.number(),
@@ -26,12 +36,7 @@ export namespace ZenData {
     allowAnonymous: z.boolean().optional(),
     allowAnonymous: z.boolean().optional(),
     byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
     byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
     stickyProvider: z.boolean().optional(),
     stickyProvider: z.boolean().optional(),
-    trial: z
-      .object({
-        limit: z.number(),
-        provider: z.string(),
-      })
-      .optional(),
+    trial: TrialSchema.optional(),
     rateLimit: z.number().optional(),
     rateLimit: z.number().optional(),
     fallbackProvider: z.string().optional(),
     fallbackProvider: z.string().optional(),
     providers: z.array(
     providers: z.array(
@@ -53,7 +58,7 @@ export namespace ZenData {
   })
   })
 
 
   const ModelsSchema = z.object({
   const ModelsSchema = z.object({
-    models: z.record(z.string(), ModelSchema),
+    models: z.record(z.string(), z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))])),
     providers: z.record(z.string(), ProviderSchema),
     providers: z.record(z.string(), ProviderSchema),
   })
   })
 
 
@@ -63,7 +68,11 @@ export namespace ZenData {
 
 
   export const list = fn(z.void(), () => {
   export const list = fn(z.void(), () => {
     const json = JSON.parse(
     const json = JSON.parse(
-      Resource.ZEN_MODELS1.value + Resource.ZEN_MODELS2.value + Resource.ZEN_MODELS3.value + Resource.ZEN_MODELS4.value,
+      Resource.ZEN_MODELS1.value +
+        Resource.ZEN_MODELS2.value +
+        Resource.ZEN_MODELS3.value +
+        Resource.ZEN_MODELS4.value +
+        Resource.ZEN_MODELS5.value,
     )
     )
     return ModelsSchema.parse(json)
     return ModelsSchema.parse(json)
   })
   })

+ 8 - 4
packages/console/core/sst-env.d.ts

@@ -50,10 +50,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
-    "Enterprise": {
-      "type": "sst.cloudflare.SolidStart"
-      "url": string
-    }
     "GITHUB_APP_ID": {
     "GITHUB_APP_ID": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
@@ -94,6 +90,10 @@ declare module "sst" {
       "type": "sst.sst.Linkable"
       "type": "sst.sst.Linkable"
       "value": string
       "value": string
     }
     }
+    "Teams": {
+      "type": "sst.cloudflare.SolidStart"
+      "url": string
+    }
     "Web": {
     "Web": {
       "type": "sst.cloudflare.Astro"
       "type": "sst.cloudflare.Astro"
       "url": string
       "url": string
@@ -114,6 +114,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
+    "ZEN_MODELS5": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
   }
 }
 }
 // cloudflare 
 // cloudflare 

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

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

+ 8 - 4
packages/console/function/sst-env.d.ts

@@ -50,10 +50,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
-    "Enterprise": {
-      "type": "sst.cloudflare.SolidStart"
-      "url": string
-    }
     "GITHUB_APP_ID": {
     "GITHUB_APP_ID": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
@@ -94,6 +90,10 @@ declare module "sst" {
       "type": "sst.sst.Linkable"
       "type": "sst.sst.Linkable"
       "value": string
       "value": string
     }
     }
+    "Teams": {
+      "type": "sst.cloudflare.SolidStart"
+      "url": string
+    }
     "Web": {
     "Web": {
       "type": "sst.cloudflare.Astro"
       "type": "sst.cloudflare.Astro"
       "url": string
       "url": string
@@ -114,6 +114,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
+    "ZEN_MODELS5": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
   }
 }
 }
 // cloudflare 
 // cloudflare 

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

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

+ 8 - 4
packages/console/resource/sst-env.d.ts

@@ -50,10 +50,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
-    "Enterprise": {
-      "type": "sst.cloudflare.SolidStart"
-      "url": string
-    }
     "GITHUB_APP_ID": {
     "GITHUB_APP_ID": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
@@ -94,6 +90,10 @@ declare module "sst" {
       "type": "sst.sst.Linkable"
       "type": "sst.sst.Linkable"
       "value": string
       "value": string
     }
     }
+    "Teams": {
+      "type": "sst.cloudflare.SolidStart"
+      "url": string
+    }
     "Web": {
     "Web": {
       "type": "sst.cloudflare.Astro"
       "type": "sst.cloudflare.Astro"
       "url": string
       "url": string
@@ -114,6 +114,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
+    "ZEN_MODELS5": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
   }
 }
 }
 // cloudflare 
 // cloudflare 

+ 1 - 1
packages/desktop/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@opencode-ai/desktop",
   "name": "@opencode-ai/desktop",
-  "version": "1.0.149",
+  "version": "1.0.150",
   "description": "",
   "description": "",
   "type": "module",
   "type": "module",
   "exports": {
   "exports": {

+ 17 - 0
packages/desktop/src/components/link.tsx

@@ -0,0 +1,17 @@
+import { ComponentProps, splitProps } from "solid-js"
+import { usePlatform } from "@/context/platform"
+
+export interface LinkProps extends ComponentProps<"button"> {
+  href: string
+}
+
+export function Link(props: LinkProps) {
+  const platform = usePlatform()
+  const [local, rest] = splitProps(props, ["href", "children"])
+
+  return (
+    <button class="text-text-strong underline" onClick={() => platform.openLink(local.href)} {...rest}>
+      {local.children}
+    </button>
+  )
+}

+ 89 - 58
packages/desktop/src/components/prompt-input.tsx

@@ -1,5 +1,17 @@
 import { useFilteredList } from "@opencode-ai/ui/hooks"
 import { useFilteredList } from "@opencode-ai/ui/hooks"
-import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createSignal } from "solid-js"
+import {
+  createEffect,
+  on,
+  Component,
+  Show,
+  For,
+  onMount,
+  onCleanup,
+  Switch,
+  Match,
+  createSignal,
+  createMemo,
+} from "solid-js"
 import { createStore } from "solid-js/store"
 import { createStore } from "solid-js/store"
 import { createFocusSignal } from "@solid-primitives/active-element"
 import { createFocusSignal } from "@solid-primitives/active-element"
 import { useLocal } from "@/context/local"
 import { useLocal } from "@/context/local"
@@ -21,7 +33,6 @@ import { popularProviders, useProviders } from "@/hooks/use-providers"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { List, ListRef } from "@opencode-ai/ui/list"
 import { List, ListRef } from "@opencode-ai/ui/list"
 import { iife } from "@opencode-ai/util/iife"
 import { iife } from "@opencode-ai/util/iife"
-import { Input } from "@opencode-ai/ui/input"
 import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
 import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
 import { IconName } from "@opencode-ai/ui/icons/provider"
 import { IconName } from "@opencode-ai/ui/icons/provider"
 
 
@@ -470,60 +481,73 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             </Button>
             </Button>
             <Show when={layout.dialog.opened() === "model"}>
             <Show when={layout.dialog.opened() === "model"}>
               <Switch>
               <Switch>
-                <Match when={providers().connected().length > 0}>
-                  <SelectDialog
-                    defaultOpen
-                    onOpenChange={(open) => {
-                      if (open) {
-                        layout.dialog.open("model")
-                      } else {
-                        layout.dialog.close("model")
-                      }
-                    }}
-                    title="Select model"
-                    placeholder="Search models"
-                    emptyMessage="No model results"
-                    key={(x) => `${x.provider.id}:${x.id}`}
-                    items={local.model.list()}
-                    current={local.model.current()}
-                    filterKeys={["provider.name", "name", "id"]}
-                    // groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
-                    groupBy={(x) => x.provider.name}
-                    sortGroupsBy={(a, b) => {
-                      if (a.category === "Recent" && b.category !== "Recent") return -1
-                      if (b.category === "Recent" && a.category !== "Recent") return 1
-                      const aProvider = a.items[0].provider.id
-                      const bProvider = b.items[0].provider.id
-                      if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
-                      if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
-                      return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
-                    }}
-                    onSelect={(x) =>
-                      local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true })
-                    }
-                    actions={
-                      <Button
-                        class="h-7 -my-1 text-14-medium"
-                        icon="plus-small"
-                        tabIndex={-1}
-                        onClick={() => layout.dialog.open("provider")}
+                <Match when={providers.paid().length > 0}>
+                  {iife(() => {
+                    const models = createMemo(() =>
+                      local.model
+                        .list()
+                        .filter((m) =>
+                          layout.connect.state() === "complete" ? m.provider.id === layout.connect.provider() : true,
+                        ),
+                    )
+                    return (
+                      <SelectDialog
+                        defaultOpen
+                        onOpenChange={(open) => {
+                          if (open) {
+                            layout.dialog.open("model")
+                          } else {
+                            layout.dialog.close("model")
+                          }
+                        }}
+                        title="Select model"
+                        placeholder="Search models"
+                        emptyMessage="No model results"
+                        key={(x) => `${x.provider.id}:${x.id}`}
+                        items={models}
+                        current={local.model.current()}
+                        filterKeys={["provider.name", "name", "id"]}
+                        // groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
+                        groupBy={(x) => x.provider.name}
+                        sortGroupsBy={(a, b) => {
+                          if (a.category === "Recent" && b.category !== "Recent") return -1
+                          if (b.category === "Recent" && a.category !== "Recent") return 1
+                          const aProvider = a.items[0].provider.id
+                          const bProvider = b.items[0].provider.id
+                          if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
+                          if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
+                          return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
+                        }}
+                        onSelect={(x) =>
+                          local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
+                            recent: true,
+                          })
+                        }
+                        actions={
+                          <Button
+                            class="h-7 -my-1 text-14-medium"
+                            icon="plus-small"
+                            tabIndex={-1}
+                            onClick={() => layout.dialog.open("provider")}
+                          >
+                            Connect provider
+                          </Button>
+                        }
                       >
                       >
-                        Connect provider
-                      </Button>
-                    }
-                  >
-                    {(i) => (
-                      <div class="w-full flex items-center gap-x-2.5">
-                        <span>{i.name}</span>
-                        <Show when={!i.cost || i.cost?.input === 0}>
-                          <Tag>Free</Tag>
-                        </Show>
-                        <Show when={i.latest}>
-                          <Tag>Latest</Tag>
-                        </Show>
-                      </div>
-                    )}
-                  </SelectDialog>
+                        {(i) => (
+                          <div class="w-full flex items-center gap-x-2.5">
+                            <span>{i.name}</span>
+                            <Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
+                              <Tag>Free</Tag>
+                            </Show>
+                            <Show when={i.latest}>
+                              <Tag>Latest</Tag>
+                            </Show>
+                          </div>
+                        )}
+                      </SelectDialog>
+                    )
+                  })}
                 </Match>
                 </Match>
                 <Match when={true}>
                 <Match when={true}>
                   {iife(() => {
                   {iife(() => {
@@ -532,6 +556,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                       if (e.key === "Escape") return
                       if (e.key === "Escape") return
                       listRef?.onKeyDown(e)
                       listRef?.onKeyDown(e)
                     }
                     }
+
+                    onMount(() => {
+                      document.addEventListener("keydown", handleKey)
+                      onCleanup(() => {
+                        document.removeEventListener("keydown", handleKey)
+                      })
+                    })
+
                     return (
                     return (
                       <Dialog
                       <Dialog
                         modal
                         modal
@@ -549,12 +581,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                           <Dialog.CloseButton tabIndex={-1} />
                           <Dialog.CloseButton tabIndex={-1} />
                         </Dialog.Header>
                         </Dialog.Header>
                         <Dialog.Body>
                         <Dialog.Body>
-                          <Input hidden type="text" class="opacity-0 size-0" autofocus onKeyDown={handleKey} />
                           <div class="flex flex-col gap-3 px-2.5">
                           <div class="flex flex-col gap-3 px-2.5">
                             <div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
                             <div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
                             <List
                             <List
                               ref={(ref) => (listRef = ref)}
                               ref={(ref) => (listRef = ref)}
-                              items={local.model.list()}
+                              items={local.model.list}
                               current={local.model.current()}
                               current={local.model.current()}
                               key={(x) => `${x.provider.id}:${x.id}`}
                               key={(x) => `${x.provider.id}:${x.id}`}
                               onSelect={(x) => {
                               onSelect={(x) => {
@@ -587,7 +618,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                                   <List
                                   <List
                                     class="w-full"
                                     class="w-full"
                                     key={(x) => x?.id}
                                     key={(x) => x?.id}
-                                    items={providers().popular()}
+                                    items={providers.popular}
                                     activeIcon="plus-small"
                                     activeIcon="plus-small"
                                     sortBy={(a, b) => {
                                     sortBy={(a, b) => {
                                       if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
                                       if (popularProviders.includes(a.id) && popularProviders.includes(b.id))

+ 53 - 34
packages/desktop/src/context/layout.tsx

@@ -6,18 +6,26 @@ import { useGlobalSync } from "./global-sync"
 import { useGlobalSDK } from "./global-sdk"
 import { useGlobalSDK } from "./global-sdk"
 import { Project } from "@opencode-ai/sdk/v2"
 import { Project } from "@opencode-ai/sdk/v2"
 
 
-const PASTEL_COLORS = [
-  "#FCEAFD", // pastel pink
-  "#FFDFBA", // pastel peach
-  "#FFFFBA", // pastel yellow
-  "#BAFFC9", // pastel green
-  "#EAF6FD", // pastel blue
-  "#EFEAFD", // pastel lavender
-  "#FEC8D8", // pastel rose
-  "#D4F0F0", // pastel cyan
-  "#FDF0EA", // pastel coral
-  "#C1E1C1", // pastel mint
-]
+const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
+
+export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
+
+export function isAvatarColorKey(value: string): value is AvatarColorKey {
+  return AVATAR_COLOR_KEYS.includes(value as AvatarColorKey)
+}
+
+export function getAvatarColors(key?: string) {
+  if (key && isAvatarColorKey(key)) {
+    return {
+      background: `var(--avatar-background-${key})`,
+      foreground: `var(--avatar-text-${key})`,
+    }
+  }
+  return {
+    background: "var(--surface-info-base)",
+    foreground: "var(--text-base)",
+  }
+}
 
 
 type Dialog = "provider" | "model" | "connect"
 type Dialog = "provider" | "model" | "connect"
 
 
@@ -45,21 +53,24 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         name: "default-layout.v7",
         name: "default-layout.v7",
       },
       },
     )
     )
-    const [ephemeral, setEphemeral] = createStore({
+    const [ephemeral, setEphemeral] = createStore<{
       connect: {
       connect: {
-        provider: undefined as undefined | string,
-        state: undefined as undefined | "pending" | "complete" | "error",
-        error: undefined as undefined | string,
-      },
+        provider?: string
+        state?: "pending" | "complete" | "error"
+        error?: string
+      }
       dialog: {
       dialog: {
-        open: undefined as undefined | Dialog,
-      },
+        open?: Dialog
+      }
+    }>({
+      connect: {},
+      dialog: {},
     })
     })
-    const usedColors = new Set<string>()
+    const usedColors = new Set<AvatarColorKey>()
 
 
-    function pickAvailableColor() {
-      const available = PASTEL_COLORS.filter((c) => !usedColors.has(c))
-      if (available.length === 0) return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)]
+    function pickAvailableColor(): AvatarColorKey {
+      const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c))
+      if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)]
       return available[Math.floor(Math.random() * available.length)]
       return available[Math.floor(Math.random() * available.length)]
     }
     }
 
 
@@ -177,22 +188,30 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
       dialog: {
       dialog: {
         opened: createMemo(() => ephemeral.dialog?.open),
         opened: createMemo(() => ephemeral.dialog?.open),
         open(dialog: Dialog) {
         open(dialog: Dialog) {
-          setEphemeral("dialog", "open", dialog)
-          if (dialog !== "connect") {
-            setEphemeral("connect", {})
-          }
+          batch(() => {
+            // if (dialog !== "connect") {
+            //   setEphemeral("connect", {})
+            // }
+            setEphemeral("dialog", "open", dialog)
+          })
         },
         },
         close(dialog: Dialog) {
         close(dialog: Dialog) {
-          if (ephemeral.dialog?.open === dialog) {
-            setEphemeral("dialog", "open", undefined)
-            setEphemeral("connect", {})
+          if (ephemeral.dialog.open === dialog) {
+            setEphemeral(
+              produce((state) => {
+                state.dialog.open = undefined
+                state.connect = {}
+              }),
+            )
           }
           }
         },
         },
         connect(provider: string) {
         connect(provider: string) {
-          batch(() => {
-            setEphemeral("dialog", "open", "connect")
-            setEphemeral("connect", { provider, state: "pending" })
-          })
+          setEphemeral(
+            produce((state) => {
+              state.dialog.open = "connect"
+              state.connect = { provider, state: "pending" }
+            }),
+          )
         },
         },
       },
       },
       connect: {
       connect: {

+ 13 - 15
packages/desktop/src/context/local.tsx

@@ -41,10 +41,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
     const providers = useProviders()
     const providers = useProviders()
 
 
     function isModelValid(model: ModelKey) {
     function isModelValid(model: ModelKey) {
-      const provider = providers().all.find((x) => x.id === model.providerID)
+      const provider = providers.all().find((x) => x.id === model.providerID)
       return (
       return (
         !!provider?.models[model.modelID] &&
         !!provider?.models[model.modelID] &&
-        providers()
+        providers
           .connected()
           .connected()
           .map((p) => p.id)
           .map((p) => p.id)
           .includes(model.providerID)
           .includes(model.providerID)
@@ -123,16 +123,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       })
       })
 
 
       const list = createMemo(() =>
       const list = createMemo(() =>
-        providers()
-          .connected()
-          .flatMap((p) =>
-            Object.values(p.models).map((m) => ({
-              ...m,
-              name: m.name.replace("(latest)", "").trim(),
-              provider: p,
-              latest: m.name.includes("(latest)"),
-            })),
-          ),
+        providers.connected().flatMap((p) =>
+          Object.values(p.models).map((m) => ({
+            ...m,
+            name: m.name.replace("(latest)", "").trim(),
+            provider: p,
+            latest: m.name.includes("(latest)"),
+          })),
+        ),
       )
       )
       const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
       const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
 
 
@@ -153,11 +151,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
           }
           }
         }
         }
 
 
-        for (const p of providers().connected()) {
-          if (p.id in providers().default) {
+        for (const p of providers.connected()) {
+          if (p.id in providers.default()) {
             return {
             return {
               providerID: p.id,
               providerID: p.id,
-              modelID: providers().default[p.id],
+              modelID: providers.default()[p.id],
             }
             }
           }
           }
         }
         }

+ 2 - 2
packages/desktop/src/context/session.tsx

@@ -62,10 +62,10 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
     const userMessages = createMemo(() =>
     const userMessages = createMemo(() =>
       messages()
       messages()
         .filter((m) => m.role === "user")
         .filter((m) => m.role === "user")
-        .sort((a, b) => b.id.localeCompare(a.id)),
+        .sort((a, b) => a.id.localeCompare(b.id)),
     )
     )
     const lastUserMessage = createMemo(() => {
     const lastUserMessage = createMemo(() => {
-      return userMessages()?.at(0)
+      return userMessages()?.at(-1)
     })
     })
     const activeMessage = createMemo(() => {
     const activeMessage = createMemo(() => {
       if (!store.messageId) return lastUserMessage()
       if (!store.messageId) return lastUserMessage()

+ 7 - 5
packages/desktop/src/hooks/use-providers.ts

@@ -17,13 +17,15 @@ export function useProviders() {
     return globalSync.data.provider
     return globalSync.data.provider
   })
   })
   const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id)))
   const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id)))
-  const paid = createMemo(() => connected().filter((p) => Object.values(p.models).find((m) => m.cost?.input)))
+  const paid = createMemo(() =>
+    connected().filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)),
+  )
   const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id)))
   const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id)))
-  return createMemo(() => ({
-    all: providers().all,
-    default: providers().default,
+  return {
+    all: createMemo(() => providers().all),
+    default: createMemo(() => providers().default),
     popular,
     popular,
     connected,
     connected,
     paid,
     paid,
-  }))
+  }
 }
 }

+ 366 - 167
packages/desktop/src/pages/layout.tsx

@@ -1,7 +1,7 @@
-import { createEffect, createMemo, For, Match, ParentProps, Show, Switch, type JSX } from "solid-js"
+import { createEffect, createMemo, For, Match, onCleanup, onMount, ParentProps, Show, Switch, type JSX } from "solid-js"
 import { DateTime } from "luxon"
 import { DateTime } from "luxon"
 import { A, useNavigate, useParams } from "@solidjs/router"
 import { A, useNavigate, useParams } from "@solidjs/router"
-import { useLayout } from "@/context/layout"
+import { useLayout, getAvatarColors } from "@/context/layout"
 import { useGlobalSync } from "@/context/global-sync"
 import { useGlobalSync } from "@/context/global-sync"
 import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
 import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
 import { Mark } from "@opencode-ai/ui/logo"
 import { Mark } from "@opencode-ai/ui/logo"
@@ -17,9 +17,9 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
 import { getFilename } from "@opencode-ai/util/path"
 import { getFilename } from "@opencode-ai/util/path"
 import { Select } from "@opencode-ai/ui/select"
 import { Select } from "@opencode-ai/ui/select"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
-import { Session, Project, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client"
+import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
 import { usePlatform } from "@/context/platform"
 import { usePlatform } from "@/context/platform"
-import { createStore } from "solid-js/store"
+import { createStore, produce } from "solid-js/store"
 import {
 import {
   DragDropProvider,
   DragDropProvider,
   DragDropSensors,
   DragDropSensors,
@@ -36,9 +36,12 @@ import { IconName } from "@opencode-ai/ui/icons/provider"
 import { popularProviders, useProviders } from "@/hooks/use-providers"
 import { popularProviders, useProviders } from "@/hooks/use-providers"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { iife } from "@opencode-ai/util/iife"
 import { iife } from "@opencode-ai/util/iife"
+import { Link } from "@/components/link"
 import { List, ListRef } from "@opencode-ai/ui/list"
 import { List, ListRef } from "@opencode-ai/ui/list"
-import { Input } from "@opencode-ai/ui/input"
+import { TextField } from "@opencode-ai/ui/text-field"
+import { showToast, Toast } from "@opencode-ai/ui/toast"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { useGlobalSDK } from "@/context/global-sdk"
+import { Spinner } from "@opencode-ai/ui/spinner"
 
 
 export default function Layout(props: ParentProps) {
 export default function Layout(props: ParentProps) {
   const [store, setStore] = createStore({
   const [store, setStore] = createStore({
@@ -177,7 +180,7 @@ export default function Layout(props: ParentProps) {
                 <Avatar
                 <Avatar
                   fallback={name()}
                   fallback={name()}
                   src={props.project.icon?.url}
                   src={props.project.icon?.url}
-                  background={props.project.icon?.color ?? "var(--surface-info-base)"}
+                  {...getAvatarColors(props.project.icon?.color)}
                   class="size-full"
                   class="size-full"
                 />
                 />
               </div>
               </div>
@@ -197,7 +200,7 @@ export default function Layout(props: ParentProps) {
               <Avatar
               <Avatar
                 fallback={name()}
                 fallback={name()}
                 src={props.project.icon?.url}
                 src={props.project.icon?.url}
-                background={props.project.icon?.color ?? "var(--surface-info-base)"}
+                {...getAvatarColors(props.project.icon?.color)}
                 class="size-full"
                 class="size-full"
               />
               />
             </div>
             </div>
@@ -228,7 +231,7 @@ export default function Layout(props: ParentProps) {
                     <Avatar
                     <Avatar
                       fallback={name()}
                       fallback={name()}
                       src={props.project.icon?.url}
                       src={props.project.icon?.url}
-                      background={props.project.icon?.color ?? "var(--surface-info-base)"}
+                      {...getAvatarColors(props.project.icon?.color)}
                       class="size-full group-hover/session:hidden"
                       class="size-full group-hover/session:hidden"
                     />
                     />
                     <Icon
                     <Icon
@@ -487,7 +490,7 @@ export default function Layout(props: ParentProps) {
           </div>
           </div>
           <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
           <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
             <Switch>
             <Switch>
-              <Match when={!providers().paid().length && layout.sidebar.opened()}>
+              <Match when={!providers.paid().length && layout.sidebar.opened()}>
                 <div class="rounded-md bg-background-stronger shadow-xs-border-base">
                 <div class="rounded-md bg-background-stronger shadow-xs-border-base">
                   <div class="p-3 flex flex-col gap-2">
                   <div class="p-3 flex flex-col gap-2">
                     <div class="text-12-medium text-text-strong">Getting started</div>
                     <div class="text-12-medium text-text-strong">Getting started</div>
@@ -533,17 +536,17 @@ export default function Layout(props: ParentProps) {
                 </Button>
                 </Button>
               </Tooltip>
               </Tooltip>
             </Show>
             </Show>
-            <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
-              <Button
-                disabled
-                class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
-                variant="ghost"
-                size="large"
-                icon="settings-gear"
-              >
-                <Show when={layout.sidebar.opened()}>Settings</Show>
-              </Button>
-            </Tooltip>
+            {/* <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}> */}
+            {/*   <Button */}
+            {/*     disabled */}
+            {/*     class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2" */}
+            {/*     variant="ghost" */}
+            {/*     size="large" */}
+            {/*     icon="settings-gear" */}
+            {/*   > */}
+            {/*     <Show when={layout.sidebar.opened()}>Settings</Show> */}
+            {/*   </Button> */}
+            {/* </Tooltip> */}
             <Tooltip placement="right" value="Share feedback" inactive={layout.sidebar.opened()}>
             <Tooltip placement="right" value="Share feedback" inactive={layout.sidebar.opened()}>
               <Button
               <Button
                 as={"a"}
                 as={"a"}
@@ -567,7 +570,7 @@ export default function Layout(props: ParentProps) {
             placeholder="Search providers"
             placeholder="Search providers"
             activeIcon="plus-small"
             activeIcon="plus-small"
             key={(x) => x?.id}
             key={(x) => x?.id}
-            items={providers().all}
+            items={providers.all}
             filterKeys={["id", "name"]}
             filterKeys={["id", "name"]}
             groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
             groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
             sortBy={(a, b) => {
             sortBy={(a, b) => {
@@ -617,27 +620,102 @@ export default function Layout(props: ParentProps) {
         </Show>
         </Show>
         <Show when={layout.dialog.opened() === "connect"}>
         <Show when={layout.dialog.opened() === "connect"}>
           {iife(() => {
           {iife(() => {
+            const providerID = createMemo(() => layout.connect.provider()!)
+            const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === providerID())!)
+            const methods = createMemo(
+              () =>
+                globalSync.data.provider_auth[providerID()] ?? [
+                  {
+                    type: "api",
+                    label: "API key",
+                  },
+                ],
+            )
             const [store, setStore] = createStore({
             const [store, setStore] = createStore({
               method: undefined as undefined | ProviderAuthMethod,
               method: undefined as undefined | ProviderAuthMethod,
+              authorization: undefined as undefined | ProviderAuthAuthorization,
+              state: "pending" as undefined | "pending" | "complete" | "error",
+              error: undefined as string | undefined,
             })
             })
-            const providerID = layout.connect.provider()!
-            const provider = globalSync.data.provider.all.find((x) => x.id === providerID)!
-            const methods = globalSync.data.provider_auth[providerID] ?? [
-              {
-                type: "api",
-                label: "API key",
-              },
-            ]
-            if (methods.length === 1) {
-              setStore("method", methods[0])
+
+            const methodIndex = createMemo(() => methods().findIndex((x) => x.label === store.method?.label))
+
+            async function selectMethod(index: number) {
+              const method = methods()[index]
+              setStore(
+                produce((draft) => {
+                  draft.method = method
+                  draft.authorization = undefined
+                  draft.state = undefined
+                  draft.error = undefined
+                }),
+              )
+
+              if (method.type === "oauth") {
+                setStore("state", "pending")
+                const start = Date.now()
+                await globalSDK.client.provider.oauth
+                  .authorize(
+                    {
+                      providerID: providerID(),
+                      method: index,
+                    },
+                    { throwOnError: true },
+                  )
+                  .then((x) => {
+                    const elapsed = Date.now() - start
+                    const delay = 1000 - elapsed
+
+                    if (delay > 0) {
+                      setTimeout(() => {
+                        setStore("state", "complete")
+                        setStore("authorization", x.data!)
+                      }, delay)
+                      return
+                    }
+                    setStore("state", "complete")
+                    setStore("authorization", x.data!)
+                  })
+                  .catch((e) => {
+                    setStore("state", "error")
+                    setStore("error", String(e))
+                  })
+              }
             }
             }
 
 
             let listRef: ListRef | undefined
             let listRef: ListRef | undefined
-            const handleKey = (e: KeyboardEvent) => {
+            function handleKey(e: KeyboardEvent) {
+              if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
+                return
+              }
               if (e.key === "Escape") return
               if (e.key === "Escape") return
               listRef?.onKeyDown(e)
               listRef?.onKeyDown(e)
             }
             }
 
 
+            onMount(() => {
+              if (methods().length === 1) {
+                selectMethod(0)
+              }
+
+              document.addEventListener("keydown", handleKey)
+              onCleanup(() => {
+                document.removeEventListener("keydown", handleKey)
+              })
+            })
+
+            async function complete() {
+              await globalSDK.client.global.dispose()
+              setTimeout(() => {
+                showToast({
+                  variant: "success",
+                  icon: "circle-check",
+                  title: `${provider().name} connected`,
+                  description: `${provider().name} models are now available to use.`,
+                })
+                layout.connect.complete()
+              }, 500)
+            }
+
             return (
             return (
               <Dialog
               <Dialog
                 modal
                 modal
@@ -657,7 +735,16 @@ export default function Layout(props: ParentProps) {
                       icon="arrow-left"
                       icon="arrow-left"
                       variant="ghost"
                       variant="ghost"
                       onClick={() => {
                       onClick={() => {
-                        if (store.method && methods.length > 1) {
+                        if (methods().length === 1) {
+                          layout.dialog.open("provider")
+                          return
+                        }
+                        if (store.authorization) {
+                          setStore("authorization", undefined)
+                          setStore("method", undefined)
+                          return
+                        }
+                        if (store.method) {
                           setStore("method", undefined)
                           setStore("method", undefined)
                           return
                           return
                         }
                         }
@@ -670,145 +757,256 @@ export default function Layout(props: ParentProps) {
                 <Dialog.Body>
                 <Dialog.Body>
                   <div class="flex flex-col gap-6 px-2.5 pb-3">
                   <div class="flex flex-col gap-6 px-2.5 pb-3">
                     <div class="px-2.5 flex gap-4 items-center">
                     <div class="px-2.5 flex gap-4 items-center">
-                      <ProviderIcon id={providerID as IconName} class="size-5 shrink-0 icon-strong-base" />
-                      <div class="text-16-medium text-text-strong">Connect {provider.name}</div>
+                      <ProviderIcon id={providerID() as IconName} class="size-5 shrink-0 icon-strong-base" />
+                      <div class="text-16-medium text-text-strong">
+                        <Switch>
+                          <Match
+                            when={providerID() === "anthropic" && store.method?.label?.toLowerCase().includes("max")}
+                          >
+                            Login with Claude Pro/Max
+                          </Match>
+                          <Match when={true}>Connect {provider().name}</Match>
+                        </Switch>
+                      </div>
                     </div>
                     </div>
-                    <Show when={store.method === undefined}>
-                      <div class="px-2.5 text-14-regular text-text-base">Select login method for {provider.name}.</div>
-                      <div class="">
-                        <Input hidden type="text" class="opacity-0 size-0" autofocus onKeyDown={handleKey} />
-                        <List
-                          ref={(ref) => (listRef = ref)}
-                          items={methods}
-                          key={(m) => m?.label}
-                          onSelect={(method) => {
-                            if (!method) return
-                            setStore("method", method)
-
-                            if (method.type === "oauth") {
-                              // const result = await sdk.client.provider.oauth.authorize({
-                              //   providerID: provider.id,
-                              //   method: index,
-                              // })
-                              // if (result.data?.method === "code") {
-                              //   dialog.replace(() => (
-                              //     <CodeMethod
-                              //       providerID={provider.id}
-                              //       title={method.label}
-                              //       index={index}
-                              //       authorization={result.data!}
-                              //     />
-                              //   ))
-                              // }
-                              // if (result.data?.method === "auto") {
-                              //   dialog.replace(() => (
-                              //     <AutoMethod
-                              //       providerID={provider.id}
-                              //       title={method.label}
-                              //       index={index}
-                              //       authorization={result.data!}
-                              //     />
-                              //   ))
-                              // }
-                            }
-                            if (method.type === "api") {
-                              // return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
-                            }
-                          }}
-                        >
-                          {(i) => (
-                            <div class="w-full flex items-center gap-x-2.5">
-                              {/* TODO: add checkmark thing */}
-                              <span>{i.label}</span>
+                    <div class="px-2.5 pb-10 flex flex-col gap-6">
+                      <Switch>
+                        <Match when={store.method === undefined}>
+                          <div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
+                          <div class="">
+                            <List
+                              ref={(ref) => (listRef = ref)}
+                              items={methods}
+                              key={(m) => m?.label}
+                              onSelect={async (method, index) => {
+                                if (!method) return
+                                selectMethod(index)
+                              }}
+                            >
+                              {(i) => (
+                                <div class="w-full flex items-center gap-x-4">
+                                  <div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
+                                    <div
+                                      class="w-2.5 h-0.5 bg-icon-strong-base hidden"
+                                      data-slot="list-item-extra-icon"
+                                    />
+                                  </div>
+                                  <span>{i.label}</span>
+                                </div>
+                              )}
+                            </List>
+                          </div>
+                        </Match>
+                        <Match when={store.state === "pending"}>
+                          <div class="text-14-regular text-text-base">
+                            <div class="flex items-center gap-x-4">
+                              <Spinner />
+                              <span>Authorization in progress...</span>
                             </div>
                             </div>
-                          )}
-                        </List>
-                      </div>
-                    </Show>
-                    <Show when={store.method?.type === "api"}>
-                      {iife(() => {
-                        const [formStore, setFormStore] = createStore({
-                          value: "",
-                          error: undefined as string | undefined,
-                        })
-
-                        async function handleSubmit(e: SubmitEvent) {
-                          e.preventDefault()
-
-                          const form = e.currentTarget as HTMLFormElement
-                          const formData = new FormData(form)
-                          const apiKey = formData.get("apiKey") as string
-
-                          if (!apiKey?.trim()) {
-                            setFormStore("error", "API key is required")
-                            return
-                          }
-
-                          setFormStore("error", undefined)
-                          await globalSDK.client.auth.set({
-                            providerID,
-                            auth: {
-                              type: "api",
-                              key: apiKey,
-                            },
-                          })
-                          await globalSDK.client.global.dispose()
-                          layout.connect.complete()
-                        }
+                          </div>
+                        </Match>
+                        <Match when={store.state === "error"}>
+                          <div class="text-14-regular text-text-base">
+                            <div class="flex items-center gap-x-4">
+                              <Icon name="circle-ban-sign" class="text-icon-critical-base" />
+                              <span>Authorization failed: {store.error}</span>
+                            </div>
+                          </div>
+                        </Match>
+                        <Match when={store.method?.type === "api"}>
+                          {iife(() => {
+                            const [formStore, setFormStore] = createStore({
+                              value: "",
+                              error: undefined as string | undefined,
+                            })
 
 
-                        return (
-                          <div class="px-2.5 pb-10 flex flex-col gap-6">
-                            <Switch>
-                              <Match when={provider.id === "opencode"}>
-                                <div class="flex flex-col gap-4">
-                                  <div class="text-14-regular text-text-base">
-                                    OpenCode Zen gives you access to a curated set of reliable optimized models for
-                                    coding agents.
-                                  </div>
-                                  <div class="text-14-regular text-text-base">
-                                    With a single API key you’ll get access to models such as Claude, GPT, Gemini, GLM
-                                    and more.
+                            async function handleSubmit(e: SubmitEvent) {
+                              e.preventDefault()
+
+                              const form = e.currentTarget as HTMLFormElement
+                              const formData = new FormData(form)
+                              const apiKey = formData.get("apiKey") as string
+
+                              if (!apiKey?.trim()) {
+                                setFormStore("error", "API key is required")
+                                return
+                              }
+
+                              setFormStore("error", undefined)
+                              await globalSDK.client.auth.set({
+                                providerID: providerID(),
+                                auth: {
+                                  type: "api",
+                                  key: apiKey,
+                                },
+                              })
+                              await complete()
+                            }
+
+                            return (
+                              <div class="flex flex-col gap-6">
+                                <Switch>
+                                  <Match when={provider().id === "opencode"}>
+                                    <div class="flex flex-col gap-4">
+                                      <div class="text-14-regular text-text-base">
+                                        OpenCode Zen gives you access to a curated set of reliable optimized models for
+                                        coding agents.
+                                      </div>
+                                      <div class="text-14-regular text-text-base">
+                                        With a single API key you’ll get access to models such as Claude, GPT, Gemini,
+                                        GLM and more.
+                                      </div>
+                                      <div class="text-14-regular text-text-base">
+                                        Visit{" "}
+                                        <Link href="https://opencode.ai/zen" tabIndex={-1}>
+                                          opencode.ai/zen
+                                        </Link>{" "}
+                                        to collect your API key.
+                                      </div>
+                                    </div>
+                                  </Match>
+                                  <Match when={true}>
+                                    <div class="text-14-regular text-text-base">
+                                      Enter your {provider().name} API key to connect your account and use{" "}
+                                      {provider().name} models in OpenCode.
+                                    </div>
+                                  </Match>
+                                </Switch>
+                                <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
+                                  <TextField
+                                    autofocus
+                                    type="text"
+                                    label={`${provider().name} API key`}
+                                    placeholder="API key"
+                                    name="apiKey"
+                                    value={formStore.value}
+                                    onChange={setFormStore.bind(null, "value")}
+                                    validationState={formStore.error ? "invalid" : undefined}
+                                    error={formStore.error}
+                                  />
+                                  <Button class="w-auto" type="submit" size="large" variant="primary">
+                                    Submit
+                                  </Button>
+                                </form>
+                              </div>
+                            )
+                          })}
+                        </Match>
+                        <Match when={store.method?.type === "oauth"}>
+                          <Switch>
+                            <Match when={store.authorization?.method === "code"}>
+                              {iife(() => {
+                                const [formStore, setFormStore] = createStore({
+                                  value: "",
+                                  error: undefined as string | undefined,
+                                })
+
+                                onMount(() => {
+                                  if (store.authorization?.method === "code" && store.authorization?.url) {
+                                    platform.openLink(store.authorization.url)
+                                  }
+                                })
+
+                                async function handleSubmit(e: SubmitEvent) {
+                                  e.preventDefault()
+
+                                  const form = e.currentTarget as HTMLFormElement
+                                  const formData = new FormData(form)
+                                  const code = formData.get("code") as string
+
+                                  if (!code?.trim()) {
+                                    setFormStore("error", "Authorization code is required")
+                                    return
+                                  }
+
+                                  setFormStore("error", undefined)
+                                  const { error } = await globalSDK.client.provider.oauth.callback({
+                                    providerID: providerID(),
+                                    method: methodIndex(),
+                                    code,
+                                  })
+                                  if (!error) {
+                                    await complete()
+                                    return
+                                  }
+                                  setFormStore("error", "Invalid authorization code")
+                                }
+
+                                return (
+                                  <div class="flex flex-col gap-6">
+                                    <div class="text-14-regular text-text-base">
+                                      Visit <Link href={store.authorization!.url}>this link</Link> to collect your
+                                      authorization code to connect your account and use {provider().name} models in
+                                      OpenCode.
+                                    </div>
+                                    <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
+                                      <TextField
+                                        autofocus
+                                        type="text"
+                                        label={`${store.method?.label} authorization code`}
+                                        placeholder="Authorization code"
+                                        name="code"
+                                        value={formStore.value}
+                                        onChange={setFormStore.bind(null, "value")}
+                                        validationState={formStore.error ? "invalid" : undefined}
+                                        error={formStore.error}
+                                      />
+                                      <Button class="w-auto" type="submit" size="large" variant="primary">
+                                        Submit
+                                      </Button>
+                                    </form>
                                   </div>
                                   </div>
-                                  <div class="text-14-regular text-text-base">
-                                    Visit{" "}
-                                    <button
-                                      tabIndex={-1}
-                                      class="text-text-strong underline"
-                                      onClick={() => platform.openLink("https://opencode.ai/zen")}
-                                    >
-                                      opencode.ai/zen
-                                    </button>{" "}
-                                    to collect your API key.
+                                )
+                              })}
+                            </Match>
+                            <Match when={store.authorization?.method === "auto"}>
+                              {iife(() => {
+                                const code = createMemo(() => {
+                                  const instructions = store.authorization?.instructions
+                                  if (instructions?.includes(":")) {
+                                    return instructions?.split(":")[1]?.trim()
+                                  }
+                                  return instructions
+                                })
+
+                                onMount(async () => {
+                                  const result = await globalSDK.client.provider.oauth.callback({
+                                    providerID: providerID(),
+                                    method: methodIndex(),
+                                  })
+                                  if (result.error) {
+                                    // TODO: show error
+                                    layout.dialog.close("connect")
+                                    return
+                                  }
+                                  await complete()
+                                })
+
+                                return (
+                                  <div class="flex flex-col gap-6">
+                                    <div class="text-14-regular text-text-base">
+                                      Visit <Link href={store.authorization!.url}>this link</Link> and enter the code
+                                      below to connect your account and use {provider().name} models in OpenCode.
+                                    </div>
+                                    <TextField
+                                      label="Confirmation code"
+                                      class="font-mono"
+                                      value={code()}
+                                      readOnly
+                                      copyable
+                                    />
+                                    <div class="text-14-regular text-text-base flex items-center gap-4">
+                                      <Spinner />
+                                      <span>Waiting for authorization...</span>
+                                    </div>
                                   </div>
                                   </div>
-                                </div>
-                              </Match>
-                              <Match when={true}>
-                                <div class="text-14-regular text-text-base">
-                                  Enter your {provider.name} API key to connect your account and use {provider.name}{" "}
-                                  models in OpenCode.
-                                </div>
-                              </Match>
-                            </Switch>
-                            <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
-                              <Input
-                                autofocus
-                                type="text"
-                                label={`${provider.name} API key`}
-                                placeholder="API key"
-                                name="apiKey"
-                                value={formStore.value}
-                                onChange={setFormStore.bind(null, "value")}
-                                validationState={formStore.error ? "invalid" : undefined}
-                                error={formStore.error}
-                              />
-                              <Button class="w-auto" type="submit" size="large" variant="primary">
-                                Submit
-                              </Button>
-                            </form>
-                          </div>
-                        )
-                      })}
-                    </Show>
+                                )
+                              })}
+                            </Match>
+                          </Switch>
+                        </Match>
+                      </Switch>
+                    </div>
                   </div>
                   </div>
                 </Dialog.Body>
                 </Dialog.Body>
               </Dialog>
               </Dialog>
@@ -816,6 +1014,7 @@ export default function Layout(props: ParentProps) {
           })}
           })}
         </Show>
         </Show>
       </div>
       </div>
+      <Toast.Region />
     </div>
     </div>
   )
   )
 }
 }

+ 0 - 1
packages/desktop/src/pages/session.tsx

@@ -415,7 +415,6 @@ export default function Page() {
                           messages={session.messages.user()}
                           messages={session.messages.user()}
                           current={session.messages.active()}
                           current={session.messages.active()}
                           onMessageSelect={session.messages.setActive}
                           onMessageSelect={session.messages.setActive}
-                          working={session.working()}
                           wide={wide()}
                           wide={wide()}
                         />
                         />
                         <SessionTurn
                         <SessionTurn

+ 1 - 1
packages/enterprise/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@opencode-ai/enterprise",
   "name": "@opencode-ai/enterprise",
-  "version": "1.0.149",
+  "version": "1.0.150",
   "private": true,
   "private": true,
   "type": "module",
   "type": "module",
   "scripts": {
   "scripts": {

+ 5 - 2
packages/enterprise/src/routes/share/[shareID].tsx

@@ -141,7 +141,10 @@ export default function () {
   const data = createAsync(
   const data = createAsync(
     async () => {
     async () => {
       if (!params.shareID) throw new Error("Missing shareID")
       if (!params.shareID) throw new Error("Missing shareID")
-      return getData(params.shareID)
+      const now = Date.now()
+      const data = getData(params.shareID)
+      console.log("getData", Date.now() - now)
+      return data
     },
     },
     {
     {
       deferStream: true,
       deferStream: true,
@@ -206,7 +209,7 @@ export default function () {
                     const messages = createMemo(() =>
                     const messages = createMemo(() =>
                       data().sessionID
                       data().sessionID
                         ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
                         ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
-                            (a, b) => b.time.created - a.time.created,
+                            (a, b) => a.time.created - b.time.created,
                           )
                           )
                         : [],
                         : [],
                     )
                     )

+ 8 - 4
packages/enterprise/sst-env.d.ts

@@ -50,10 +50,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
-    "Enterprise": {
-      "type": "sst.cloudflare.SolidStart"
-      "url": string
-    }
     "GITHUB_APP_ID": {
     "GITHUB_APP_ID": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
@@ -94,6 +90,10 @@ declare module "sst" {
       "type": "sst.sst.Linkable"
       "type": "sst.sst.Linkable"
       "value": string
       "value": string
     }
     }
+    "Teams": {
+      "type": "sst.cloudflare.SolidStart"
+      "url": string
+    }
     "Web": {
     "Web": {
       "type": "sst.cloudflare.Astro"
       "type": "sst.cloudflare.Astro"
       "url": string
       "url": string
@@ -114,6 +114,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
+    "ZEN_MODELS5": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
   }
 }
 }
 // cloudflare 
 // cloudflare 

+ 6 - 6
packages/extensions/zed/extension.toml

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

+ 1 - 1
packages/function/package.json

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

+ 8 - 4
packages/function/sst-env.d.ts

@@ -50,10 +50,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
-    "Enterprise": {
-      "type": "sst.cloudflare.SolidStart"
-      "url": string
-    }
     "GITHUB_APP_ID": {
     "GITHUB_APP_ID": {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
@@ -94,6 +90,10 @@ declare module "sst" {
       "type": "sst.sst.Linkable"
       "type": "sst.sst.Linkable"
       "value": string
       "value": string
     }
     }
+    "Teams": {
+      "type": "sst.cloudflare.SolidStart"
+      "url": string
+    }
     "Web": {
     "Web": {
       "type": "sst.cloudflare.Astro"
       "type": "sst.cloudflare.Astro"
       "url": string
       "url": string
@@ -114,6 +114,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "type": "sst.sst.Secret"
       "value": string
       "value": string
     }
     }
+    "ZEN_MODELS5": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
   }
 }
 }
 // cloudflare 
 // cloudflare 

+ 1 - 1
packages/opencode/package.json

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

+ 238 - 2
packages/opencode/src/acp/agent.ts

@@ -28,7 +28,7 @@ import { Config } from "@/config/config"
 import { Todo } from "@/session/todo"
 import { Todo } from "@/session/todo"
 import { z } from "zod"
 import { z } from "zod"
 import { LoadAPIKeyError } from "ai"
 import { LoadAPIKeyError } from "ai"
-import type { OpencodeClient } from "@opencode-ai/sdk/v2"
+import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
 
 
 export namespace ACP {
 export namespace ACP {
   const log = Log.create({ service: "acp-agent" })
   const log = Log.create({ service: "acp-agent" })
@@ -386,7 +386,7 @@ export namespace ACP {
 
 
         log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
         log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
 
 
-        const load = await this.loadSession({
+        const load = await this.loadSessionMode({
           cwd: directory,
           cwd: directory,
           mcpServers: params.mcpServers,
           mcpServers: params.mcpServers,
           sessionId,
           sessionId,
@@ -412,6 +412,242 @@ export namespace ACP {
     }
     }
 
 
     async loadSession(params: LoadSessionRequest) {
     async loadSession(params: LoadSessionRequest) {
+      const directory = params.cwd
+      const sessionId = params.sessionId
+
+      try {
+        const model = await defaultModel(this.config, directory)
+
+        // Store ACP session state
+        const state = await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model)
+
+        log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
+
+        const mode = await this.loadSessionMode({
+          cwd: directory,
+          mcpServers: params.mcpServers,
+          sessionId,
+        })
+
+        this.setupEventSubscriptions(state)
+
+        // Replay session history
+        const messages = await this.sdk.session
+          .messages(
+            {
+              sessionID: sessionId,
+              directory,
+            },
+            { throwOnError: true },
+          )
+          .then((x) => x.data)
+          .catch((err) => {
+            log.error("unexpected error when fetching message", { error: err })
+            return undefined
+          })
+
+        for (const msg of messages ?? []) {
+          log.debug("replay message", msg)
+          await this.processMessage(msg)
+        }
+
+        return mode
+      } catch (e) {
+        const error = MessageV2.fromError(e, {
+          providerID: this.config.defaultModel?.providerID ?? "unknown",
+        })
+        if (LoadAPIKeyError.isInstance(error)) {
+          throw RequestError.authRequired()
+        }
+        throw e
+      }
+    }
+
+    private async processMessage(message: SessionMessageResponse) {
+      log.debug("process message", message)
+      if (message.info.role !== "assistant" && message.info.role !== "user") return
+      const sessionId = message.info.sessionID
+
+      for (const part of message.parts) {
+        if (part.type === "tool") {
+          switch (part.state.status) {
+            case "pending":
+              await this.connection
+                .sessionUpdate({
+                  sessionId,
+                  update: {
+                    sessionUpdate: "tool_call",
+                    toolCallId: part.callID,
+                    title: part.tool,
+                    kind: toToolKind(part.tool),
+                    status: "pending",
+                    locations: [],
+                    rawInput: {},
+                  },
+                })
+                .catch((err) => {
+                  log.error("failed to send tool pending to ACP", { error: err })
+                })
+              break
+            case "running":
+              await this.connection
+                .sessionUpdate({
+                  sessionId,
+                  update: {
+                    sessionUpdate: "tool_call_update",
+                    toolCallId: part.callID,
+                    status: "in_progress",
+                    locations: toLocations(part.tool, part.state.input),
+                    rawInput: part.state.input,
+                  },
+                })
+                .catch((err) => {
+                  log.error("failed to send tool in_progress to ACP", { error: err })
+                })
+              break
+            case "completed":
+              const kind = toToolKind(part.tool)
+              const content: ToolCallContent[] = [
+                {
+                  type: "content",
+                  content: {
+                    type: "text",
+                    text: part.state.output,
+                  },
+                },
+              ]
+
+              if (kind === "edit") {
+                const input = part.state.input
+                const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
+                const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
+                const newText =
+                  typeof input["newString"] === "string"
+                    ? input["newString"]
+                    : typeof input["content"] === "string"
+                      ? input["content"]
+                      : ""
+                content.push({
+                  type: "diff",
+                  path: filePath,
+                  oldText,
+                  newText,
+                })
+              }
+
+              if (part.tool === "todowrite") {
+                const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
+                if (parsedTodos.success) {
+                  await this.connection
+                    .sessionUpdate({
+                      sessionId,
+                      update: {
+                        sessionUpdate: "plan",
+                        entries: parsedTodos.data.map((todo) => {
+                          const status: PlanEntry["status"] =
+                            todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
+                          return {
+                            priority: "medium",
+                            status,
+                            content: todo.content,
+                          }
+                        }),
+                      },
+                    })
+                    .catch((err) => {
+                      log.error("failed to send session update for todo", { error: err })
+                    })
+                } else {
+                  log.error("failed to parse todo output", { error: parsedTodos.error })
+                }
+              }
+
+              await this.connection
+                .sessionUpdate({
+                  sessionId,
+                  update: {
+                    sessionUpdate: "tool_call_update",
+                    toolCallId: part.callID,
+                    status: "completed",
+                    kind,
+                    content,
+                    title: part.state.title,
+                    rawOutput: {
+                      output: part.state.output,
+                      metadata: part.state.metadata,
+                    },
+                  },
+                })
+                .catch((err) => {
+                  log.error("failed to send tool completed to ACP", { error: err })
+                })
+              break
+            case "error":
+              await this.connection
+                .sessionUpdate({
+                  sessionId,
+                  update: {
+                    sessionUpdate: "tool_call_update",
+                    toolCallId: part.callID,
+                    status: "failed",
+                    content: [
+                      {
+                        type: "content",
+                        content: {
+                          type: "text",
+                          text: part.state.error,
+                        },
+                      },
+                    ],
+                    rawOutput: {
+                      error: part.state.error,
+                    },
+                  },
+                })
+                .catch((err) => {
+                  log.error("failed to send tool error to ACP", { error: err })
+                })
+              break
+          }
+        } else if (part.type === "text") {
+          if (part.text) {
+            await this.connection
+              .sessionUpdate({
+                sessionId,
+                update: {
+                  sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk",
+                  content: {
+                    type: "text",
+                    text: part.text,
+                  },
+                },
+              })
+              .catch((err) => {
+                log.error("failed to send text to ACP", { error: err })
+              })
+          }
+        } else if (part.type === "reasoning") {
+          if (part.text) {
+            await this.connection
+              .sessionUpdate({
+                sessionId,
+                update: {
+                  sessionUpdate: "agent_thought_chunk",
+                  content: {
+                    type: "text",
+                    text: part.text,
+                  },
+                },
+              })
+              .catch((err) => {
+                log.error("failed to send reasoning to ACP", { error: err })
+              })
+          }
+        }
+      }
+    }
+
+    private async loadSessionMode(params: LoadSessionRequest) {
       const directory = params.cwd
       const directory = params.cwd
       const model = await defaultModel(this.config, directory)
       const model = await defaultModel(this.config, directory)
       const sessionId = params.sessionId
       const sessionId = params.sessionId

+ 31 - 0
packages/opencode/src/acp/session.ts

@@ -40,6 +40,37 @@ export class ACPSessionManager {
     return state
     return state
   }
   }
 
 
+  async load(
+    sessionId: string,
+    cwd: string,
+    mcpServers: McpServer[],
+    model?: ACPSessionState["model"],
+  ): Promise<ACPSessionState> {
+    const session = await this.sdk.session
+      .get(
+        {
+          sessionID: sessionId,
+          directory: cwd,
+        },
+        { throwOnError: true },
+      )
+      .then((x) => x.data!)
+
+    const resolvedModel = model
+
+    const state: ACPSessionState = {
+      id: sessionId,
+      cwd,
+      mcpServers,
+      createdAt: new Date(session.time.created),
+      model: resolvedModel,
+    }
+    log.info("loading_session", { state })
+
+    this.sessions.set(sessionId, state)
+    return state
+  }
+
   get(sessionId: string): ACPSessionState {
   get(sessionId: string): ACPSessionState {
     const session = this.sessions.get(sessionId)
     const session = this.sessions.get(sessionId)
     if (!session) {
     if (!session) {

+ 1 - 0
packages/opencode/src/cli/cmd/debug/lsp.ts

@@ -17,6 +17,7 @@ const DiagnosticsCommand = cmd({
   async handler(args) {
   async handler(args) {
     await bootstrap(process.cwd(), async () => {
     await bootstrap(process.cwd(), async () => {
       await LSP.touchFile(args.file, true)
       await LSP.touchFile(args.file, true)
+      await Bun.sleep(1000)
       process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
       process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
     })
     })
   },
   },

+ 34 - 14
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -107,7 +107,9 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
     render(
     render(
       () => {
       () => {
         return (
         return (
-          <ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} />}>
+          <ErrorBoundary
+            fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} mode={mode} />}
+          >
             <ArgsProvider {...input.args}>
             <ArgsProvider {...input.args}>
               <ExitProvider onExit={onExit}>
               <ExitProvider onExit={onExit}>
                 <KVProvider>
                 <KVProvider>
@@ -536,7 +538,12 @@ function App() {
   )
   )
 }
 }
 
 
-function ErrorComponent(props: { error: Error; reset: () => void; onExit: () => Promise<void> }) {
+function ErrorComponent(props: {
+  error: Error
+  reset: () => void
+  onExit: () => Promise<void>
+  mode?: "dark" | "light"
+}) {
   const term = useTerminalDimensions()
   const term = useTerminalDimensions()
   useKeyboard((evt) => {
   useKeyboard((evt) => {
     if (evt.ctrl && evt.name === "c") {
     if (evt.ctrl && evt.name === "c") {
@@ -547,6 +554,15 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () =>
 
 
   const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml")
   const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml")
 
 
+  // Choose safe fallback colors per mode since theme context may not be available
+  const isLight = props.mode === "light"
+  const colors = {
+    bg: isLight ? "#ffffff" : "#0a0a0a",
+    text: isLight ? "#1a1a1a" : "#eeeeee",
+    muted: isLight ? "#8a8a8a" : "#808080",
+    primary: isLight ? "#3b7dd8" : "#fab283",
+  }
+
   if (props.error.message) {
   if (props.error.message) {
     issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`)
     issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`)
   }
   }
@@ -567,27 +583,31 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () =>
   }
   }
 
 
   return (
   return (
-    <box flexDirection="column" gap={1}>
+    <box flexDirection="column" gap={1} backgroundColor={colors.bg}>
       <box flexDirection="row" gap={1} alignItems="center">
       <box flexDirection="row" gap={1} alignItems="center">
-        <text attributes={TextAttributes.BOLD}>Please report an issue.</text>
-        <box onMouseUp={copyIssueURL} backgroundColor="#565f89" padding={1}>
-          <text attributes={TextAttributes.BOLD}>Copy issue URL (exception info pre-filled)</text>
+        <text attributes={TextAttributes.BOLD} fg={colors.text}>
+          Please report an issue.
+        </text>
+        <box onMouseUp={copyIssueURL} backgroundColor={colors.primary} padding={1}>
+          <text attributes={TextAttributes.BOLD} fg={colors.bg}>
+            Copy issue URL (exception info pre-filled)
+          </text>
         </box>
         </box>
-        {copied() && <text>Successfully copied</text>}
+        {copied() && <text fg={colors.muted}>Successfully copied</text>}
       </box>
       </box>
       <box flexDirection="row" gap={2} alignItems="center">
       <box flexDirection="row" gap={2} alignItems="center">
-        <text>A fatal error occurred!</text>
-        <box onMouseUp={props.reset} backgroundColor="#565f89" padding={1}>
-          <text>Reset TUI</text>
+        <text fg={colors.text}>A fatal error occurred!</text>
+        <box onMouseUp={props.reset} backgroundColor={colors.primary} padding={1}>
+          <text fg={colors.bg}>Reset TUI</text>
         </box>
         </box>
-        <box onMouseUp={props.onExit} backgroundColor="#565f89" padding={1}>
-          <text>Exit</text>
+        <box onMouseUp={props.onExit} backgroundColor={colors.primary} padding={1}>
+          <text fg={colors.bg}>Exit</text>
         </box>
         </box>
       </box>
       </box>
       <scrollbox height={Math.floor(term().height * 0.7)}>
       <scrollbox height={Math.floor(term().height * 0.7)}>
-        <text>{props.error.stack}</text>
+        <text fg={colors.muted}>{props.error.stack}</text>
       </scrollbox>
       </scrollbox>
-      <text>{props.error.message}</text>
+      <text fg={colors.text}>{props.error.message}</text>
     </box>
     </box>
   )
   )
 }
 }

+ 4 - 2
packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx

@@ -122,7 +122,9 @@ function AutoMethod(props: AutoMethodProps) {
   return (
   return (
     <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
     <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
       <box flexDirection="row" justifyContent="space-between">
       <box flexDirection="row" justifyContent="space-between">
-        <text attributes={TextAttributes.BOLD}>{props.title}</text>
+        <text attributes={TextAttributes.BOLD} fg={theme.text}>
+          {props.title}
+        </text>
         <text fg={theme.textMuted}>esc</text>
         <text fg={theme.textMuted}>esc</text>
       </box>
       </box>
       <box gap={1}>
       <box gap={1}>
@@ -198,7 +200,7 @@ function ApiMethod(props: ApiMethodProps) {
             <text fg={theme.textMuted}>
             <text fg={theme.textMuted}>
               OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key.
               OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key.
             </text>
             </text>
-            <text>
+            <text fg={theme.text}>
               Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
               Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
             </text>
             </text>
           </box>
           </box>

+ 6 - 0
packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx

@@ -8,6 +8,7 @@ import { Keybind } from "@/util/keybind"
 import { useTheme } from "../context/theme"
 import { useTheme } from "../context/theme"
 import { useSDK } from "../context/sdk"
 import { useSDK } from "../context/sdk"
 import { DialogSessionRename } from "./dialog-session-rename"
 import { DialogSessionRename } from "./dialog-session-rename"
+import "opentui-spinner/solid"
 
 
 export function DialogSessionList() {
 export function DialogSessionList() {
   const dialog = useDialog()
   const dialog = useDialog()
@@ -22,6 +23,8 @@ export function DialogSessionList() {
 
 
   const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
   const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
 
 
+  const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
+
   const options = createMemo(() => {
   const options = createMemo(() => {
     const today = new Date().toDateString()
     const today = new Date().toDateString()
     return sync.data.session
     return sync.data.session
@@ -34,12 +37,15 @@ export function DialogSessionList() {
           category = "Today"
           category = "Today"
         }
         }
         const isDeleting = toDelete() === x.id
         const isDeleting = toDelete() === x.id
+        const status = sync.data.session_status[x.id]
+        const isWorking = status?.type === "busy"
         return {
         return {
           title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title,
           title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title,
           bg: isDeleting ? theme.error : undefined,
           bg: isDeleting ? theme.error : undefined,
           value: x.id,
           value: x.id,
           category,
           category,
           footer: Locale.time(x.time.updated),
           footer: Locale.time(x.time.updated),
+          gutter: isWorking ? <spinner frames={spinnerFrames} interval={80} color={theme.primary} /> : undefined,
         }
         }
       })
       })
       .slice(0, 150)
       .slice(0, 150)

+ 1 - 1
packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx

@@ -19,7 +19,7 @@ export function DialogStatus() {
         </text>
         </text>
         <text fg={theme.textMuted}>esc</text>
         <text fg={theme.textMuted}>esc</text>
       </box>
       </box>
-      <Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text>No MCP Servers</text>}>
+      <Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
         <box>
         <box>
           <text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
           <text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
           <For each={Object.entries(sync.data.mcp)}>
           <For each={Object.entries(sync.data.mcp)}>

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

@@ -25,6 +25,7 @@ import { Locale } from "@/util/locale"
 import { createColors, createFrames } from "../../ui/spinner.ts"
 import { createColors, createFrames } from "../../ui/spinner.ts"
 import { useDialog } from "@tui/ui/dialog"
 import { useDialog } from "@tui/ui/dialog"
 import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
 import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
+import { DialogAlert } from "../../ui/dialog-alert"
 import { useToast } from "../../ui/toast"
 import { useToast } from "../../ui/toast"
 
 
 export type PromptProps = {
 export type PromptProps = {
@@ -908,9 +909,14 @@ export function Prompt(props: PromptProps) {
                       if (!r) return
                       if (!r) return
                       if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
                       if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
                         return "gemini is way too hot right now"
                         return "gemini is way too hot right now"
-                      if (r.message.length > 50) return r.message.slice(0, 50) + "..."
+                      if (r.message.length > 80) return r.message.slice(0, 80) + "..."
                       return r.message
                       return r.message
                     })
                     })
+                    const isTruncated = createMemo(() => {
+                      const r = retry()
+                      if (!r) return false
+                      return r.message.length > 120
+                    })
                     const [seconds, setSeconds] = createSignal(0)
                     const [seconds, setSeconds] = createSignal(0)
                     onMount(() => {
                     onMount(() => {
                       const timer = setInterval(() => {
                       const timer = setInterval(() => {
@@ -922,12 +928,28 @@ export function Prompt(props: PromptProps) {
                         clearInterval(timer)
                         clearInterval(timer)
                       })
                       })
                     })
                     })
+                    const handleMessageClick = () => {
+                      const r = retry()
+                      if (!r) return
+                      if (isTruncated()) {
+                        DialogAlert.show(dialog, "Retry Error", r.message)
+                      }
+                    }
+
+                    const retryText = () => {
+                      const r = retry()
+                      if (!r) return ""
+                      const baseMessage = message()
+                      const truncatedHint = isTruncated() ? " (click to expand)" : ""
+                      const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]`
+                      return baseMessage + truncatedHint + retryInfo
+                    }
+
                     return (
                     return (
                       <Show when={retry()}>
                       <Show when={retry()}>
-                        <text fg={theme.error}>
-                          {message()} [retrying {seconds() > 0 ? `in ${seconds()}s ` : ""}
-                          attempt #{retry()!.attempt}]
-                        </text>
+                        <box onMouseUp={handleMessageClick}>
+                          <text fg={theme.error}>{retryText()}</text>
+                        </box>
                       </Show>
                       </Show>
                     )
                     )
                   })()}
                   })()}

+ 10 - 2
packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx

@@ -5,8 +5,13 @@ import type { TextPart } from "@opencode-ai/sdk/v2"
 import { Locale } from "@/util/locale"
 import { Locale } from "@/util/locale"
 import { DialogMessage } from "./dialog-message"
 import { DialogMessage } from "./dialog-message"
 import { useDialog } from "../../ui/dialog"
 import { useDialog } from "../../ui/dialog"
+import type { PromptInfo } from "../../component/prompt/history"
 
 
-export function DialogTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
+export function DialogTimeline(props: {
+  sessionID: string
+  onMove: (messageID: string) => void
+  setPrompt?: (prompt: PromptInfo) => void
+}) {
   const sync = useSync()
   const sync = useSync()
   const dialog = useDialog()
   const dialog = useDialog()
 
 
@@ -26,10 +31,13 @@ export function DialogTimeline(props: { sessionID: string; onMove: (messageID: s
         value: message.id,
         value: message.id,
         footer: Locale.time(message.time.created),
         footer: Locale.time(message.time.created),
         onSelect: (dialog) => {
         onSelect: (dialog) => {
-          dialog.replace(() => <DialogMessage messageID={message.id} sessionID={props.sessionID} />)
+          dialog.replace(() => (
+            <DialogMessage messageID={message.id} sessionID={props.sessionID} setPrompt={props.setPrompt} />
+          ))
         },
         },
       })
       })
     }
     }
+    result.reverse()
     return result
     return result
   })
   })
 
 

+ 1 - 0
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

@@ -289,6 +289,7 @@ export function Session() {
               if (child) scroll.scrollBy(child.y - scroll.y - 1)
               if (child) scroll.scrollBy(child.y - scroll.y - 1)
             }}
             }}
             sessionID={route.sessionID}
             sessionID={route.sessionID}
+            setPrompt={(promptInfo) => prompt.set(promptInfo)}
           />
           />
         ))
         ))
       },
       },

+ 5 - 3
packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx

@@ -259,9 +259,11 @@ export function Sidebar(props: { sessionID: string }) {
               flexDirection="row"
               flexDirection="row"
               gap={1}
               gap={1}
             >
             >
-              <text flexShrink={0}>⬖</text>
+              <text flexShrink={0} fg={theme.text}>
+                ⬖
+              </text>
               <box flexGrow={1} gap={1}>
               <box flexGrow={1} gap={1}>
-                <text>
+                <text fg={theme.text}>
                   <b>Getting started</b>
                   <b>Getting started</b>
                 </text>
                 </text>
                 <text fg={theme.textMuted}>OpenCode includes free models so you can start immediately.</text>
                 <text fg={theme.textMuted}>OpenCode includes free models so you can start immediately.</text>
@@ -269,7 +271,7 @@ export function Sidebar(props: { sessionID: string }) {
                   Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc
                   Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc
                 </text>
                 </text>
                 <box flexDirection="row" gap={1} justifyContent="space-between">
                 <box flexDirection="row" gap={1} justifyContent="space-between">
-                  <text>Connect provider</text>
+                  <text fg={theme.text}>Connect provider</text>
                   <text fg={theme.textMuted}>/connect</text>
                   <text fg={theme.textMuted}>/connect</text>
                 </box>
                 </box>
               </box>
               </box>

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

@@ -22,7 +22,9 @@ export function DialogAlert(props: DialogAlertProps) {
   return (
   return (
     <box paddingLeft={2} paddingRight={2} gap={1}>
     <box paddingLeft={2} paddingRight={2} gap={1}>
       <box flexDirection="row" justifyContent="space-between">
       <box flexDirection="row" justifyContent="space-between">
-        <text attributes={TextAttributes.BOLD}>{props.title}</text>
+        <text attributes={TextAttributes.BOLD} fg={theme.text}>
+          {props.title}
+        </text>
         <text fg={theme.textMuted}>esc</text>
         <text fg={theme.textMuted}>esc</text>
       </box>
       </box>
       <box paddingBottom={1}>
       <box paddingBottom={1}>

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

@@ -34,7 +34,9 @@ export function DialogConfirm(props: DialogConfirmProps) {
   return (
   return (
     <box paddingLeft={2} paddingRight={2} gap={1}>
     <box paddingLeft={2} paddingRight={2} gap={1}>
       <box flexDirection="row" justifyContent="space-between">
       <box flexDirection="row" justifyContent="space-between">
-        <text attributes={TextAttributes.BOLD}>{props.title}</text>
+        <text attributes={TextAttributes.BOLD} fg={theme.text}>
+          {props.title}
+        </text>
         <text fg={theme.textMuted}>esc</text>
         <text fg={theme.textMuted}>esc</text>
       </box>
       </box>
       <box paddingBottom={1}>
       <box paddingBottom={1}>

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

@@ -18,7 +18,9 @@ export function DialogHelp() {
   return (
   return (
     <box paddingLeft={2} paddingRight={2} gap={1}>
     <box paddingLeft={2} paddingRight={2} gap={1}>
       <box flexDirection="row" justifyContent="space-between">
       <box flexDirection="row" justifyContent="space-between">
-        <text attributes={TextAttributes.BOLD}>Help</text>
+        <text attributes={TextAttributes.BOLD} fg={theme.text}>
+          Help
+        </text>
         <text fg={theme.textMuted}>esc/enter</text>
         <text fg={theme.textMuted}>esc/enter</text>
       </box>
       </box>
       <box paddingBottom={1}>
       <box paddingBottom={1}>

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

@@ -35,7 +35,9 @@ export function DialogPrompt(props: DialogPromptProps) {
   return (
   return (
     <box paddingLeft={2} paddingRight={2} gap={1}>
     <box paddingLeft={2} paddingRight={2} gap={1}>
       <box flexDirection="row" justifyContent="space-between">
       <box flexDirection="row" justifyContent="space-between">
-        <text attributes={TextAttributes.BOLD}>{props.title}</text>
+        <text attributes={TextAttributes.BOLD} fg={theme.text}>
+          {props.title}
+        </text>
         <text fg={theme.textMuted}>esc</text>
         <text fg={theme.textMuted}>esc</text>
       </box>
       </box>
       <box gap={1}>
       <box gap={1}>

+ 9 - 1
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx

@@ -36,6 +36,7 @@ export interface DialogSelectOption<T = any> {
   category?: string
   category?: string
   disabled?: boolean
   disabled?: boolean
   bg?: RGBA
   bg?: RGBA
+  gutter?: JSX.Element
   onSelect?: (ctx: DialogContext, trigger?: "prompt") => void
   onSelect?: (ctx: DialogContext, trigger?: "prompt") => void
 }
 }
 
 
@@ -239,7 +240,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
                         moveTo(index)
                         moveTo(index)
                       }}
                       }}
                       backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
                       backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
-                      paddingLeft={current() ? 1 : 3}
+                      paddingLeft={current() || option.gutter ? 1 : 3}
                       paddingRight={3}
                       paddingRight={3}
                       gap={1}
                       gap={1}
                     >
                     >
@@ -249,6 +250,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
                         description={option.description !== category ? option.description : undefined}
                         description={option.description !== category ? option.description : undefined}
                         active={active()}
                         active={active()}
                         current={current()}
                         current={current()}
+                        gutter={option.gutter}
                       />
                       />
                     </box>
                     </box>
                   )
                   )
@@ -282,6 +284,7 @@ function Option(props: {
   active?: boolean
   active?: boolean
   current?: boolean
   current?: boolean
   footer?: JSX.Element | string
   footer?: JSX.Element | string
+  gutter?: JSX.Element
   onMouseOver?: () => void
   onMouseOver?: () => void
 }) {
 }) {
   const { theme } = useTheme()
   const { theme } = useTheme()
@@ -294,6 +297,11 @@ function Option(props: {
         </text>
         </text>
       </Show>
       </Show>
+      <Show when={!props.current && props.gutter}>
+        <box flexShrink={0} marginRight={0.5}>
+          {props.gutter}
+        </box>
+      </Show>
       <text
       <text
         flexGrow={1}
         flexGrow={1}
         fg={props.active ? fg : props.current ? theme.primary : theme.text}
         fg={props.active ? fg : props.current ? theme.primary : theme.text}

+ 2 - 1
packages/opencode/src/config/config.ts

@@ -1,5 +1,6 @@
 import { Log } from "../util/log"
 import { Log } from "../util/log"
 import path from "path"
 import path from "path"
+import { pathToFileURL } from "url"
 import os from "os"
 import os from "os"
 import z from "zod"
 import z from "zod"
 import { Filesystem } from "../util/filesystem"
 import { Filesystem } from "../util/filesystem"
@@ -297,7 +298,7 @@ export namespace Config {
       dot: true,
       dot: true,
       cwd: dir,
       cwd: dir,
     })) {
     })) {
-      plugins.push("file://" + item)
+      plugins.push(pathToFileURL(item).href)
     }
     }
     return plugins
     return plugins
   }
   }

+ 1 - 0
packages/opencode/src/flag/flag.ts

@@ -11,6 +11,7 @@ export namespace Flag {
   export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
   export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
   export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
   export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
   export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
   export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
+  export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"
 
 
   // Experimental
   // Experimental
   export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
   export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")

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

@@ -6,6 +6,7 @@ import z from "zod"
 import { NamedError } from "@opencode-ai/util/error"
 import { NamedError } from "@opencode-ai/util/error"
 import { Log } from "../util/log"
 import { Log } from "../util/log"
 import { iife } from "@/util/iife"
 import { iife } from "@/util/iife"
+import { Flag } from "../flag/flag"
 
 
 declare global {
 declare global {
   const OPENCODE_VERSION: string
   const OPENCODE_VERSION: string
@@ -162,7 +163,7 @@ export namespace Installation {
 
 
   export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
   export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
   export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
   export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
-  export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}`
+  export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}`
 
 
   export async function latest(installMethod?: Method) {
   export async function latest(installMethod?: Method) {
     const detectedMethod = installMethod || (await method())
     const detectedMethod = installMethod || (await method())

+ 7 - 6
packages/opencode/src/lsp/client.ts

@@ -1,6 +1,7 @@
 import { BusEvent } from "@/bus/bus-event"
 import { BusEvent } from "@/bus/bus-event"
 import { Bus } from "@/bus"
 import { Bus } from "@/bus"
 import path from "path"
 import path from "path"
+import { pathToFileURL, fileURLToPath } from "url"
 import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
 import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
 import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
 import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
 import { Log } from "../util/log"
 import { Log } from "../util/log"
@@ -46,7 +47,7 @@ export namespace LSPClient {
 
 
     const diagnostics = new Map<string, Diagnostic[]>()
     const diagnostics = new Map<string, Diagnostic[]>()
     connection.onNotification("textDocument/publishDiagnostics", (params) => {
     connection.onNotification("textDocument/publishDiagnostics", (params) => {
-      const path = new URL(params.uri).pathname
+      const path = fileURLToPath(params.uri)
       l.info("textDocument/publishDiagnostics", {
       l.info("textDocument/publishDiagnostics", {
         path,
         path,
       })
       })
@@ -68,7 +69,7 @@ export namespace LSPClient {
     connection.onRequest("workspace/workspaceFolders", async () => [
     connection.onRequest("workspace/workspaceFolders", async () => [
       {
       {
         name: "workspace",
         name: "workspace",
-        uri: "file://" + input.root,
+        uri: pathToFileURL(input.root).href,
       },
       },
     ])
     ])
     connection.listen()
     connection.listen()
@@ -76,12 +77,12 @@ export namespace LSPClient {
     l.info("sending initialize")
     l.info("sending initialize")
     await withTimeout(
     await withTimeout(
       connection.sendRequest("initialize", {
       connection.sendRequest("initialize", {
-        rootUri: "file://" + input.root,
+        rootUri: pathToFileURL(input.root).href,
         processId: input.server.process.pid,
         processId: input.server.process.pid,
         workspaceFolders: [
         workspaceFolders: [
           {
           {
             name: "workspace",
             name: "workspace",
-            uri: "file://" + input.root,
+            uri: pathToFileURL(input.root).href,
           },
           },
         ],
         ],
         initializationOptions: {
         initializationOptions: {
@@ -154,7 +155,7 @@ export namespace LSPClient {
             })
             })
             await connection.sendNotification("textDocument/didChange", {
             await connection.sendNotification("textDocument/didChange", {
               textDocument: {
               textDocument: {
-                uri: `file://` + input.path,
+                uri: pathToFileURL(input.path).href,
                 version: next,
                 version: next,
               },
               },
               contentChanges: [{ text }],
               contentChanges: [{ text }],
@@ -166,7 +167,7 @@ export namespace LSPClient {
           diagnostics.delete(input.path)
           diagnostics.delete(input.path)
           await connection.sendNotification("textDocument/didOpen", {
           await connection.sendNotification("textDocument/didOpen", {
             textDocument: {
             textDocument: {
-              uri: `file://` + input.path,
+              uri: pathToFileURL(input.path).href,
               languageId,
               languageId,
               version: 0,
               version: 0,
               text,
               text,

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

@@ -3,6 +3,7 @@ import { Bus } from "@/bus"
 import { Log } from "../util/log"
 import { Log } from "../util/log"
 import { LSPClient } from "./client"
 import { LSPClient } from "./client"
 import path from "path"
 import path from "path"
+import { pathToFileURL } from "url"
 import { LSPServer } from "./server"
 import { LSPServer } from "./server"
 import z from "zod"
 import z from "zod"
 import { Config } from "../config/config"
 import { Config } from "../config/config"
@@ -270,7 +271,7 @@ export namespace LSP {
     return run((client) => {
     return run((client) => {
       return client.connection.sendRequest("textDocument/hover", {
       return client.connection.sendRequest("textDocument/hover", {
         textDocument: {
         textDocument: {
-          uri: `file://${input.file}`,
+          uri: pathToFileURL(input.file).href,
         },
         },
         position: {
         position: {
           line: input.line,
           line: input.line,

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

@@ -226,7 +226,10 @@ export namespace ProviderTransform {
       }
       }
     }
     }
 
 
-    if (model.providerID === "baseten") {
+    if (
+      model.providerID === "baseten" ||
+      (model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(model.api.id))
+    ) {
       result["chat_template_args"] = { enable_thinking: true }
       result["chat_template_args"] = { enable_thinking: true }
     }
     }
 
 

+ 5 - 3
packages/opencode/src/server/server.ts

@@ -791,9 +791,11 @@ export namespace Server {
           "json",
           "json",
           z.object({
           z.object({
             title: z.string().optional(),
             title: z.string().optional(),
-            time: z.object({
-              archived: z.number().optional(),
-            }),
+            time: z
+              .object({
+                archived: z.number().optional(),
+              })
+              .optional(),
           }),
           }),
         ),
         ),
         async (c) => {
         async (c) => {

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

@@ -5,6 +5,7 @@ import z from "zod"
 import { Identifier } from "../id/id"
 import { Identifier } from "../id/id"
 import { MessageV2 } from "./message-v2"
 import { MessageV2 } from "./message-v2"
 import { Log } from "../util/log"
 import { Log } from "../util/log"
+import { Flag } from "../flag/flag"
 import { SessionRevert } from "./revert"
 import { SessionRevert } from "./revert"
 import { Session } from "."
 import { Session } from "."
 import { Agent } from "../agent/agent"
 import { Agent } from "../agent/agent"
@@ -29,7 +30,7 @@ import PROMPT_PLAN from "../session/prompt/plan.txt"
 import BUILD_SWITCH from "../session/prompt/build-switch.txt"
 import BUILD_SWITCH from "../session/prompt/build-switch.txt"
 import MAX_STEPS from "../session/prompt/max-steps.txt"
 import MAX_STEPS from "../session/prompt/max-steps.txt"
 import { defer } from "../util/defer"
 import { defer } from "../util/defer"
-import { mergeDeep, pipe } from "remeda"
+import { clone, mergeDeep, pipe } from "remeda"
 import { ToolRegistry } from "../tool/registry"
 import { ToolRegistry } from "../tool/registry"
 import { Wildcard } from "../util/wildcard"
 import { Wildcard } from "../util/wildcard"
 import { MCP } from "../mcp"
 import { MCP } from "../mcp"
@@ -520,28 +521,33 @@ export namespace SessionPrompt {
         })
         })
       }
       }
 
 
-      const messages = [
+      // Deep copy message history so that modifications made by plugins do not
+      // affect the original messages
+      const sessionMessages = clone(
+        msgs.filter((m) => {
+          if (m.info.role !== "assistant" || m.info.error === undefined) {
+            return true
+          }
+          if (
+            MessageV2.AbortedError.isInstance(m.info.error) &&
+            m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
+          ) {
+            return true
+          }
+          return false
+        }),
+      )
+
+      await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: sessionMessages })
+
+      const messages: ModelMessage[] = [
         ...system.map(
         ...system.map(
           (x): ModelMessage => ({
           (x): ModelMessage => ({
             role: "system",
             role: "system",
             content: x,
             content: x,
           }),
           }),
         ),
         ),
-        ...MessageV2.toModelMessage(
-          msgs.filter((m) => {
-            if (m.info.role !== "assistant" || m.info.error === undefined) {
-              return true
-            }
-            if (
-              MessageV2.AbortedError.isInstance(m.info.error) &&
-              m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
-            ) {
-              return true
-            }
-
-            return false
-          }),
-        ),
+        ...MessageV2.toModelMessage(sessionMessages),
         ...(isLastStep
         ...(isLastStep
           ? [
           ? [
               {
               {
@@ -551,6 +557,7 @@ export namespace SessionPrompt {
             ]
             ]
           : []),
           : []),
       ]
       ]
+
       const result = await processor.process({
       const result = await processor.process({
         onError(error) {
         onError(error) {
           log.error("stream error", {
           log.error("stream error", {
@@ -584,6 +591,7 @@ export namespace SessionPrompt {
                 "x-opencode-project": Instance.project.id,
                 "x-opencode-project": Instance.project.id,
                 "x-opencode-session": sessionID,
                 "x-opencode-session": sessionID,
                 "x-opencode-request": lastUser.id,
                 "x-opencode-request": lastUser.id,
+                "x-opencode-client": Flag.OPENCODE_CLIENT,
               }
               }
             : undefined),
             : undefined),
           ...model.headers,
           ...model.headers,

+ 2 - 2
packages/opencode/src/tool/bash.ts

@@ -82,7 +82,7 @@ export const BashTool = Tool.define("bash", async () => {
   log.info("bash tool using shell", { shell })
   log.info("bash tool using shell", { shell })
 
 
   return {
   return {
-    description: DESCRIPTION,
+    description: DESCRIPTION.replaceAll("${directory}", Instance.directory),
     parameters: z.object({
     parameters: z.object({
       command: z.string().describe("The command to execute"),
       command: z.string().describe("The command to execute"),
       timeout: z.number().describe("Optional timeout in milliseconds").optional(),
       timeout: z.number().describe("Optional timeout in milliseconds").optional(),
@@ -188,7 +188,7 @@ export const BashTool = Tool.define("bash", async () => {
           const action = Wildcard.allStructured({ head: command[0], tail: command.slice(1) }, permissions)
           const action = Wildcard.allStructured({ head: command[0], tail: command.slice(1) }, permissions)
           if (action === "deny") {
           if (action === "deny") {
             throw new Error(
             throw new Error(
-              `The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${JSON.stringify(permissions)}`,
+              `The user has specifically restricted access to this command: "${command.join(" ")}", you are not allowed to execute it. The user has these settings configured: ${JSON.stringify(permissions)}`,
             )
             )
           }
           }
           if (action === "ask") {
           if (action === "ask") {

+ 2 - 0
packages/opencode/src/tool/bash.txt

@@ -1,5 +1,7 @@
 Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
 Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
 
 
+All commands run in ${directory} by default. Use the `workdir` parameter if you need to run a command in a different directory.
+
 Before executing the command, please follow these steps:
 Before executing the command, please follow these steps:
 
 
 1. Directory Verification:
 1. Directory Verification:

+ 0 - 1
packages/opencode/src/tool/webfetch.txt

@@ -11,4 +11,3 @@ Usage notes:
   - The prompt should describe what information you want to extract from the page
   - The prompt should describe what information you want to extract from the page
   - This tool is read-only and does not modify any files
   - This tool is read-only and does not modify any files
   - Results may be summarized if the content is very large
   - Results may be summarized if the content is very large
-  - Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL

+ 4 - 1
packages/opencode/src/util/log.ts

@@ -50,7 +50,10 @@ export namespace Log {
   export function file() {
   export function file() {
     return logpath
     return logpath
   }
   }
-  let write = (msg: any) => Bun.stderr.write(msg)
+  let write = (msg: any) => {
+    process.stderr.write(msg)
+    return msg.length
+  }
 
 
   export async function init(options: Options) {
   export async function init(options: Options) {
     if (options.level) level = options.level
     if (options.level) level = options.level

+ 1 - 1
packages/plugin/package.json

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

+ 10 - 0
packages/plugin/src/index.ts

@@ -6,6 +6,7 @@ import type {
   Provider,
   Provider,
   Permission,
   Permission,
   UserMessage,
   UserMessage,
+  Message,
   Part,
   Part,
   Auth,
   Auth,
   Config,
   Config,
@@ -175,6 +176,15 @@ export interface Hooks {
       metadata: any
       metadata: any
     },
     },
   ) => Promise<void>
   ) => Promise<void>
+  "experimental.chat.messages.transform"?: (
+    input: {},
+    output: {
+      messages: {
+        info: Message
+        parts: Part[]
+      }[]
+    },
+  ) => Promise<void>
   "experimental.text.complete"?: (
   "experimental.text.complete"?: (
     input: { sessionID: string; messageID: string; partID: string },
     input: { sessionID: string; messageID: string; partID: string },
     output: { text: string },
     output: { text: string },

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

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

+ 1 - 1
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -2407,7 +2407,7 @@ export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses]
 export type SessionUpdateData = {
 export type SessionUpdateData = {
   body?: {
   body?: {
     title?: string
     title?: string
-    time: {
+    time?: {
       archived?: number
       archived?: number
     }
     }
   }
   }

+ 1 - 2
packages/sdk/openapi.json

@@ -1190,8 +1190,7 @@
                       }
                       }
                     }
                     }
                   }
                   }
-                },
-                "required": ["time"]
+                }
               }
               }
             }
             }
           }
           }

+ 1 - 1
packages/slack/package.json

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

+ 3 - 1
packages/tauri/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@opencode-ai/tauri",
   "name": "@opencode-ai/tauri",
   "private": true,
   "private": true,
-  "version": "1.0.149",
+  "version": "1.0.150",
   "type": "module",
   "type": "module",
   "scripts": {
   "scripts": {
     "typecheck": "tsgo -b",
     "typecheck": "tsgo -b",
@@ -18,7 +18,9 @@
     "@tauri-apps/plugin-opener": "^2",
     "@tauri-apps/plugin-opener": "^2",
     "@tauri-apps/plugin-process": "~2",
     "@tauri-apps/plugin-process": "~2",
     "@tauri-apps/plugin-shell": "~2",
     "@tauri-apps/plugin-shell": "~2",
+    "@tauri-apps/plugin-store": "~2",
     "@tauri-apps/plugin-updater": "~2",
     "@tauri-apps/plugin-updater": "~2",
+    "@tauri-apps/plugin-window-state": "~2",
     "solid-js": "catalog:"
     "solid-js": "catalog:"
   },
   },
   "devDependencies": {
   "devDependencies": {

+ 45 - 0
packages/tauri/src-tauri/Cargo.lock

@@ -2513,7 +2513,9 @@ dependencies = [
  "tauri-plugin-opener",
  "tauri-plugin-opener",
  "tauri-plugin-process",
  "tauri-plugin-process",
  "tauri-plugin-shell",
  "tauri-plugin-shell",
+ "tauri-plugin-store",
  "tauri-plugin-updater",
  "tauri-plugin-updater",
+ "tauri-plugin-window-state",
  "tokio",
  "tokio",
 ]
 ]
 
 
@@ -4175,6 +4177,22 @@ dependencies = [
  "tokio",
  "tokio",
 ]
 ]
 
 
+[[package]]
+name = "tauri-plugin-store"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59a77036340a97eb5bbe1b3209c31e5f27f75e6f92a52fd9dd4b211ef08bf310"
+dependencies = [
+ "dunce",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.17",
+ "tokio",
+ "tracing",
+]
+
 [[package]]
 [[package]]
 name = "tauri-plugin-updater"
 name = "tauri-plugin-updater"
 version = "2.9.0"
 version = "2.9.0"
@@ -4207,6 +4225,21 @@ dependencies = [
  "zip",
  "zip",
 ]
 ]
 
 
+[[package]]
+name = "tauri-plugin-window-state"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704"
+dependencies = [
+ "bitflags 2.10.0",
+ "log",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.17",
+]
+
 [[package]]
 [[package]]
 name = "tauri-runtime"
 name = "tauri-runtime"
 version = "2.9.1"
 version = "2.9.1"
@@ -4440,10 +4473,22 @@ dependencies = [
  "pin-project-lite",
  "pin-project-lite",
  "signal-hook-registry",
  "signal-hook-registry",
  "socket2",
  "socket2",
+ "tokio-macros",
  "tracing",
  "tracing",
  "windows-sys 0.61.2",
  "windows-sys 0.61.2",
 ]
 ]
 
 
+[[package]]
+name = "tokio-macros"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
 [[package]]
 [[package]]
 name = "tokio-rustls"
 name = "tokio-rustls"
 version = "0.26.4"
 version = "0.26.4"

+ 3 - 1
packages/tauri/src-tauri/Cargo.toml

@@ -23,9 +23,11 @@ tauri-plugin-opener = "2"
 tauri-plugin-shell = "2"
 tauri-plugin-shell = "2"
 tauri-plugin-dialog = "2"
 tauri-plugin-dialog = "2"
 tauri-plugin-updater = "2"
 tauri-plugin-updater = "2"
+tauri-plugin-process = "2"
+tauri-plugin-store = "2"
+tauri-plugin-window-state = "2"
 
 
 serde = { version = "1", features = ["derive"] }
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 serde_json = "1"
 tokio = "1.48.0"
 tokio = "1.48.0"
 listeners = "0.3"
 listeners = "0.3"
-tauri-plugin-process = "2"

+ 3 - 1
packages/tauri/src-tauri/capabilities/default.json

@@ -11,6 +11,8 @@
     "shell:default",
     "shell:default",
     "updater:default",
     "updater:default",
     "dialog:default",
     "dialog:default",
-    "process:default"
+    "process:default",
+    "store:default",
+    "window-state:default"
   ]
   ]
 }
 }

+ 10 - 2
packages/tauri/src-tauri/src/lib.rs

@@ -4,7 +4,7 @@ use std::{
     sync::{Arc, Mutex},
     sync::{Arc, Mutex},
     time::{Duration, Instant},
     time::{Duration, Instant},
 };
 };
-use tauri::{AppHandle, Manager, RunEvent, WebviewUrl, WebviewWindow};
+use tauri::{AppHandle, LogicalSize, Manager, Monitor, RunEvent, WebviewUrl, WebviewWindow};
 use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
 use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
 use tauri_plugin_shell::process::{CommandChild, CommandEvent};
 use tauri_plugin_shell::process::{CommandChild, CommandEvent};
 use tauri_plugin_shell::ShellExt;
 use tauri_plugin_shell::ShellExt;
@@ -67,6 +67,7 @@ fn spawn_sidecar(app: &AppHandle, port: u16) -> CommandChild {
         .sidecar("opencode")
         .sidecar("opencode")
         .unwrap()
         .unwrap()
         .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
         .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
+        .env("OPENCODE_CLIENT", "desktop")
         .args(["serve", &format!("--port={port}")])
         .args(["serve", &format!("--port={port}")])
         .spawn()
         .spawn()
         .expect("Failed to spawn opencode");
         .expect("Failed to spawn opencode");
@@ -106,6 +107,8 @@ pub fn run() {
     let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
     let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
 
 
     let mut builder = tauri::Builder::default()
     let mut builder = tauri::Builder::default()
+        .plugin(tauri_plugin_window_state::Builder::new().build())
+        .plugin(tauri_plugin_store::Builder::new().build())
         .plugin(tauri_plugin_dialog::init())
         .plugin(tauri_plugin_dialog::init())
         .plugin(tauri_plugin_shell::init())
         .plugin(tauri_plugin_shell::init())
         .plugin(tauri_plugin_process::init())
         .plugin(tauri_plugin_process::init())
@@ -166,10 +169,15 @@ pub fn run() {
                     None
                     None
                 };
                 };
 
 
+                let primary_monitor = app.primary_monitor().ok().flatten();
+                let size = primary_monitor
+                    .map(|m| m.size().to_logical(m.scale_factor()))
+                    .unwrap_or(LogicalSize::new(1920, 1080));
+
                 let mut window_builder =
                 let mut window_builder =
                     WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
                     WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
                         .title("OpenCode")
                         .title("OpenCode")
-                        .inner_size(800.0, 600.0)
+                        .inner_size(size.width as f64, size.height as f64)
                         .decorations(true)
                         .decorations(true)
                         .zoom_hotkeys_enabled(true)
                         .zoom_hotkeys_enabled(true)
                         .initialization_script(format!(
                         .initialization_script(format!(

+ 1 - 1
packages/tauri/src-tauri/tauri.conf.json

@@ -19,7 +19,7 @@
   },
   },
   "bundle": {
   "bundle": {
     "active": true,
     "active": true,
-    "targets": ["deb", "rpm", "appimage", "dmg", "nsis"],
+    "targets": ["deb", "rpm", "dmg", "nsis"],
     "icon": ["icons/32x32.png", "icons/128x128.png", "icons/[email protected]", "icons/icon.icns", "icons/icon.ico"],
     "icon": ["icons/32x32.png", "icons/128x128.png", "icons/[email protected]", "icons/icon.icns", "icons/icon.ico"],
     "externalBin": ["sidecars/opencode"],
     "externalBin": ["sidecars/opencode"],
     "createUpdaterArtifacts": true,
     "createUpdaterArtifacts": true,

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@opencode-ai/ui",
   "name": "@opencode-ai/ui",
-  "version": "1.0.149",
+  "version": "1.0.150",
   "type": "module",
   "type": "module",
   "exports": {
   "exports": {
     "./*": "./src/components/*.tsx",
     "./*": "./src/components/*.tsx",

+ 2 - 1
packages/ui/src/components/avatar.css

@@ -1,5 +1,6 @@
 [data-component="avatar"] {
 [data-component="avatar"] {
   --avatar-bg: var(--color-surface-info-base);
   --avatar-bg: var(--color-surface-info-base);
+  --avatar-fg: var(--color-text-base);
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
@@ -10,7 +11,7 @@
   font-weight: 500;
   font-weight: 500;
   text-transform: uppercase;
   text-transform: uppercase;
   background-color: var(--avatar-bg);
   background-color: var(--avatar-bg);
-  color: oklch(from var(--avatar-bg) calc(l * 0.72) calc(c * 8) h);
+  color: var(--avatar-fg);
 }
 }
 
 
 [data-component="avatar"][data-has-image] {
 [data-component="avatar"][data-has-image] {

+ 12 - 1
packages/ui/src/components/avatar.tsx

@@ -4,11 +4,21 @@ export interface AvatarProps extends ComponentProps<"div"> {
   fallback: string
   fallback: string
   src?: string
   src?: string
   background?: string
   background?: string
+  foreground?: string
   size?: "small" | "normal" | "large"
   size?: "small" | "normal" | "large"
 }
 }
 
 
 export function Avatar(props: AvatarProps) {
 export function Avatar(props: AvatarProps) {
-  const [split, rest] = splitProps(props, ["fallback", "src", "background", "size", "class", "classList", "style"])
+  const [split, rest] = splitProps(props, [
+    "fallback",
+    "src",
+    "background",
+    "foreground",
+    "size",
+    "class",
+    "classList",
+    "style",
+  ])
   const src = split.src // did this so i can zero it out to test fallback
   const src = split.src // did this so i can zero it out to test fallback
   return (
   return (
     <div
     <div
@@ -23,6 +33,7 @@ export function Avatar(props: AvatarProps) {
       style={{
       style={{
         ...(typeof split.style === "object" ? split.style : {}),
         ...(typeof split.style === "object" ? split.style : {}),
         ...(!src && split.background ? { "--avatar-bg": split.background } : {}),
         ...(!src && split.background ? { "--avatar-bg": split.background } : {}),
+        ...(!src && split.foreground ? { "--avatar-fg": split.foreground } : {}),
       }}
       }}
     >
     >
       <Show when={src} fallback={split.fallback?.[0]}>
       <Show when={src} fallback={split.fallback?.[0]}>

+ 11 - 0
packages/ui/src/components/dialog.css

@@ -88,8 +88,19 @@
         flex-direction: column;
         flex-direction: column;
         flex: 1;
         flex: 1;
         overflow-y: auto;
         overflow-y: auto;
+
+        &:focus-visible {
+          outline: none;
+        }
+      }
+      &:focus-visible {
+        outline: none;
       }
       }
     }
     }
+
+    &:focus-visible {
+      outline: none;
+    }
   }
   }
 }
 }
 
 

+ 3 - 0
packages/ui/src/components/icon.tsx

@@ -47,6 +47,9 @@ const icons = {
   "layout-bottom-partial": `<path d="M2.5 17.5L2.5 12.2059L17.5 12.2059L17.5 17.5L2.5 17.5Z" fill="currentColor" fill-opacity="40%" /><path d="M2.5 17.5L2.5 2.5M2.5 17.5L17.5 17.5M2.5 17.5L2.5 12.2059M2.5 2.5L17.5 2.5M2.5 2.5L2.5 12.2059M17.5 2.5L17.5 17.5M17.5 2.5L17.5 12.2059M17.5 17.5L17.5 12.2059M17.5 12.2059L2.5 12.2059" stroke="currentColor" stroke-linecap="square"/>`,
   "layout-bottom-partial": `<path d="M2.5 17.5L2.5 12.2059L17.5 12.2059L17.5 17.5L2.5 17.5Z" fill="currentColor" fill-opacity="40%" /><path d="M2.5 17.5L2.5 2.5M2.5 17.5L17.5 17.5M2.5 17.5L2.5 12.2059M2.5 2.5L17.5 2.5M2.5 2.5L2.5 12.2059M17.5 2.5L17.5 17.5M17.5 2.5L17.5 12.2059M17.5 17.5L17.5 12.2059M17.5 12.2059L2.5 12.2059" stroke="currentColor" stroke-linecap="square"/>`,
   "layout-bottom-full": `<path d="M2.5 17.5L2.5 12.2059L17.5 12.2059L17.5 17.5L2.5 17.5Z" fill="currentColor"/><path d="M2.5 17.5L2.5 2.5M2.5 17.5L17.5 17.5M2.5 17.5L2.5 12.2059M2.5 2.5L17.5 2.5M2.5 2.5L2.5 12.2059M17.5 2.5L17.5 17.5M17.5 2.5L17.5 12.2059M17.5 17.5L17.5 12.2059M17.5 12.2059L2.5 12.2059" stroke="currentColor" stroke-linecap="square"/>`,
   "layout-bottom-full": `<path d="M2.5 17.5L2.5 12.2059L17.5 12.2059L17.5 17.5L2.5 17.5Z" fill="currentColor"/><path d="M2.5 17.5L2.5 2.5M2.5 17.5L17.5 17.5M2.5 17.5L2.5 12.2059M2.5 2.5L17.5 2.5M2.5 2.5L2.5 12.2059M17.5 2.5L17.5 17.5M17.5 2.5L17.5 12.2059M17.5 17.5L17.5 12.2059M17.5 12.2059L2.5 12.2059" stroke="currentColor" stroke-linecap="square"/>`,
   "dot-grid": `<path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" fill="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" fill="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" fill="currentColor"/><path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" stroke="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" stroke="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" stroke="currentColor"/>`,
   "dot-grid": `<path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" fill="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" fill="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" fill="currentColor"/><path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" stroke="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" stroke="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" stroke="currentColor"/>`,
+  "circle-check": `<path d="M12.4987 7.91732L8.7487 12.5007L7.08203 10.834M17.9154 10.0007C17.9154 14.3729 14.371 17.9173 9.9987 17.9173C5.62644 17.9173 2.08203 14.3729 2.08203 10.0007C2.08203 5.6284 5.62644 2.08398 9.9987 2.08398C14.371 2.08398 17.9154 5.6284 17.9154 10.0007Z" stroke="currentColor" stroke-linecap="square"/>`,
+  copy: `<path d="M6.2513 6.24935V2.91602H17.0846V13.7493H13.7513M13.7513 6.24935V17.0827H2.91797V6.24935H13.7513Z" stroke="currentColor" stroke-linecap="round"/>`,
+  check: `<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square"/>`,
 }
 }
 
 
 export interface IconProps extends ComponentProps<"svg"> {
 export interface IconProps extends ComponentProps<"svg"> {

+ 3 - 4
packages/ui/src/components/list.css

@@ -98,16 +98,15 @@
             display: block;
             display: block;
           }
           }
           [data-slot="list-item-extra-icon"] {
           [data-slot="list-item-extra-icon"] {
+            display: block !important;
             color: var(--icon-strong-base) !important;
             color: var(--icon-strong-base) !important;
           }
           }
         }
         }
         &:active {
         &:active {
           background: var(--surface-raised-base-active);
           background: var(--surface-raised-base-active);
         }
         }
-        &:hover {
-          [data-slot="list-item-extra-icon"] {
-            color: var(--icon-strong-base) !important;
-          }
+        &:focus-visible {
+          outline: none;
         }
         }
       }
       }
     }
     }

+ 4 - 17
packages/ui/src/components/message-nav.tsx

@@ -1,7 +1,6 @@
 import { UserMessage } from "@opencode-ai/sdk/v2"
 import { UserMessage } from "@opencode-ai/sdk/v2"
-import { ComponentProps, createMemo, For, Match, Show, splitProps, Switch } from "solid-js"
+import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js"
 import { DiffChanges } from "./diff-changes"
 import { DiffChanges } from "./diff-changes"
-import { Spinner } from "./spinner"
 import { Tooltip } from "@kobalte/core/tooltip"
 import { Tooltip } from "@kobalte/core/tooltip"
 
 
 export function MessageNav(
 export function MessageNav(
@@ -9,20 +8,15 @@ export function MessageNav(
     messages: UserMessage[]
     messages: UserMessage[]
     current?: UserMessage
     current?: UserMessage
     size: "normal" | "compact"
     size: "normal" | "compact"
-    working?: boolean
     onMessageSelect: (message: UserMessage) => void
     onMessageSelect: (message: UserMessage) => void
   },
   },
 ) {
 ) {
-  const [local, others] = splitProps(props, ["messages", "current", "size", "working", "onMessageSelect"])
-  const lastUserMessage = createMemo(() => {
-    return local.messages?.at(0)
-  })
+  const [local, others] = splitProps(props, ["messages", "current", "size", "onMessageSelect"])
 
 
   const content = () => (
   const content = () => (
     <ul role="list" data-component="message-nav" data-size={local.size} {...others}>
     <ul role="list" data-component="message-nav" data-size={local.size} {...others}>
       <For each={local.messages}>
       <For each={local.messages}>
         {(message) => {
         {(message) => {
-          const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && local.working)
           const handleClick = () => local.onMessageSelect(message)
           const handleClick = () => local.onMessageSelect(message)
 
 
           return (
           return (
@@ -35,14 +29,7 @@ export function MessageNav(
                 </Match>
                 </Match>
                 <Match when={local.size === "normal"}>
                 <Match when={local.size === "normal"}>
                   <button data-slot="message-nav-message-button" onClick={handleClick}>
                   <button data-slot="message-nav-message-button" onClick={handleClick}>
-                    <Switch>
-                      <Match when={messageWorking()}>
-                        <Spinner />
-                      </Match>
-                      <Match when={true}>
-                        <DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
-                      </Match>
-                    </Switch>
+                    <DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
                     <div
                     <div
                       data-slot="message-nav-title-preview"
                       data-slot="message-nav-title-preview"
                       data-active={message.id === local.current?.id || undefined}
                       data-active={message.id === local.current?.id || undefined}
@@ -64,7 +51,7 @@ export function MessageNav(
   return (
   return (
     <Switch>
     <Switch>
       <Match when={local.size === "compact"}>
       <Match when={local.size === "compact"}>
-        <Tooltip openDelay={0} closeDelay={300} placement="left-start" gutter={-65} shift={-16} overlap>
+        <Tooltip openDelay={0} closeDelay={300} placement="right-start" gutter={-40} shift={-10} overlap>
           <Tooltip.Trigger as="div">{content()}</Tooltip.Trigger>
           <Tooltip.Trigger as="div">{content()}</Tooltip.Trigger>
           <Tooltip.Portal>
           <Tooltip.Portal>
             <Tooltip.Content data-slot="message-nav-tooltip">
             <Tooltip.Content data-slot="message-nav-tooltip">

+ 2 - 2
packages/ui/src/components/select-dialog.tsx

@@ -3,7 +3,7 @@ import { Dialog, DialogProps } from "./dialog"
 import { Icon } from "./icon"
 import { Icon } from "./icon"
 import { IconButton } from "./icon-button"
 import { IconButton } from "./icon-button"
 import { List, ListRef, ListProps } from "./list"
 import { List, ListRef, ListProps } from "./list"
-import { Input } from "./input"
+import { TextField } from "./text-field"
 
 
 interface SelectDialogProps<T>
 interface SelectDialogProps<T>
   extends Omit<ListProps<T>, "filter">,
   extends Omit<ListProps<T>, "filter">,
@@ -55,7 +55,7 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
         <div data-component="select-dialog-input">
         <div data-component="select-dialog-input">
           <div data-slot="select-dialog-input-container">
           <div data-slot="select-dialog-input-container">
             <Icon name="magnifying-glass" />
             <Icon name="magnifying-glass" />
-            <Input
+            <TextField
               ref={inputRef}
               ref={inputRef}
               autofocus
               autofocus
               variant="ghost"
               variant="ghost"

+ 1 - 12
packages/ui/src/components/session-message-rail.tsx

@@ -6,21 +6,12 @@ import "./session-message-rail.css"
 export interface SessionMessageRailProps extends ComponentProps<"div"> {
 export interface SessionMessageRailProps extends ComponentProps<"div"> {
   messages: UserMessage[]
   messages: UserMessage[]
   current?: UserMessage
   current?: UserMessage
-  working?: boolean
   wide?: boolean
   wide?: boolean
   onMessageSelect: (message: UserMessage) => void
   onMessageSelect: (message: UserMessage) => void
 }
 }
 
 
 export function SessionMessageRail(props: SessionMessageRailProps) {
 export function SessionMessageRail(props: SessionMessageRailProps) {
-  const [local, others] = splitProps(props, [
-    "messages",
-    "current",
-    "working",
-    "wide",
-    "onMessageSelect",
-    "class",
-    "classList",
-  ])
+  const [local, others] = splitProps(props, ["messages", "current", "wide", "onMessageSelect", "class", "classList"])
 
 
   return (
   return (
     <Show when={(local.messages?.length ?? 0) > 1}>
     <Show when={(local.messages?.length ?? 0) > 1}>
@@ -39,7 +30,6 @@ export function SessionMessageRail(props: SessionMessageRailProps) {
             current={local.current}
             current={local.current}
             onMessageSelect={local.onMessageSelect}
             onMessageSelect={local.onMessageSelect}
             size="compact"
             size="compact"
-            working={local.working}
           />
           />
         </div>
         </div>
         <div data-slot="session-message-rail-full">
         <div data-slot="session-message-rail-full">
@@ -48,7 +38,6 @@ export function SessionMessageRail(props: SessionMessageRailProps) {
             current={local.current}
             current={local.current}
             onMessageSelect={local.onMessageSelect}
             onMessageSelect={local.onMessageSelect}
             size={local.wide ? "normal" : "compact"}
             size={local.wide ? "normal" : "compact"}
-            working={local.working}
           />
           />
         </div>
         </div>
       </div>
       </div>

+ 2 - 2
packages/ui/src/components/session-turn.tsx

@@ -42,10 +42,10 @@ export function SessionTurn(
   const userMessages = createMemo(() =>
   const userMessages = createMemo(() =>
     messages()
     messages()
       .filter((m) => m.role === "user")
       .filter((m) => m.role === "user")
-      .sort((a, b) => b.id.localeCompare(a.id)),
+      .sort((a, b) => a.id.localeCompare(b.id)),
   )
   )
   const lastUserMessage = createMemo(() => {
   const lastUserMessage = createMemo(() => {
-    return userMessages()?.at(0)
+    return userMessages()?.at(-1)
   })
   })
   const message = createMemo(() => userMessages()?.find((m) => m.id === props.messageID))
   const message = createMemo(() => userMessages()?.find((m) => m.id === props.messageID))
 
 

+ 44 - 18
packages/ui/src/components/input.css → packages/ui/src/components/text-field.css

@@ -40,6 +40,37 @@
       letter-spacing: var(--letter-spacing-normal);
       letter-spacing: var(--letter-spacing-normal);
     }
     }
 
 
+    [data-slot="input-wrapper"] {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      width: 100%;
+      padding-right: 4px;
+
+      border-radius: var(--radius-md);
+      border: 1px solid var(--border-weak-base);
+      background: var(--input-base);
+
+      &:focus-within {
+        /* border/shadow-xs/select */
+        box-shadow:
+          0 0 0 3px var(--border-weak-selected),
+          0 0 0 1px var(--border-selected),
+          0 1px 2px -1px rgba(19, 16, 16, 0.25),
+          0 1px 2px 0 rgba(19, 16, 16, 0.08),
+          0 1px 3px 0 rgba(19, 16, 16, 0.12);
+      }
+
+      &:has([data-invalid]) {
+        background: var(--surface-critical-weak);
+        border: 1px solid var(--border-critical-selected);
+      }
+
+      &:not(:has([data-slot="input-copy-button"])) {
+        padding-right: 0;
+      }
+    }
+
     [data-slot="input-input"] {
     [data-slot="input-input"] {
       color: var(--text-strong);
       color: var(--text-strong);
 
 
@@ -47,12 +78,11 @@
       height: 32px;
       height: 32px;
       padding: 2px 12px;
       padding: 2px 12px;
       align-items: center;
       align-items: center;
-      gap: 8px;
-      align-self: stretch;
+      flex: 1;
+      min-width: 0;
 
 
-      border-radius: var(--radius-md);
-      border: 1px solid var(--border-weak-base);
-      background: var(--input-base);
+      background: transparent;
+      border: none;
 
 
       /* text-14-regular */
       /* text-14-regular */
       font-family: var(--font-family-sans);
       font-family: var(--font-family-sans);
@@ -64,19 +94,6 @@
 
 
       &:focus {
       &:focus {
         outline: none;
         outline: none;
-
-        /* border/shadow-xs/select */
-        box-shadow:
-          0 0 0 3px var(--border-weak-selected),
-          0 0 0 1px var(--border-selected),
-          0 1px 2px -1px rgba(19, 16, 16, 0.25),
-          0 1px 2px 0 rgba(19, 16, 16, 0.08),
-          0 1px 3px 0 rgba(19, 16, 16, 0.12);
-      }
-
-      &[data-invalid] {
-        background: var(--surface-critical-weak);
-        border: 1px solid var(--border-critical-selected);
       }
       }
 
 
       &::placeholder {
       &::placeholder {
@@ -84,6 +101,15 @@
       }
       }
     }
     }
 
 
+    [data-slot="input-copy-button"] {
+      flex-shrink: 0;
+      color: var(--icon-base);
+
+      &:hover {
+        color: var(--icon-strong-base);
+      }
+    }
+
     [data-slot="input-error"] {
     [data-slot="input-error"] {
       color: var(--text-on-critical-base);
       color: var(--text-on-critical-base);
 
 

+ 35 - 7
packages/ui/src/components/input.tsx → packages/ui/src/components/text-field.tsx

@@ -1,8 +1,10 @@
 import { TextField as Kobalte } from "@kobalte/core/text-field"
 import { TextField as Kobalte } from "@kobalte/core/text-field"
-import { Show, splitProps } from "solid-js"
+import { createSignal, Show, splitProps } from "solid-js"
 import type { ComponentProps } from "solid-js"
 import type { ComponentProps } from "solid-js"
+import { IconButton } from "./icon-button"
+import { Tooltip } from "./tooltip"
 
 
-export interface InputProps
+export interface TextFieldProps
   extends ComponentProps<typeof Kobalte.Input>,
   extends ComponentProps<typeof Kobalte.Input>,
     Partial<
     Partial<
       Pick<
       Pick<
@@ -20,13 +22,13 @@ export interface InputProps
     > {
     > {
   label?: string
   label?: string
   hideLabel?: boolean
   hideLabel?: boolean
-  hidden?: boolean
   description?: string
   description?: string
   error?: string
   error?: string
   variant?: "normal" | "ghost"
   variant?: "normal" | "ghost"
+  copyable?: boolean
 }
 }
 
 
-export function Input(props: InputProps) {
+export function TextField(props: TextFieldProps) {
   const [local, others] = splitProps(props, [
   const [local, others] = splitProps(props, [
     "name",
     "name",
     "defaultValue",
     "defaultValue",
@@ -39,12 +41,21 @@ export function Input(props: InputProps) {
     "readOnly",
     "readOnly",
     "class",
     "class",
     "label",
     "label",
-    "hidden",
     "hideLabel",
     "hideLabel",
     "description",
     "description",
     "error",
     "error",
     "variant",
     "variant",
+    "copyable",
   ])
   ])
+  const [copied, setCopied] = createSignal(false)
+
+  async function handleCopy() {
+    const value = local.value ?? local.defaultValue ?? ""
+    await navigator.clipboard.writeText(value)
+    setCopied(true)
+    setTimeout(() => setCopied(false), 2000)
+  }
+
   return (
   return (
     <Kobalte
     <Kobalte
       data-component="input"
       data-component="input"
@@ -57,7 +68,6 @@ export function Input(props: InputProps) {
       required={local.required}
       required={local.required}
       disabled={local.disabled}
       disabled={local.disabled}
       readOnly={local.readOnly}
       readOnly={local.readOnly}
-      style={{ height: local.hidden ? 0 : undefined }}
       validationState={local.validationState}
       validationState={local.validationState}
     >
     >
       <Show when={local.label}>
       <Show when={local.label}>
@@ -65,7 +75,20 @@ export function Input(props: InputProps) {
           {local.label}
           {local.label}
         </Kobalte.Label>
         </Kobalte.Label>
       </Show>
       </Show>
-      <Kobalte.Input {...others} data-slot="input-input" class={local.class} />
+      <div data-slot="input-wrapper">
+        <Kobalte.Input {...others} data-slot="input-input" class={local.class} />
+        <Show when={local.copyable}>
+          <Tooltip value={copied() ? "Copied" : "Copy to clipboard"} placement="top" gutter={8}>
+            <IconButton
+              type="button"
+              icon={copied() ? "check" : "copy"}
+              variant="ghost"
+              onClick={handleCopy}
+              data-slot="input-copy-button"
+            />
+          </Tooltip>
+        </Show>
+      </div>
       <Show when={local.description}>
       <Show when={local.description}>
         <Kobalte.Description data-slot="input-description">{local.description}</Kobalte.Description>
         <Kobalte.Description data-slot="input-description">{local.description}</Kobalte.Description>
       </Show>
       </Show>
@@ -73,3 +96,8 @@ export function Input(props: InputProps) {
     </Kobalte>
     </Kobalte>
   )
   )
 }
 }
+
+/** @deprecated Use TextField instead */
+export const Input = TextField
+/** @deprecated Use TextFieldProps instead */
+export type InputProps = TextFieldProps

+ 203 - 0
packages/ui/src/components/toast.css

@@ -0,0 +1,203 @@
+[data-component="toast-region"] {
+  position: fixed;
+  bottom: 32px;
+  right: 32px;
+  z-index: 1000;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  max-width: 400px;
+  width: 100%;
+  pointer-events: none;
+
+  [data-slot="toast-list"] {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    list-style: none;
+    margin: 0;
+    padding: 0;
+  }
+}
+
+[data-component="toast"] {
+  display: flex;
+  align-items: flex-start;
+  gap: 20px;
+  padding: 16px 20px;
+  pointer-events: auto;
+  transition: all 150ms ease-out;
+
+  border-radius: var(--radius-lg);
+  border: 1px solid var(--border-weak-base);
+  background: var(--surface-float-base);
+  color: var(--text-inverted-base);
+  box-shadow: var(--shadow-md);
+
+  [data-slot="toast-inner"] {
+    display: flex;
+    align-items: flex-start;
+    gap: 10px;
+  }
+
+  &[data-opened] {
+    animation: toastPopIn 150ms ease-out;
+  }
+
+  &[data-closed] {
+    animation: toastPopOut 100ms ease-in forwards;
+  }
+
+  &[data-swipe="move"] {
+    transform: translateX(var(--kb-toast-swipe-move-x));
+  }
+
+  &[data-swipe="cancel"] {
+    transform: translateX(0);
+    transition: transform 200ms ease-out;
+  }
+
+  &[data-swipe="end"] {
+    animation: toastSwipeOut 100ms ease-out forwards;
+  }
+
+  /* &[data-variant="success"] { */
+  /*   border-color: var(--color-semantic-positive); */
+  /* } */
+  /**/
+  /* &[data-variant="error"] { */
+  /*   border-color: var(--color-semantic-danger); */
+  /* } */
+  /**/
+  /* &[data-variant="loading"] { */
+  /*   border-color: var(--color-semantic-info); */
+  /* } */
+
+  [data-slot="toast-icon"] {
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    [data-component="icon"] {
+      color: rgba(253, 252, 252, 0.94);
+    }
+  }
+
+  [data-slot="toast-content"] {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 2px;
+    min-width: 0;
+  }
+
+  [data-slot="toast-title"] {
+    color: var(--text-inverted-strong);
+
+    /* text-14-medium */
+    font-family: var(--font-family-sans);
+    font-size: 14px;
+    font-style: normal;
+    font-weight: var(--font-weight-medium);
+    line-height: var(--line-height-large); /* 142.857% */
+    letter-spacing: var(--letter-spacing-normal);
+
+    margin: 0;
+  }
+
+  [data-slot="toast-description"] {
+    color: var(--text-inverted-base);
+
+    /* text-14-regular */
+    font-family: var(--font-family-sans);
+    font-size: var(--font-size-base);
+    font-style: normal;
+    font-weight: var(--font-weight-regular);
+    line-height: var(--line-height-x-large); /* 171.429% */
+    letter-spacing: var(--letter-spacing-normal);
+
+    margin: 0;
+  }
+
+  [data-slot="toast-actions"] {
+    display: flex;
+    gap: 16px;
+    margin-top: 8px;
+  }
+
+  [data-slot="toast-action"] {
+    background: none;
+    border: none;
+    padding: 0;
+    cursor: pointer;
+
+    color: var(--text-inverted-strong);
+    font-family: var(--font-family-sans);
+    font-size: var(--font-size-base);
+    font-weight: var(--font-weight-medium);
+    line-height: var(--line-height-large);
+    letter-spacing: var(--letter-spacing-normal);
+
+    &:hover {
+      text-decoration: underline;
+    }
+
+    &:last-child {
+      color: var(--text-inverted-weak);
+    }
+  }
+
+  [data-slot="toast-close-button"] {
+    flex-shrink: 0;
+  }
+
+  [data-slot="toast-progress-track"] {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 3px;
+    background-color: var(--surface-base);
+    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
+    overflow: hidden;
+  }
+
+  [data-slot="toast-progress-fill"] {
+    height: 100%;
+    width: var(--kb-toast-progress-fill-width);
+    background-color: var(--color-primary);
+    transition: width 250ms linear;
+  }
+}
+
+@keyframes toastPopIn {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes toastPopOut {
+  from {
+    opacity: 1;
+    transform: translateY(0);
+  }
+  to {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+}
+
+@keyframes toastSwipeOut {
+  from {
+    transform: translateX(var(--kb-toast-swipe-end-x));
+  }
+  to {
+    transform: translateX(100%);
+  }
+}

+ 160 - 0
packages/ui/src/components/toast.tsx

@@ -0,0 +1,160 @@
+import { Toast as Kobalte, toaster } from "@kobalte/core/toast"
+import type { ToastRootProps, ToastCloseButtonProps, ToastTitleProps, ToastDescriptionProps } from "@kobalte/core/toast"
+import type { ComponentProps, JSX } from "solid-js"
+import { Show } from "solid-js"
+import { Portal } from "solid-js/web"
+import { Icon, type IconProps } from "./icon"
+import { IconButton } from "./icon-button"
+
+export interface ToastRegionProps extends ComponentProps<typeof Kobalte.Region> {}
+
+function ToastRegion(props: ToastRegionProps) {
+  return (
+    <Portal>
+      <Kobalte.Region data-component="toast-region" {...props}>
+        <Kobalte.List data-slot="toast-list" />
+      </Kobalte.Region>
+    </Portal>
+  )
+}
+
+export interface ToastRootComponentProps extends ToastRootProps {
+  class?: string
+  classList?: ComponentProps<"li">["classList"]
+  children?: JSX.Element
+}
+
+function ToastRoot(props: ToastRootComponentProps) {
+  return (
+    <Kobalte
+      data-component="toast"
+      classList={{
+        ...(props.classList ?? {}),
+        [props.class ?? ""]: !!props.class,
+      }}
+      {...props}
+    />
+  )
+}
+
+function ToastIcon(props: { name: IconProps["name"] }) {
+  return (
+    <div data-slot="toast-icon">
+      <Icon name={props.name} />
+    </div>
+  )
+}
+
+function ToastContent(props: ComponentProps<"div">) {
+  return <div data-slot="toast-content" {...props} />
+}
+
+function ToastTitle(props: ToastTitleProps & ComponentProps<"div">) {
+  return <Kobalte.Title data-slot="toast-title" {...props} />
+}
+
+function ToastDescription(props: ToastDescriptionProps & ComponentProps<"div">) {
+  return <Kobalte.Description data-slot="toast-description" {...props} />
+}
+
+function ToastActions(props: ComponentProps<"div">) {
+  return <div data-slot="toast-actions" {...props} />
+}
+
+function ToastCloseButton(props: ToastCloseButtonProps & ComponentProps<"button">) {
+  return <Kobalte.CloseButton data-slot="toast-close-button" as={IconButton} icon="close" variant="ghost" {...props} />
+}
+
+function ToastProgressTrack(props: ComponentProps<typeof Kobalte.ProgressTrack>) {
+  return <Kobalte.ProgressTrack data-slot="toast-progress-track" {...props} />
+}
+
+function ToastProgressFill(props: ComponentProps<typeof Kobalte.ProgressFill>) {
+  return <Kobalte.ProgressFill data-slot="toast-progress-fill" {...props} />
+}
+
+export const Toast = Object.assign(ToastRoot, {
+  Region: ToastRegion,
+  Icon: ToastIcon,
+  Content: ToastContent,
+  Title: ToastTitle,
+  Description: ToastDescription,
+  Actions: ToastActions,
+  CloseButton: ToastCloseButton,
+  ProgressTrack: ToastProgressTrack,
+  ProgressFill: ToastProgressFill,
+})
+
+export { toaster }
+
+export type ToastVariant = "default" | "success" | "error" | "loading"
+
+export interface ToastAction {
+  label: string
+  onClick: () => void
+}
+
+export interface ToastOptions {
+  title?: string
+  description?: string
+  icon?: IconProps["name"]
+  variant?: ToastVariant
+  duration?: number
+  actions?: ToastAction[]
+}
+
+export function showToast(options: ToastOptions | string) {
+  const opts = typeof options === "string" ? { description: options } : options
+  return toaster.show((props) => (
+    <Toast toastId={props.toastId} duration={opts.duration} data-variant={opts.variant ?? "default"}>
+      <Show when={opts.icon}>
+        <Toast.Icon name={opts.icon!} />
+      </Show>
+      <Toast.Content>
+        <Show when={opts.title}>
+          <Toast.Title>{opts.title}</Toast.Title>
+        </Show>
+        <Show when={opts.description}>
+          <Toast.Description>{opts.description}</Toast.Description>
+        </Show>
+        <Show when={opts.actions?.length}>
+          <Toast.Actions>
+            {opts.actions!.map((action) => (
+              <button data-slot="toast-action" onClick={action.onClick}>
+                {action.label}
+              </button>
+            ))}
+          </Toast.Actions>
+        </Show>
+      </Toast.Content>
+      <Toast.CloseButton />
+    </Toast>
+  ))
+}
+
+export interface ToastPromiseOptions<T, U = unknown> {
+  loading?: JSX.Element
+  success?: (data: T) => JSX.Element
+  error?: (error: U) => JSX.Element
+}
+
+export function showPromiseToast<T, U = unknown>(
+  promise: Promise<T> | (() => Promise<T>),
+  options: ToastPromiseOptions<T, U>,
+) {
+  return toaster.promise(promise, (props) => (
+    <Toast
+      toastId={props.toastId}
+      data-variant={props.state === "pending" ? "loading" : props.state === "fulfilled" ? "success" : "error"}
+    >
+      <Toast.Content>
+        <Toast.Description>
+          {props.state === "pending" && options.loading}
+          {props.state === "fulfilled" && options.success?.(props.data!)}
+          {props.state === "rejected" && options.error?.(props.error)}
+        </Toast.Description>
+      </Toast.Content>
+      <Toast.CloseButton />
+    </Toast>
+  ))
+}

+ 1 - 0
packages/ui/src/components/tooltip.css

@@ -7,6 +7,7 @@
   max-width: 320px;
   max-width: 320px;
   border-radius: var(--radius-md);
   border-radius: var(--radius-md);
   background-color: var(--surface-float-base);
   background-color: var(--surface-float-base);
+  color: var(--text-inverted-base);
   color: rgba(253, 252, 252, 0.94);
   color: rgba(253, 252, 252, 0.94);
   padding: 2px 8px;
   padding: 2px 8px;
   border: 0.5px solid rgba(253, 252, 252, 0.2);
   border: 0.5px solid rgba(253, 252, 252, 0.2);

+ 2 - 2
packages/ui/src/hooks/use-filtered-list.tsx

@@ -5,7 +5,7 @@ import { createStore } from "solid-js/store"
 import { createList } from "solid-list"
 import { createList } from "solid-list"
 
 
 export interface FilteredListProps<T> {
 export interface FilteredListProps<T> {
-  items: T[] | ((filter: string) => Promise<T[]>)
+  items: (filter: string) => T[] | Promise<T[]>
   key: (item: T) => string
   key: (item: T) => string
   filterKeys?: string[]
   filterKeys?: string[]
   current?: T
   current?: T
@@ -22,7 +22,7 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
     () => store.filter,
     () => store.filter,
     async (filter) => {
     async (filter) => {
       const needle = filter?.toLowerCase()
       const needle = filter?.toLowerCase()
-      const all = (typeof props.items === "function" ? await props.items(needle) : props.items) || []
+      const all = (await props.items(needle)) || []
       const result = pipe(
       const result = pipe(
         all,
         all,
         (x) => {
         (x) => {

+ 2 - 1
packages/ui/src/styles/index.css

@@ -21,7 +21,7 @@
 @import "../components/provider-icon.css" layer(components);
 @import "../components/provider-icon.css" layer(components);
 @import "../components/icon.css" layer(components);
 @import "../components/icon.css" layer(components);
 @import "../components/icon-button.css" layer(components);
 @import "../components/icon-button.css" layer(components);
-@import "../components/input.css" layer(components);
+@import "../components/text-field.css" layer(components);
 @import "../components/list.css" layer(components);
 @import "../components/list.css" layer(components);
 @import "../components/logo.css" layer(components);
 @import "../components/logo.css" layer(components);
 @import "../components/markdown.css" layer(components);
 @import "../components/markdown.css" layer(components);
@@ -38,6 +38,7 @@
 @import "../components/sticky-accordion-header.css" layer(components);
 @import "../components/sticky-accordion-header.css" layer(components);
 @import "../components/tabs.css" layer(components);
 @import "../components/tabs.css" layer(components);
 @import "../components/tag.css" layer(components);
 @import "../components/tag.css" layer(components);
+@import "../components/toast.css" layer(components);
 @import "../components/tooltip.css" layer(components);
 @import "../components/tooltip.css" layer(components);
 @import "../components/typewriter.css" layer(components);
 @import "../components/typewriter.css" layer(components);
 
 

+ 1 - 1
packages/util/package.json

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

+ 1 - 1
packages/web/package.json

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

+ 7 - 5
packages/web/src/content/docs/ecosystem.mdx

@@ -25,16 +25,18 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
 | [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth)               | Use Antigravity's free models instead of API billing                  |
 | [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth)               | Use Antigravity's free models instead of API billing                  |
 | [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs                 |
 | [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs                 |
 | [opencode-wakatime](https://github.com/angristan/opencode-wakatime)                               | Track OpenCode usage with Wakatime                                    |
 | [opencode-wakatime](https://github.com/angristan/opencode-wakatime)                               | Track OpenCode usage with Wakatime                                    |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main)   | Clean up markdown tables produced by LLMs                             |
 
 
 ---
 ---
 
 
 ## Projects
 ## Projects
 
 
-| Name                                                          | Description                                                |
-| ------------------------------------------------------------- | ---------------------------------------------------------- |
-| [kimaki](https://github.com/remorses/kimaki)                  | Discord bot to control OpenCode sessions, built on the SDK |
-| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Neovim plugin for editor-aware prompts, built on the API   |
-| [portal](https://github.com/hosenur/portal)                   | Mobile-first web UI for OpenCode over Tailscale/VPN        |
+| Name                                                                              | Description                                                |
+| --------------------------------------------------------------------------------- | ---------------------------------------------------------- |
+| kimaki (https://github.com/remorses/kimaki)                                       | Discord bot to control OpenCode sessions, built on the SDK |
+| opencode.nvim (https://github.com/NickvanDyke/opencode.nvim)                      | Neovim plugin for editor-aware prompts, built on the API   |
+| portal (https://github.com/hosenur/portal)                                        | Mobile-first web UI for OpenCode over Tailscale/VPN        |
+| opencode plugin template (https://github.com/zenobi-us/opencode-plugin-template/) | Template for building OpenCode plugins                     |
 
 
 ---
 ---
 
 

+ 18 - 18
packages/web/src/content/docs/github.mdx

@@ -1,17 +1,17 @@
 ---
 ---
 title: GitHub
 title: GitHub
-description: Use opencode in GitHub issues and pull-requests.
+description: Use OpenCode in GitHub issues and pull-requests.
 ---
 ---
 
 
-opencode integrates with your GitHub workflow. Mention `/opencode` or `/oc` in your comment, and opencode will execute tasks within your GitHub Actions runner.
+OpenCode integrates with your GitHub workflow. Mention `/opencode` or `/oc` in your comment, and OpenCode will execute tasks within your GitHub Actions runner.
 
 
 ---
 ---
 
 
 ## Features
 ## Features
 
 
-- **Triage issues**: Ask opencode to look into an issue and explain it to you.
-- **Fix and implement**: Ask opencode to fix an issue or implement a feature. And it will work in a new branch and submits a PR with all the changes.
-- **Secure**: opencode runs inside your GitHub's runners.
+- **Triage issues**: Ask OpenCode to look into an issue and explain it to you.
+- **Fix and implement**: Ask OpenCode to fix an issue or implement a feature. And it will work in a new branch and submits a PR with all the changes.
+- **Secure**: OpenCode runs inside your GitHub's runners.
 
 
 ---
 ---
 
 
@@ -62,7 +62,7 @@ Or you can set it up manually.
            with:
            with:
              fetch-depth: 1
              fetch-depth: 1
 
 
-         - name: Run opencode
+         - name: Run OpenCode
            uses: sst/opencode/github@latest
            uses: sst/opencode/github@latest
            env:
            env:
              ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
              ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -80,12 +80,12 @@ Or you can set it up manually.
 
 
 ## Configuration
 ## Configuration
 
 
-- `model`: The model to use with opencode. Takes the format of `provider/model`. This is **required**.
-- `share`: Whether to share the opencode session. Defaults to **true** for public repositories.
-- `prompt`: Optional custom prompt to override the default behavior. Use this to customize how opencode processes requests.
-- `token`: Optional GitHub access token for performing operations such as creating comments, committing changes, and opening pull requests. By default, opencode uses the installation access token from the opencode GitHub App, so commits, comments, and pull requests appear as coming from the app.
+- `model`: The model to use with OpenCode. Takes the format of `provider/model`. This is **required**.
+- `share`: Whether to share the OpenCode session. Defaults to **true** for public repositories.
+- `prompt`: Optional custom prompt to override the default behavior. Use this to customize how OpenCode processes requests.
+- `token`: Optional GitHub access token for performing operations such as creating comments, committing changes, and opening pull requests. By default, OpenCode uses the installation access token from the OpenCode GitHub App, so commits, comments, and pull requests appear as coming from the app.
 
 
-  Alternatively, you can use the GitHub Action runner's [built-in `GITHUB_TOKEN`](https://docs.github.com/en/actions/tutorials/authenticate-with-github_token) without installing the opencode GitHub App. Just make sure to grant the required permissions in your workflow:
+  Alternatively, you can use the GitHub Action runner's [built-in `GITHUB_TOKEN`](https://docs.github.com/en/actions/tutorials/authenticate-with-github_token) without installing the OpenCode GitHub App. Just make sure to grant the required permissions in your workflow:
 
 
   ```yaml
   ```yaml
   permissions:
   permissions:
@@ -101,7 +101,7 @@ Or you can set it up manually.
 
 
 ## Custom prompts
 ## Custom prompts
 
 
-Override the default prompt to customize opencode's behavior for your workflow.
+Override the default prompt to customize OpenCode's behavior for your workflow.
 
 
 ```yaml title=".github/workflows/opencode.yml"
 ```yaml title=".github/workflows/opencode.yml"
 - uses: sst/opencode/github@latest
 - uses: sst/opencode/github@latest
@@ -120,7 +120,7 @@ This is useful for enforcing specific review criteria, coding standards, or focu
 
 
 ## Examples
 ## Examples
 
 
-Here are some examples of how you can use opencode in GitHub.
+Here are some examples of how you can use OpenCode in GitHub.
 
 
 - **Explain an issue**
 - **Explain an issue**
 
 
@@ -130,7 +130,7 @@ Here are some examples of how you can use opencode in GitHub.
   /opencode explain this issue
   /opencode explain this issue
   ```
   ```
 
 
-  opencode will read the entire thread, including all comments, and reply with a clear explanation.
+  OpenCode will read the entire thread, including all comments, and reply with a clear explanation.
 
 
 - **Fix an issue**
 - **Fix an issue**
 
 
@@ -140,7 +140,7 @@ Here are some examples of how you can use opencode in GitHub.
   /opencode fix this
   /opencode fix this
   ```
   ```
 
 
-  And opencode will create a new branch, implement the changes, and open a PR with the changes.
+  And OpenCode will create a new branch, implement the changes, and open a PR with the changes.
 
 
 - **Review PRs and make changes**
 - **Review PRs and make changes**
 
 
@@ -150,18 +150,18 @@ Here are some examples of how you can use opencode in GitHub.
   Delete the attachment from S3 when the note is removed /oc
   Delete the attachment from S3 when the note is removed /oc
   ```
   ```
 
 
-  opencode will implement the requested change and commit it to the same PR.
+  OpenCode will implement the requested change and commit it to the same PR.
 
 
 - **Review specific code lines**
 - **Review specific code lines**
 
 
-  Leave a comment directly on code lines in the PR's "Files" tab. opencode automatically detects the file, line numbers, and diff context to provide precise responses.
+  Leave a comment directly on code lines in the PR's "Files" tab. OpenCode automatically detects the file, line numbers, and diff context to provide precise responses.
 
 
   ```
   ```
   [Comment on specific lines in Files tab]
   [Comment on specific lines in Files tab]
   /oc add error handling here
   /oc add error handling here
   ```
   ```
 
 
-  When commenting on specific lines, opencode receives:
+  When commenting on specific lines, OpenCode receives:
   - The exact file being reviewed
   - The exact file being reviewed
   - The specific lines of code
   - The specific lines of code
   - The surrounding diff context
   - The surrounding diff context

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio