Переглянути джерело

Merge branch 'dev' into llm-centralization

Dax Raad 4 місяців тому
батько
коміт
cf83f59dc6
100 змінених файлів з 3844 додано та 1105 видалено
  1. 1 0
      .github/workflows/opencode.yml
  2. 15 10
      .github/workflows/publish.yml
  3. 2 2
      .github/workflows/sync-zed-extension.yml
  4. 1 1
      .opencode/opencode.jsonc
  5. 2 2
      README.md
  6. 167 165
      STATS.md
  7. 52 29
      bun.lock
  8. 19 0
      github/action.yml
  9. 3 3
      infra/enterprise.ts
  10. 6 0
      infra/stage.ts
  11. 1 1
      nix/hashes.json
  12. 1 1
      package.json
  13. 1 1
      packages/console/app/package.json
  14. 3 1
      packages/console/app/src/app.tsx
  15. BIN
      packages/console/app/src/asset/lander/desktop-app-icon.png
  16. BIN
      packages/console/app/src/asset/lander/opencode-desktop-icon.png
  17. BIN
      packages/console/app/src/asset/lander/opencode-min.mp4
  18. 1 4
      packages/console/app/src/component/email-signup.tsx
  19. 8 1
      packages/console/app/src/component/header.tsx
  20. 4 4
      packages/console/app/src/config.ts
  21. 26 1
      packages/console/app/src/routes/brand/index.css
  22. 751 0
      packages/console/app/src/routes/download/index.css
  23. 171 0
      packages/console/app/src/routes/download/index.tsx
  24. 27 2
      packages/console/app/src/routes/enterprise/index.css
  25. 117 9
      packages/console/app/src/routes/index.css
  26. 49 32
      packages/console/app/src/routes/index.tsx
  27. 20 0
      packages/console/app/src/routes/t/[...path].tsx
  28. 28 3
      packages/console/app/src/routes/zen/index.css
  29. 1 1
      packages/console/app/src/routes/zen/index.tsx
  30. 1 1
      packages/console/app/src/routes/zen/util/handler.ts
  31. 2 1
      packages/console/app/src/style/token/font.css
  32. 1 1
      packages/console/core/package.json
  33. 1 1
      packages/console/function/package.json
  34. 1 1
      packages/console/mail/package.json
  35. 1 1
      packages/desktop/package.json
  36. 7 1
      packages/desktop/src/app.tsx
  37. 17 0
      packages/desktop/src/components/link.tsx
  38. 224 53
      packages/desktop/src/components/prompt-input.tsx
  39. 99 79
      packages/desktop/src/context/global-sync.tsx
  40. 126 18
      packages/desktop/src/context/layout.tsx
  41. 29 8
      packages/desktop/src/context/local.tsx
  42. 3 3
      packages/desktop/src/context/session.tsx
  43. 9 26
      packages/desktop/src/context/sync.tsx
  44. 31 0
      packages/desktop/src/hooks/use-providers.ts
  45. 2 2
      packages/desktop/src/pages/home.tsx
  46. 530 21
      packages/desktop/src/pages/layout.tsx
  47. 0 1
      packages/desktop/src/pages/session.tsx
  48. 2 2
      packages/desktop/tsconfig.json
  49. 2 1
      packages/enterprise/package.json
  50. 0 2
      packages/enterprise/src/entry-server.tsx
  51. 3 0
      packages/enterprise/src/routes/index.tsx
  52. 258 213
      packages/enterprise/src/routes/share/[shareID].tsx
  53. 8 1
      packages/enterprise/vite.config.ts
  54. 6 6
      packages/extensions/zed/extension.toml
  55. 1 1
      packages/function/package.json
  56. 1 1
      packages/opencode/Dockerfile
  57. 4 4
      packages/opencode/package.json
  58. 238 2
      packages/opencode/src/acp/agent.ts
  59. 31 0
      packages/opencode/src/acp/session.ts
  60. 8 9
      packages/opencode/src/bus/index.ts
  61. 158 136
      packages/opencode/src/cli/cmd/auth.ts
  62. 46 22
      packages/opencode/src/cli/cmd/github.ts
  63. 38 18
      packages/opencode/src/cli/cmd/tui/app.tsx
  64. 1 1
      packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
  65. 4 2
      packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
  66. 6 0
      packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
  67. 1 1
      packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
  68. 93 35
      packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
  69. 2 1
      packages/opencode/src/cli/cmd/tui/context/directory.ts
  70. 4 14
      packages/opencode/src/cli/cmd/tui/context/keybind.tsx
  71. 4 0
      packages/opencode/src/cli/cmd/tui/context/sync.tsx
  72. 16 16
      packages/opencode/src/cli/cmd/tui/context/theme/orng.json
  73. 10 2
      packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx
  74. 3 3
      packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
  75. 11 6
      packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
  76. 5 3
      packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
  77. 3 1
      packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx
  78. 3 1
      packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx
  79. 3 1
      packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx
  80. 3 1
      packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx
  81. 9 1
      packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
  82. 80 3
      packages/opencode/src/config/config.ts
  83. 3 0
      packages/opencode/src/flag/flag.ts
  84. 2 1
      packages/opencode/src/installation/index.ts
  85. 7 6
      packages/opencode/src/lsp/client.ts
  86. 2 1
      packages/opencode/src/lsp/index.ts
  87. 4 8
      packages/opencode/src/plugin/index.ts
  88. 10 0
      packages/opencode/src/project/instance.ts
  89. 1 1
      packages/opencode/src/project/project.ts
  90. 1 0
      packages/opencode/src/provider/models.ts
  91. 12 0
      packages/opencode/src/provider/provider.ts
  92. 30 32
      packages/opencode/src/provider/transform.ts
  93. 50 5
      packages/opencode/src/server/server.ts
  94. 1 1
      packages/opencode/src/session/compaction.ts
  95. 3 23
      packages/opencode/src/session/index.ts
  96. 38 6
      packages/opencode/src/session/prompt.ts
  97. 2 2
      packages/opencode/src/session/summary.ts
  98. 7 8
      packages/opencode/src/share/share-next.ts
  99. 2 2
      packages/opencode/src/tool/bash.ts
  100. 43 8
      packages/opencode/src/tool/bash.txt

+ 1 - 0
.github/workflows/opencode.yml

@@ -29,5 +29,6 @@ jobs:
         uses: sst/opencode/github@latest
         env:
           OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
+          OPENCODE_PERMISSION: '{"bash": "deny"}'
         with:
           model: opencode/claude-haiku-4-5

+ 15 - 10
.github/workflows/publish.yml

@@ -55,7 +55,7 @@ jobs:
 
       - name: Install OpenCode
         if: inputs.bump || inputs.version
-        run: curl -fsSL https://opencode.ai/install | bash
+        run: bun i -g [email protected]
 
       - name: Login to GitHub Container Registry
         uses: docker/login-action@v3
@@ -70,8 +70,8 @@ jobs:
           registry-url: "https://registry.npmjs.org"
 
       - name: Publish
-        run: |
-          ./script/publish.ts
+        id: publish
+        run: ./script/publish.ts
         env:
           OPENCODE_BUMP: ${{ inputs.bump }}
           OPENCODE_VERSION: ${{ inputs.version }}
@@ -79,9 +79,12 @@ jobs:
           AUR_KEY: ${{ secrets.AUR_KEY }}
           GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
           NPM_CONFIG_PROVENANCE: false
+    outputs:
+      releaseId: ${{ steps.publish.outputs.releaseId }}
+      tagName: ${{ steps.publish.outputs.tagName }}
 
   publish-tauri:
-    if: false # inputs.bump || inputs.version
+    needs: publish
     continue-on-error: true
     strategy:
       fail-fast: false
@@ -91,9 +94,9 @@ jobs:
             target: x86_64-apple-darwin
           - host: macos-latest
             target: aarch64-apple-darwin
-          - host: windows-latest
+          - host: blacksmith-4vcpu-windows-2025
             target: x86_64-pc-windows-msvc
-          - host: ubuntu-24.04
+          - host: blacksmith-4vcpu-ubuntu-2404
             target: x86_64-unknown-linux-gnu
     runs-on: ${{ matrix.settings.host }}
     steps:
@@ -126,7 +129,7 @@ jobs:
       - uses: ./.github/actions/setup-bun
 
       - name: install dependencies (ubuntu only)
-        if: startsWith(matrix.settings.host, 'ubuntu')
+        if: contains(matrix.settings.host, 'ubuntu')
         run: |
           sudo apt-get update
           sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
@@ -158,7 +161,7 @@ jobs:
 
       # Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
       - run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage
-        if: startsWith(matrix.settings.host, 'ubuntu')
+        if: contains(matrix.settings.host, 'ubuntu')
 
       - name: Build and upload artifacts
         uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
@@ -176,7 +179,9 @@ jobs:
         with:
           projectPath: packages/tauri
           uploadWorkflowArtifacts: true
-          tauriScript: ${{ (startsWith(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
+          tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
           args: --target ${{ matrix.settings.target }}
           updaterJsonPreferNsis: true
-          # releaseId: TODO
+          releaseId: ${{ needs.publish.outputs.releaseId }}
+          tagName: ${{ needs.publish.outputs.tagName }}
+          releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]

+ 2 - 2
.github/workflows/sync-zed-extension.yml

@@ -2,8 +2,8 @@ name: "sync-zed-extension"
 
 on:
   workflow_dispatch:
-  release:
-    types: [published]
+  # release:
+  #   types: [published]
 
 jobs:
   zed:

+ 1 - 1
.opencode/opencode.jsonc

@@ -1,6 +1,6 @@
 {
   "$schema": "https://opencode.ai/config.json",
-  "plugin": ["opencode-openai-codex-auth"],
+  // "plugin": ["opencode-openai-codex-auth"],
   // "enterprise": {
   //   "url": "https://enterprise.dev.opencode.ai",
   // },

+ 2 - 2
README.md

@@ -7,7 +7,7 @@
     </picture>
   </a>
 </p>
-<p align="center">The AI coding agent built for the terminal.</p>
+<p align="center">The open source AI coding agent.</p>
 <p align="center">
   <a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
   <a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
@@ -30,7 +30,7 @@ scoop bucket add extras; scoop install extras/opencode  # Windows
 choco install opencode             # Windows
 brew install opencode              # macOS and Linux
 paru -S opencode-bin               # Arch Linux
-mise use --pin -g ubi:sst/opencode # Any OS
+mise use -g ubi:sst/opencode # Any OS
 nix run nixpkgs#opencode           # or github:sst/opencode for latest dev branch
 ```
 

+ 167 - 165
STATS.md

@@ -1,167 +1,169 @@
 # Download Stats
 
-| Date       | GitHub Downloads    | npm Downloads     | Total               |
-| ---------- | ------------------- | ----------------- | ------------------- |
-| 2025-06-29 | 18,789 (+0)         | 39,420 (+0)       | 58,209 (+0)         |
-| 2025-06-30 | 20,127 (+1,338)     | 41,059 (+1,639)   | 61,186 (+2,977)     |
-| 2025-07-01 | 22,108 (+1,981)     | 43,745 (+2,686)   | 65,853 (+4,667)     |
-| 2025-07-02 | 24,814 (+2,706)     | 46,168 (+2,423)   | 70,982 (+5,129)     |
-| 2025-07-03 | 27,834 (+3,020)     | 49,955 (+3,787)   | 77,789 (+6,807)     |
-| 2025-07-04 | 30,608 (+2,774)     | 54,758 (+4,803)   | 85,366 (+7,577)     |
-| 2025-07-05 | 32,524 (+1,916)     | 58,371 (+3,613)   | 90,895 (+5,529)     |
-| 2025-07-06 | 33,766 (+1,242)     | 59,694 (+1,323)   | 93,460 (+2,565)     |
-| 2025-07-08 | 38,052 (+4,286)     | 64,468 (+4,774)   | 102,520 (+9,060)    |
-| 2025-07-09 | 40,924 (+2,872)     | 67,935 (+3,467)   | 108,859 (+6,339)    |
-| 2025-07-10 | 43,796 (+2,872)     | 71,402 (+3,467)   | 115,198 (+6,339)    |
-| 2025-07-11 | 46,982 (+3,186)     | 77,462 (+6,060)   | 124,444 (+9,246)    |
-| 2025-07-12 | 49,302 (+2,320)     | 82,177 (+4,715)   | 131,479 (+7,035)    |
-| 2025-07-13 | 50,803 (+1,501)     | 86,394 (+4,217)   | 137,197 (+5,718)    |
-| 2025-07-14 | 53,283 (+2,480)     | 87,860 (+1,466)   | 141,143 (+3,946)    |
-| 2025-07-15 | 57,590 (+4,307)     | 91,036 (+3,176)   | 148,626 (+7,483)    |
-| 2025-07-16 | 62,313 (+4,723)     | 95,258 (+4,222)   | 157,571 (+8,945)    |
-| 2025-07-17 | 66,684 (+4,371)     | 100,048 (+4,790)  | 166,732 (+9,161)    |
-| 2025-07-18 | 70,379 (+3,695)     | 102,587 (+2,539)  | 172,966 (+6,234)    |
-| 2025-07-19 | 73,497 (+3,117)     | 105,904 (+3,317)  | 179,401 (+6,434)    |
-| 2025-07-20 | 76,453 (+2,956)     | 109,044 (+3,140)  | 185,497 (+6,096)    |
-| 2025-07-21 | 80,197 (+3,744)     | 113,537 (+4,493)  | 193,734 (+8,237)    |
-| 2025-07-22 | 84,251 (+4,054)     | 118,073 (+4,536)  | 202,324 (+8,590)    |
-| 2025-07-23 | 88,589 (+4,338)     | 121,436 (+3,363)  | 210,025 (+7,701)    |
-| 2025-07-24 | 92,469 (+3,880)     | 124,091 (+2,655)  | 216,560 (+6,535)    |
-| 2025-07-25 | 96,417 (+3,948)     | 126,985 (+2,894)  | 223,402 (+6,842)    |
-| 2025-07-26 | 100,646 (+4,229)    | 131,411 (+4,426)  | 232,057 (+8,655)    |
-| 2025-07-27 | 102,644 (+1,998)    | 134,736 (+3,325)  | 237,380 (+5,323)    |
-| 2025-07-28 | 105,446 (+2,802)    | 136,016 (+1,280)  | 241,462 (+4,082)    |
-| 2025-07-29 | 108,998 (+3,552)    | 137,542 (+1,526)  | 246,540 (+5,078)    |
-| 2025-07-30 | 113,544 (+4,546)    | 140,317 (+2,775)  | 253,861 (+7,321)    |
-| 2025-07-31 | 118,339 (+4,795)    | 143,344 (+3,027)  | 261,683 (+7,822)    |
-| 2025-08-01 | 123,539 (+5,200)    | 146,680 (+3,336)  | 270,219 (+8,536)    |
-| 2025-08-02 | 127,864 (+4,325)    | 149,236 (+2,556)  | 277,100 (+6,881)    |
-| 2025-08-03 | 131,397 (+3,533)    | 150,451 (+1,215)  | 281,848 (+4,748)    |
-| 2025-08-04 | 136,266 (+4,869)    | 153,260 (+2,809)  | 289,526 (+7,678)    |
-| 2025-08-05 | 141,596 (+5,330)    | 155,752 (+2,492)  | 297,348 (+7,822)    |
-| 2025-08-06 | 147,067 (+5,471)    | 158,309 (+2,557)  | 305,376 (+8,028)    |
-| 2025-08-07 | 152,591 (+5,524)    | 160,889 (+2,580)  | 313,480 (+8,104)    |
-| 2025-08-08 | 158,187 (+5,596)    | 163,448 (+2,559)  | 321,635 (+8,155)    |
-| 2025-08-09 | 162,770 (+4,583)    | 165,721 (+2,273)  | 328,491 (+6,856)    |
-| 2025-08-10 | 165,695 (+2,925)    | 167,109 (+1,388)  | 332,804 (+4,313)    |
-| 2025-08-11 | 169,297 (+3,602)    | 167,953 (+844)    | 337,250 (+4,446)    |
-| 2025-08-12 | 176,307 (+7,010)    | 171,876 (+3,923)  | 348,183 (+10,933)   |
-| 2025-08-13 | 182,997 (+6,690)    | 177,182 (+5,306)  | 360,179 (+11,996)   |
-| 2025-08-14 | 189,063 (+6,066)    | 179,741 (+2,559)  | 368,804 (+8,625)    |
-| 2025-08-15 | 193,608 (+4,545)    | 181,792 (+2,051)  | 375,400 (+6,596)    |
-| 2025-08-16 | 198,118 (+4,510)    | 184,558 (+2,766)  | 382,676 (+7,276)    |
-| 2025-08-17 | 201,299 (+3,181)    | 186,269 (+1,711)  | 387,568 (+4,892)    |
-| 2025-08-18 | 204,559 (+3,260)    | 187,399 (+1,130)  | 391,958 (+4,390)    |
-| 2025-08-19 | 209,814 (+5,255)    | 189,668 (+2,269)  | 399,482 (+7,524)    |
-| 2025-08-20 | 214,497 (+4,683)    | 191,481 (+1,813)  | 405,978 (+6,496)    |
-| 2025-08-21 | 220,465 (+5,968)    | 194,784 (+3,303)  | 415,249 (+9,271)    |
-| 2025-08-22 | 225,899 (+5,434)    | 197,204 (+2,420)  | 423,103 (+7,854)    |
-| 2025-08-23 | 229,005 (+3,106)    | 199,238 (+2,034)  | 428,243 (+5,140)    |
-| 2025-08-24 | 232,098 (+3,093)    | 201,157 (+1,919)  | 433,255 (+5,012)    |
-| 2025-08-25 | 236,607 (+4,509)    | 202,650 (+1,493)  | 439,257 (+6,002)    |
-| 2025-08-26 | 242,783 (+6,176)    | 205,242 (+2,592)  | 448,025 (+8,768)    |
-| 2025-08-27 | 248,409 (+5,626)    | 205,242 (+0)      | 453,651 (+5,626)    |
-| 2025-08-28 | 252,796 (+4,387)    | 205,242 (+0)      | 458,038 (+4,387)    |
-| 2025-08-29 | 256,045 (+3,249)    | 211,075 (+5,833)  | 467,120 (+9,082)    |
-| 2025-08-30 | 258,863 (+2,818)    | 212,397 (+1,322)  | 471,260 (+4,140)    |
-| 2025-08-31 | 262,004 (+3,141)    | 213,944 (+1,547)  | 475,948 (+4,688)    |
-| 2025-09-01 | 265,359 (+3,355)    | 215,115 (+1,171)  | 480,474 (+4,526)    |
-| 2025-09-02 | 270,483 (+5,124)    | 217,075 (+1,960)  | 487,558 (+7,084)    |
-| 2025-09-03 | 274,793 (+4,310)    | 219,755 (+2,680)  | 494,548 (+6,990)    |
-| 2025-09-04 | 280,430 (+5,637)    | 222,103 (+2,348)  | 502,533 (+7,985)    |
-| 2025-09-05 | 283,769 (+3,339)    | 223,793 (+1,690)  | 507,562 (+5,029)    |
-| 2025-09-06 | 286,245 (+2,476)    | 225,036 (+1,243)  | 511,281 (+3,719)    |
-| 2025-09-07 | 288,623 (+2,378)    | 225,866 (+830)    | 514,489 (+3,208)    |
-| 2025-09-08 | 293,341 (+4,718)    | 227,073 (+1,207)  | 520,414 (+5,925)    |
-| 2025-09-09 | 300,036 (+6,695)    | 229,788 (+2,715)  | 529,824 (+9,410)    |
-| 2025-09-10 | 307,287 (+7,251)    | 233,435 (+3,647)  | 540,722 (+10,898)   |
-| 2025-09-11 | 314,083 (+6,796)    | 237,356 (+3,921)  | 551,439 (+10,717)   |
-| 2025-09-12 | 321,046 (+6,963)    | 240,728 (+3,372)  | 561,774 (+10,335)   |
-| 2025-09-13 | 324,894 (+3,848)    | 245,539 (+4,811)  | 570,433 (+8,659)    |
-| 2025-09-14 | 328,876 (+3,982)    | 248,245 (+2,706)  | 577,121 (+6,688)    |
-| 2025-09-15 | 334,201 (+5,325)    | 250,983 (+2,738)  | 585,184 (+8,063)    |
-| 2025-09-16 | 342,609 (+8,408)    | 255,264 (+4,281)  | 597,873 (+12,689)   |
-| 2025-09-17 | 351,117 (+8,508)    | 260,970 (+5,706)  | 612,087 (+14,214)   |
-| 2025-09-18 | 358,717 (+7,600)    | 266,922 (+5,952)  | 625,639 (+13,552)   |
-| 2025-09-19 | 365,401 (+6,684)    | 271,859 (+4,937)  | 637,260 (+11,621)   |
-| 2025-09-20 | 372,092 (+6,691)    | 276,917 (+5,058)  | 649,009 (+11,749)   |
-| 2025-09-21 | 377,079 (+4,987)    | 280,261 (+3,344)  | 657,340 (+8,331)    |
-| 2025-09-22 | 382,492 (+5,413)    | 284,009 (+3,748)  | 666,501 (+9,161)    |
-| 2025-09-23 | 387,008 (+4,516)    | 289,129 (+5,120)  | 676,137 (+9,636)    |
-| 2025-09-24 | 393,325 (+6,317)    | 294,927 (+5,798)  | 688,252 (+12,115)   |
-| 2025-09-25 | 398,879 (+5,554)    | 301,663 (+6,736)  | 700,542 (+12,290)   |
-| 2025-09-26 | 404,334 (+5,455)    | 306,713 (+5,050)  | 711,047 (+10,505)   |
-| 2025-09-27 | 411,618 (+7,284)    | 317,763 (+11,050) | 729,381 (+18,334)   |
-| 2025-09-28 | 414,910 (+3,292)    | 322,522 (+4,759)  | 737,432 (+8,051)    |
-| 2025-09-29 | 419,919 (+5,009)    | 328,033 (+5,511)  | 747,952 (+10,520)   |
-| 2025-09-30 | 427,991 (+8,072)    | 336,472 (+8,439)  | 764,463 (+16,511)   |
-| 2025-10-01 | 433,591 (+5,600)    | 341,742 (+5,270)  | 775,333 (+10,870)   |
-| 2025-10-02 | 440,852 (+7,261)    | 348,099 (+6,357)  | 788,951 (+13,618)   |
-| 2025-10-03 | 446,829 (+5,977)    | 359,937 (+11,838) | 806,766 (+17,815)   |
-| 2025-10-04 | 452,561 (+5,732)    | 370,386 (+10,449) | 822,947 (+16,181)   |
-| 2025-10-05 | 455,559 (+2,998)    | 374,745 (+4,359)  | 830,304 (+7,357)    |
-| 2025-10-06 | 460,927 (+5,368)    | 379,489 (+4,744)  | 840,416 (+10,112)   |
-| 2025-10-07 | 467,336 (+6,409)    | 385,438 (+5,949)  | 852,774 (+12,358)   |
-| 2025-10-08 | 474,643 (+7,307)    | 394,139 (+8,701)  | 868,782 (+16,008)   |
-| 2025-10-09 | 479,203 (+4,560)    | 400,526 (+6,387)  | 879,729 (+10,947)   |
-| 2025-10-10 | 484,374 (+5,171)    | 406,015 (+5,489)  | 890,389 (+10,660)   |
-| 2025-10-11 | 488,427 (+4,053)    | 414,699 (+8,684)  | 903,126 (+12,737)   |
-| 2025-10-12 | 492,125 (+3,698)    | 418,745 (+4,046)  | 910,870 (+7,744)    |
-| 2025-10-14 | 505,130 (+13,005)   | 429,286 (+10,541) | 934,416 (+23,546)   |
-| 2025-10-15 | 512,717 (+7,587)    | 439,290 (+10,004) | 952,007 (+17,591)   |
-| 2025-10-16 | 517,719 (+5,002)    | 447,137 (+7,847)  | 964,856 (+12,849)   |
-| 2025-10-17 | 526,239 (+8,520)    | 457,467 (+10,330) | 983,706 (+18,850)   |
-| 2025-10-18 | 531,564 (+5,325)    | 465,272 (+7,805)  | 996,836 (+13,130)   |
-| 2025-10-19 | 536,209 (+4,645)    | 469,078 (+3,806)  | 1,005,287 (+8,451)  |
-| 2025-10-20 | 541,264 (+5,055)    | 472,952 (+3,874)  | 1,014,216 (+8,929)  |
-| 2025-10-21 | 548,721 (+7,457)    | 479,703 (+6,751)  | 1,028,424 (+14,208) |
-| 2025-10-22 | 557,949 (+9,228)    | 491,395 (+11,692) | 1,049,344 (+20,920) |
-| 2025-10-23 | 564,716 (+6,767)    | 498,736 (+7,341)  | 1,063,452 (+14,108) |
-| 2025-10-24 | 572,692 (+7,976)    | 506,905 (+8,169)  | 1,079,597 (+16,145) |
-| 2025-10-25 | 578,927 (+6,235)    | 516,129 (+9,224)  | 1,095,056 (+15,459) |
-| 2025-10-26 | 584,409 (+5,482)    | 521,179 (+5,050)  | 1,105,588 (+10,532) |
-| 2025-10-27 | 589,999 (+5,590)    | 526,001 (+4,822)  | 1,116,000 (+10,412) |
-| 2025-10-28 | 595,776 (+5,777)    | 532,438 (+6,437)  | 1,128,214 (+12,214) |
-| 2025-10-29 | 606,259 (+10,483)   | 542,064 (+9,626)  | 1,148,323 (+20,109) |
-| 2025-10-30 | 613,746 (+7,487)    | 542,064 (+0)      | 1,155,810 (+7,487)  |
-| 2025-10-30 | 617,846 (+4,100)    | 555,026 (+12,962) | 1,172,872 (+17,062) |
-| 2025-10-31 | 626,612 (+8,766)    | 564,579 (+9,553)  | 1,191,191 (+18,319) |
-| 2025-11-01 | 636,100 (+9,488)    | 581,806 (+17,227) | 1,217,906 (+26,715) |
-| 2025-11-02 | 644,067 (+7,967)    | 590,004 (+8,198)  | 1,234,071 (+16,165) |
-| 2025-11-03 | 653,130 (+9,063)    | 597,139 (+7,135)  | 1,250,269 (+16,198) |
-| 2025-11-04 | 663,912 (+10,782)   | 608,056 (+10,917) | 1,271,968 (+21,699) |
-| 2025-11-05 | 675,074 (+11,162)   | 619,690 (+11,634) | 1,294,764 (+22,796) |
-| 2025-11-06 | 686,252 (+11,178)   | 630,885 (+11,195) | 1,317,137 (+22,373) |
-| 2025-11-07 | 696,646 (+10,394)   | 642,146 (+11,261) | 1,338,792 (+21,655) |
-| 2025-11-08 | 706,035 (+9,389)    | 653,489 (+11,343) | 1,359,524 (+20,732) |
-| 2025-11-09 | 713,462 (+7,427)    | 660,459 (+6,970)  | 1,373,921 (+14,397) |
-| 2025-11-10 | 722,288 (+8,826)    | 668,225 (+7,766)  | 1,390,513 (+16,592) |
-| 2025-11-11 | 729,769 (+7,481)    | 677,501 (+9,276)  | 1,407,270 (+16,757) |
-| 2025-11-12 | 740,180 (+10,411)   | 686,454 (+8,953)  | 1,426,634 (+19,364) |
-| 2025-11-13 | 749,905 (+9,725)    | 696,157 (+9,703)  | 1,446,062 (+19,428) |
-| 2025-11-14 | 759,928 (+10,023)   | 705,237 (+9,080)  | 1,465,165 (+19,103) |
-| 2025-11-15 | 765,955 (+6,027)    | 712,870 (+7,633)  | 1,478,825 (+13,660) |
-| 2025-11-16 | 771,069 (+5,114)    | 716,596 (+3,726)  | 1,487,665 (+8,840)  |
-| 2025-11-17 | 780,161 (+9,092)    | 723,339 (+6,743)  | 1,503,500 (+15,835) |
-| 2025-11-18 | 791,563 (+11,402)   | 732,544 (+9,205)  | 1,524,107 (+20,607) |
-| 2025-11-19 | 804,409 (+12,846)   | 747,624 (+15,080) | 1,552,033 (+27,926) |
-| 2025-11-20 | 814,620 (+10,211)   | 757,907 (+10,283) | 1,572,527 (+20,494) |
-| 2025-11-21 | 826,309 (+11,689)   | 769,307 (+11,400) | 1,595,616 (+23,089) |
-| 2025-11-22 | 837,269 (+10,960)   | 780,996 (+11,689) | 1,618,265 (+22,649) |
-| 2025-11-23 | 846,609 (+9,340)    | 795,069 (+14,073) | 1,641,678 (+23,413) |
-| 2025-11-24 | 856,733 (+10,124)   | 804,033 (+8,964)  | 1,660,766 (+19,088) |
-| 2025-11-25 | 869,423 (+12,690)   | 817,339 (+13,306) | 1,686,762 (+25,996) |
-| 2025-11-26 | 881,414 (+11,991)   | 832,518 (+15,179) | 1,713,932 (+27,170) |
-| 2025-11-27 | 893,960 (+12,546)   | 846,180 (+13,662) | 1,740,140 (+26,208) |
-| 2025-11-28 | 901,741 (+7,781)    | 856,482 (+10,302) | 1,758,223 (+18,083) |
-| 2025-11-29 | 908,689 (+6,948)    | 863,361 (+6,879)  | 1,772,050 (+13,827) |
-| 2025-11-30 | 916,116 (+7,427)    | 870,194 (+6,833)  | 1,786,310 (+14,260) |
-| 2025-12-01 | 925,898 (+9,782)    | 876,500 (+6,306)  | 1,802,398 (+16,088) |
-| 2025-12-02 | 939,250 (+13,352)   | 890,919 (+14,419) | 1,830,169 (+27,771) |
-| 2025-12-03 | 952,249 (+12,999)   | 903,713 (+12,794) | 1,855,962 (+25,793) |
-| 2025-12-04 | 965,611 (+13,362)   | 916,471 (+12,758) | 1,882,082 (+26,120) |
-| 2025-12-05 | 977,996 (+12,385)   | 930,616 (+14,145) | 1,908,612 (+26,530) |
-| 2025-12-06 | 987,884 (+9,888)    | 943,773 (+13,157) | 1,931,657 (+23,045) |
-| 2025-12-07 | 994,046 (+6,162)    | 951,425 (+7,652)  | 1,945,471 (+13,814) |
-| 2025-12-08 | 1,000,898 (+6,852)  | 957,149 (+5,724)  | 1,958,047 (+12,576) |
-| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
+| Date       | GitHub Downloads    | npm Downloads       | Total               |
+| ---------- | ------------------- | ------------------- | ------------------- |
+| 2025-06-29 | 18,789 (+0)         | 39,420 (+0)         | 58,209 (+0)         |
+| 2025-06-30 | 20,127 (+1,338)     | 41,059 (+1,639)     | 61,186 (+2,977)     |
+| 2025-07-01 | 22,108 (+1,981)     | 43,745 (+2,686)     | 65,853 (+4,667)     |
+| 2025-07-02 | 24,814 (+2,706)     | 46,168 (+2,423)     | 70,982 (+5,129)     |
+| 2025-07-03 | 27,834 (+3,020)     | 49,955 (+3,787)     | 77,789 (+6,807)     |
+| 2025-07-04 | 30,608 (+2,774)     | 54,758 (+4,803)     | 85,366 (+7,577)     |
+| 2025-07-05 | 32,524 (+1,916)     | 58,371 (+3,613)     | 90,895 (+5,529)     |
+| 2025-07-06 | 33,766 (+1,242)     | 59,694 (+1,323)     | 93,460 (+2,565)     |
+| 2025-07-08 | 38,052 (+4,286)     | 64,468 (+4,774)     | 102,520 (+9,060)    |
+| 2025-07-09 | 40,924 (+2,872)     | 67,935 (+3,467)     | 108,859 (+6,339)    |
+| 2025-07-10 | 43,796 (+2,872)     | 71,402 (+3,467)     | 115,198 (+6,339)    |
+| 2025-07-11 | 46,982 (+3,186)     | 77,462 (+6,060)     | 124,444 (+9,246)    |
+| 2025-07-12 | 49,302 (+2,320)     | 82,177 (+4,715)     | 131,479 (+7,035)    |
+| 2025-07-13 | 50,803 (+1,501)     | 86,394 (+4,217)     | 137,197 (+5,718)    |
+| 2025-07-14 | 53,283 (+2,480)     | 87,860 (+1,466)     | 141,143 (+3,946)    |
+| 2025-07-15 | 57,590 (+4,307)     | 91,036 (+3,176)     | 148,626 (+7,483)    |
+| 2025-07-16 | 62,313 (+4,723)     | 95,258 (+4,222)     | 157,571 (+8,945)    |
+| 2025-07-17 | 66,684 (+4,371)     | 100,048 (+4,790)    | 166,732 (+9,161)    |
+| 2025-07-18 | 70,379 (+3,695)     | 102,587 (+2,539)    | 172,966 (+6,234)    |
+| 2025-07-19 | 73,497 (+3,117)     | 105,904 (+3,317)    | 179,401 (+6,434)    |
+| 2025-07-20 | 76,453 (+2,956)     | 109,044 (+3,140)    | 185,497 (+6,096)    |
+| 2025-07-21 | 80,197 (+3,744)     | 113,537 (+4,493)    | 193,734 (+8,237)    |
+| 2025-07-22 | 84,251 (+4,054)     | 118,073 (+4,536)    | 202,324 (+8,590)    |
+| 2025-07-23 | 88,589 (+4,338)     | 121,436 (+3,363)    | 210,025 (+7,701)    |
+| 2025-07-24 | 92,469 (+3,880)     | 124,091 (+2,655)    | 216,560 (+6,535)    |
+| 2025-07-25 | 96,417 (+3,948)     | 126,985 (+2,894)    | 223,402 (+6,842)    |
+| 2025-07-26 | 100,646 (+4,229)    | 131,411 (+4,426)    | 232,057 (+8,655)    |
+| 2025-07-27 | 102,644 (+1,998)    | 134,736 (+3,325)    | 237,380 (+5,323)    |
+| 2025-07-28 | 105,446 (+2,802)    | 136,016 (+1,280)    | 241,462 (+4,082)    |
+| 2025-07-29 | 108,998 (+3,552)    | 137,542 (+1,526)    | 246,540 (+5,078)    |
+| 2025-07-30 | 113,544 (+4,546)    | 140,317 (+2,775)    | 253,861 (+7,321)    |
+| 2025-07-31 | 118,339 (+4,795)    | 143,344 (+3,027)    | 261,683 (+7,822)    |
+| 2025-08-01 | 123,539 (+5,200)    | 146,680 (+3,336)    | 270,219 (+8,536)    |
+| 2025-08-02 | 127,864 (+4,325)    | 149,236 (+2,556)    | 277,100 (+6,881)    |
+| 2025-08-03 | 131,397 (+3,533)    | 150,451 (+1,215)    | 281,848 (+4,748)    |
+| 2025-08-04 | 136,266 (+4,869)    | 153,260 (+2,809)    | 289,526 (+7,678)    |
+| 2025-08-05 | 141,596 (+5,330)    | 155,752 (+2,492)    | 297,348 (+7,822)    |
+| 2025-08-06 | 147,067 (+5,471)    | 158,309 (+2,557)    | 305,376 (+8,028)    |
+| 2025-08-07 | 152,591 (+5,524)    | 160,889 (+2,580)    | 313,480 (+8,104)    |
+| 2025-08-08 | 158,187 (+5,596)    | 163,448 (+2,559)    | 321,635 (+8,155)    |
+| 2025-08-09 | 162,770 (+4,583)    | 165,721 (+2,273)    | 328,491 (+6,856)    |
+| 2025-08-10 | 165,695 (+2,925)    | 167,109 (+1,388)    | 332,804 (+4,313)    |
+| 2025-08-11 | 169,297 (+3,602)    | 167,953 (+844)      | 337,250 (+4,446)    |
+| 2025-08-12 | 176,307 (+7,010)    | 171,876 (+3,923)    | 348,183 (+10,933)   |
+| 2025-08-13 | 182,997 (+6,690)    | 177,182 (+5,306)    | 360,179 (+11,996)   |
+| 2025-08-14 | 189,063 (+6,066)    | 179,741 (+2,559)    | 368,804 (+8,625)    |
+| 2025-08-15 | 193,608 (+4,545)    | 181,792 (+2,051)    | 375,400 (+6,596)    |
+| 2025-08-16 | 198,118 (+4,510)    | 184,558 (+2,766)    | 382,676 (+7,276)    |
+| 2025-08-17 | 201,299 (+3,181)    | 186,269 (+1,711)    | 387,568 (+4,892)    |
+| 2025-08-18 | 204,559 (+3,260)    | 187,399 (+1,130)    | 391,958 (+4,390)    |
+| 2025-08-19 | 209,814 (+5,255)    | 189,668 (+2,269)    | 399,482 (+7,524)    |
+| 2025-08-20 | 214,497 (+4,683)    | 191,481 (+1,813)    | 405,978 (+6,496)    |
+| 2025-08-21 | 220,465 (+5,968)    | 194,784 (+3,303)    | 415,249 (+9,271)    |
+| 2025-08-22 | 225,899 (+5,434)    | 197,204 (+2,420)    | 423,103 (+7,854)    |
+| 2025-08-23 | 229,005 (+3,106)    | 199,238 (+2,034)    | 428,243 (+5,140)    |
+| 2025-08-24 | 232,098 (+3,093)    | 201,157 (+1,919)    | 433,255 (+5,012)    |
+| 2025-08-25 | 236,607 (+4,509)    | 202,650 (+1,493)    | 439,257 (+6,002)    |
+| 2025-08-26 | 242,783 (+6,176)    | 205,242 (+2,592)    | 448,025 (+8,768)    |
+| 2025-08-27 | 248,409 (+5,626)    | 205,242 (+0)        | 453,651 (+5,626)    |
+| 2025-08-28 | 252,796 (+4,387)    | 205,242 (+0)        | 458,038 (+4,387)    |
+| 2025-08-29 | 256,045 (+3,249)    | 211,075 (+5,833)    | 467,120 (+9,082)    |
+| 2025-08-30 | 258,863 (+2,818)    | 212,397 (+1,322)    | 471,260 (+4,140)    |
+| 2025-08-31 | 262,004 (+3,141)    | 213,944 (+1,547)    | 475,948 (+4,688)    |
+| 2025-09-01 | 265,359 (+3,355)    | 215,115 (+1,171)    | 480,474 (+4,526)    |
+| 2025-09-02 | 270,483 (+5,124)    | 217,075 (+1,960)    | 487,558 (+7,084)    |
+| 2025-09-03 | 274,793 (+4,310)    | 219,755 (+2,680)    | 494,548 (+6,990)    |
+| 2025-09-04 | 280,430 (+5,637)    | 222,103 (+2,348)    | 502,533 (+7,985)    |
+| 2025-09-05 | 283,769 (+3,339)    | 223,793 (+1,690)    | 507,562 (+5,029)    |
+| 2025-09-06 | 286,245 (+2,476)    | 225,036 (+1,243)    | 511,281 (+3,719)    |
+| 2025-09-07 | 288,623 (+2,378)    | 225,866 (+830)      | 514,489 (+3,208)    |
+| 2025-09-08 | 293,341 (+4,718)    | 227,073 (+1,207)    | 520,414 (+5,925)    |
+| 2025-09-09 | 300,036 (+6,695)    | 229,788 (+2,715)    | 529,824 (+9,410)    |
+| 2025-09-10 | 307,287 (+7,251)    | 233,435 (+3,647)    | 540,722 (+10,898)   |
+| 2025-09-11 | 314,083 (+6,796)    | 237,356 (+3,921)    | 551,439 (+10,717)   |
+| 2025-09-12 | 321,046 (+6,963)    | 240,728 (+3,372)    | 561,774 (+10,335)   |
+| 2025-09-13 | 324,894 (+3,848)    | 245,539 (+4,811)    | 570,433 (+8,659)    |
+| 2025-09-14 | 328,876 (+3,982)    | 248,245 (+2,706)    | 577,121 (+6,688)    |
+| 2025-09-15 | 334,201 (+5,325)    | 250,983 (+2,738)    | 585,184 (+8,063)    |
+| 2025-09-16 | 342,609 (+8,408)    | 255,264 (+4,281)    | 597,873 (+12,689)   |
+| 2025-09-17 | 351,117 (+8,508)    | 260,970 (+5,706)    | 612,087 (+14,214)   |
+| 2025-09-18 | 358,717 (+7,600)    | 266,922 (+5,952)    | 625,639 (+13,552)   |
+| 2025-09-19 | 365,401 (+6,684)    | 271,859 (+4,937)    | 637,260 (+11,621)   |
+| 2025-09-20 | 372,092 (+6,691)    | 276,917 (+5,058)    | 649,009 (+11,749)   |
+| 2025-09-21 | 377,079 (+4,987)    | 280,261 (+3,344)    | 657,340 (+8,331)    |
+| 2025-09-22 | 382,492 (+5,413)    | 284,009 (+3,748)    | 666,501 (+9,161)    |
+| 2025-09-23 | 387,008 (+4,516)    | 289,129 (+5,120)    | 676,137 (+9,636)    |
+| 2025-09-24 | 393,325 (+6,317)    | 294,927 (+5,798)    | 688,252 (+12,115)   |
+| 2025-09-25 | 398,879 (+5,554)    | 301,663 (+6,736)    | 700,542 (+12,290)   |
+| 2025-09-26 | 404,334 (+5,455)    | 306,713 (+5,050)    | 711,047 (+10,505)   |
+| 2025-09-27 | 411,618 (+7,284)    | 317,763 (+11,050)   | 729,381 (+18,334)   |
+| 2025-09-28 | 414,910 (+3,292)    | 322,522 (+4,759)    | 737,432 (+8,051)    |
+| 2025-09-29 | 419,919 (+5,009)    | 328,033 (+5,511)    | 747,952 (+10,520)   |
+| 2025-09-30 | 427,991 (+8,072)    | 336,472 (+8,439)    | 764,463 (+16,511)   |
+| 2025-10-01 | 433,591 (+5,600)    | 341,742 (+5,270)    | 775,333 (+10,870)   |
+| 2025-10-02 | 440,852 (+7,261)    | 348,099 (+6,357)    | 788,951 (+13,618)   |
+| 2025-10-03 | 446,829 (+5,977)    | 359,937 (+11,838)   | 806,766 (+17,815)   |
+| 2025-10-04 | 452,561 (+5,732)    | 370,386 (+10,449)   | 822,947 (+16,181)   |
+| 2025-10-05 | 455,559 (+2,998)    | 374,745 (+4,359)    | 830,304 (+7,357)    |
+| 2025-10-06 | 460,927 (+5,368)    | 379,489 (+4,744)    | 840,416 (+10,112)   |
+| 2025-10-07 | 467,336 (+6,409)    | 385,438 (+5,949)    | 852,774 (+12,358)   |
+| 2025-10-08 | 474,643 (+7,307)    | 394,139 (+8,701)    | 868,782 (+16,008)   |
+| 2025-10-09 | 479,203 (+4,560)    | 400,526 (+6,387)    | 879,729 (+10,947)   |
+| 2025-10-10 | 484,374 (+5,171)    | 406,015 (+5,489)    | 890,389 (+10,660)   |
+| 2025-10-11 | 488,427 (+4,053)    | 414,699 (+8,684)    | 903,126 (+12,737)   |
+| 2025-10-12 | 492,125 (+3,698)    | 418,745 (+4,046)    | 910,870 (+7,744)    |
+| 2025-10-14 | 505,130 (+13,005)   | 429,286 (+10,541)   | 934,416 (+23,546)   |
+| 2025-10-15 | 512,717 (+7,587)    | 439,290 (+10,004)   | 952,007 (+17,591)   |
+| 2025-10-16 | 517,719 (+5,002)    | 447,137 (+7,847)    | 964,856 (+12,849)   |
+| 2025-10-17 | 526,239 (+8,520)    | 457,467 (+10,330)   | 983,706 (+18,850)   |
+| 2025-10-18 | 531,564 (+5,325)    | 465,272 (+7,805)    | 996,836 (+13,130)   |
+| 2025-10-19 | 536,209 (+4,645)    | 469,078 (+3,806)    | 1,005,287 (+8,451)  |
+| 2025-10-20 | 541,264 (+5,055)    | 472,952 (+3,874)    | 1,014,216 (+8,929)  |
+| 2025-10-21 | 548,721 (+7,457)    | 479,703 (+6,751)    | 1,028,424 (+14,208) |
+| 2025-10-22 | 557,949 (+9,228)    | 491,395 (+11,692)   | 1,049,344 (+20,920) |
+| 2025-10-23 | 564,716 (+6,767)    | 498,736 (+7,341)    | 1,063,452 (+14,108) |
+| 2025-10-24 | 572,692 (+7,976)    | 506,905 (+8,169)    | 1,079,597 (+16,145) |
+| 2025-10-25 | 578,927 (+6,235)    | 516,129 (+9,224)    | 1,095,056 (+15,459) |
+| 2025-10-26 | 584,409 (+5,482)    | 521,179 (+5,050)    | 1,105,588 (+10,532) |
+| 2025-10-27 | 589,999 (+5,590)    | 526,001 (+4,822)    | 1,116,000 (+10,412) |
+| 2025-10-28 | 595,776 (+5,777)    | 532,438 (+6,437)    | 1,128,214 (+12,214) |
+| 2025-10-29 | 606,259 (+10,483)   | 542,064 (+9,626)    | 1,148,323 (+20,109) |
+| 2025-10-30 | 613,746 (+7,487)    | 542,064 (+0)        | 1,155,810 (+7,487)  |
+| 2025-10-30 | 617,846 (+4,100)    | 555,026 (+12,962)   | 1,172,872 (+17,062) |
+| 2025-10-31 | 626,612 (+8,766)    | 564,579 (+9,553)    | 1,191,191 (+18,319) |
+| 2025-11-01 | 636,100 (+9,488)    | 581,806 (+17,227)   | 1,217,906 (+26,715) |
+| 2025-11-02 | 644,067 (+7,967)    | 590,004 (+8,198)    | 1,234,071 (+16,165) |
+| 2025-11-03 | 653,130 (+9,063)    | 597,139 (+7,135)    | 1,250,269 (+16,198) |
+| 2025-11-04 | 663,912 (+10,782)   | 608,056 (+10,917)   | 1,271,968 (+21,699) |
+| 2025-11-05 | 675,074 (+11,162)   | 619,690 (+11,634)   | 1,294,764 (+22,796) |
+| 2025-11-06 | 686,252 (+11,178)   | 630,885 (+11,195)   | 1,317,137 (+22,373) |
+| 2025-11-07 | 696,646 (+10,394)   | 642,146 (+11,261)   | 1,338,792 (+21,655) |
+| 2025-11-08 | 706,035 (+9,389)    | 653,489 (+11,343)   | 1,359,524 (+20,732) |
+| 2025-11-09 | 713,462 (+7,427)    | 660,459 (+6,970)    | 1,373,921 (+14,397) |
+| 2025-11-10 | 722,288 (+8,826)    | 668,225 (+7,766)    | 1,390,513 (+16,592) |
+| 2025-11-11 | 729,769 (+7,481)    | 677,501 (+9,276)    | 1,407,270 (+16,757) |
+| 2025-11-12 | 740,180 (+10,411)   | 686,454 (+8,953)    | 1,426,634 (+19,364) |
+| 2025-11-13 | 749,905 (+9,725)    | 696,157 (+9,703)    | 1,446,062 (+19,428) |
+| 2025-11-14 | 759,928 (+10,023)   | 705,237 (+9,080)    | 1,465,165 (+19,103) |
+| 2025-11-15 | 765,955 (+6,027)    | 712,870 (+7,633)    | 1,478,825 (+13,660) |
+| 2025-11-16 | 771,069 (+5,114)    | 716,596 (+3,726)    | 1,487,665 (+8,840)  |
+| 2025-11-17 | 780,161 (+9,092)    | 723,339 (+6,743)    | 1,503,500 (+15,835) |
+| 2025-11-18 | 791,563 (+11,402)   | 732,544 (+9,205)    | 1,524,107 (+20,607) |
+| 2025-11-19 | 804,409 (+12,846)   | 747,624 (+15,080)   | 1,552,033 (+27,926) |
+| 2025-11-20 | 814,620 (+10,211)   | 757,907 (+10,283)   | 1,572,527 (+20,494) |
+| 2025-11-21 | 826,309 (+11,689)   | 769,307 (+11,400)   | 1,595,616 (+23,089) |
+| 2025-11-22 | 837,269 (+10,960)   | 780,996 (+11,689)   | 1,618,265 (+22,649) |
+| 2025-11-23 | 846,609 (+9,340)    | 795,069 (+14,073)   | 1,641,678 (+23,413) |
+| 2025-11-24 | 856,733 (+10,124)   | 804,033 (+8,964)    | 1,660,766 (+19,088) |
+| 2025-11-25 | 869,423 (+12,690)   | 817,339 (+13,306)   | 1,686,762 (+25,996) |
+| 2025-11-26 | 881,414 (+11,991)   | 832,518 (+15,179)   | 1,713,932 (+27,170) |
+| 2025-11-27 | 893,960 (+12,546)   | 846,180 (+13,662)   | 1,740,140 (+26,208) |
+| 2025-11-28 | 901,741 (+7,781)    | 856,482 (+10,302)   | 1,758,223 (+18,083) |
+| 2025-11-29 | 908,689 (+6,948)    | 863,361 (+6,879)    | 1,772,050 (+13,827) |
+| 2025-11-30 | 916,116 (+7,427)    | 870,194 (+6,833)    | 1,786,310 (+14,260) |
+| 2025-12-01 | 925,898 (+9,782)    | 876,500 (+6,306)    | 1,802,398 (+16,088) |
+| 2025-12-02 | 939,250 (+13,352)   | 890,919 (+14,419)   | 1,830,169 (+27,771) |
+| 2025-12-03 | 952,249 (+12,999)   | 903,713 (+12,794)   | 1,855,962 (+25,793) |
+| 2025-12-04 | 965,611 (+13,362)   | 916,471 (+12,758)   | 1,882,082 (+26,120) |
+| 2025-12-05 | 977,996 (+12,385)   | 930,616 (+14,145)   | 1,908,612 (+26,530) |
+| 2025-12-06 | 987,884 (+9,888)    | 943,773 (+13,157)   | 1,931,657 (+23,045) |
+| 2025-12-07 | 994,046 (+6,162)    | 951,425 (+7,652)    | 1,945,471 (+13,814) |
+| 2025-12-08 | 1,000,898 (+6,852)  | 957,149 (+5,724)    | 1,958,047 (+12,576) |
+| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773)   | 1,985,410 (+27,363) |
+| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786)   | 2,017,599 (+32,189) |
+| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |

+ 52 - 29
bun.lock

@@ -20,7 +20,7 @@
     },
     "packages/console/app": {
       "name": "@opencode-ai/console-app",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@cloudflare/vite-plugin": "1.15.2",
         "@ibm/plex": "6.4.1",
@@ -48,7 +48,7 @@
     },
     "packages/console/core": {
       "name": "@opencode-ai/console-core",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "@jsx-email/render": "1.1.1",
@@ -75,7 +75,7 @@
     },
     "packages/console/function": {
       "name": "@opencode-ai/console-function",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
@@ -99,7 +99,7 @@
     },
     "packages/console/mail": {
       "name": "@opencode-ai/console-mail",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@jsx-email/all": "2.2.3",
         "@jsx-email/cli": "1.4.3",
@@ -123,7 +123,7 @@
     },
     "packages/desktop": {
       "name": "@opencode-ai/desktop",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -168,7 +168,7 @@
     },
     "packages/enterprise": {
       "name": "@opencode-ai/enterprise",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@opencode-ai/ui": "workspace:*",
         "@opencode-ai/util": "workspace:*",
@@ -179,6 +179,7 @@
         "aws4fetch": "^1.0.20",
         "hono": "catalog:",
         "hono-openapi": "catalog:",
+        "js-base64": "3.7.7",
         "luxon": "catalog:",
         "nitro": "3.0.1-alpha.1",
         "solid-js": "catalog:",
@@ -196,7 +197,7 @@
     },
     "packages/function": {
       "name": "@opencode-ai/function",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "22.0.0",
@@ -212,7 +213,7 @@
     },
     "packages/opencode": {
       "name": "opencode",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "bin": {
         "opencode": "./bin/opencode",
       },
@@ -241,9 +242,9 @@
         "@opencode-ai/script": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/util": "workspace:*",
-        "@openrouter/ai-sdk-provider": "1.2.8",
-        "@opentui/core": "0.1.59",
-        "@opentui/solid": "0.1.59",
+        "@openrouter/ai-sdk-provider": "1.5.2",
+        "@opentui/core": "0.0.0-20251211-4403a69a",
+        "@opentui/solid": "0.0.0-20251211-4403a69a",
         "@parcel/watcher": "2.5.1",
         "@pierre/precision-diffs": "catalog:",
         "@solid-primitives/event-bus": "1.1.2",
@@ -304,7 +305,7 @@
     },
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "zod": "catalog:",
@@ -324,7 +325,7 @@
     },
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "devDependencies": {
         "@hey-api/openapi-ts": "0.88.1",
         "@tsconfig/node22": "catalog:",
@@ -335,7 +336,7 @@
     },
     "packages/slack": {
       "name": "@opencode-ai/slack",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@slack/bolt": "^3.17.1",
@@ -348,7 +349,7 @@
     },
     "packages/tauri": {
       "name": "@opencode-ai/tauri",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@opencode-ai/desktop": "workspace:*",
         "@tauri-apps/api": "^2",
@@ -370,7 +371,7 @@
     },
     "packages/ui": {
       "name": "@opencode-ai/ui",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -402,7 +403,7 @@
     },
     "packages/util": {
       "name": "@opencode-ai/util",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "zod": "catalog:",
       },
@@ -413,7 +414,7 @@
     },
     "packages/web": {
       "name": "@opencode-ai/web",
-      "version": "1.0.141",
+      "version": "1.0.150",
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
@@ -462,7 +463,7 @@
     "@hono/zod-validator": "0.4.2",
     "@kobalte/core": "0.13.11",
     "@openauthjs/openauth": "0.0.0-20250322224806",
-    "@pierre/precision-diffs": "0.6.0-beta.10",
+    "@pierre/precision-diffs": "0.6.1",
     "@solidjs/meta": "0.29.4",
     "@solidjs/router": "0.15.4",
     "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
@@ -1141,27 +1142,27 @@
 
     "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
 
-    "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.2.8", "", { "dependencies": { "@openrouter/sdk": "^0.1.8" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-pQT8AzZBKg9f4bkt4doF486ZlhK0XjKkevrLkiqYgfh1Jplovieu28nK4Y+xy3sF18/mxjqh9/2y6jh01qzLrA=="],
+    "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.5.2", "", { "dependencies": { "@openrouter/sdk": "^0.1.27" }, "peerDependencies": { "@toon-format/toon": "^2.0.0", "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" }, "optionalPeers": ["@toon-format/toon"] }, "sha512-3Th0vmJ9pjnwcPc2H1f59Mb0LFvwaREZAScfOQIpUxAHjZ7ZawVKDP27qgsteZPmMYqccNMy4r4Y3kgUnNcKAg=="],
 
     "@openrouter/sdk": ["@openrouter/[email protected]", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="],
 
     "@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
-    "@opentui/core": ["@opentui/core@0.1.59", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.59", "@opentui/core-darwin-x64": "0.1.59", "@opentui/core-linux-arm64": "0.1.59", "@opentui/core-linux-x64": "0.1.59", "@opentui/core-win32-arm64": "0.1.59", "@opentui/core-win32-x64": "0.1.59", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-vOtEvIulvfCOWJy0EfKAPzAMtDTmC+S0boGYrefjLqIp7tp+bbVJuXVh/8bz6GQTPmbQC6MIk6bv/ij3pdUVkA=="],
+    "@opentui/core": ["@opentui/core@0.0.0-20251211-4403a69a", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-darwin-x64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-x64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-x64": "0.0.0-20251211-4403a69a", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-wTZKcokyU9yiDqyC0Pvf9eRSdT73s4Ynerkit/z8Af++tynqrTlZHZCXK3o42Ff7itCSILmijcTU94n69aEypA=="],
 
-    "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.59", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JQWq7W/wkmTujW/2/Ig0d7S+701rul87LSW5txQ+GM4o6EWchqHrELwo6jcZpczsyOEj4fXxI2O8l4OVYyMa9A=="],
+    "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VAYjTa+Eiauy8gETXadD8y0PE6ppnKasDK1X354VoexZiWFR3r7rkL+TfDfk7whhqXDYyT44JDT1QmCAhVXRzQ=="],
 
-    "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.59", "", { "os": "darwin", "cpu": "x64" }, "sha512-GzafWzMP9Lt4AzUwQAk02lxgITgfvvo33OLCN265LtQBO8w23u0eB7Fjs9W+nmtcvzXtB9q6HuA0PvP9a3OioA=="],
+    "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "x64" }, "sha512-n9oVMpsojlILj1soORZzZ2Mjh8Zl73ZNcY7ot0iRmOjBDccrjDTsqKfxoGjKNd/xJSphLeu1LYGlcI5O5OczWQ=="],
 
-    "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.59", "", { "os": "linux", "cpu": "arm64" }, "sha512-QMMFg3dr2v43g3jICgzNFYQyU4YL3zHw733MVJINC+c882+qiQ8l0utTFoVEx/iRYeBzFvMVrKZ4f6G8fFrtrw=="],
+    "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "arm64" }, "sha512-vf4eUjPMI4ANitK4MpTGenZFddKgQD/K21aN6cZjusnH3mTEJAoIR7GbNtMdz3qclU43ajpzTID9sAwhshwdVQ=="],
 
-    "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.59", "", { "os": "linux", "cpu": "x64" }, "sha512-XSblVjhW/7+Xs+/o+xJHwHn74nw9j69mnPAFiNdH0d8ilP4j09nUYHZOvQ89sHZaMYeSIuJEciHnh/qP0n5QXQ=="],
+    "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "x64" }, "sha512-61635Up0YvVJ8gZ2eMiL1c8OfA+U6wAzT++LoaurNjbmsUAlKHws6MZdqTLw7aspJJVGsRFbA6d1Y+gXFxbDrQ=="],
 
-    "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.59", "", { "os": "win32", "cpu": "arm64" }, "sha512-GU5pPUcTpYmeOUYKpQgAPx0VKBMrfz5LNZlK8gm/jlo2CbLrIW7QLMWCoxncVZmNYqYJeG+KUZkmXYe5KLPXCQ=="],
+    "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "arm64" }, "sha512-3lUddTJGKZ6uU388eU79MY//IEbgGENCITetDrrRp7v9L1AxMntE1ihf6HniziwBvKKJcsUfqLiJWcq0WPZw2w=="],
 
-    "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.59", "", { "os": "win32", "cpu": "x64" }, "sha512-InIawEI0TOG8MBBpavMq31WBRBjJ6XPuqFcsDnjqDJcXrRbNkguRW3PNXEwlyaU4tXHfYOsdlPpRtsysS8X/bQ=="],
+    "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "x64" }, "sha512-Xwc1gqYsn8UZNTzNKkigZozAhBNBGbfX2B/I/aSbyqL0h8+XIInOodI0urzJWc0B6aEv/IDiT6Rm3coXFikLIg=="],
 
-    "@opentui/solid": ["@opentui/solid@0.1.59", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.59", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-O88a/+YHkHlDC4IxbrfWD2ZWlpkpu4oXC2FCLTK8taaUAnLYoybxdrMpv1+o8u8KoWXOoZmEHdntdO9O4abHnQ=="],
+    "@opentui/solid": ["@opentui/solid@0.0.0-20251211-4403a69a", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251211-4403a69a", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-vuLppAdd1Qgaqhie3q2TuEr+8udjT4d8uVg5arvCe1AUDVs19I8kvadVCfzGUVmtXgFIOEakbiv6AxDq5v9Zig=="],
 
     "@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
 
@@ -1277,7 +1278,7 @@
 
     "@petamoriken/float16": ["@petamoriken/[email protected]", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
 
-    "@pierre/precision-diffs": ["@pierre/[email protected].0-beta.10", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-2rdd1Q1xJbB0Z4oUbm0Ybrr2gLFEdvNetZLadJboZSFL7Q4gFujdQZfXfV3vB9X+esjt++v0nzb3mioW25BOTA=="],
+    "@pierre/precision-diffs": ["@pierre/[email protected]", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-HXafRSOly6B0rRt6fuP0yy1MimHJMQ2NNnBGcIHhHwsgK4WWs+SBWRWt1usdgz0NIuSgXdIyQn8HY3F1jKyDBQ=="],
 
     "@pkgjs/parseargs": ["@pkgjs/[email protected]", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
 
@@ -4277,6 +4278,10 @@
 
     "openid-client/jose": ["[email protected]", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
 
+    "opentui-spinner/@opentui/core": ["@opentui/[email protected]", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.60", "@opentui/core-darwin-x64": "0.1.60", "@opentui/core-linux-arm64": "0.1.60", "@opentui/core-linux-x64": "0.1.60", "@opentui/core-win32-arm64": "0.1.60", "@opentui/core-win32-x64": "0.1.60", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-28jphd0AJo48uvEuKXcT9pJhgAu8I2rEJhPt25cc5ipJ2iw/eDk1uoxrbID80MPDqgOEzN21vXmzXwCd6ao+hg=="],
+
+    "opentui-spinner/@opentui/solid": ["@opentui/[email protected]", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.60", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pn91stzAHNGWaNL6h39q55bq3G1/DLqxKtT3wVsRAV68dHfPpwmqikX1nEJZK8OU84ZTPS9Ly9fz8po2Mot2uQ=="],
+
     "p-locate/p-limit": ["[email protected]", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
 
     "parse-bmfont-xml/xml2js": ["[email protected]", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
@@ -4853,6 +4858,22 @@
 
     "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["[email protected]", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
 
+    "opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N4feqnOBDA4O4yocpat5vOiV06HqJVwJGx8rEZE9DiOtl1i+1cPQ1Lx6+zWdLhbrVBJ0ENhb7Azox8sXkm/+5Q=="],
+
+    "opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-+z3q4WaoIs7ANU8+eTFlvnfCjAS81rk81TOdZm4TJ53Ti3/B+yheWtnV/mLpLLhvZDz2VUVxxRmfDrGMnJb4fQ=="],
+
+    "opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-/Q65sjqVGB9ygJ6lStI8n1X6RyfmJZC8XofRGEuFiMLiWcWC/xoBtztdL8LAIvHQy42y2+pl9zIiW0fWSQ0wjw=="],
+
+    "opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-AegF+g7OguIpjZKN+PS55sc3ZFY6fj+fLwfETbSRGw6NqX+aiwpae0Y3gXX1s298Yq5yQEzMXnARTCJTGH4uzg=="],
+
+    "opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-fbkq8MOZJgT3r9q3JWqsfVxRpQ1SlbmhmvB35BzukXnZBK8eA178wbSadGH6irMDrkSIYye9WYddHI/iXjmgVQ=="],
+
+    "opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-OebCL7f9+CKodBw0G+NvKIcc74bl6/sBEHfb73cACdJDJKh+T3C3Vt9H3kQQ0m1C8wRAqX6rh706OArk1pUb2A=="],
+
+    "opentui-spinner/@opentui/solid/@babel/core": ["@babel/[email protected]", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
+
+    "opentui-spinner/@opentui/solid/babel-preset-solid": ["[email protected]", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="],
+
     "parse-bmfont-xml/xml2js/sax": ["[email protected]", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
 
     "pkg-up/find-up/locate-path": ["[email protected]", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
@@ -5031,6 +5052,8 @@
 
     "opencontrol/@modelcontextprotocol/sdk/raw-body/http-errors": ["[email protected]", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
 
+    "opentui-spinner/@opentui/solid/@babel/core/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
     "pkg-up/find-up/locate-path/p-locate": ["[email protected]", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
 
     "pkg-up/find-up/locate-path/path-exists": ["[email protected]", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],

+ 19 - 0
github/action.yml

@@ -20,10 +20,29 @@ inputs:
 runs:
   using: "composite"
   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
+      if: steps.cache.outputs.cache-hit != 'true'
       shell: 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
       shell: bash
       id: run_opencode

+ 3 - 3
infra/enterprise.ts

@@ -1,10 +1,10 @@
 import { SECRET } from "./secret"
-import { domain } from "./stage"
+import { domain, shortDomain } from "./stage"
 
 const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
 
-const enterprise = new sst.cloudflare.x.SolidStart("Enterprise", {
-  domain: "enterprise." + domain,
+const teams = new sst.cloudflare.x.SolidStart("Teams", {
+  domain: shortDomain,
   path: "packages/enterprise",
   buildCommand: "bun run build:cloudflare",
   environment: {

+ 6 - 0
infra/stage.ts

@@ -11,3 +11,9 @@ new cloudflare.RegionalHostname("RegionalHostname", {
   regionKey: "us",
   zoneId: zoneID,
 })
+
+export const shortDomain = (() => {
+  if ($app.stage === "production") return "opncd.ai"
+  if ($app.stage === "dev") return "dev.opncd.ai"
+  return `${$app.stage}.dev.opncd.ai`
+})()

+ 1 - 1
nix/hashes.json

@@ -1,3 +1,3 @@
 {
-  "nodeModules": "sha256-lM/7mkrPHz5E6YOMjWspfRhKjwav9ANrLt9kYlpPkEI="
+  "nodeModules": "sha256-3GaqUwomnIUW8MqUi1jDVPHQ/C5Z+D9wMR//tAGxvSQ="
 }

+ 1 - 1
package.json

@@ -30,7 +30,7 @@
       "@tsconfig/bun": "1.0.9",
       "@cloudflare/workers-types": "4.20251008.0",
       "@openauthjs/openauth": "0.0.0-20250322224806",
-      "@pierre/precision-diffs": "0.6.0-beta.10",
+      "@pierre/precision-diffs": "0.6.1",
       "@tailwindcss/vite": "4.1.11",
       "diff": "8.0.2",
       "ai": "5.0.97",

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

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

+ 3 - 1
packages/console/app/src/app.tsx

@@ -3,6 +3,7 @@ import { Router } from "@solidjs/router"
 import { FileRoutes } from "@solidjs/start/router"
 import { Suspense } from "solid-js"
 import { Favicon } from "@opencode-ai/ui/favicon"
+import { Font } from "@opencode-ai/ui/font"
 import "@ibm/plex/css/ibm-plex.css"
 import "./app.css"
 
@@ -13,8 +14,9 @@ export default function App() {
       root={(props) => (
         <MetaProvider>
           <Title>opencode</Title>
-          <Meta name="description" content="OpenCode - The AI coding agent built for the terminal." />
+          <Meta name="description" content="OpenCode - The open source coding agent." />
           <Favicon />
+          <Font />
           <Suspense>{props.children}</Suspense>
         </MetaProvider>
       )}

BIN
packages/console/app/src/asset/lander/desktop-app-icon.png


BIN
packages/console/app/src/asset/lander/opencode-desktop-icon.png


BIN
packages/console/app/src/asset/lander/opencode-min.mp4


+ 1 - 4
packages/console/app/src/component/email-signup.tsx

@@ -25,11 +25,8 @@ export function EmailSignup() {
   const submission = useSubmission(emailSignup)
   return (
     <section data-component="email">
-      <div data-slot="dock">
-        <img src={dock} alt="" />
-      </div>
       <div data-slot="section-title">
-        <h3>OpenCode will be available on desktop soon</h3>
+        <h3>Be the first to know when we release new products</h3>
         <p>Join the waitlist for early access.</p>
       </div>
       <form data-slot="form" action={emailSignup} method="post">

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

@@ -34,7 +34,7 @@ const fetchSvgContent = async (svgPath: string): Promise<string> => {
   }
 }
 
-export function Header(props: { zen?: boolean }) {
+export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
   const navigate = useNavigate()
   const githubData = createAsync(() => github())
   const starCount = createMemo(() =>
@@ -243,6 +243,13 @@ export function Header(props: { zen?: boolean }) {
                     </Match>
                   </Switch>
                 </li>
+                <Show when={!props.hideGetStarted}>
+                  <li>
+                    <A href="/download" data-slot="cta-button">
+                      Get started for free
+                    </A>
+                  </li>
+                </Show>
               </ul>
             </nav>
           </div>

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

@@ -9,8 +9,8 @@ export const config = {
   github: {
     repoUrl: "https://github.com/sst/opencode",
     starsFormatted: {
-      compact: "35K",
-      full: "35,000",
+      compact: "38K",
+      full: "38,000",
     },
   },
 
@@ -22,8 +22,8 @@ export const config = {
 
   // Static stats (used on landing page)
   stats: {
-    contributors: "350",
-    commits: "5,000",
+    contributors: "375",
+    commits: "5,250",
     monthlyUsers: "400,000",
   },
 } as const

+ 26 - 1
packages/console/app/src/routes/brand/index.css

@@ -84,7 +84,16 @@
       ul {
         display: flex;
         justify-content: space-between;
+        align-items: center;
         gap: 48px;
+
+        @media (max-width: 55rem) {
+          gap: 32px;
+        }
+
+        @media (max-width: 48rem) {
+          gap: 24px;
+        }
         li {
           display: inline-block;
           a {
@@ -98,6 +107,22 @@
             text-underline-offset: 2px;
             text-decoration-thickness: 1px;
           }
+          [data-slot="cta-button"] {
+            background: var(--color-background-strong);
+            color: var(--color-text-inverted);
+            padding: 8px 16px;
+            border-radius: 4px;
+            font-weight: 500;
+            text-decoration: none;
+
+            @media (max-width: 55rem) {
+              display: none;
+            }
+          }
+          [data-slot="cta-button"]:hover {
+            background: var(--color-background-strong-hover);
+            text-decoration: none;
+          }
         }
       }
 
@@ -266,7 +291,7 @@
 
     h1 {
       font-size: 1.5rem;
-      font-weight: 500;
+      font-weight: 700;
       color: var(--color-text-strong);
       margin-bottom: 1rem;
     }

+ 751 - 0
packages/console/app/src/routes/download/index.css

@@ -0,0 +1,751 @@
+::selection {
+  background: var(--color-background-interactive);
+  color: var(--color-text-strong);
+
+  @media (prefers-color-scheme: dark) {
+    background: var(--color-background-interactive);
+    color: var(--color-text-inverted);
+  }
+}
+
+[data-page="download"] {
+  --color-background: hsl(0, 20%, 99%);
+  --color-background-weak: hsl(0, 8%, 97%);
+  --color-background-weak-hover: hsl(0, 8%, 94%);
+  --color-background-strong: hsl(0, 5%, 12%);
+  --color-background-strong-hover: hsl(0, 5%, 18%);
+  --color-background-interactive: hsl(62, 84%, 88%);
+  --color-background-interactive-weaker: hsl(64, 74%, 95%);
+
+  --color-text: hsl(0, 1%, 39%);
+  --color-text-weak: hsl(0, 1%, 60%);
+  --color-text-weaker: hsl(30, 2%, 81%);
+  --color-text-strong: hsl(0, 5%, 12%);
+  --color-text-inverted: hsl(0, 20%, 99%);
+  --color-text-success: hsl(119, 100%, 35%);
+
+  --color-border: hsl(30, 2%, 81%);
+  --color-border-weak: hsl(0, 1%, 85%);
+
+  --color-icon: hsl(0, 1%, 55%);
+  --color-success: hsl(142, 76%, 36%);
+
+  background: var(--color-background);
+  font-family: var(--font-mono);
+  color: var(--color-text);
+  padding-bottom: 5rem;
+  overflow-x: hidden;
+
+  @media (prefers-color-scheme: dark) {
+    --color-background: hsl(0, 9%, 7%);
+    --color-background-weak: hsl(0, 6%, 10%);
+    --color-background-weak-hover: hsl(0, 6%, 15%);
+    --color-background-strong: hsl(0, 15%, 94%);
+    --color-background-strong-hover: hsl(0, 15%, 97%);
+    --color-background-interactive: hsl(62, 100%, 90%);
+    --color-background-interactive-weaker: hsl(60, 20%, 8%);
+
+    --color-text: hsl(0, 4%, 71%);
+    --color-text-weak: hsl(0, 2%, 49%);
+    --color-text-weaker: hsl(0, 3%, 28%);
+    --color-text-strong: hsl(0, 15%, 94%);
+    --color-text-inverted: hsl(0, 9%, 7%);
+    --color-text-success: hsl(119, 60%, 72%);
+
+    --color-border: hsl(0, 3%, 28%);
+    --color-border-weak: hsl(0, 4%, 23%);
+
+    --color-icon: hsl(10, 3%, 43%);
+    --color-success: hsl(142, 76%, 46%);
+  }
+
+  /* Header and Footer styles - copied from enterprise */
+  [data-component="top"] {
+    padding: 24px 5rem;
+    height: 80px;
+    position: sticky;
+    top: 0;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background: var(--color-background);
+    border-bottom: 1px solid var(--color-border-weak);
+    z-index: 10;
+
+    @media (max-width: 60rem) {
+      padding: 24px 1.5rem;
+    }
+
+    img {
+      height: 34px;
+      width: auto;
+    }
+
+    [data-component="nav-desktop"] {
+      ul {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        gap: 48px;
+
+        @media (max-width: 55rem) {
+          gap: 32px;
+        }
+
+        @media (max-width: 48rem) {
+          gap: 24px;
+        }
+        li {
+          display: inline-block;
+          a {
+            text-decoration: none;
+            span {
+              color: var(--color-text-weak);
+            }
+          }
+          a:hover {
+            text-decoration: underline;
+            text-underline-offset: 2px;
+            text-decoration-thickness: 1px;
+          }
+          [data-slot="cta-button"] {
+            background: var(--color-background-strong);
+            color: var(--color-text-inverted);
+            padding: 8px 16px;
+            border-radius: 4px;
+            font-weight: 500;
+            text-decoration: none;
+
+            @media (max-width: 55rem) {
+              display: none;
+            }
+          }
+          [data-slot="cta-button"]:hover {
+            background: var(--color-background-strong-hover);
+            text-decoration: none;
+          }
+        }
+      }
+
+      @media (max-width: 40rem) {
+        display: none;
+      }
+    }
+
+    [data-component="nav-mobile"] {
+      button > svg {
+        color: var(--color-icon);
+      }
+    }
+
+    [data-component="nav-mobile-toggle"] {
+      border: none;
+      background: none;
+      outline: none;
+      height: 40px;
+      width: 40px;
+      cursor: pointer;
+      margin-right: -8px;
+    }
+
+    [data-component="nav-mobile-toggle"]:hover {
+      background: var(--color-background-weak);
+    }
+
+    [data-component="nav-mobile"] {
+      display: none;
+
+      @media (max-width: 40rem) {
+        display: block;
+
+        [data-component="nav-mobile-icon"] {
+          cursor: pointer;
+          height: 40px;
+          width: 40px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+        }
+
+        [data-component="nav-mobile-menu-list"] {
+          position: fixed;
+          background: var(--color-background);
+          top: 80px;
+          left: 0;
+          right: 0;
+          height: 100vh;
+
+          ul {
+            list-style: none;
+            padding: 20px 0;
+
+            li {
+              a {
+                text-decoration: none;
+                padding: 20px;
+                display: block;
+
+                span {
+                  color: var(--color-text-weak);
+                }
+              }
+
+              a:hover {
+                background: var(--color-background-weak);
+              }
+            }
+          }
+        }
+      }
+    }
+
+    [data-slot="logo dark"] {
+      display: none;
+    }
+
+    @media (prefers-color-scheme: dark) {
+      [data-slot="logo light"] {
+        display: none;
+      }
+      [data-slot="logo dark"] {
+        display: block;
+      }
+    }
+  }
+
+  [data-component="footer"] {
+    border-top: 1px solid var(--color-border-weak);
+    display: flex;
+    flex-direction: row;
+
+    @media (max-width: 65rem) {
+      border-bottom: 1px solid var(--color-border-weak);
+    }
+
+    [data-slot="cell"] {
+      flex: 1;
+      text-align: center;
+
+      a {
+        text-decoration: none;
+        padding: 2rem 0;
+        width: 100%;
+        display: block;
+
+        span {
+          color: var(--color-text-weak);
+
+          @media (max-width: 40rem) {
+            display: none;
+          }
+        }
+      }
+
+      a:hover {
+        background: var(--color-background-weak);
+        text-decoration: underline;
+        text-underline-offset: 2px;
+        text-decoration-thickness: 1px;
+      }
+    }
+
+    [data-slot="cell"] + [data-slot="cell"] {
+      border-left: 1px solid var(--color-border-weak);
+
+      @media (max-width: 40rem) {
+        border-left: none;
+      }
+    }
+
+    @media (max-width: 25rem) {
+      flex-wrap: wrap;
+
+      [data-slot="cell"] {
+        flex: 1 0 100%;
+        border-left: none;
+        border-top: 1px solid var(--color-border-weak);
+      }
+
+      [data-slot="cell"]:nth-child(1) {
+        border-top: none;
+      }
+    }
+  }
+
+  [data-component="container"] {
+    max-width: 67.5rem;
+    margin: 0 auto;
+    border: 1px solid var(--color-border-weak);
+    border-top: none;
+
+    @media (max-width: 65rem) {
+      border: none;
+    }
+  }
+
+  [data-component="content"] {
+    padding: 6rem 5rem;
+
+    @media (max-width: 60rem) {
+      padding: 4rem 1.5rem;
+    }
+  }
+
+  [data-component="legal"] {
+    color: var(--color-text-weak);
+    text-align: center;
+    padding: 2rem 5rem;
+    display: flex;
+    gap: 32px;
+    justify-content: center;
+
+    @media (max-width: 60rem) {
+      padding: 2rem 1.5rem;
+    }
+
+    a {
+      color: var(--color-text-weak);
+      text-decoration: none;
+    }
+
+    a:hover {
+      color: var(--color-text);
+      text-decoration: underline;
+    }
+  }
+
+  /* Download Hero Section */
+  [data-component="download-hero"] {
+    display: grid;
+    grid-template-columns: 260px 1fr;
+    gap: 4rem;
+    padding-bottom: 2rem;
+    margin-bottom: 4rem;
+
+    @media (max-width: 50rem) {
+      grid-template-columns: 1fr;
+      gap: 1.5rem;
+      padding-bottom: 2rem;
+      margin-bottom: 2rem;
+    }
+
+    [data-component="hero-icon"] {
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+
+      @media (max-width: 40rem) {
+        display: none;
+      }
+
+      [data-slot="icon-placeholder"] {
+        width: 120px;
+        height: 120px;
+        background: var(--color-background-weak);
+        border: 1px solid var(--color-border-weak);
+        border-radius: 24px;
+
+        @media (max-width: 50rem) {
+          width: 80px;
+          height: 80px;
+        }
+      }
+
+      img {
+        width: 120px;
+        height: 120px;
+        border-radius: 24px;
+        box-shadow:
+          0 1.467px 2.847px 0 rgba(0, 0, 0, 0.42),
+          0 0.779px 1.512px 0 rgba(0, 0, 0, 0.34),
+          0 0.324px 0.629px 0 rgba(0, 0, 0, 0.24);
+
+        @media (max-width: 50rem) {
+          width: 80px;
+          height: 80px;
+          border-radius: 16px;
+        }
+      }
+
+      @media (max-width: 50rem) {
+        justify-content: flex-start;
+      }
+    }
+
+    [data-component="hero-text"] {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+
+      h1 {
+        font-size: 1.5rem;
+        font-weight: 700;
+        color: var(--color-text-strong);
+        margin-bottom: 4px;
+
+        @media (max-width: 40rem) {
+          margin-bottom: 1rem;
+        }
+      }
+
+      p {
+        color: var(--color-text);
+        margin-bottom: 12px;
+
+        @media (max-width: 40rem) {
+          margin-bottom: 2.5rem;
+          line-height: 1.6;
+        }
+      }
+
+      [data-component="download-button"] {
+        padding: 8px 20px 8px 16px;
+        background: var(--color-background-strong);
+        color: var(--color-text-inverted);
+        border: none;
+        border-radius: 4px;
+        font-weight: 500;
+        cursor: pointer;
+        display: inline-flex;
+        align-items: center;
+        gap: 10px;
+        transition: all 0.2s ease;
+        text-decoration: none;
+        width: fit-content;
+
+        &:hover:not(:disabled) {
+          background: var(--color-background-strong-hover);
+        }
+
+        &:active {
+          transform: scale(0.98);
+        }
+
+        &:disabled {
+          opacity: 0.6;
+          cursor: not-allowed;
+        }
+      }
+    }
+  }
+
+  /* Download Sections */
+  [data-component="download-section"] {
+    display: grid;
+    grid-template-columns: 260px 1fr;
+    gap: 4rem;
+    margin-bottom: 4rem;
+
+    @media (max-width: 50rem) {
+      grid-template-columns: 1fr;
+      gap: 1rem;
+      margin-bottom: 3rem;
+    }
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    [data-component="section-label"] {
+      font-weight: 500;
+      color: var(--color-text-strong);
+      padding-top: 1rem;
+
+      span {
+        color: var(--color-text-weaker);
+      }
+
+      @media (max-width: 50rem) {
+        padding-top: 0;
+        padding-bottom: 0.5rem;
+      }
+    }
+
+    [data-component="section-content"] {
+      display: flex;
+      flex-direction: column;
+      gap: 0;
+    }
+  }
+
+  /* CLI Rows */
+  button[data-component="cli-row"] {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 1rem 0.5rem 1rem 1.5rem;
+    margin: 0 -0.5rem 0 -1.5rem;
+    background: none;
+    border: none;
+    border-radius: 4px;
+    width: calc(100% + 2rem);
+    text-align: left;
+    cursor: pointer;
+    transition: background 0.15s ease;
+
+    &:hover {
+      background: var(--color-background-weak);
+    }
+
+    code {
+      font-family: var(--font-mono);
+      color: var(--color-text-weak);
+
+      strong {
+        color: var(--color-text-strong);
+        font-weight: 500;
+      }
+    }
+
+    [data-component="copy-status"] {
+      display: flex;
+      align-items: center;
+      opacity: 0;
+      transition: opacity 0.15s ease;
+      color: var(--color-icon);
+
+      svg {
+        width: 18px;
+        height: 18px;
+      }
+
+      [data-slot="copy"] {
+        display: block;
+      }
+
+      [data-slot="check"] {
+        display: none;
+      }
+    }
+
+    &:hover [data-component="copy-status"] {
+      opacity: 1;
+    }
+
+    &[data-copied] [data-component="copy-status"] {
+      opacity: 1;
+
+      [data-slot="copy"] {
+        display: none;
+      }
+
+      [data-slot="check"] {
+        display: block;
+      }
+    }
+  }
+
+  /* Download Rows */
+  [data-component="download-row"] {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 0.75rem 0.5rem 0.75rem 1.5rem;
+    margin: 0 -0.5rem 0 -1.5rem;
+    border-radius: 4px;
+    transition: background 0.15s ease;
+
+    &:hover {
+      background: var(--color-background-weak);
+    }
+
+    [data-component="download-info"] {
+      display: flex;
+      align-items: center;
+      gap: 0.75rem;
+
+      [data-slot="icon"] {
+        width: 20px;
+        height: 20px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: var(--color-icon);
+
+        svg {
+          width: 20px;
+          height: 20px;
+        }
+
+        img {
+          width: 20px;
+          height: 20px;
+        }
+      }
+
+      span {
+        color: var(--color-text);
+      }
+    }
+
+    [data-component="action-button"] {
+      padding: 6px 16px;
+      background: var(--color-background);
+      color: var(--color-text);
+      border: 1px solid var(--color-border);
+      border-radius: 4px;
+      font-weight: 500;
+      cursor: pointer;
+      text-decoration: none;
+      transition: all 0.2s ease;
+
+      &:hover {
+        background: var(--color-background-weak);
+        border-color: var(--color-border);
+        text-decoration: none;
+      }
+
+      &:active {
+        transform: scale(0.98);
+      }
+    }
+  }
+
+  a {
+    color: var(--color-text-strong);
+    text-decoration: underline;
+    text-underline-offset: 2px;
+    text-decoration-thickness: 1px;
+
+    &:hover {
+      text-decoration-thickness: 2px;
+    }
+  }
+
+  /* Narrow screen font sizes */
+  @media (max-width: 40rem) {
+    [data-component="download-section"] {
+      [data-component="section-label"] {
+        font-size: 14px;
+      }
+    }
+
+    button[data-component="cli-row"] {
+      margin: 0;
+      padding: 1rem 0;
+      width: 100%;
+      overflow: hidden;
+
+      code {
+        font-size: 14px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        display: block;
+        max-width: calc(100vw - 80px);
+      }
+
+      [data-component="copy-status"] {
+        opacity: 1 !important;
+        flex-shrink: 0;
+      }
+    }
+
+    [data-component="download-row"] {
+      margin: 0;
+      padding: 0.75rem 0;
+
+      [data-component="download-info"] span {
+        font-size: 14px;
+      }
+
+      [data-component="action-button"] {
+        font-size: 14px;
+        padding-left: 8px;
+        padding-right: 8px;
+      }
+    }
+  }
+
+  @media (max-width: 22.5rem) {
+    [data-slot="hide-narrow"] {
+      display: none;
+    }
+  }
+
+  /* FAQ Section */
+  [data-component="faq"] {
+    border-top: 1px solid var(--color-border-weak);
+    padding: 4rem 5rem;
+    margin-top: 4rem;
+
+    @media (max-width: 60rem) {
+      padding: 3rem 1.5rem;
+      margin-top: 3rem;
+    }
+
+    [data-slot="section-title"] {
+      margin-bottom: 24px;
+
+      h3 {
+        font-size: 16px;
+        font-weight: 700;
+        color: var(--color-text-strong);
+        margin-bottom: 12px;
+      }
+    }
+
+    ul {
+      padding: 0;
+
+      li {
+        list-style: none;
+        margin-bottom: 24px;
+        line-height: 200%;
+      }
+    }
+
+    [data-slot="faq-question"] {
+      display: flex;
+      gap: 16px;
+      margin-bottom: 8px;
+      color: var(--color-text-strong);
+      font-weight: 500;
+      cursor: pointer;
+      background: none;
+      border: none;
+      padding: 0;
+      align-items: start;
+      min-height: 24px;
+
+      svg {
+        margin-top: 2px;
+      }
+
+      [data-slot="faq-icon-plus"] {
+        flex-shrink: 0;
+        color: var(--color-text-weak);
+        margin-top: 2px;
+
+        [data-closed] & {
+          display: block;
+        }
+        [data-expanded] & {
+          display: none;
+        }
+      }
+      [data-slot="faq-icon-minus"] {
+        flex-shrink: 0;
+        color: var(--color-text-weak);
+        margin-top: 2px;
+
+        [data-closed] & {
+          display: none;
+        }
+        [data-expanded] & {
+          display: block;
+        }
+      }
+      [data-slot="faq-question-text"] {
+        flex-grow: 1;
+        text-align: left;
+      }
+    }
+
+    [data-slot="faq-answer"] {
+      margin-left: 40px;
+      margin-bottom: 32px;
+      line-height: 200%;
+    }
+  }
+}

Різницю між файлами не показано, бо вона завелика
+ 171 - 0
packages/console/app/src/routes/download/index.tsx


+ 27 - 2
packages/console/app/src/routes/enterprise/index.css

@@ -84,7 +84,16 @@
       ul {
         display: flex;
         justify-content: space-between;
+        align-items: center;
         gap: 48px;
+
+        @media (max-width: 55rem) {
+          gap: 32px;
+        }
+
+        @media (max-width: 48rem) {
+          gap: 24px;
+        }
         li {
           display: inline-block;
           a {
@@ -98,6 +107,22 @@
             text-underline-offset: 2px;
             text-decoration-thickness: 1px;
           }
+          [data-slot="cta-button"] {
+            background: var(--color-background-strong);
+            color: var(--color-text-inverted);
+            padding: 8px 16px;
+            border-radius: 4px;
+            font-weight: 500;
+            text-decoration: none;
+
+            @media (max-width: 55rem) {
+              display: none;
+            }
+          }
+          [data-slot="cta-button"]:hover {
+            background: var(--color-background-strong-hover);
+            text-decoration: none;
+          }
         }
       }
 
@@ -289,7 +314,7 @@
   [data-component="enterprise-column-1"] {
     h1 {
       font-size: 1.5rem;
-      font-weight: 500;
+      font-weight: 700;
       color: var(--color-text-strong);
       margin-bottom: 1rem;
     }
@@ -441,7 +466,7 @@
 
       h3 {
         font-size: 16px;
-        font-weight: 500;
+        font-weight: 700;
         color: var(--color-text-strong);
         margin-bottom: 12px;
       }

+ 117 - 9
packages/console/app/src/routes/index.css

@@ -16,6 +16,8 @@
   --color-background-strong-hover: hsl(0, 5%, 18%);
   --color-background-interactive: hsl(62, 84%, 88%);
   --color-background-interactive-weaker: hsl(64, 74%, 95%);
+  --color-surface-raised-base: hsla(0, 100%, 3%, 0.01);
+  --color-surface-raised-base-active: hsla(0, 100%, 17%, 0.06);
 
   --color-text: hsl(0, 1%, 39%);
   --color-text-weak: hsl(0, 1%, 60%);
@@ -24,7 +26,7 @@
   --color-text-inverted: hsl(0, 20%, 99%);
 
   --color-border: hsl(30, 2%, 81%);
-  --color-border-weak: hsl(0, 1%, 85%);
+  --color-border-weak: hsla(0, 100%, 3%, 0.12);
 
   --color-icon: hsl(0, 1%, 55%);
 }
@@ -62,6 +64,14 @@ body {
   }
 }
 
+[data-slot="br"] {
+  display: block;
+
+  @media (max-width: 60rem) {
+    display: none;
+  }
+}
+
 [data-page="opencode"] {
   background: var(--color-background);
   --padding: 5rem;
@@ -215,7 +225,16 @@ body {
       ul {
         display: flex;
         justify-content: space-between;
+        align-items: center;
         gap: 48px;
+
+        @media (max-width: 55rem) {
+          gap: 32px;
+        }
+
+        @media (max-width: 48rem) {
+          gap: 24px;
+        }
         li {
           display: inline-block;
           a {
@@ -229,6 +248,25 @@ body {
             text-underline-offset: var(--space-1);
             text-decoration-thickness: 1px;
           }
+          [data-slot="cta-button"] {
+            background: var(--color-background-strong);
+            color: var(--color-text-inverted);
+            padding: 8px 16px 8px 10px;
+            border-radius: 4px;
+            font-weight: 500;
+            text-decoration: none;
+            display: flex;
+            align-items: center;
+            gap: 8px;
+
+            @media (max-width: 55rem) {
+              display: none;
+            }
+          }
+          [data-slot="cta-button"]:hover {
+            background: var(--color-background-strong-hover);
+            text-decoration: none;
+          }
         }
       }
 
@@ -322,7 +360,7 @@ body {
     display: flex;
     flex-direction: column;
     max-width: 100%;
-    padding: calc(var(--vertical-padding) * 2) var(--padding);
+    padding: calc(var(--vertical-padding) * 1.5) var(--padding);
 
     @media (max-width: 30rem) {
       padding: var(--vertical-padding) var(--padding);
@@ -426,7 +464,7 @@ body {
         cursor: pointer;
         align-items: center;
         color: var(--color-text);
-        gap: var(--space-1);
+        gap: 16px;
         color: var(--color-text);
         padding: 8px 16px 8px 8px;
         border-radius: 4px;
@@ -465,6 +503,77 @@ body {
     }
   }
 
+  [data-component="desktop-app-banner"] {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    margin-bottom: 32px;
+
+    [data-slot="badge"] {
+      background: var(--color-background-strong);
+      color: var(--color-text-inverted);
+      font-weight: 500;
+      padding: 4px 8px;
+      line-height: 1;
+      flex-shrink: 0;
+    }
+
+    [data-slot="content"] {
+      display: flex;
+      align-items: center;
+      gap: 4px;
+    }
+
+    [data-slot="text"] {
+      color: var(--color-text-strong);
+      line-height: 1.4;
+
+      @media (max-width: 30.625rem) {
+        display: none;
+      }
+    }
+
+    [data-slot="platforms"] {
+      @media (max-width: 49.125rem) {
+        display: none;
+      }
+    }
+
+    [data-slot="link"] {
+      color: var(--color-text-weak);
+      white-space: nowrap;
+      text-decoration: none;
+
+      @media (max-width: 30.625rem) {
+        display: none;
+      }
+    }
+
+    [data-slot="link"]:hover {
+      color: var(--color-text);
+      text-decoration: underline;
+      text-underline-offset: 2px;
+      text-decoration-thickness: 1px;
+    }
+
+    [data-slot="link-mobile"] {
+      display: none;
+      color: var(--color-text-strong);
+      white-space: nowrap;
+      text-decoration: none;
+
+      @media (max-width: 30.625rem) {
+        display: inline;
+      }
+    }
+
+    [data-slot="link-mobile"]:hover {
+      text-decoration: underline;
+      text-underline-offset: 2px;
+      text-decoration-thickness: 1px;
+    }
+  }
+
   [data-slot="hero-copy"] {
     [data-slot="releases"] {
       background: none;
@@ -492,7 +601,7 @@ body {
     h1 {
       font-size: 38px;
       color: var(--color-text-strong);
-      font-weight: 500;
+      font-weight: 700;
       margin-bottom: 8px;
 
       @media (max-width: 60rem) {
@@ -502,7 +611,7 @@ body {
 
     p {
       color: var(--color-text);
-      margin-bottom: 40px;
+      margin-bottom: 32px;
       max-width: 82%;
 
       @media (max-width: 50rem) {
@@ -518,7 +627,6 @@ body {
       border-radius: 4px;
       font-weight: 500;
       cursor: pointer;
-      margin-bottom: 80px;
       display: flex;
       width: fit-content;
       gap: 12px;
@@ -596,7 +704,7 @@ body {
 
     h3 {
       font-size: 16px;
-      font-weight: 500;
+      font-weight: 700;
       color: var(--color-text-strong);
       margin-bottom: 12px;
     }
@@ -701,7 +809,7 @@ body {
     [data-slot="privacy-title"] {
       h3 {
         font-size: 16px;
-        font-weight: 500;
+        font-weight: 700;
         color: var(--color-text-strong);
         margin-bottom: 12px;
       }
@@ -727,7 +835,7 @@ body {
   [data-slot="zen-cta-copy"] {
     strong {
       color: var(--color-text-strong);
-      font-weight: 500;
+      font-weight: 700;
       margin-bottom: 16px;
       display: block;
     }

+ 49 - 32
packages/console/app/src/routes/index.tsx

@@ -53,16 +53,17 @@ export default function Home() {
         <div data-component="content">
           <section data-component="hero">
             <div data-slot="hero-copy">
-              <a data-slot="releases" href={release()?.url ?? `${config.github.repoUrl}/releases`} target="_blank">
-                What’s new in {release()?.name ?? "the latest release"}
-              </a>
-              <h1>The open source coding agent</h1>
+              {/*<a data-slot="releases"*/}
+              {/*   href={release()?.url ?? `${config.github.repoUrl}/releases`}*/}
+              {/*   target="_blank">*/}
+              {/*  What’s new in {release()?.name ?? "the latest release"}*/}
+              {/*</a>*/}
+              <h1>The open source AI coding agent</h1>
               <p>
-                OpenCode includes free models or connect from any provider to <br />
-                use other models, including Claude, GPT, Gemini and more.
+                Free models included or connect any model from any provider, <span data-slot="br"></span>including
+                Claude, GPT, Gemini and more.
               </p>
             </div>
-            <p data-slot="installation-instructions">Install and use. No account, no email, and no credit card.</p>
             <div data-slot="installation">
               <Tabs
                 as="section"
@@ -141,11 +142,6 @@ export default function Home() {
                 </div>
               </Tabs>
             </div>
-            <p data-slot="installation-options">
-              Available in terminal, web, and desktop (coming soon).
-              <br />
-              Extensions for VS Code, Cursor, Windsurf, and more.
-            </p>
           </section>
 
           <section data-component="video">
@@ -157,15 +153,9 @@ export default function Home() {
           <section data-component="what">
             <div data-slot="section-title">
               <h3>What is OpenCode?</h3>
-              <p>OpenCode is an open source agent that helps you write and run code directly from the terminal.</p>
+              <p>OpenCode is an open source agent that helps you write code in your terminal, IDE, or desktop.</p>
             </div>
             <ul>
-              <li>
-                <span>[*]</span>
-                <div>
-                  <strong>Native TUI</strong> A responsive, native, themeable terminal UI
-                </div>
-              </li>
               <li>
                 <span>[*]</span>
                 <div>
@@ -199,7 +189,7 @@ export default function Home() {
               <li>
                 <span>[*]</span>
                 <div>
-                  <strong>Any editor</strong> OpenCode runs in your terminal, pair it with any IDE
+                  <strong>Any editor</strong> Available as a terminal interface, desktop app, and IDE extension
                 </div>
               </li>
             </ul>
@@ -651,9 +641,8 @@ export default function Home() {
             <ul>
               <li>
                 <Faq question="What is OpenCode?">
-                  OpenCode is an open source agent that helps you write and run code directly from the terminal. You can
-                  pair OpenCode with any AI model, and because it’s terminal-based you can pair it with your preferred
-                  code editor.
+                  OpenCode is an open source agent that helps you write and run code with any AI model. It's available
+                  as a terminal-based interface, desktop app, or IDE extension.
                 </Faq>
               </li>
               <li>
@@ -663,29 +652,38 @@ export default function Home() {
               </li>
               <li>
                 <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">
                     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>
               </li>
               <li>
                 <Faq question="Can I only use OpenCode in the terminal?">
-                  Yes, for now. We are actively working on a desktop app. Join the waitlist for early access.
+                  Not anymore! OpenCode is now available as an app for your desktop.
                 </Faq>
               </li>
               <li>
                 <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>
               </li>
               <li>
                 <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>.
                 </Faq>
               </li>
@@ -745,6 +743,17 @@ export default function Home() {
                     />
                   </svg>
                 </div>
+                <div>
+                  <svg
+                    width="24"
+                    height="24"
+                    viewBox="0 0 50 50"
+                    fill="currentColor"
+                    xmlns="http://www.w3.org/2000/svg"
+                  >
+                    <path d="M49.04,24.001l-1.082-0.043h-0.001C36.134,23.492,26.508,13.866,26.042,2.043L25.999,0.96C25.978,0.424,25.537,0,25,0	s-0.978,0.424-0.999,0.96l-0.043,1.083C23.492,13.866,13.866,23.492,2.042,23.958L0.96,24.001C0.424,24.022,0,24.463,0,25	c0,0.537,0.424,0.978,0.961,0.999l1.082,0.042c11.823,0.467,21.449,10.093,21.915,21.916l0.043,1.083C24.022,49.576,24.463,50,25,50	s0.978-0.424,0.999-0.96l0.043-1.083c0.466-11.823,10.092-21.449,21.915-21.916l1.082-0.042C49.576,25.978,50,25.537,50,25	C50,24.463,49.576,24.022,49.04,24.001z"></path>
+                  </svg>
+                </div>
                 <div>
                   <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                     <path
@@ -775,6 +784,14 @@ export default function Home() {
                     />
                   </svg>
                 </div>
+                <div>
+                  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                    <path
+                      d="M12.0962 3L10.0998 5.6577H1.59858L3.59417 3H12.0972H12.0962ZM22.3162 18.3432L20.3215 21H11.8497L13.8425 18.3432H22.3162ZM23 3L9.492 21H1L14.508 3H23Z"
+                      fill="black"
+                    />
+                  </svg>
+                </div>
               </div>
               <A href="/zen">
                 <span>Learn about Zen </span>

+ 20 - 0
packages/console/app/src/routes/t/[...path].tsx

@@ -0,0 +1,20 @@
+import type { APIEvent } from "@solidjs/start/server"
+
+async function handler(evt: APIEvent) {
+  const req = evt.request.clone()
+  const url = new URL(req.url)
+  const targetUrl = `https://enterprise.opencode.ai/${url.pathname}${url.search}`
+  const response = await fetch(targetUrl, {
+    method: req.method,
+    headers: req.headers,
+    body: req.body,
+  })
+  return response
+}
+
+export const GET = handler
+export const POST = handler
+export const PUT = handler
+export const DELETE = handler
+export const OPTIONS = handler
+export const PATCH = handler

+ 28 - 3
packages/console/app/src/routes/zen/index.css

@@ -147,7 +147,16 @@ body {
       ul {
         display: flex;
         justify-content: space-between;
+        align-items: center;
         gap: 48px;
+
+        @media (max-width: 55rem) {
+          gap: 32px;
+        }
+
+        @media (max-width: 48rem) {
+          gap: 24px;
+        }
         li {
           display: inline-block;
           a {
@@ -161,6 +170,22 @@ body {
             text-underline-offset: var(--space-1);
             text-decoration-thickness: 1px;
           }
+          [data-slot="cta-button"] {
+            background: var(--color-background-strong);
+            color: var(--color-text-inverted);
+            padding: 8px 16px;
+            border-radius: 4px;
+            font-weight: 500;
+            text-decoration: none;
+
+            @media (max-width: 55rem) {
+              display: none;
+            }
+          }
+          [data-slot="cta-button"]:hover {
+            background: var(--color-background-strong-hover);
+            text-decoration: none;
+          }
         }
       }
 
@@ -280,7 +305,7 @@ body {
     h1 {
       font-size: 28px;
       color: var(--color-text-strong);
-      font-weight: 500;
+      font-weight: 700;
       margin-bottom: 16px;
       display: block;
 
@@ -369,7 +394,7 @@ body {
 
     h3 {
       font-size: 16px;
-      font-weight: 500;
+      font-weight: 700;
       color: var(--color-text-strong);
       margin-bottom: 12px;
     }
@@ -442,7 +467,7 @@ body {
     [data-slot="privacy-title"] {
       h3 {
         font-size: 16px;
-        font-weight: 500;
+        font-weight: 700;
         color: var(--color-text);
         margin-bottom: 12px;
       }

+ 1 - 1
packages/console/app/src/routes/zen/index.tsx

@@ -38,7 +38,7 @@ export default function Home() {
       <Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
 
       <div data-component="container">
-        <Header zen />
+        <Header zen hideGetStarted />
 
         <div data-component="content">
           <section data-component="hero">

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

@@ -588,7 +588,7 @@ export async function handler(
       tx
         .update(KeyTable)
         .set({ timeUsed: sql`now()` })
-        .where(eq(KeyTable.id, authInfo.apiKeyId)),
+        .where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
     )
   }
 

+ 2 - 1
packages/console/app/src/style/token/font.css

@@ -15,6 +15,7 @@ body {
   --font-size-9xl: 8rem;
 
   --font-mono:
-    "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+    "Berkeley Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
+    "Courier New", monospace;
   --font-sans: var(--font-mono);
 }

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

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

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

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

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

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

+ 1 - 1
packages/desktop/package.json

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

+ 7 - 1
packages/desktop/src/app.tsx

@@ -15,8 +15,14 @@ import { GlobalSDKProvider } from "./context/global-sdk"
 import { SessionProvider } from "./context/session"
 import { Show } from "solid-js"
 
+declare global {
+  interface Window {
+    __OPENCODE__?: { updaterEnabled?: boolean; port?: number }
+  }
+}
+
 const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
-const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
+const port = window.__OPENCODE__?.port ?? import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
 
 const url =
   new URLSearchParams(document.location.search).get("url") ||

+ 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>
+  )
+}

+ 224 - 53
packages/desktop/src/components/prompt-input.tsx

@@ -1,9 +1,20 @@
 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 { createFocusSignal } from "@solid-primitives/active-element"
 import { useLocal } from "@/context/local"
-import { DateTime } from "luxon"
 import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "@/context/session"
 import { useSDK } from "@/context/sdk"
 import { useNavigate } from "@solidjs/router"
@@ -14,10 +25,16 @@ import { Button } from "@opencode-ai/ui/button"
 import { Icon } from "@opencode-ai/ui/icon"
 import { Tooltip } from "@opencode-ai/ui/tooltip"
 import { IconButton } from "@opencode-ai/ui/icon-button"
-import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
 import { Select } from "@opencode-ai/ui/select"
+import { Tag } from "@opencode-ai/ui/tag"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import { type IconName } from "@opencode-ai/ui/icons/provider"
+import { useLayout } from "@/context/layout"
+import { popularProviders, useProviders } from "@/hooks/use-providers"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List, ListRef } from "@opencode-ai/ui/list"
+import { iife } from "@opencode-ai/util/iife"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
+import { IconName } from "@opencode-ai/ui/icons/provider"
 
 interface PromptInputProps {
   class?: string
@@ -58,6 +75,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   const sync = useSync()
   const local = useLocal()
   const session = useSession()
+  const layout = useLayout()
+  const providers = useProviders()
   let editorRef!: HTMLDivElement
 
   const [store, setStore] = createStore<{
@@ -455,55 +474,207 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
               class="capitalize"
               variant="ghost"
             />
-            <SelectDialog
-              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)}
-              sortGroupsBy={(a, b) => {
-                const order = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
-                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 (order.includes(aProvider) && !order.includes(bProvider)) return -1
-                if (!order.includes(aProvider) && order.includes(bProvider)) return 1
-                return order.indexOf(aProvider) - order.indexOf(bProvider)
-              }}
-              onSelect={(x) =>
-                local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true })
-              }
-              trigger={
-                <Button as="div" variant="ghost">
-                  {local.model.current()?.name ?? "Select model"}
-                  <span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
-                  <Icon name="chevron-down" size="small" />
-                </Button>
-              }
-            >
-              {(i) => (
-                <div class="w-full flex items-center justify-between gap-x-3">
-                  <div class="flex items-center gap-x-2.5 text-text-muted grow min-w-0">
-                    <ProviderIcon name={i.provider.id as IconName} class="size-6 p-0.5 shrink-0" />
-                    <div class="flex gap-x-3 items-baseline flex-[1_0_0]">
-                      <span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
-                      <Show when={false}>
-                        <span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
-                          {DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")}
-                        </span>
-                      </Show>
-                    </div>
-                  </div>
-                  <Show when={!i.cost || i.cost?.input === 0}>
-                    <div class="overflow-hidden text-12-medium text-text-strong">Free</div>
-                  </Show>
-                </div>
-              )}
-            </SelectDialog>
+            <Button as="div" variant="ghost" onClick={() => layout.dialog.open("model")}>
+              {local.model.current()?.name ?? "Select model"}
+              <span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
+              <Icon name="chevron-down" size="small" />
+            </Button>
+            <Show when={layout.dialog.opened() === "model"}>
+              <Switch>
+                <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>
+                        }
+                      >
+                        {(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 when={true}>
+                  {iife(() => {
+                    let listRef: ListRef | undefined
+                    const handleKey = (e: KeyboardEvent) => {
+                      if (e.key === "Escape") return
+                      listRef?.onKeyDown(e)
+                    }
+
+                    onMount(() => {
+                      document.addEventListener("keydown", handleKey)
+                      onCleanup(() => {
+                        document.removeEventListener("keydown", handleKey)
+                      })
+                    })
+
+                    return (
+                      <Dialog
+                        modal
+                        defaultOpen
+                        onOpenChange={(open) => {
+                          if (open) {
+                            layout.dialog.open("model")
+                          } else {
+                            layout.dialog.close("model")
+                          }
+                        }}
+                      >
+                        <Dialog.Header>
+                          <Dialog.Title>Select model</Dialog.Title>
+                          <Dialog.CloseButton tabIndex={-1} />
+                        </Dialog.Header>
+                        <Dialog.Body>
+                          <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>
+                            <List
+                              ref={(ref) => (listRef = ref)}
+                              items={local.model.list}
+                              current={local.model.current()}
+                              key={(x) => `${x.provider.id}:${x.id}`}
+                              onSelect={(x) => {
+                                local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
+                                  recent: true,
+                                })
+                                layout.dialog.close("model")
+                              }}
+                            >
+                              {(i) => (
+                                <div class="w-full flex items-center gap-x-2.5">
+                                  <span>{i.name}</span>
+                                  <Tag>Free</Tag>
+                                  <Show when={i.latest}>
+                                    <Tag>Latest</Tag>
+                                  </Show>
+                                </div>
+                              )}
+                            </List>
+                            <div />
+                            <div />
+                          </div>
+                          <div class="px-1.5 pb-1.5">
+                            <div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
+                              <div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
+                                <div class="px-2 text-14-medium text-text-base">
+                                  Add more models from popular providers
+                                </div>
+                                <div class="w-full">
+                                  <List
+                                    class="w-full"
+                                    key={(x) => x?.id}
+                                    items={providers.popular}
+                                    activeIcon="plus-small"
+                                    sortBy={(a, b) => {
+                                      if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
+                                        return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
+                                      return a.name.localeCompare(b.name)
+                                    }}
+                                    onSelect={(x) => {
+                                      if (!x) return
+                                      layout.dialog.connect(x.id)
+                                    }}
+                                  >
+                                    {(i) => (
+                                      <div class="w-full flex items-center gap-x-4">
+                                        <ProviderIcon
+                                          data-slot="list-item-extra-icon"
+                                          id={i.id as IconName}
+                                          // TODO: clean this up after we update icon in models.dev
+                                          classList={{
+                                            "text-icon-weak-base": true,
+                                            "size-4 mx-0.5": i.id === "opencode",
+                                            "size-5": i.id !== "opencode",
+                                          }}
+                                        />
+                                        <span>{i.name}</span>
+                                        <Show when={i.id === "opencode"}>
+                                          <Tag>Recommended</Tag>
+                                        </Show>
+                                        <Show when={i.id === "anthropic"}>
+                                          <div class="text-14-regular text-text-weak">
+                                            Connect with Claude Pro/Max or API key
+                                          </div>
+                                        </Show>
+                                      </div>
+                                    )}
+                                  </List>
+                                  <Button
+                                    variant="ghost"
+                                    class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
+                                    icon="dot-grid"
+                                    onClick={() => {
+                                      layout.dialog.open("provider")
+                                    }}
+                                  >
+                                    View all providers
+                                  </Button>
+                                </div>
+                              </div>
+                            </div>
+                          </div>
+                        </Dialog.Body>
+                      </Dialog>
+                    )
+                  })}
+                </Match>
+              </Switch>
+            </Show>
           </div>
           <Tooltip
             placement="top"

+ 99 - 79
packages/desktop/src/context/global-sync.tsx

@@ -1,60 +1,31 @@
-import type {
-  Message,
-  Agent,
-  Provider,
-  Session,
-  Part,
-  Config,
-  Path,
-  File,
-  FileNode,
-  Project,
-  FileDiff,
-  Todo,
-  SessionStatus,
-} from "@opencode-ai/sdk/v2"
+import {
+  type Message,
+  type Agent,
+  type Session,
+  type Part,
+  type Config,
+  type Path,
+  type File,
+  type FileNode,
+  type Project,
+  type FileDiff,
+  type Todo,
+  type SessionStatus,
+  type ProviderListResponse,
+  type ProviderAuthResponse,
+  createOpencodeClient,
+} from "@opencode-ai/sdk/v2/client"
 import { createStore, produce, reconcile } from "solid-js/store"
 import { Binary } from "@opencode-ai/util/binary"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useGlobalSDK } from "./global-sdk"
-
-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
-]
-
-function pickAvailableColor(usedColors: Set<string>) {
-  const available = PASTEL_COLORS.filter((c) => !usedColors.has(c))
-  if (available.length === 0) return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)]
-  return available[Math.floor(Math.random() * available.length)]
-}
-
-async function ensureProjectColor(
-  project: Project,
-  sdk: ReturnType<typeof useGlobalSDK>,
-  usedColors: Set<string>,
-): Promise<Project> {
-  if (project.icon?.color) return project
-  const color = pickAvailableColor(usedColors)
-  usedColors.add(color)
-  const updated = { ...project, icon: { ...project.icon, color } }
-  sdk.client.project.update({ projectID: project.id, icon: { color } })
-  return updated
-}
+import { onMount } from "solid-js"
 
 type State = {
   ready: boolean
-  provider: Provider[]
   agent: Agent[]
   project: string
+  provider: ProviderListResponse
   config: Config
   path: Path
   session: Session[]
@@ -81,26 +52,58 @@ type State = {
 export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
   name: "GlobalSync",
   init: () => {
+    const globalSDK = useGlobalSDK()
     const [globalStore, setGlobalStore] = createStore<{
       ready: boolean
-      projects: Project[]
+      project: Project[]
+      provider: ProviderListResponse
+      provider_auth: ProviderAuthResponse
       children: Record<string, State>
     }>({
       ready: false,
-      projects: [],
+      project: [],
+      provider: { all: [], connected: [], default: {} },
+      provider_auth: {},
       children: {},
     })
 
+    async function bootstrapInstance(directory: string) {
+      const [store, setStore] = child(directory)
+      const sdk = createOpencodeClient({
+        baseUrl: globalSDK.url,
+        directory,
+      })
+      const load = {
+        project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
+        provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
+        path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
+        agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
+        session: () =>
+          sdk.session.list().then((x) => {
+            const sessions = (x.data ?? [])
+              .slice()
+              .sort((a, b) => a.id.localeCompare(b.id))
+              .slice(0, store.limit)
+            setStore("session", sessions)
+          }),
+        status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
+        config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
+        changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
+        node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
+      }
+      await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
+    }
+
     const children: Record<string, ReturnType<typeof createStore<State>>> = {}
     function child(directory: string) {
       if (!children[directory]) {
         setGlobalStore("children", directory, {
           project: "",
+          provider: { all: [], connected: [], default: {} },
           config: {},
-          path: { state: "", config: "", worktree: "", directory: "" },
+          path: { state: "", config: "", worktree: "", directory: "", home: "" },
           ready: false,
           agent: [],
-          provider: [],
           session: [],
           session_status: {},
           session_diff: {},
@@ -112,32 +115,33 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
           changes: [],
         })
         children[directory] = createStore(globalStore.children[directory])
+        bootstrapInstance(directory)
       }
       return children[directory]
     }
 
-    const sdk = useGlobalSDK()
-    sdk.event.listen((e) => {
+    globalSDK.event.listen((e) => {
       const directory = e.name
       const event = e.details
 
       if (directory === "global") {
         switch (event.type) {
+          case "global.disposed": {
+            bootstrap()
+            break
+          }
           case "project.updated": {
-            const usedColors = new Set(globalStore.projects.map((p) => p.icon?.color).filter(Boolean) as string[])
-            ensureProjectColor(event.properties, sdk, usedColors).then((project) => {
-              const result = Binary.search(globalStore.projects, project.id, (s) => s.id)
-              if (result.found) {
-                setGlobalStore("projects", result.index, reconcile(project))
-                return
-              }
-              setGlobalStore(
-                "projects",
-                produce((draft) => {
-                  draft.splice(result.index, 0, project)
-                }),
-              )
-            })
+            const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
+            if (result.found) {
+              setGlobalStore("project", result.index, reconcile(event.properties))
+              return
+            }
+            setGlobalStore(
+              "project",
+              produce((draft) => {
+                draft.splice(result.index, 0, event.properties)
+              }),
+            )
             break
           }
         }
@@ -146,6 +150,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
 
       const [store, setStore] = child(directory)
       switch (event.type) {
+        case "server.instance.disposed": {
+          bootstrapInstance(directory)
+          break
+        }
         case "session.updated": {
           const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
           if (result.found) {
@@ -214,17 +222,28 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
       }
     })
 
-    Promise.all([
-      sdk.client.project.list().then(async (x) => {
-        const filtered = x.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
-        const usedColors = new Set(filtered.map((p) => p.icon?.color).filter(Boolean) as string[])
-        const projects = await Promise.all(filtered.map((p) => ensureProjectColor(p, sdk, usedColors)))
-        setGlobalStore(
-          "projects",
-          projects.sort((a, b) => a.id.localeCompare(b.id)),
-        )
-      }),
-    ]).then(() => setGlobalStore("ready", true))
+    async function bootstrap() {
+      return Promise.all([
+        globalSDK.client.project.list().then(async (x) => {
+          setGlobalStore(
+            "project",
+            x
+              .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
+              .sort((a, b) => a.id.localeCompare(b.id)),
+          )
+        }),
+        globalSDK.client.provider.list().then((x) => {
+          setGlobalStore("provider", x.data ?? {})
+        }),
+        globalSDK.client.provider.auth().then((x) => {
+          setGlobalStore("provider_auth", x.data ?? {})
+        }),
+      ]).then(() => setGlobalStore("ready", true))
+    }
+
+    onMount(() => {
+      bootstrap()
+    })
 
     return {
       data: globalStore,
@@ -232,6 +251,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
         return globalStore.ready
       },
       child,
+      bootstrap,
     }
   },
 })

+ 126 - 18
packages/desktop/src/context/layout.tsx

@@ -1,9 +1,33 @@
-import { createStore } from "solid-js/store"
-import { createMemo, onMount } from "solid-js"
+import { createStore, produce } from "solid-js/store"
+import { batch, createMemo, onMount } from "solid-js"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { makePersisted } from "@solid-primitives/storage"
 import { useGlobalSync } from "./global-sync"
 import { useGlobalSDK } from "./global-sdk"
+import { Project } from "@opencode-ai/sdk/v2"
+
+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"
 
 export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
   name: "Layout",
@@ -26,9 +50,52 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         },
       }),
       {
-        name: "default-layout.v6",
+        name: "default-layout.v7",
       },
     )
+    const [ephemeral, setEphemeral] = createStore<{
+      connect: {
+        provider?: string
+        state?: "pending" | "complete" | "error"
+        error?: string
+      }
+      dialog: {
+        open?: Dialog
+      }
+    }>({
+      connect: {},
+      dialog: {},
+    })
+    const usedColors = new Set<AvatarColorKey>()
+
+    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)]
+    }
+
+    function enrich(project: { worktree: string; expanded: boolean }) {
+      const metadata = globalSync.data.project.find((x) => x.worktree === project.worktree)
+      if (!metadata) return []
+      return [
+        {
+          ...project,
+          ...metadata,
+        },
+      ]
+    }
+
+    function colorize(project: Project & { expanded: boolean }) {
+      if (project.icon?.color) return project
+      const color = pickAvailableColor()
+      usedColors.add(color)
+      project.icon = { ...project.icon, color }
+      globalSdk.client.project.update({ projectID: project.id, icon: { color } })
+      return project
+    }
+
+    const enriched = createMemo(() => store.projects.flatMap(enrich))
+    const list = createMemo(() => enriched().flatMap(colorize))
 
     async function loadProjectSessions(directory: string) {
       const [, setStore] = globalSync.child(directory)
@@ -43,30 +110,19 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
 
     onMount(() => {
       Promise.all(
-        store.projects.map(({ worktree }) => {
-          return loadProjectSessions(worktree)
+        store.projects.map((project) => {
+          return loadProjectSessions(project.worktree)
         }),
       )
     })
 
-    function enrich(project: { worktree: string; expanded: boolean }) {
-      const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree)
-      if (!metadata) return []
-      return [
-        {
-          ...project,
-          ...metadata,
-        },
-      ]
-    }
-
     return {
       projects: {
-        list: createMemo(() => store.projects.flatMap(enrich)),
+        list,
         open(directory: string) {
           if (store.projects.find((x) => x.worktree === directory)) return
           loadProjectSessions(directory)
-          setStore("projects", (x) => [...x, { worktree: directory, expanded: true }])
+          setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
         },
         close(directory: string) {
           setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
@@ -129,6 +185,58 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           setStore("review", "state", "tab")
         },
       },
+      dialog: {
+        opened: createMemo(() => ephemeral.dialog?.open),
+        open(dialog: Dialog) {
+          batch(() => {
+            // if (dialog !== "connect") {
+            //   setEphemeral("connect", {})
+            // }
+            setEphemeral("dialog", "open", dialog)
+          })
+        },
+        close(dialog: Dialog) {
+          if (ephemeral.dialog.open === dialog) {
+            setEphemeral(
+              produce((state) => {
+                state.dialog.open = undefined
+                state.connect = {}
+              }),
+            )
+          }
+        },
+        connect(provider: string) {
+          setEphemeral(
+            produce((state) => {
+              state.dialog.open = "connect"
+              state.connect = { provider, state: "pending" }
+            }),
+          )
+        },
+      },
+      connect: {
+        provider: createMemo(() => ephemeral.connect.provider),
+        state: createMemo(() => ephemeral.connect.state),
+        complete() {
+          setEphemeral(
+            produce((state) => {
+              state.dialog.open = "model"
+              state.connect.state = "complete"
+            }),
+          )
+        },
+        error(message: string) {
+          setEphemeral(
+            produce((state) => {
+              state.connect.state = "error"
+              state.connect.error = message
+            }),
+          )
+        },
+        clear() {
+          setEphemeral("connect", {})
+        },
+      },
     }
   },
 })

+ 29 - 8
packages/desktop/src/context/local.tsx

@@ -6,6 +6,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useSDK } from "./sdk"
 import { useSync } from "./sync"
 import { base64Encode } from "@opencode-ai/util/encode"
+import { useProviders } from "@/hooks/use-providers"
 
 export type LocalFile = FileNode &
   Partial<{
@@ -25,6 +26,7 @@ export type View = LocalFile["view"]
 
 export type LocalModel = Omit<Model, "provider"> & {
   provider: Provider
+  latest?: boolean
 }
 export type ModelKey = { providerID: string; modelID: string }
 
@@ -36,10 +38,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
   init: () => {
     const sdk = useSDK()
     const sync = useSync()
+    const providers = useProviders()
 
     function isModelValid(model: ModelKey) {
-      const provider = sync.data.provider.find((x) => x.id === model.providerID)
-      return !!provider?.models[model.modelID]
+      const provider = providers.all().find((x) => x.id === model.providerID)
+      return (
+        !!provider?.models[model.modelID] &&
+        providers
+          .connected()
+          .map((p) => p.id)
+          .includes(model.providerID)
+      )
     }
 
     function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
@@ -114,7 +123,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       })
 
       const list = createMemo(() =>
-        sync.data.provider.flatMap((p) => Object.values(p.models).map((m) => ({ ...m, provider: p }) as LocalModel)),
+        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)
 
@@ -134,12 +150,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
             return item
           }
         }
-        const provider = sync.data.provider[0]
-        const model = Object.values(provider.models)[0]
-        return {
-          providerID: provider.id,
-          modelID: model.id,
+
+        for (const p of providers.connected()) {
+          if (p.id in providers.default()) {
+            return {
+              providerID: p.id,
+              modelID: providers.default()[p.id],
+            }
+          }
         }
+
+        throw new Error("No default model found")
       })
 
       const currentModel = createMemo(() => {

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

@@ -62,10 +62,10 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
     const userMessages = createMemo(() =>
       messages()
         .filter((m) => m.role === "user")
-        .sort((a, b) => b.id.localeCompare(a.id)),
+        .sort((a, b) => a.id.localeCompare(b.id)),
     )
     const lastUserMessage = createMemo(() => {
-      return userMessages()?.at(0)
+      return userMessages()?.at(-1)
     })
     const activeMessage = createMemo(() => {
       if (!store.messageId) return lastUserMessage()
@@ -94,7 +94,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
       () => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage,
     )
     const model = createMemo(() =>
-      last() ? sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
+      last() ? sync.data.provider.all.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
     )
     const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
 

+ 9 - 26
packages/desktop/src/context/sync.tsx

@@ -11,28 +11,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
     const globalSync = useGlobalSync()
     const sdk = useSDK()
     const [store, setStore] = globalSync.child(sdk.directory)
-
-    const load = {
-      project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
-      provider: () => sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
-      path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
-      agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
-      session: () =>
-        sdk.client.session.list().then((x) => {
-          const sessions = (x.data ?? [])
-            .slice()
-            .sort((a, b) => a.id.localeCompare(b.id))
-            .slice(0, store.limit)
-          setStore("session", sessions)
-        }),
-      status: () => sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
-      config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
-      changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
-      node: () => sdk.client.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
-    }
-
-    Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
-
     const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
 
     return {
@@ -42,8 +20,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
         return store.ready
       },
       get project() {
-        const match = Binary.search(globalSync.data.projects, store.project, (p) => p.id)
-        if (match.found) return globalSync.data.projects[match.index]
+        const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
+        if (match.found) return globalSync.data.project[match.index]
         return undefined
       },
       session: {
@@ -78,11 +56,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
         },
         fetch: async (count = 10) => {
           setStore("limit", (x) => x + count)
-          await load.session()
+          await sdk.client.session.list().then((x) => {
+            const sessions = (x.data ?? [])
+              .slice()
+              .sort((a, b) => a.id.localeCompare(b.id))
+              .slice(0, store.limit)
+            setStore("session", sessions)
+          })
         },
         more: createMemo(() => store.session.length >= store.limit),
       },
-      load,
       absolute,
       get directory() {
         return store.path.directory

+ 31 - 0
packages/desktop/src/hooks/use-providers.ts

@@ -0,0 +1,31 @@
+import { useGlobalSync } from "@/context/global-sync"
+import { base64Decode } from "@opencode-ai/util/encode"
+import { useParams } from "@solidjs/router"
+import { createMemo } from "solid-js"
+
+export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
+
+export function useProviders() {
+  const params = useParams()
+  const globalSync = useGlobalSync()
+  const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
+  const providers = createMemo(() => {
+    if (currentDirectory()) {
+      const [projectStore] = globalSync.child(currentDirectory())
+      return projectStore.provider
+    }
+    return globalSync.data.provider
+  })
+  const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id)))
+  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)))
+  return {
+    all: createMemo(() => providers().all),
+    default: createMemo(() => providers().default),
+    popular,
+    connected,
+    paid,
+  }
+}

+ 2 - 2
packages/desktop/src/pages/home.tsx

@@ -38,7 +38,7 @@ export default function Home() {
     <div class="mx-auto mt-55">
       <Logo class="w-xl opacity-12" />
       <Switch>
-        <Match when={sync.data.projects.length > 0}>
+        <Match when={sync.data.project.length > 0}>
           <div class="mt-20 w-full flex flex-col gap-4">
             <div class="flex gap-2 items-center justify-between pl-3">
               <div class="text-14-medium text-text-strong">Recent projects</div>
@@ -50,7 +50,7 @@ export default function Home() {
             </div>
             <ul class="flex flex-col gap-2">
               <For
-                each={sync.data.projects
+                each={sync.data.project
                   .toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
                   .slice(0, 5)}
               >

+ 530 - 21
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 { A, useNavigate, useParams } from "@solidjs/router"
-import { useLayout } from "@/context/layout"
+import { useLayout, getAvatarColors } from "@/context/layout"
 import { useGlobalSync } from "@/context/global-sync"
 import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
 import { Mark } from "@opencode-ai/ui/logo"
@@ -9,6 +9,7 @@ import { Avatar } from "@opencode-ai/ui/avatar"
 import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
 import { Button } from "@opencode-ai/ui/button"
 import { Icon } from "@opencode-ai/ui/icon"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Tooltip } from "@opencode-ai/ui/tooltip"
 import { Collapsible } from "@opencode-ai/ui/collapsible"
@@ -16,9 +17,9 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
 import { getFilename } from "@opencode-ai/util/path"
 import { Select } from "@opencode-ai/ui/select"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
-import { Session, Project } from "@opencode-ai/sdk/v2/client"
+import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
 import { usePlatform } from "@/context/platform"
-import { createStore } from "solid-js/store"
+import { createStore, produce } from "solid-js/store"
 import {
   DragDropProvider,
   DragDropSensors,
@@ -29,6 +30,18 @@ import {
   useDragDropContext,
 } from "@thisbeyond/solid-dnd"
 import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
+import { SelectDialog } from "@opencode-ai/ui/select-dialog"
+import { Tag } from "@opencode-ai/ui/tag"
+import { IconName } from "@opencode-ai/ui/icons/provider"
+import { popularProviders, useProviders } from "@/hooks/use-providers"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { iife } from "@opencode-ai/util/iife"
+import { Link } from "@/components/link"
+import { List, ListRef } from "@opencode-ai/ui/list"
+import { TextField } from "@opencode-ai/ui/text-field"
+import { showToast, Toast } from "@opencode-ai/ui/toast"
+import { useGlobalSDK } from "@/context/global-sdk"
+import { Spinner } from "@opencode-ai/ui/spinner"
 
 export default function Layout(props: ParentProps) {
   const [store, setStore] = createStore({
@@ -37,6 +50,7 @@ export default function Layout(props: ParentProps) {
   })
 
   const params = useParams()
+  const globalSDK = useGlobalSDK()
   const globalSync = useGlobalSync()
   const layout = useLayout()
   const platform = usePlatform()
@@ -44,6 +58,7 @@ export default function Layout(props: ParentProps) {
   const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
   const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
   const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
+  const providers = useProviders()
 
   function navigateToProject(directory: string | undefined) {
     if (!directory) return
@@ -82,12 +97,21 @@ export default function Layout(props: ParentProps) {
     }
   }
 
+  async function connectProvider() {
+    layout.dialog.open("provider")
+  }
+
   createEffect(() => {
     if (!params.dir || !params.id) return
     const directory = base64Decode(params.dir)
     setStore("lastSession", directory, params.id)
   })
 
+  createEffect(() => {
+    const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
+    document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
+  })
+
   function getDraggableId(event: unknown): string | undefined {
     if (typeof event !== "object" || event === null) return undefined
     if (!("draggable" in event)) return undefined
@@ -156,7 +180,7 @@ export default function Layout(props: ParentProps) {
                 <Avatar
                   fallback={name()}
                   src={props.project.icon?.url}
-                  background={props.project.icon?.color ?? "var(--surface-info-base)"}
+                  {...getAvatarColors(props.project.icon?.color)}
                   class="size-full"
                 />
               </div>
@@ -176,7 +200,7 @@ export default function Layout(props: ParentProps) {
               <Avatar
                 fallback={name()}
                 src={props.project.icon?.url}
-                background={props.project.icon?.color ?? "var(--surface-info-base)"}
+                {...getAvatarColors(props.project.icon?.color)}
                 class="size-full"
               />
             </div>
@@ -207,7 +231,7 @@ export default function Layout(props: ParentProps) {
                     <Avatar
                       fallback={name()}
                       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"
                     />
                     <Icon
@@ -419,7 +443,7 @@ export default function Layout(props: ParentProps) {
               <Button
                 variant="ghost"
                 size="large"
-                class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg"
+                class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
                 onClick={layout.sidebar.toggle}
               >
                 <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
@@ -465,10 +489,44 @@ export default function Layout(props: ParentProps) {
             </DragDropProvider>
           </div>
           <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
+            <Switch>
+              <Match when={!providers.paid().length && layout.sidebar.opened()}>
+                <div class="rounded-md bg-background-stronger shadow-xs-border-base">
+                  <div class="p-3 flex flex-col gap-2">
+                    <div class="text-12-medium text-text-strong">Getting started</div>
+                    <div class="text-text-base">OpenCode includes free models so you can start immediately.</div>
+                    <div class="text-text-base">Connect any provider to use models, inc. Claude, GPT, Gemini etc.</div>
+                  </div>
+                  <Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
+                    <Button
+                      class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px"
+                      size="large"
+                      icon="plus-small"
+                      onClick={connectProvider}
+                    >
+                      <Show when={layout.sidebar.opened()}>Connect provider</Show>
+                    </Button>
+                  </Tooltip>
+                </div>
+              </Match>
+              <Match when={true}>
+                <Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
+                  <Button
+                    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="plus-small"
+                    onClick={connectProvider}
+                  >
+                    <Show when={layout.sidebar.opened()}>Connect provider</Show>
+                  </Button>
+                </Tooltip>
+              </Match>
+            </Switch>
             <Show when={platform.openDirectoryPickerDialog}>
               <Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
                 <Button
-                  class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
+                  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="folder-add-left"
@@ -478,23 +536,23 @@ export default function Layout(props: ParentProps) {
                 </Button>
               </Tooltip>
             </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"
-                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()}>
               <Button
                 as={"a"}
                 href="https://opencode.ai/desktop-feedback"
                 target="_blank"
-                class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
+                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="bubble-5"
@@ -505,7 +563,458 @@ export default function Layout(props: ParentProps) {
           </div>
         </div>
         <main class="size-full overflow-x-hidden flex flex-col items-start">{props.children}</main>
+        <Show when={layout.dialog.opened() === "provider"}>
+          <SelectDialog
+            defaultOpen
+            title="Connect provider"
+            placeholder="Search providers"
+            activeIcon="plus-small"
+            key={(x) => x?.id}
+            items={providers.all}
+            filterKeys={["id", "name"]}
+            groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
+            sortBy={(a, b) => {
+              if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
+                return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
+              return a.name.localeCompare(b.name)
+            }}
+            sortGroupsBy={(a, b) => {
+              if (a.category === "Popular" && b.category !== "Popular") return -1
+              if (b.category === "Popular" && a.category !== "Popular") return 1
+              return 0
+            }}
+            onSelect={(x) => {
+              if (!x) return
+              layout.dialog.connect(x.id)
+            }}
+            onOpenChange={(open) => {
+              if (open) {
+                layout.dialog.open("provider")
+              } else {
+                layout.dialog.close("provider")
+              }
+            }}
+          >
+            {(i) => (
+              <div class="px-1.25 w-full flex items-center gap-x-4">
+                <ProviderIcon
+                  data-slot="list-item-extra-icon"
+                  id={i.id as IconName}
+                  // TODO: clean this up after we update icon in models.dev
+                  classList={{
+                    "text-icon-weak-base": true,
+                    "size-4 mx-0.5": i.id === "opencode",
+                    "size-5": i.id !== "opencode",
+                  }}
+                />
+                <span>{i.name}</span>
+                <Show when={i.id === "opencode"}>
+                  <Tag>Recommended</Tag>
+                </Show>
+                <Show when={i.id === "anthropic"}>
+                  <div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
+                </Show>
+              </div>
+            )}
+          </SelectDialog>
+        </Show>
+        <Show when={layout.dialog.opened() === "connect"}>
+          {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({
+              method: undefined as undefined | ProviderAuthMethod,
+              authorization: undefined as undefined | ProviderAuthAuthorization,
+              state: "pending" as undefined | "pending" | "complete" | "error",
+              error: undefined as string | undefined,
+            })
+
+            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
+            function handleKey(e: KeyboardEvent) {
+              if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
+                return
+              }
+              if (e.key === "Escape") return
+              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 (
+              <Dialog
+                modal
+                defaultOpen
+                onOpenChange={(open) => {
+                  if (open) {
+                    layout.dialog.open("connect")
+                  } else {
+                    layout.dialog.close("connect")
+                  }
+                }}
+              >
+                <Dialog.Header class="px-4.5">
+                  <Dialog.Title class="flex items-center">
+                    <IconButton
+                      tabIndex={-1}
+                      icon="arrow-left"
+                      variant="ghost"
+                      onClick={() => {
+                        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)
+                          return
+                        }
+                        layout.dialog.open("provider")
+                      }}
+                    />
+                  </Dialog.Title>
+                  <Dialog.CloseButton tabIndex={-1} />
+                </Dialog.Header>
+                <Dialog.Body>
+                  <div class="flex flex-col gap-6 px-2.5 pb-3">
+                    <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">
+                        <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 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>
+                        </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,
+                            })
+
+                            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>
+                                )
+                              })}
+                            </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>
+                                )
+                              })}
+                            </Match>
+                          </Switch>
+                        </Match>
+                      </Switch>
+                    </div>
+                  </div>
+                </Dialog.Body>
+              </Dialog>
+            )
+          })}
+        </Show>
       </div>
+      <Toast.Region />
     </div>
   )
 }

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

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

+ 2 - 2
packages/desktop/tsconfig.json

@@ -14,11 +14,11 @@
     "strict": true,
     "noEmit": false,
     "emitDeclarationOnly": true,
-    "outDir": "ts-dist",
+    "outDir": "node_modules/.ts-dist",
     "isolatedModules": true,
     "paths": {
       "@/*": ["./src/*"]
     }
   },
-  "exclude": ["dist"]
+  "exclude": ["dist", "ts-dist"]
 }

+ 2 - 1
packages/enterprise/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/enterprise",
-  "version": "1.0.141",
+  "version": "1.0.150",
   "private": true,
   "type": "module",
   "scripts": {
@@ -20,6 +20,7 @@
     "@solidjs/meta": "catalog:",
     "hono": "catalog:",
     "hono-openapi": "catalog:",
+    "js-base64": "3.7.7",
     "luxon": "catalog:",
     "nitro": "3.0.1-alpha.1",
     "solid-js": "catalog:",

+ 0 - 2
packages/enterprise/src/entry-server.tsx

@@ -11,8 +11,6 @@ export default createHandler(() => (
           <title>OpenCode</title>
           <meta name="theme-color" content="#F8F7F7" />
           <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
-          <meta property="og:image" content="/social-share.png" />
-          <meta property="twitter:image" content="/social-share.png" />
           {assets}
         </head>
         <body class="antialiased overscroll-none select-none text-12-regular">

+ 3 - 0
packages/enterprise/src/routes/index.tsx

@@ -0,0 +1,3 @@
+export default function () {
+  return <div>Hello World</div>
+}

+ 258 - 213
packages/enterprise/src/routes/share/[shareID].tsx

@@ -23,6 +23,8 @@ import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precis
 import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr"
 import { clientOnly } from "@solidjs/start"
 import { type IconName } from "@opencode-ai/ui/icons/provider"
+import { Meta } from "@solidjs/meta"
+import { Base64 } from "js-base64"
 
 const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff })))
 
@@ -41,6 +43,7 @@ const getData = query(async (shareID) => {
   const data = await Share.data(shareID)
   const result: {
     sessionID: string
+    shareID: string
     session: Session[]
     session_diff: {
       [sessionID: string]: FileDiff[]
@@ -65,6 +68,7 @@ const getData = query(async (shareID) => {
     }
   } = {
     sessionID: share.sessionID,
+    shareID,
     session: [],
     session_diff: {
       [share.sessionID]: [],
@@ -134,10 +138,18 @@ const getData = query(async (shareID) => {
 
 export default function () {
   const params = useParams()
-  const data = createAsync(async () => {
-    if (!params.shareID) throw new Error("Missing shareID")
-    return getData(params.shareID)
-  })
+  const data = createAsync(
+    async () => {
+      if (!params.shareID) throw new Error("Missing shareID")
+      const now = Date.now()
+      const data = getData(params.shareID)
+      console.log("getData", Date.now() - now)
+      return data
+    },
+    {
+      deferStream: true,
+    },
+  )
 
   createEffect(() => {
     console.log(data())
@@ -153,244 +165,277 @@ export default function () {
         )
       }}
     >
+      <Meta name="robots" content="noindex, nofollow" />
       <Show when={data()}>
         {(data) => {
           const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id))
           if (!match().found) throw new Error(`Session ${data().sessionID} not found`)
           const info = createMemo(() => data().session[match().index])
+          const ogImage = createMemo(() => {
+            const models = new Set<string>()
+            const messages = data().message[data().sessionID] ?? []
+            for (const msg of messages) {
+              if (msg.role === "assistant" && msg.modelID) {
+                models.add(msg.modelID)
+              }
+            }
+            const modelIDs = Array.from(models)
+            const encodedTitle = encodeURIComponent(Base64.encode(encodeURIComponent(info().title.substring(0, 700))))
+            let modelParam: string
+            if (modelIDs.length === 1) {
+              modelParam = modelIDs[0]
+            } else if (modelIDs.length === 2) {
+              modelParam = encodeURIComponent(`${modelIDs[0]} & ${modelIDs[1]}`)
+            } else if (modelIDs.length > 2) {
+              modelParam = encodeURIComponent(`${modelIDs[0]} & ${modelIDs.length - 1} others`)
+            } else {
+              modelParam = "unknown"
+            }
+            const version = `v${info().version}`
+            return `https://social-cards.sst.dev/opencode-share/${encodedTitle}.png?model=${modelParam}&version=${version}&id=${data().shareID}`
+          })
 
           return (
-            <DiffComponentProvider component={ClientOnlyDiff}>
-              <DataProvider data={data()} directory={info().directory}>
-                {iife(() => {
-                  const [store, setStore] = createStore({
-                    messageId: undefined as string | undefined,
-                  })
-                  const messages = createMemo(() =>
-                    data().sessionID
-                      ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
-                          (a, b) => b.time.created - a.time.created,
-                        )
-                      : [],
-                  )
-                  const firstUserMessage = createMemo(() => messages().at(0))
-                  const activeMessage = createMemo(
-                    () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
-                  )
-                  function setActiveMessage(message: UserMessage | undefined) {
-                    if (message) {
-                      setStore("messageId", message.id)
-                    } else {
-                      setStore("messageId", undefined)
+            <>
+              <Meta name="description" content="opencode - The AI coding agent built for the terminal." />
+              <Meta property="og:image" content={ogImage()} />
+              <Meta name="twitter:image" content={ogImage()} />
+              <DiffComponentProvider component={ClientOnlyDiff}>
+                <DataProvider data={data()} directory={info().directory}>
+                  {iife(() => {
+                    const [store, setStore] = createStore({
+                      messageId: undefined as string | undefined,
+                    })
+                    const messages = createMemo(() =>
+                      data().sessionID
+                        ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
+                            (a, b) => a.time.created - b.time.created,
+                          )
+                        : [],
+                    )
+                    const firstUserMessage = createMemo(() => messages().at(0))
+                    const activeMessage = createMemo(
+                      () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
+                    )
+                    function setActiveMessage(message: UserMessage | undefined) {
+                      if (message) {
+                        setStore("messageId", message.id)
+                      } else {
+                        setStore("messageId", undefined)
+                      }
                     }
-                  }
-                  const provider = createMemo(() => activeMessage()?.model?.providerID)
-                  const modelID = createMemo(() => activeMessage()?.model?.modelID)
-                  const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
-                  const diffs = createMemo(() => {
-                    const diffs = data().session_diff[data().sessionID] ?? []
-                    const preloaded = data().session_diff_preload[data().sessionID] ?? []
-                    return diffs.map((diff) => ({
-                      ...diff,
-                      preloaded: preloaded.find((d) => d.newFile.name === diff.file),
-                    }))
-                  })
-                  const splitDiffs = createMemo(() => {
-                    const diffs = data().session_diff[data().sessionID] ?? []
-                    const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
-                    return diffs.map((diff) => ({
-                      ...diff,
-                      preloaded: preloaded.find((d) => d.newFile.name === diff.file),
-                    }))
-                  })
+                    const provider = createMemo(() => activeMessage()?.model?.providerID)
+                    const modelID = createMemo(() => activeMessage()?.model?.modelID)
+                    const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
+                    const diffs = createMemo(() => {
+                      const diffs = data().session_diff[data().sessionID] ?? []
+                      const preloaded = data().session_diff_preload[data().sessionID] ?? []
+                      return diffs.map((diff) => ({
+                        ...diff,
+                        preloaded: preloaded.find((d) => d.newFile.name === diff.file),
+                      }))
+                    })
+                    const splitDiffs = createMemo(() => {
+                      const diffs = data().session_diff[data().sessionID] ?? []
+                      const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
+                      return diffs.map((diff) => ({
+                        ...diff,
+                        preloaded: preloaded.find((d) => d.newFile.name === diff.file),
+                      }))
+                    })
 
-                  const title = () => (
-                    <div class="flex flex-col gap-4">
-                      <div class="h-8 flex gap-4 items-center justify-start self-stretch">
-                        <div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
-                          <Mark class="shrink-0 w-3 my-0.5" />
-                          <div class="text-12-mono text-text-base">v{info().version}</div>
-                        </div>
-                        <div class="flex gap-2 items-center">
-                          <ProviderIcon name={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
-                          <div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
-                        </div>
-                        <div class="text-12-regular text-text-weaker">
-                          {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
+                    const title = () => (
+                      <div class="flex flex-col gap-4">
+                        <div class="h-8 flex gap-4 items-center justify-start self-stretch">
+                          <div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
+                            <Mark class="shrink-0 w-3 my-0.5" />
+                            <div class="text-12-mono text-text-base">v{info().version}</div>
+                          </div>
+                          <div class="flex gap-2 items-center">
+                            <ProviderIcon id={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
+                            <div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
+                          </div>
+                          <div class="text-12-regular text-text-weaker">
+                            {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
+                          </div>
                         </div>
+                        <div class="text-left text-16-medium text-text-strong">{info().title}</div>
                       </div>
-                      <div class="text-left text-16-medium text-text-strong">{info().title}</div>
-                    </div>
-                  )
+                    )
 
-                  const turns = () => (
-                    <div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
-                      <div class="px-4">{title()}</div>
-                      <div class="flex flex-col gap-15 items-start justify-start mt-4">
-                        <For each={messages()}>
-                          {(message) => (
-                            <SessionTurn
-                              sessionID={data().sessionID}
-                              messageID={message.id}
-                              classes={{
-                                root: "min-w-0 w-full relative",
-                                content:
-                                  "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
-                                container: "px-4",
-                              }}
-                            />
-                          )}
-                        </For>
-                      </div>
-                      <div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
-                        <Logo class="w-58.5 opacity-12" />
+                    const turns = () => (
+                      <div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
+                        <div class="px-4">{title()}</div>
+                        <div class="flex flex-col gap-15 items-start justify-start mt-4">
+                          <For each={messages()}>
+                            {(message) => (
+                              <SessionTurn
+                                sessionID={data().sessionID}
+                                messageID={message.id}
+                                classes={{
+                                  root: "min-w-0 w-full relative",
+                                  content:
+                                    "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
+                                  container: "px-4",
+                                }}
+                              />
+                            )}
+                          </For>
+                        </div>
+                        <div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
+                          <Logo class="w-58.5 opacity-12" />
+                        </div>
                       </div>
-                    </div>
-                  )
+                    )
 
-                  const wide = createMemo(() => diffs().length === 0)
+                    const wide = createMemo(() => diffs().length === 0)
 
-                  return (
-                    <div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
-                      <header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
-                        <div class="">
-                          <a href="https://opencode.ai">
-                            <Mark />
-                          </a>
-                        </div>
-                        <div class="flex gap-3 items-center">
-                          <IconButton
-                            as={"a"}
-                            href="https://github.com/sst/opencode"
-                            target="_blank"
-                            icon="github"
-                            variant="ghost"
-                          />
-                          <IconButton
-                            as={"a"}
-                            href="https://opencode.ai/discord"
-                            target="_blank"
-                            icon="discord"
-                            variant="ghost"
-                          />
-                        </div>
-                      </header>
-                      <div class="select-text flex flex-col flex-1 min-h-0">
-                        <div
-                          classList={{ "hidden w-full flex-1 min-h-0": true, "md:flex": wide(), "lg:flex": !wide() }}
-                        >
+                    return (
+                      <div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
+                        <header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
+                          <div class="">
+                            <a href="https://opencode.ai">
+                              <Mark />
+                            </a>
+                          </div>
+                          <div class="flex gap-3 items-center">
+                            <IconButton
+                              as={"a"}
+                              href="https://github.com/sst/opencode"
+                              target="_blank"
+                              icon="github"
+                              variant="ghost"
+                            />
+                            <IconButton
+                              as={"a"}
+                              href="https://opencode.ai/discord"
+                              target="_blank"
+                              icon="discord"
+                              variant="ghost"
+                            />
+                          </div>
+                        </header>
+                        <div class="select-text flex flex-col flex-1 min-h-0">
                           <div
-                            classList={{
-                              "@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
-                              "mx-auto max-w-146": !wide(),
-                            }}
+                            classList={{ "hidden w-full flex-1 min-h-0": true, "md:flex": wide(), "lg:flex": !wide() }}
                           >
                             <div
                               classList={{
-                                "w-full flex justify-start items-start min-w-0": true,
-                                "max-w-146 mx-auto px-6": wide(),
-                                "pr-6 pl-18": !wide() && messages().length > 1,
-                                "px-6": !wide() && messages().length === 1,
+                                "@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
+                                "mx-auto max-w-146": !wide(),
                               }}
                             >
-                              {title()}
-                            </div>
-                            <div class="flex items-start justify-start h-full min-h-0">
-                              <SessionMessageRail
-                                messages={messages()}
-                                current={activeMessage()}
-                                onMessageSelect={setActiveMessage}
-                                wide={wide()}
-                              />
-                              <SessionTurn
-                                sessionID={data().sessionID}
-                                messageID={store.messageId ?? firstUserMessage()!.id!}
-                                classes={{
-                                  root: "grow",
-                                  content: "flex flex-col justify-between items-start",
-                                  container:
-                                    "w-full pb-20 " +
-                                    (wide() ? "max-w-146 mx-auto px-6" : messages().length > 1 ? "pr-6 pl-18" : "px-6"),
+                              <div
+                                classList={{
+                                  "w-full flex justify-start items-start min-w-0": true,
+                                  "max-w-146 mx-auto px-6": wide(),
+                                  "pr-6 pl-18": !wide() && messages().length > 1,
+                                  "px-6": !wide() && messages().length === 1,
                                 }}
                               >
-                                <div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
-                                  <Logo class="w-58.5 opacity-12" />
-                                </div>
-                              </SessionTurn>
-                            </div>
-                          </div>
-                          <Show when={diffs().length > 0}>
-                            <DiffComponentProvider component={SSRDiff}>
-                              <div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
-                                <SessionReview
-                                  class="@4xl:hidden"
-                                  diffs={diffs()}
-                                  classes={{
-                                    root: "pb-20",
-                                    header: "px-6",
-                                    container: "px-6",
-                                  }}
+                                {title()}
+                              </div>
+                              <div class="flex items-start justify-start h-full min-h-0">
+                                <SessionMessageRail
+                                  messages={messages()}
+                                  current={activeMessage()}
+                                  onMessageSelect={setActiveMessage}
+                                  wide={wide()}
                                 />
-                                <SessionReview
-                                  split
-                                  class="hidden @4xl:flex"
-                                  diffs={splitDiffs()}
+                                <SessionTurn
+                                  sessionID={data().sessionID}
+                                  messageID={store.messageId ?? firstUserMessage()!.id!}
                                   classes={{
-                                    root: "pb-20",
-                                    header: "px-6",
-                                    container: "px-6",
+                                    root: "grow",
+                                    content: "flex flex-col justify-between items-start",
+                                    container:
+                                      "w-full pb-20 " +
+                                      (wide()
+                                        ? "max-w-146 mx-auto px-6"
+                                        : messages().length > 1
+                                          ? "pr-6 pl-18"
+                                          : "px-6"),
                                   }}
-                                />
+                                >
+                                  <div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
+                                    <Logo class="w-58.5 opacity-12" />
+                                  </div>
+                                </SessionTurn>
                               </div>
-                            </DiffComponentProvider>
-                          </Show>
-                        </div>
-                        <Switch>
-                          <Match when={diffs().length > 0}>
-                            <Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
-                              <Tabs.List>
-                                <Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
-                                  Session
-                                </Tabs.Trigger>
-                                <Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
-                                  {diffs().length} Files Changed
-                                </Tabs.Trigger>
-                              </Tabs.List>
-                              <Tabs.Content value="session" class="!overflow-hidden">
-                                {turns()}
-                              </Tabs.Content>
-                              <Tabs.Content
-                                forceMount
-                                value="review"
-                                class="!overflow-hidden hidden data-[selected]:block"
-                              >
-                                <div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
-                                  <DiffComponentProvider component={SSRDiff}>
-                                    <SessionReview
-                                      diffs={diffs()}
-                                      classes={{
-                                        root: "pb-20",
-                                        header: "px-4",
-                                        container: "px-4",
-                                      }}
-                                    />
-                                  </DiffComponentProvider>
-                                </div>
-                              </Tabs.Content>
-                            </Tabs>
-                          </Match>
-                          <Match when={true}>
-                            <div classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}>
-                              {turns()}
                             </div>
-                          </Match>
-                        </Switch>
+                            <Show when={diffs().length > 0}>
+                              <DiffComponentProvider component={SSRDiff}>
+                                <div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
+                                  <SessionReview
+                                    class="@4xl:hidden"
+                                    diffs={diffs()}
+                                    classes={{
+                                      root: "pb-20",
+                                      header: "px-6",
+                                      container: "px-6",
+                                    }}
+                                  />
+                                  <SessionReview
+                                    split
+                                    class="hidden @4xl:flex"
+                                    diffs={splitDiffs()}
+                                    classes={{
+                                      root: "pb-20",
+                                      header: "px-6",
+                                      container: "px-6",
+                                    }}
+                                  />
+                                </div>
+                              </DiffComponentProvider>
+                            </Show>
+                          </div>
+                          <Switch>
+                            <Match when={diffs().length > 0}>
+                              <Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
+                                <Tabs.List>
+                                  <Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
+                                    Session
+                                  </Tabs.Trigger>
+                                  <Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
+                                    {diffs().length} Files Changed
+                                  </Tabs.Trigger>
+                                </Tabs.List>
+                                <Tabs.Content value="session" class="!overflow-hidden">
+                                  {turns()}
+                                </Tabs.Content>
+                                <Tabs.Content
+                                  forceMount
+                                  value="review"
+                                  class="!overflow-hidden hidden data-[selected]:block"
+                                >
+                                  <div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
+                                    <DiffComponentProvider component={SSRDiff}>
+                                      <SessionReview
+                                        diffs={diffs()}
+                                        classes={{
+                                          root: "pb-20",
+                                          header: "px-4",
+                                          container: "px-4",
+                                        }}
+                                      />
+                                    </DiffComponentProvider>
+                                  </div>
+                                </Tabs.Content>
+                              </Tabs>
+                            </Match>
+                            <Match when={true}>
+                              <div classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}>
+                                {turns()}
+                              </div>
+                            </Match>
+                          </Switch>
+                        </div>
                       </div>
-                    </div>
-                  )
-                })}
-              </DataProvider>
-            </DiffComponentProvider>
+                    )
+                  })}
+                </DataProvider>
+              </DiffComponentProvider>
+            </>
           )
         }}
       </Show>

+ 8 - 1
packages/enterprise/vite.config.ts

@@ -18,7 +18,14 @@ const nitroConfig: any = (() => {
 })()
 
 export default defineConfig({
-  plugins: [tailwindcss(), solidStart() as PluginOption, nitro(nitroConfig)],
+  plugins: [
+    tailwindcss(),
+    solidStart() as PluginOption,
+    nitro({
+      ...nitroConfig,
+      baseURL: process.env.OPENCODE_BASE_URL,
+    }),
+  ],
   server: {
     host: "0.0.0.0",
     allowedHosts: true,

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

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

+ 1 - 1
packages/function/package.json

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

+ 1 - 1
packages/opencode/Dockerfile

@@ -4,7 +4,7 @@ FROM alpine
 # On ephemeral containers, the cache is not useful
 ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
 ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
-RUN apk add libgcc libstdc++
+RUN apk add libgcc libstdc++ ripgrep
 ADD ./dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
 RUN opencode --version
 ENTRYPOINT ["opencode"]

+ 4 - 4
packages/opencode/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "1.0.141",
+  "version": "1.0.150",
   "name": "opencode",
   "type": "module",
   "private": true,
@@ -70,9 +70,9 @@
     "@opencode-ai/script": "workspace:*",
     "@opencode-ai/sdk": "workspace:*",
     "@opencode-ai/util": "workspace:*",
-    "@openrouter/ai-sdk-provider": "1.2.8",
-    "@opentui/core": "0.1.59",
-    "@opentui/solid": "0.1.59",
+    "@openrouter/ai-sdk-provider": "1.5.2",
+    "@opentui/core": "0.0.0-20251211-4403a69a",
+    "@opentui/solid": "0.0.0-20251211-4403a69a",
     "@parcel/watcher": "2.5.1",
     "@pierre/precision-diffs": "catalog:",
     "@solid-primitives/event-bus": "1.1.2",

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

@@ -28,7 +28,7 @@ import { Config } from "@/config/config"
 import { Todo } from "@/session/todo"
 import { z } from "zod"
 import { LoadAPIKeyError } from "ai"
-import type { OpencodeClient } from "@opencode-ai/sdk/v2"
+import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
 
 export namespace ACP {
   const log = Log.create({ service: "acp-agent" })
@@ -386,7 +386,7 @@ export namespace ACP {
 
         log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
 
-        const load = await this.loadSession({
+        const load = await this.loadSessionMode({
           cwd: directory,
           mcpServers: params.mcpServers,
           sessionId,
@@ -412,6 +412,242 @@ export namespace ACP {
     }
 
     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 model = await defaultModel(this.config, directory)
       const sessionId = params.sessionId

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

@@ -40,6 +40,37 @@ export class ACPSessionManager {
     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 {
     const session = this.sessions.get(sessionId)
     if (!session) {

+ 8 - 9
packages/opencode/src/bus/index.ts

@@ -7,7 +7,13 @@ import { GlobalBus } from "./global"
 export namespace Bus {
   const log = Log.create({ service: "bus" })
   type Subscription = (event: any) => void
-  const disposedEventType = "server.instance.disposed"
+
+  export const InstanceDisposed = BusEvent.define(
+    "server.instance.disposed",
+    z.object({
+      directory: z.string(),
+    }),
+  )
 
   const state = Instance.state(
     () => {
@@ -21,7 +27,7 @@ export namespace Bus {
       const wildcard = entry.subscriptions.get("*")
       if (!wildcard) return
       const event = {
-        type: disposedEventType,
+        type: InstanceDisposed.type,
         properties: {
           directory: Instance.directory,
         },
@@ -32,13 +38,6 @@ export namespace Bus {
     },
   )
 
-  export const InstanceDisposed = BusEvent.define(
-    disposedEventType,
-    z.object({
-      directory: z.string(),
-    }),
-  )
-
   export async function publish<Definition extends BusEvent.Definition>(
     def: Definition,
     properties: z.output<Definition["properties"]>,

+ 158 - 136
packages/opencode/src/cli/cmd/auth.ts

@@ -10,6 +10,154 @@ import { Config } from "../../config/config"
 import { Global } from "../../global"
 import { Plugin } from "../../plugin"
 import { Instance } from "../../project/instance"
+import type { Hooks } from "@opencode-ai/plugin"
+
+type PluginAuth = NonNullable<Hooks["auth"]>
+
+/**
+ * Handle plugin-based authentication flow.
+ * Returns true if auth was handled, false if it should fall through to default handling.
+ */
+async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): Promise<boolean> {
+  let index = 0
+  if (plugin.auth.methods.length > 1) {
+    const method = await prompts.select({
+      message: "Login method",
+      options: [
+        ...plugin.auth.methods.map((x, index) => ({
+          label: x.label,
+          value: index.toString(),
+        })),
+      ],
+    })
+    if (prompts.isCancel(method)) throw new UI.CancelledError()
+    index = parseInt(method)
+  }
+  const method = plugin.auth.methods[index]
+
+  // Handle prompts for all auth types
+  await new Promise((resolve) => setTimeout(resolve, 10))
+  const inputs: Record<string, string> = {}
+  if (method.prompts) {
+    for (const prompt of method.prompts) {
+      if (prompt.condition && !prompt.condition(inputs)) {
+        continue
+      }
+      if (prompt.type === "select") {
+        const value = await prompts.select({
+          message: prompt.message,
+          options: prompt.options,
+        })
+        if (prompts.isCancel(value)) throw new UI.CancelledError()
+        inputs[prompt.key] = value
+      } else {
+        const value = await prompts.text({
+          message: prompt.message,
+          placeholder: prompt.placeholder,
+          validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined,
+        })
+        if (prompts.isCancel(value)) throw new UI.CancelledError()
+        inputs[prompt.key] = value
+      }
+    }
+  }
+
+  if (method.type === "oauth") {
+    const authorize = await method.authorize(inputs)
+
+    if (authorize.url) {
+      prompts.log.info("Go to: " + authorize.url)
+    }
+
+    if (authorize.method === "auto") {
+      if (authorize.instructions) {
+        prompts.log.info(authorize.instructions)
+      }
+      const spinner = prompts.spinner()
+      spinner.start("Waiting for authorization...")
+      const result = await authorize.callback()
+      if (result.type === "failed") {
+        spinner.stop("Failed to authorize", 1)
+      }
+      if (result.type === "success") {
+        const saveProvider = result.provider ?? provider
+        if ("refresh" in result) {
+          const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
+          await Auth.set(saveProvider, {
+            type: "oauth",
+            refresh,
+            access,
+            expires,
+            ...extraFields,
+          })
+        }
+        if ("key" in result) {
+          await Auth.set(saveProvider, {
+            type: "api",
+            key: result.key,
+          })
+        }
+        spinner.stop("Login successful")
+      }
+    }
+
+    if (authorize.method === "code") {
+      const code = await prompts.text({
+        message: "Paste the authorization code here: ",
+        validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+      })
+      if (prompts.isCancel(code)) throw new UI.CancelledError()
+      const result = await authorize.callback(code)
+      if (result.type === "failed") {
+        prompts.log.error("Failed to authorize")
+      }
+      if (result.type === "success") {
+        const saveProvider = result.provider ?? provider
+        if ("refresh" in result) {
+          const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
+          await Auth.set(saveProvider, {
+            type: "oauth",
+            refresh,
+            access,
+            expires,
+            ...extraFields,
+          })
+        }
+        if ("key" in result) {
+          await Auth.set(saveProvider, {
+            type: "api",
+            key: result.key,
+          })
+        }
+        prompts.log.success("Login successful")
+      }
+    }
+
+    prompts.outro("Done")
+    return true
+  }
+
+  if (method.type === "api") {
+    if (method.authorize) {
+      const result = await method.authorize(inputs)
+      if (result.type === "failed") {
+        prompts.log.error("Failed to authorize")
+      }
+      if (result.type === "success") {
+        const saveProvider = result.provider ?? provider
+        await Auth.set(saveProvider, {
+          type: "api",
+          key: result.key,
+        })
+        prompts.log.success("Login successful")
+      }
+      prompts.outro("Done")
+      return true
+    }
+  }
+
+  return false
+}
 
 export const AuthCommand = cmd({
   command: "auth",
@@ -160,142 +308,8 @@ export const AuthLoginCommand = cmd({
 
         const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
         if (plugin && plugin.auth) {
-          let index = 0
-          if (plugin.auth.methods.length > 1) {
-            const method = await prompts.select({
-              message: "Login method",
-              options: [
-                ...plugin.auth.methods.map((x, index) => ({
-                  label: x.label,
-                  value: index.toString(),
-                })),
-              ],
-            })
-            if (prompts.isCancel(method)) throw new UI.CancelledError()
-            index = parseInt(method)
-          }
-          const method = plugin.auth.methods[index]
-
-          // Handle prompts for all auth types
-          await new Promise((resolve) => setTimeout(resolve, 10))
-          const inputs: Record<string, string> = {}
-          if (method.prompts) {
-            for (const prompt of method.prompts) {
-              if (prompt.condition && !prompt.condition(inputs)) {
-                continue
-              }
-              if (prompt.type === "select") {
-                const value = await prompts.select({
-                  message: prompt.message,
-                  options: prompt.options,
-                })
-                if (prompts.isCancel(value)) throw new UI.CancelledError()
-                inputs[prompt.key] = value
-              } else {
-                const value = await prompts.text({
-                  message: prompt.message,
-                  placeholder: prompt.placeholder,
-                  validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined,
-                })
-                if (prompts.isCancel(value)) throw new UI.CancelledError()
-                inputs[prompt.key] = value
-              }
-            }
-          }
-
-          if (method.type === "oauth") {
-            const authorize = await method.authorize(inputs)
-
-            if (authorize.url) {
-              prompts.log.info("Go to: " + authorize.url)
-            }
-
-            if (authorize.method === "auto") {
-              if (authorize.instructions) {
-                prompts.log.info(authorize.instructions)
-              }
-              const spinner = prompts.spinner()
-              spinner.start("Waiting for authorization...")
-              const result = await authorize.callback()
-              if (result.type === "failed") {
-                spinner.stop("Failed to authorize", 1)
-              }
-              if (result.type === "success") {
-                const saveProvider = result.provider ?? provider
-                if ("refresh" in result) {
-                  const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
-                  await Auth.set(saveProvider, {
-                    type: "oauth",
-                    refresh,
-                    access,
-                    expires,
-                    ...extraFields,
-                  })
-                }
-                if ("key" in result) {
-                  await Auth.set(saveProvider, {
-                    type: "api",
-                    key: result.key,
-                  })
-                }
-                spinner.stop("Login successful")
-              }
-            }
-
-            if (authorize.method === "code") {
-              const code = await prompts.text({
-                message: "Paste the authorization code here: ",
-                validate: (x) => (x && x.length > 0 ? undefined : "Required"),
-              })
-              if (prompts.isCancel(code)) throw new UI.CancelledError()
-              const result = await authorize.callback(code)
-              if (result.type === "failed") {
-                prompts.log.error("Failed to authorize")
-              }
-              if (result.type === "success") {
-                const saveProvider = result.provider ?? provider
-                if ("refresh" in result) {
-                  const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
-                  await Auth.set(saveProvider, {
-                    type: "oauth",
-                    refresh,
-                    access,
-                    expires,
-                    ...extraFields,
-                  })
-                }
-                if ("key" in result) {
-                  await Auth.set(saveProvider, {
-                    type: "api",
-                    key: result.key,
-                  })
-                }
-                prompts.log.success("Login successful")
-              }
-            }
-
-            prompts.outro("Done")
-            return
-          }
-
-          if (method.type === "api") {
-            if (method.authorize) {
-              const result = await method.authorize(inputs)
-              if (result.type === "failed") {
-                prompts.log.error("Failed to authorize")
-              }
-              if (result.type === "success") {
-                const saveProvider = result.provider ?? provider
-                await Auth.set(saveProvider, {
-                  type: "api",
-                  key: result.key,
-                })
-                prompts.log.success("Login successful")
-              }
-              prompts.outro("Done")
-              return
-            }
-          }
+          const handled = await handlePluginAuth({ auth: plugin.auth }, provider)
+          if (handled) return
         }
 
         if (provider === "other") {
@@ -306,6 +320,14 @@ export const AuthLoginCommand = cmd({
           if (prompts.isCancel(provider)) throw new UI.CancelledError()
           provider = provider.replace(/^@ai-sdk\//, "")
           if (prompts.isCancel(provider)) throw new UI.CancelledError()
+
+          // Check if a plugin provides auth for this custom provider
+          const customPlugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
+          if (customPlugin && customPlugin.auth) {
+            const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider)
+            if (handled) return
+          }
+
           prompts.log.warn(
             `This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
           )

+ 46 - 22
packages/opencode/src/cli/cmd/github.ts

@@ -124,6 +124,8 @@ type IssueQueryResponse = {
   }
 }
 
+const AGENT_USERNAME = "opencode-agent[bot]"
+const AGENT_REACTION = "eyes"
 const WORKFLOW_FILE = ".github/workflows/opencode.yml"
 
 export const GithubCommand = cmd({
@@ -403,12 +405,12 @@ export const GithubRunCommand = cmd({
       let appToken: string
       let octoRest: Octokit
       let octoGraph: typeof graphql
-      let commentId: number
       let gitConfig: string
       let session: { id: string; title: string; version: string }
       let shareId: string | undefined
       let exitCode = 0
       type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
+      const triggerCommentId = payload.comment.id
 
       try {
         const actionToken = isMock ? args.token! : await getOidcToken()
@@ -422,8 +424,7 @@ export const GithubRunCommand = cmd({
         await configureGit(appToken)
         await assertPermissions()
 
-        const comment = await createComment()
-        commentId = comment.data.id
+        await addReaction()
 
         // Setup opencode session
         const repoData = await fetchRepo()
@@ -455,7 +456,8 @@ export const GithubRunCommand = cmd({
               await pushToLocalBranch(summary, uncommittedChanges)
             }
             const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
-            await updateComment(`${response}${footer({ image: !hasShared })}`)
+            await createComment(`${response}${footer({ image: !hasShared })}`)
+            await removeReaction()
           }
           // Fork PR
           else {
@@ -469,7 +471,8 @@ export const GithubRunCommand = cmd({
               await pushToForkBranch(summary, prData, uncommittedChanges)
             }
             const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
-            await updateComment(`${response}${footer({ image: !hasShared })}`)
+            await createComment(`${response}${footer({ image: !hasShared })}`)
+            await removeReaction()
           }
         }
         // Issue
@@ -489,9 +492,11 @@ export const GithubRunCommand = cmd({
               summary,
               `${response}\n\nCloses #${issueId}${footer({ image: true })}`,
             )
-            await updateComment(`Created PR #${pr}${footer({ image: true })}`)
+            await createComment(`Created PR #${pr}${footer({ image: true })}`)
+            await removeReaction()
           } else {
-            await updateComment(`${response}${footer({ image: true })}`)
+            await createComment(`${response}${footer({ image: true })}`)
+            await removeReaction()
           }
         }
       } catch (e: any) {
@@ -503,7 +508,8 @@ export const GithubRunCommand = cmd({
         } else if (e instanceof Error) {
           msg = e.message
         }
-        await updateComment(`${msg}${footer()}`)
+        await createComment(`${msg}${footer()}`)
+        await removeReaction()
         core.setFailed(msg)
         // Also output the clean error message for the action to capture
         //core.setOutput("prepare_error", e.message);
@@ -808,8 +814,8 @@ export const GithubRunCommand = cmd({
 
         await $`git config --local --unset-all ${config}`
         await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"`
-        await $`git config --global user.name "opencode-agent[bot]"`
-        await $`git config --global user.email "opencode-agent[bot]@users.noreply.github.com"`
+        await $`git config --global user.name "${AGENT_USERNAME}"`
+        await $`git config --global user.email "${AGENT_USERNAME}@users.noreply.github.com"`
       }
 
       async function restoreGitConfig() {
@@ -931,24 +937,42 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
         if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
       }
 
-      async function createComment() {
-        console.log("Creating comment...")
-        return await octoRest.rest.issues.createComment({
+      async function addReaction() {
+        console.log("Adding reaction...")
+        return await octoRest.rest.reactions.createForIssueComment({
           owner,
           repo,
-          issue_number: issueId,
-          body: `[Working...](${runUrl})`,
+          comment_id: triggerCommentId,
+          content: AGENT_REACTION,
         })
       }
 
-      async function updateComment(body: string) {
-        if (!commentId) return
+      async function removeReaction() {
+        console.log("Removing reaction...")
+        const reactions = await octoRest.rest.reactions.listForIssueComment({
+          owner,
+          repo,
+          comment_id: triggerCommentId,
+          content: AGENT_REACTION,
+        })
+
+        const eyesReaction = reactions.data.find((r) => r.user?.login === AGENT_USERNAME)
+        if (!eyesReaction) return
 
-        console.log("Updating comment...")
-        return await octoRest.rest.issues.updateComment({
+        await octoRest.rest.reactions.deleteForIssueComment({
           owner,
           repo,
-          comment_id: commentId,
+          comment_id: triggerCommentId,
+          reaction_id: eyesReaction.id,
+        })
+      }
+
+      async function createComment(body: string) {
+        console.log("Creating comment...")
+        return await octoRest.rest.issues.createComment({
+          owner,
+          repo,
+          issue_number: issueId,
           body,
         })
       }
@@ -1029,7 +1053,7 @@ query($owner: String!, $repo: String!, $number: Int!) {
         const comments = (issue.comments?.nodes || [])
           .filter((c) => {
             const id = parseInt(c.databaseId)
-            return id !== commentId && id !== payload.comment.id
+            return id !== payload.comment.id
           })
           .map((c) => `  - ${c.author.login} at ${c.createdAt}: ${c.body}`)
 
@@ -1148,7 +1172,7 @@ query($owner: String!, $repo: String!, $number: Int!) {
         const comments = (pr.comments?.nodes || [])
           .filter((c) => {
             const id = parseInt(c.databaseId)
-            return id !== commentId && id !== payload.comment.id
+            return id !== payload.comment.id
           })
           .map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
 

+ 38 - 18
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -107,7 +107,9 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
     render(
       () => {
         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}>
               <ExitProvider onExit={onExit}>
                 <KVProvider>
@@ -144,7 +146,7 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
         targetFps: 60,
         gatherStats: false,
         exitOnCtrlC: false,
-        useKittyKeyboard: true,
+        useKittyKeyboard: {},
       },
     )
   })
@@ -173,20 +175,20 @@ function App() {
   // Update terminal window title based on current route and session
   createEffect(() => {
     if (route.data.type === "home") {
-      renderer.setTerminalTitle("opencode")
+      renderer.setTerminalTitle("OpenCode")
       return
     }
 
     if (route.data.type === "session") {
       const session = sync.session.get(route.data.sessionID)
       if (!session || SessionApi.isDefaultTitle(session.title)) {
-        renderer.setTerminalTitle("opencode")
+        renderer.setTerminalTitle("OpenCode")
         return
       }
 
       // Truncate title to 40 chars max
       const title = session.title.length > 40 ? session.title.slice(0, 37) + "..." : session.title
-      renderer.setTerminalTitle(`oc | ${title}`)
+      renderer.setTerminalTitle(`OC | ${title}`)
     }
   })
 
@@ -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()
   useKeyboard((evt) => {
     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")
 
+  // 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) {
     issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`)
   }
@@ -567,27 +583,31 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () =>
   }
 
   return (
-    <box flexDirection="column" gap={1}>
+    <box flexDirection="column" gap={1} backgroundColor={colors.bg}>
       <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>
-        {copied() && <text>Successfully copied</text>}
+        {copied() && <text fg={colors.muted}>Successfully copied</text>}
       </box>
       <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 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>
       <scrollbox height={Math.floor(term().height * 0.7)}>
-        <text>{props.error.stack}</text>
+        <text fg={colors.muted}>{props.error.stack}</text>
       </scrollbox>
-      <text>{props.error.message}</text>
+      <text fg={colors.text}>{props.error.message}</text>
     </box>
   )
 }

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

@@ -199,7 +199,7 @@ export function DialogModel(props: { providerID?: string }) {
     <DialogSelect
       keybind={[
         {
-          keybind: { ctrl: true, name: "a", meta: false, shift: false, leader: false },
+          keybind: Keybind.parse("ctrl+a")[0],
           title: connected() ? "Connect provider" : "View all providers",
           onTrigger() {
             dialog.replace(() => <DialogProvider />)

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

@@ -122,7 +122,9 @@ function AutoMethod(props: AutoMethodProps) {
   return (
     <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
       <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>
       </box>
       <box gap={1}>
@@ -198,7 +200,7 @@ function ApiMethod(props: ApiMethodProps) {
             <text fg={theme.textMuted}>
               OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key.
             </text>
-            <text>
+            <text fg={theme.text}>
               Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
             </text>
           </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 { useSDK } from "../context/sdk"
 import { DialogSessionRename } from "./dialog-session-rename"
+import "opentui-spinner/solid"
 
 export function DialogSessionList() {
   const dialog = useDialog()
@@ -22,6 +23,8 @@ export function DialogSessionList() {
 
   const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
 
+  const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
+
   const options = createMemo(() => {
     const today = new Date().toDateString()
     return sync.data.session
@@ -34,12 +37,15 @@ export function DialogSessionList() {
           category = "Today"
         }
         const isDeleting = toDelete() === x.id
+        const status = sync.data.session_status[x.id]
+        const isWorking = status?.type === "busy"
         return {
           title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title,
           bg: isDeleting ? theme.error : undefined,
           value: x.id,
           category,
           footer: Locale.time(x.time.updated),
+          gutter: isWorking ? <spinner frames={spinnerFrames} interval={80} color={theme.primary} /> : undefined,
         }
       })
       .slice(0, 150)

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

@@ -19,7 +19,7 @@ export function DialogStatus() {
         </text>
         <text fg={theme.textMuted}>esc</text>
       </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>
           <text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
           <For each={Object.entries(sync.data.mcp)}>

+ 93 - 35
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -10,6 +10,7 @@ import { useSync } from "@tui/context/sync"
 import { Identifier } from "@/id/id"
 import { createStore, produce } from "solid-js/store"
 import { useKeybind } from "@tui/context/keybind"
+import { Keybind } from "@/util/keybind"
 import { usePromptHistory, type PromptInfo } from "./history"
 import { type AutocompleteRef, Autocomplete } from "./autocomplete"
 import { useCommandDialog } from "../dialog-command"
@@ -24,6 +25,7 @@ import { Locale } from "@/util/locale"
 import { createColors, createFrames } from "../../ui/spinner.ts"
 import { useDialog } from "@tui/ui/dialog"
 import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
+import { DialogAlert } from "../../ui/dialog-alert"
 import { useToast } from "../../ui/toast"
 
 export type PromptProps = {
@@ -46,6 +48,61 @@ export type PromptRef = {
 
 const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
 
+const TEXTAREA_ACTIONS = [
+  "submit",
+  "newline",
+  "move-left",
+  "move-right",
+  "move-up",
+  "move-down",
+  "select-left",
+  "select-right",
+  "select-up",
+  "select-down",
+  "line-home",
+  "line-end",
+  "select-line-home",
+  "select-line-end",
+  "visual-line-home",
+  "visual-line-end",
+  "select-visual-line-home",
+  "select-visual-line-end",
+  "buffer-home",
+  "buffer-end",
+  "select-buffer-home",
+  "select-buffer-end",
+  "delete-line",
+  "delete-to-line-end",
+  "delete-to-line-start",
+  "backspace",
+  "delete",
+  "undo",
+  "redo",
+  "word-forward",
+  "word-backward",
+  "select-word-forward",
+  "select-word-backward",
+  "delete-word-forward",
+  "delete-word-backward",
+] as const
+
+function mapTextareaKeybindings(
+  keybinds: Record<string, Keybind.Info[]>,
+  action: (typeof TEXTAREA_ACTIONS)[number],
+): KeyBinding[] {
+  const configKey = `input_${action.replace(/-/g, "_")}`
+  const bindings = keybinds[configKey]
+  if (!bindings) return []
+  return bindings.map((binding) => ({
+    name: binding.name,
+    ctrl: binding.ctrl || undefined,
+    meta: binding.meta || undefined,
+    shift: binding.shift || undefined,
+    super: binding.super || undefined,
+    action,
+  }))
+}
+
 export function Prompt(props: PromptProps) {
   let input: TextareaRenderable
   let anchor: BoxRenderable
@@ -76,26 +133,12 @@ export function Prompt(props: PromptProps) {
   }
 
   const textareaKeybindings = createMemo(() => {
-    const newlineBindings = keybind.all.input_newline || []
-    const submitBindings = keybind.all.input_submit || []
+    const keybinds = keybind.all
 
     return [
       { name: "return", action: "submit" },
       { name: "return", meta: true, action: "newline" },
-      ...newlineBindings.map((binding) => ({
-        name: binding.name,
-        ctrl: binding.ctrl || undefined,
-        meta: binding.meta || undefined,
-        shift: binding.shift || undefined,
-        action: "newline" as const,
-      })),
-      ...submitBindings.map((binding) => ({
-        name: binding.name,
-        ctrl: binding.ctrl || undefined,
-        meta: binding.meta || undefined,
-        shift: binding.shift || undefined,
-        action: "submit" as const,
-      })),
+      ...TEXTAREA_ACTIONS.flatMap((action) => mapTextareaKeybindings(keybinds, action)),
     ] satisfies KeyBinding[]
   })
 
@@ -199,7 +242,7 @@ export function Prompt(props: PromptProps) {
           const content = await Editor.open({ value, renderer })
           if (!content) return
 
-          input.setText(content, { history: false })
+          input.setText(content)
 
           // Update positions for nonTextParts based on their location in new content
           // Filter out parts whose virtual text was deleted
@@ -390,7 +433,7 @@ export function Prompt(props: PromptProps) {
       input.blur()
     },
     set(prompt) {
-      input.setText(prompt.input, { history: false })
+      input.setText(prompt.input)
       setStore("prompt", prompt)
       restoreExtmarksFromParts(prompt.parts)
       input.gotoBufferEnd()
@@ -410,6 +453,11 @@ export function Prompt(props: PromptProps) {
     if (props.disabled) return
     if (autocomplete.visible) return
     if (!store.prompt.input) return
+    const trimmed = store.prompt.input.trim()
+    if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
+      exit()
+      return
+    }
     const selectedModel = local.model.current()
     if (!selectedModel) {
       promptModelWarning()
@@ -683,17 +731,6 @@ export function Prompt(props: PromptProps) {
                   setStore("extmarkToPartIndex", new Map())
                   return
                 }
-                if (keybind.match("input_forward_delete", e) && store.prompt.input !== "") {
-                  const cursorOffset = input.cursorOffset
-                  if (cursorOffset < input.plainText.length) {
-                    const text = input.plainText
-                    const newText = text.slice(0, cursorOffset) + text.slice(cursorOffset + 1)
-                    input.setText(newText)
-                    input.cursorOffset = cursorOffset
-                  }
-                  e.preventDefault()
-                  return
-                }
                 if (keybind.match("app_exit", e)) {
                   await exit()
                   return
@@ -720,7 +757,7 @@ export function Prompt(props: PromptProps) {
                     const item = history.move(direction, input.plainText)
 
                     if (item) {
-                      input.setText(item.input, { history: false })
+                      input.setText(item.input)
                       setStore("prompt", item)
                       restoreExtmarksFromParts(item.parts)
                       e.preventDefault()
@@ -872,9 +909,14 @@ export function Prompt(props: PromptProps) {
                       if (!r) return
                       if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
                         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
                     })
+                    const isTruncated = createMemo(() => {
+                      const r = retry()
+                      if (!r) return false
+                      return r.message.length > 120
+                    })
                     const [seconds, setSeconds] = createSignal(0)
                     onMount(() => {
                       const timer = setInterval(() => {
@@ -886,12 +928,28 @@ export function Prompt(props: PromptProps) {
                         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 (
                       <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>
                     )
                   })()}

+ 2 - 1
packages/opencode/src/cli/cmd/tui/context/directory.ts

@@ -5,7 +5,8 @@ import { Global } from "@/global"
 export function useDirectory() {
   const sync = useSync()
   return createMemo(() => {
-    const result = process.cwd().replace(Global.Path.home, "~")
+    const directory = sync.data.path.directory || process.cwd()
+    const result = directory.replace(Global.Path.home, "~")
     if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
     return result
   })

+ 4 - 14
packages/opencode/src/cli/cmd/tui/context/keybind.tsx

@@ -73,21 +73,11 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
         return store.leader
       },
       parse(evt: ParsedKey): Keybind.Info {
-        if (evt.name === "\x1F")
-          return {
-            ctrl: true,
-            name: "_",
-            shift: false,
-            leader: false,
-            meta: false,
-          }
-        return {
-          ctrl: evt.ctrl,
-          name: evt.name,
-          shift: evt.shift,
-          leader: store.leader,
-          meta: evt.meta,
+        // Handle special case for Ctrl+Underscore (represented as \x1F)
+        if (evt.name === "\x1F") {
+          return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader)
         }
+        return Keybind.fromParsedKey(evt, store.leader)
       },
       match(key: keyof KeybindsConfig, evt: ParsedKey) {
         const keybind = keybinds()[key]

+ 4 - 0
packages/opencode/src/cli/cmd/tui/context/sync.tsx

@@ -24,6 +24,7 @@ import type { Snapshot } from "@/snapshot"
 import { useExit } from "./exit"
 import { batch, onMount } from "solid-js"
 import { Log } from "@/util/log"
+import type { Path } from "@opencode-ai/sdk"
 
 export const { use: useSync, provider: SyncProvider } = createSimpleContext({
   name: "Sync",
@@ -62,6 +63,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
       }
       formatter: FormatterStatus[]
       vcs: VcsInfo | undefined
+      path: Path
     }>({
       provider_next: {
         all: [],
@@ -86,6 +88,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
       mcp: {},
       formatter: [],
       vcs: undefined,
+      path: { state: "", config: "", worktree: "", directory: "" },
     })
 
     const sdk = useSDK()
@@ -286,6 +289,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
             sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
             sdk.client.provider.auth().then((x) => setStore("provider_auth", x.data ?? {})),
             sdk.client.vcs.get().then((x) => setStore("vcs", x.data)),
+            sdk.client.path.get().then((x) => setStore("path", x.data!)),
           ]).then(() => {
             setStore("status", "complete")
           })

+ 16 - 16
packages/opencode/src/cli/cmd/tui/context/theme/orng.json

@@ -17,7 +17,7 @@
     "darkAccent": "#FFF7F1",
     "darkRed": "#e06c75",
     "darkOrange": "#EC5B2B",
-    "darkGreen": "#7fd88f",
+    "darkBlue": "#6ba1e6",
     "darkCyan": "#56b6c2",
     "darkYellow": "#e5c07b",
     "lightStep1": "#ffffff",
@@ -36,7 +36,7 @@
     "lightAccent": "#c94d24",
     "lightRed": "#d1383d",
     "lightOrange": "#EC5B2B",
-    "lightGreen": "#3d9a57",
+    "lightBlue": "#0062d1",
     "lightCyan": "#318795",
     "lightYellow": "#b0851f"
   },
@@ -62,8 +62,8 @@
       "light": "lightOrange"
     },
     "success": {
-      "dark": "darkGreen",
-      "light": "lightGreen"
+      "dark": "darkBlue",
+      "light": "lightBlue"
     },
     "info": {
       "dark": "darkCyan",
@@ -102,8 +102,8 @@
       "light": "lightStep6"
     },
     "diffAdded": {
-      "dark": "#4fd6be",
-      "light": "#1e725c"
+      "dark": "#6ba1e6",
+      "light": "#0062d1"
     },
     "diffRemoved": {
       "dark": "#c53b53",
@@ -118,16 +118,16 @@
       "light": "#7086b5"
     },
     "diffHighlightAdded": {
-      "dark": "#b8db87",
-      "light": "#4db380"
+      "dark": "#6ba1e6",
+      "light": "#0062d1"
     },
     "diffHighlightRemoved": {
       "dark": "#e26a75",
       "light": "#f52a65"
     },
     "diffAddedBg": {
-      "dark": "#20303b",
-      "light": "#d5e5d5"
+      "dark": "#1a2a3d",
+      "light": "#e0edfa"
     },
     "diffRemovedBg": {
       "dark": "#37222c",
@@ -142,8 +142,8 @@
       "light": "lightStep3"
     },
     "diffAddedLineNumberBg": {
-      "dark": "#1b2b34",
-      "light": "#c5d5c5"
+      "dark": "#162535",
+      "light": "#d0e5f5"
     },
     "diffRemovedLineNumberBg": {
       "dark": "#2d1f26",
@@ -166,8 +166,8 @@
       "light": "lightCyan"
     },
     "markdownCode": {
-      "dark": "darkGreen",
-      "light": "lightGreen"
+      "dark": "darkBlue",
+      "light": "lightBlue"
     },
     "markdownBlockQuote": {
       "dark": "#FFF7F1",
@@ -222,8 +222,8 @@
       "light": "lightRed"
     },
     "syntaxString": {
-      "dark": "darkGreen",
-      "light": "lightGreen"
+      "dark": "darkBlue",
+      "light": "lightBlue"
     },
     "syntaxNumber": {
       "dark": "#FFF7F1",

+ 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 { DialogMessage } from "./dialog-message"
 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 dialog = useDialog()
 
@@ -26,10 +31,13 @@ export function DialogTimeline(props: { sessionID: string; onMove: (messageID: s
         value: message.id,
         footer: Locale.time(message.time.created),
         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
   })
 

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

@@ -10,7 +10,7 @@ export function Footer() {
   const { theme } = useTheme()
   const sync = useSync()
   const route = useRoute()
-  const mcp = createMemo(() => Object.keys(sync.data.mcp))
+  const mcp = createMemo(() => Object.values(sync.data.mcp).filter((x) => x.status === "connected").length)
   const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed"))
   const lsp = createMemo(() => Object.keys(sync.data.lsp))
   const permissions = createMemo(() => {
@@ -66,7 +66,7 @@ export function Footer() {
             <text fg={theme.text}>
               <span style={{ fg: theme.success }}>•</span> {lsp().length} LSP
             </text>
-            <Show when={mcp().length}>
+            <Show when={mcp()}>
               <text fg={theme.text}>
                 <Switch>
                   <Match when={mcpError()}>
@@ -76,7 +76,7 @@ export function Footer() {
                     <span style={{ fg: theme.success }}>⊙ </span>
                   </Match>
                 </Switch>
-                {mcp().length} MCP
+                {mcp()} MCP
               </text>
             </Show>
             <text fg={theme.textMuted}>/status</text>

+ 11 - 6
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)
             }}
             sessionID={route.sessionID}
+            setPrompt={(promptInfo) => prompt.set(promptInfo)}
           />
         ))
       },
@@ -894,7 +895,7 @@ export function Session() {
                                 <box marginTop={1}>
                                   <For each={revert()!.diffFiles}>
                                     {(file) => (
-                                      <text>
+                                      <text fg={theme.text}>
                                         {file.filename}
                                         <Show when={file.additions > 0}>
                                           <span style={{ fg: theme.diffAdded }}> +{file.additions}</span>
@@ -1503,11 +1504,15 @@ ToolRegistry.register<typeof TaskTool>({
         <Show when={props.metadata.summary?.length}>
           <box>
             <For each={props.metadata.summary ?? []}>
-              {(task) => (
-                <text style={{ fg: task.state.status === "error" ? theme.error : theme.textMuted }}>
-                  ∟ {Locale.titlecase(task.tool)} {task.state.status === "completed" ? task.state.title : ""}
-                </text>
-              )}
+              {(task, index) => {
+                const summary = props.metadata.summary ?? []
+                return (
+                  <text style={{ fg: task.state.status === "error" ? theme.error : theme.textMuted }}>
+                    {index() === summary.length - 1 ? "└" : "├"} {Locale.titlecase(task.tool)}{" "}
+                    {task.state.status === "completed" ? task.state.title : ""}
+                  </text>
+                )
+              }}
             </For>
           </box>
         </Show>

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

@@ -259,9 +259,11 @@ export function Sidebar(props: { sessionID: string }) {
               flexDirection="row"
               gap={1}
             >
-              <text flexShrink={0}>⬖</text>
+              <text flexShrink={0} fg={theme.text}>
+                ⬖
+              </text>
               <box flexGrow={1} gap={1}>
-                <text>
+                <text fg={theme.text}>
                   <b>Getting started</b>
                 </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
                 </text>
                 <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>
                 </box>
               </box>

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

@@ -22,7 +22,9 @@ export function DialogAlert(props: DialogAlertProps) {
   return (
     <box paddingLeft={2} paddingRight={2} gap={1}>
       <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>
       </box>
       <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 (
     <box paddingLeft={2} paddingRight={2} gap={1}>
       <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>
       </box>
       <box paddingBottom={1}>

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

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

+ 80 - 3
packages/opencode/src/config/config.ts

@@ -1,5 +1,6 @@
 import { Log } from "../util/log"
 import path from "path"
+import { pathToFileURL } from "url"
 import os from "os"
 import z from "zod"
 import { Filesystem } from "../util/filesystem"
@@ -297,7 +298,7 @@ export namespace Config {
       dot: true,
       cwd: dir,
     })) {
-      plugins.push("file://" + item)
+      plugins.push(pathToFileURL(item).href)
     }
     return plugins
   }
@@ -464,10 +465,86 @@ export namespace Config {
       agent_cycle: z.string().optional().default("tab").describe("Next agent"),
       agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
       input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
-      input_forward_delete: z.string().optional().default("ctrl+d").describe("Forward delete"),
       input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
       input_submit: z.string().optional().default("return").describe("Submit input"),
-      input_newline: z.string().optional().default("shift+return,ctrl+j").describe("Insert newline in input"),
+      input_newline: z
+        .string()
+        .optional()
+        .default("shift+return,ctrl+return,alt+return,ctrl+j")
+        .describe("Insert newline in input"),
+      input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"),
+      input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"),
+      input_move_up: z.string().optional().default("up").describe("Move cursor up in input"),
+      input_move_down: z.string().optional().default("down").describe("Move cursor down in input"),
+      input_select_left: z.string().optional().default("shift+left").describe("Select left in input"),
+      input_select_right: z.string().optional().default("shift+right").describe("Select right in input"),
+      input_select_up: z.string().optional().default("shift+up").describe("Select up in input"),
+      input_select_down: z.string().optional().default("shift+down").describe("Select down in input"),
+      input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"),
+      input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"),
+      input_select_line_home: z
+        .string()
+        .optional()
+        .default("ctrl+shift+a")
+        .describe("Select to start of line in input"),
+      input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"),
+      input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"),
+      input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"),
+      input_select_visual_line_home: z
+        .string()
+        .optional()
+        .default("alt+shift+a")
+        .describe("Select to start of visual line in input"),
+      input_select_visual_line_end: z
+        .string()
+        .optional()
+        .default("alt+shift+e")
+        .describe("Select to end of visual line in input"),
+      input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"),
+      input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"),
+      input_select_buffer_home: z
+        .string()
+        .optional()
+        .default("shift+home")
+        .describe("Select to start of buffer in input"),
+      input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"),
+      input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"),
+      input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"),
+      input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
+      input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
+      input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
+      input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"),
+      input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
+      input_word_forward: z
+        .string()
+        .optional()
+        .default("alt+f,alt+right,ctrl+right")
+        .describe("Move word forward in input"),
+      input_word_backward: z
+        .string()
+        .optional()
+        .default("alt+b,alt+left,ctrl+left")
+        .describe("Move word backward in input"),
+      input_select_word_forward: z
+        .string()
+        .optional()
+        .default("alt+shift+f,alt+shift+right")
+        .describe("Select word forward in input"),
+      input_select_word_backward: z
+        .string()
+        .optional()
+        .default("alt+shift+b,alt+shift+left")
+        .describe("Select word backward in input"),
+      input_delete_word_forward: z
+        .string()
+        .optional()
+        .default("alt+d,alt+delete,ctrl+delete")
+        .describe("Delete word forward in input"),
+      input_delete_word_backward: z
+        .string()
+        .optional()
+        .default("ctrl+w,ctrl+backspace,alt+backspace")
+        .describe("Delete word backward in input"),
       history_previous: z.string().optional().default("up").describe("Previous history item"),
       history_next: z.string().optional().default("down").describe("Next history item"),
       session_child_cycle: z.string().optional().default("<leader>right").describe("Next child session"),

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

@@ -11,9 +11,12 @@ export namespace Flag {
   export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
   export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
   export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
+  export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"
 
   // Experimental
   export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
+  export const OPENCODE_EXPERIMENTAL_ICON_DISCOVERY =
+    OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY")
   export const OPENCODE_EXPERIMENTAL_WATCHER = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WATCHER")
   export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT")
   export const OPENCODE_ENABLE_EXA =

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

@@ -6,6 +6,7 @@ import z from "zod"
 import { NamedError } from "@opencode-ai/util/error"
 import { Log } from "../util/log"
 import { iife } from "@/util/iife"
+import { Flag } from "../flag/flag"
 
 declare global {
   const OPENCODE_VERSION: string
@@ -162,7 +163,7 @@ export namespace Installation {
 
   export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "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) {
     const detectedMethod = installMethod || (await method())

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

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

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

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

+ 4 - 8
packages/opencode/src/plugin/index.ts

@@ -61,14 +61,10 @@ export namespace Plugin {
     for (const hook of await state().then((x) => x.hooks)) {
       const fn = hook[name]
       if (!fn) continue
-      try {
-        // @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
-        // give up.
-        // try-counter: 2
-        await fn(input, output)
-      } catch (e) {
-        log.error("failed to trigger hook", { name, error: e })
-      }
+      // @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
+      // give up.
+      // try-counter: 2
+      await fn(input, output)
     }
     return output
   }

+ 10 - 0
packages/opencode/src/project/instance.ts

@@ -3,6 +3,7 @@ import { Context } from "../util/context"
 import { Project } from "./project"
 import { State } from "./state"
 import { iife } from "@/util/iife"
+import { GlobalBus } from "@/bus/global"
 
 interface Context {
   directory: string
@@ -52,6 +53,15 @@ export const Instance = {
     Log.Default.info("disposing instance", { directory: Instance.directory })
     await State.dispose(Instance.directory)
     cache.delete(Instance.directory)
+    GlobalBus.emit("event", {
+      directory: Instance.directory,
+      payload: {
+        type: "server.instance.disposed",
+        properties: {
+          directory: Instance.directory,
+        },
+      },
+    })
   },
   async disposeAll() {
     Log.Default.info("disposing all instances")

+ 1 - 1
packages/opencode/src/project/project.ts

@@ -107,7 +107,7 @@ export namespace Project {
         await migrateFromGlobal(id, worktree)
       }
     }
-    if (Flag.OPENCODE_EXPERIMENTAL) discover(existing)
+    if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing)
     const result: Info = {
       ...existing,
       worktree,

+ 1 - 0
packages/opencode/src/provider/models.ts

@@ -12,6 +12,7 @@ export namespace ModelsDev {
   export const Model = z.object({
     id: z.string(),
     name: z.string(),
+    family: z.string().optional(),
     release_date: z.string(),
     attachment: z.boolean(),
     reasoning: z.boolean(),

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

@@ -318,6 +318,16 @@ export namespace Provider {
         },
       }
     },
+    cerebras: async () => {
+      return {
+        autoload: false,
+        options: {
+          headers: {
+            "X-Cerebras-3rd-Party-Integration": "opencode",
+          },
+        },
+      }
+    },
   }
 
   export const Model = z
@@ -330,6 +340,7 @@ export namespace Provider {
         npm: z.string(),
       }),
       name: z.string(),
+      family: z.string().optional(),
       capabilities: z.object({
         temperature: z.boolean(),
         reasoning: z.boolean(),
@@ -407,6 +418,7 @@ export namespace Provider {
       id: model.id,
       providerID: provider.id,
       name: model.name,
+      family: model.family,
       api: {
         id: model.id,
         url: provider.api!,

+ 30 - 32
packages/opencode/src/provider/transform.ts

@@ -74,23 +74,28 @@ export namespace ProviderTransform {
       return result
     }
 
-    // DeepSeek: Handle reasoning_content for tool call continuations
-    // - With tool calls: Include reasoning_content in providerOptions so model can continue reasoning
-    // - Without tool calls: Strip reasoning (new turn doesn't need previous reasoning)
-    // See: https://api-docs.deepseek.com/guides/thinking_mode
-    if (model.providerID === "deepseek" || model.api.id.toLowerCase().includes("deepseek")) {
+    // TODO: rm later
+    const bugged =
+      (model.id === "kimi-k2-thinking" && model.providerID === "opencode") ||
+      (model.id === "moonshotai/Kimi-K2-Thinking" && model.providerID === "baseten")
+    if (
+      model.providerID === "deepseek" ||
+      model.api.id.toLowerCase().includes("deepseek") ||
+      (model.capabilities.interleaved &&
+        typeof model.capabilities.interleaved === "object" &&
+        model.capabilities.interleaved.field === "reasoning_content" &&
+        !bugged)
+    ) {
       return msgs.map((msg) => {
         if (msg.role === "assistant" && Array.isArray(msg.content)) {
           const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
-          const hasToolCalls = msg.content.some((part: any) => part.type === "tool-call")
           const reasoningText = reasoningParts.map((part: any) => part.text).join("")
 
           // Filter out reasoning parts from content
           const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
 
-          // If this message has tool calls and reasoning, include reasoning_content
-          // so DeepSeek can continue reasoning after tool execution
-          if (hasToolCalls && reasoningText) {
+          // Include reasoning_content directly on the message for all assistant messages
+          if (reasoningText) {
             return {
               ...msg,
               content: filteredContent,
@@ -104,12 +109,12 @@ export namespace ProviderTransform {
             }
           }
 
-          // For final answers (no tool calls), just strip reasoning
           return {
             ...msg,
             content: filteredContent,
           }
         }
+
         return msg
       })
     }
@@ -212,24 +217,33 @@ export namespace ProviderTransform {
   ): Record<string, any> {
     const result: Record<string, any> = {}
 
-    // switch to providerID later, for now use this
     if (model.api.npm === "@openrouter/ai-sdk-provider") {
       result["usage"] = {
         include: true,
       }
+      if (model.api.id.includes("gemini-3")) {
+        result["reasoning"] = { effort: "high" }
+      }
+    }
+
+    if (
+      model.providerID === "baseten" ||
+      (model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(model.api.id))
+    ) {
+      result["chat_template_args"] = { enable_thinking: true }
     }
 
     if (model.providerID === "openai" || providerOptions?.setCacheKey) {
       result["promptCacheKey"] = sessionID
     }
 
-    if (
-      model.providerID === "google" ||
-      (model.providerID.startsWith("opencode") && model.api.id.includes("gemini-3"))
-    ) {
+    if (model.api.npm === "@ai-sdk/google" || model.api.npm === "@ai-sdk/google-vertex") {
       result["thinkingConfig"] = {
         includeThoughts: true,
       }
+      if (model.api.id.includes("gemini-3")) {
+        result["thinkingConfig"]["thinkingLevel"] = "high"
+      }
     }
 
     if (model.api.id.includes("gpt-5") && !model.api.id.includes("gpt-5-chat")) {
@@ -273,23 +287,7 @@ export namespace ProviderTransform {
     return options
   }
 
-  export function providerOptions(model: Provider.Model, options: { [x: string]: any }, messages: ModelMessage[]) {
-    if (model.capabilities.interleaved && typeof model.capabilities.interleaved === "object") {
-      const cot = []
-      const assistantMessages = messages.filter((msg) => msg.role === "assistant")
-      for (const msg of assistantMessages) {
-        for (const part of msg.content) {
-          if (typeof part === "string") {
-            continue
-          }
-          if (part.type === "reasoning") {
-            cot.push(part)
-          }
-        }
-      }
-      options[model.capabilities.interleaved.field] = cot
-    }
-
+  export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
     switch (model.api.npm) {
       case "@ai-sdk/openai":
       case "@ai-sdk/azure":

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

@@ -10,7 +10,7 @@ import { proxy } from "hono/proxy"
 import { Session } from "../session"
 import z from "zod"
 import { Provider } from "../provider/provider"
-import { mapValues } from "remeda"
+import { filter, mapValues, sortBy, pipe } from "remeda"
 import { NamedError } from "@opencode-ai/util/error"
 import { ModelsDev } from "../provider/models"
 import { Ripgrep } from "../file/ripgrep"
@@ -56,6 +56,7 @@ export namespace Server {
 
   export const Event = {
     Connected: BusEvent.define("server.connected", z.object({})),
+    Disposed: BusEvent.define("global.disposed", z.object({})),
   }
 
   const app = new Hono()
@@ -140,6 +141,35 @@ export namespace Server {
           })
         },
       )
+      .post(
+        "/global/dispose",
+        describeRoute({
+          summary: "Dispose instance",
+          description: "Clean up and dispose all OpenCode instances, releasing all resources.",
+          operationId: "global.dispose",
+          responses: {
+            200: {
+              description: "Global disposed",
+              content: {
+                "application/json": {
+                  schema: resolver(z.boolean()),
+                },
+              },
+            },
+          },
+        }),
+        async (c) => {
+          await Instance.disposeAll()
+          GlobalBus.emit("event", {
+            directory: "global",
+            payload: {
+              type: Event.Disposed.type,
+              properties: {},
+            },
+          })
+          return c.json(true)
+        },
+      )
       .use(async (c, next) => {
         const directory = c.req.query("directory") ?? c.req.header("x-opencode-directory") ?? process.cwd()
         return Instance.provide({
@@ -483,6 +513,7 @@ export namespace Server {
                   schema: resolver(
                     z
                       .object({
+                        home: z.string(),
                         state: z.string(),
                         config: z.string(),
                         worktree: z.string(),
@@ -499,6 +530,7 @@ export namespace Server {
         }),
         async (c) => {
           return c.json({
+            home: Global.Path.home,
             state: Global.Path.state,
             config: Global.Path.config,
             worktree: Instance.worktree,
@@ -549,7 +581,11 @@ export namespace Server {
         }),
         async (c) => {
           const sessions = await Array.fromAsync(Session.list())
-          sessions.sort((a, b) => b.time.updated - a.time.updated)
+          pipe(
+            await Array.fromAsync(Session.list()),
+            filter((s) => !s.time.archived),
+            sortBy((s) => s.time.updated),
+          )
           return c.json(sessions)
         },
       )
@@ -755,6 +791,11 @@ export namespace Server {
           "json",
           z.object({
             title: z.string().optional(),
+            time: z
+              .object({
+                archived: z.number().optional(),
+              })
+              .optional(),
           }),
         ),
         async (c) => {
@@ -765,6 +806,7 @@ export namespace Server {
             if (updates.title !== undefined) {
               session.title = updates.title
             }
+            if (updates.time?.archived !== undefined) session.time.archived = updates.time.archived
           })
 
           return c.json(updatedSession)
@@ -1460,12 +1502,15 @@ export namespace Server {
             }
           }
 
-          const providers = mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x))
-          const connected = await Provider.list().then((x) => Object.keys(x))
+          const connected = await Provider.list()
+          const providers = Object.assign(
+            mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
+            connected,
+          )
           return c.json({
             all: Object.values(providers),
             default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
-            connected,
+            connected: Object.keys(connected),
           })
         },
       )

+ 1 - 1
packages/opencode/src/session/compaction.ts

@@ -142,7 +142,7 @@ export namespace SessionCompaction {
           content: [
             {
               type: "text",
-              text: "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly.",
+              text: "Provide a detailed prompt for continuing our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next considering new session will not have access to our conversation.",
             },
           ],
         },

+ 3 - 23
packages/opencode/src/session/index.ts

@@ -60,6 +60,7 @@ export namespace Session {
         created: z.number(),
         updated: z.number(),
         compacting: z.number().optional(),
+        archived: z.number().optional(),
       }),
       revert: z
         .object({
@@ -222,34 +223,13 @@ export namespace Session {
     if (cfg.share === "disabled") {
       throw new Error("Sharing is disabled in configuration")
     }
-
-    if (cfg.enterprise?.url) {
-      const { ShareNext } = await import("@/share/share-next")
-      const share = await ShareNext.create(id)
-      await update(id, (draft) => {
-        draft.share = {
-          url: share.url,
-        }
-      })
-    }
-
-    const session = await get(id)
-    if (session.share) return session.share
-    const { Share } = await import("../share/share")
-    const share = await Share.create(id)
+    const { ShareNext } = await import("@/share/share-next")
+    const share = await ShareNext.create(id)
     await update(id, (draft) => {
       draft.share = {
         url: share.url,
       }
     })
-    await Storage.write(["share", id], share)
-    await Share.sync("session/info/" + id, session)
-    for (const msg of await messages({ sessionID: id })) {
-      await Share.sync("session/message/" + id + "/" + msg.info.id, msg.info)
-      for (const part of msg.parts) {
-        await Share.sync("session/part/" + id + "/" + msg.info.id + "/" + part.id, part)
-      }
-    }
     return share
   })
 

+ 38 - 6
packages/opencode/src/session/prompt.ts

@@ -5,6 +5,7 @@ import z from "zod"
 import { Identifier } from "../id/id"
 import { MessageV2 } from "./message-v2"
 import { Log } from "../util/log"
+import { Flag } from "../flag/flag"
 import { SessionRevert } from "./revert"
 import { Session } from "."
 import { Agent } from "../agent/agent"
@@ -21,7 +22,7 @@ import PROMPT_PLAN from "../session/prompt/plan.txt"
 import BUILD_SWITCH from "../session/prompt/build-switch.txt"
 import MAX_STEPS from "../session/prompt/max-steps.txt"
 import { defer } from "../util/defer"
-import { mergeDeep, pipe } from "remeda"
+import { clone, mergeDeep, pipe } from "remeda"
 import { ToolRegistry } from "../tool/registry"
 import { Wildcard } from "../util/wildcard"
 import { MCP } from "../mcp"
@@ -331,6 +332,7 @@ export namespace SessionPrompt {
             },
           },
         })) as MessageV2.ToolPart
+        let executionError: Error | undefined
         const result = await taskTool
           .execute(
             {
@@ -355,7 +357,11 @@ export namespace SessionPrompt {
               },
             },
           )
-          .catch(() => {})
+          .catch((error) => {
+            executionError = error
+            log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
+            return undefined
+          })
         assistantMessage.finish = "tool-calls"
         assistantMessage.time.completed = Date.now()
         await Session.updateMessage(assistantMessage)
@@ -381,7 +387,7 @@ export namespace SessionPrompt {
             ...part,
             state: {
               status: "error",
-              error: "Tool execution failed",
+              error: executionError ? `Tool execution failed: ${executionError.message}` : "Tool execution failed",
               time: {
                 start: part.state.status === "running" ? part.state.time.start : Date.now(),
                 end: Date.now(),
@@ -1088,8 +1094,8 @@ export namespace SessionPrompt {
       },
     }
     await Session.updatePart(part)
-    const shell = process.env["SHELL"] ?? "bash"
-    const shellName = path.basename(shell)
+    const shell = process.env["SHELL"] ?? (process.platform === "win32" ? process.env["COMSPEC"] || "cmd.exe" : "bash")
+    const shellName = path.basename(shell).toLowerCase()
 
     const invocations: Record<string, { args: string[] }> = {
       nu: {
@@ -1119,6 +1125,14 @@ export namespace SessionPrompt {
           `,
         ],
       },
+      // Windows cmd.exe
+      "cmd.exe": {
+        args: ["/c", input.command],
+      },
+      // Windows PowerShell
+      "powershell.exe": {
+        args: ["-NoProfile", "-Command", input.command],
+      },
       // Fallback: any shell that doesn't match those above
       "": {
         args: ["-c", "-l", `${input.command}`],
@@ -1130,7 +1144,7 @@ export namespace SessionPrompt {
 
     const proc = spawn(shell, args, {
       cwd: Instance.directory,
-      detached: true,
+      detached: process.platform !== "win32",
       stdio: ["ignore", "pipe", "pipe"],
       env: {
         ...process.env,
@@ -1308,6 +1322,7 @@ export namespace SessionPrompt {
       input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
         .length === 1
     if (!isFirst) return
+<<<<<<< HEAD
     const agent = await Agent.get("summary")
     if (!agent) return
     const result = await LLM.stream({
@@ -1325,6 +1340,23 @@ export namespace SessionPrompt {
       abort: new AbortController().signal,
       sessionID: input.session.id,
       retries: 2,
+=======
+    const cfg = await Config.get()
+    const small =
+      (await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
+    const language = await Provider.getLanguage(small)
+    const provider = await Provider.getProvider(small.providerID)
+    const options = pipe(
+      {},
+      mergeDeep(ProviderTransform.options(small, input.session.id, provider?.options)),
+      mergeDeep(ProviderTransform.smallOptions(small)),
+      mergeDeep(small.options),
+    )
+    await generateText({
+      // use higher # for reasoning models since reasoning tokens eat up a lot of the budget
+      maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
+      providerOptions: ProviderTransform.providerOptions(small, options),
+>>>>>>> dev
       messages: [
         {
           role: "user",

+ 2 - 2
packages/opencode/src/session/summary.ts

@@ -91,7 +91,7 @@ export namespace SessionSummary {
     if (textPart && !userMsg.summary?.title) {
       const result = await generateText({
         maxOutputTokens: small.capabilities.reasoning ? 1500 : 20,
-        providerOptions: ProviderTransform.providerOptions(small, options, []),
+        providerOptions: ProviderTransform.providerOptions(small, options),
         messages: [
           ...SystemPrompt.title(small.providerID).map(
             (x): ModelMessage => ({
@@ -144,7 +144,7 @@ export namespace SessionSummary {
         const result = await generateText({
           model: language,
           maxOutputTokens: 100,
-          providerOptions: ProviderTransform.providerOptions(small, options, []),
+          providerOptions: ProviderTransform.providerOptions(small, options),
           messages: [
             ...SystemPrompt.summarize(small.providerID).map(
               (x): ModelMessage => ({

+ 7 - 8
packages/opencode/src/share/share-next.ts

@@ -11,9 +11,11 @@ import type * as SDK from "@opencode-ai/sdk/v2"
 export namespace ShareNext {
   const log = Log.create({ service: "share-next" })
 
+  async function url() {
+    return Config.get().then((x) => x.enterprise?.url ?? "https://opncd.ai")
+  }
+
   export async function init() {
-    const config = await Config.get()
-    if (!config.enterprise) return
     Bus.subscribe(Session.Event.Updated, async (evt) => {
       await sync(evt.properties.info.id, [
         {
@@ -62,8 +64,7 @@ export namespace ShareNext {
 
   export async function create(sessionID: string) {
     log.info("creating share", { sessionID })
-    const url = await Config.get().then((x) => x.enterprise!.url)
-    const result = await fetch(`${url}/api/share`, {
+    const result = await fetch(`${await url()}/api/share`, {
       method: "POST",
       headers: {
         "Content-Type": "application/json",
@@ -126,11 +127,10 @@ export namespace ShareNext {
       const queued = queue.get(sessionID)
       if (!queued) return
       queue.delete(sessionID)
-      const url = await Config.get().then((x) => x.enterprise!.url)
       const share = await get(sessionID)
       if (!share) return
 
-      await fetch(`${url}/api/share/${share.id}/sync`, {
+      await fetch(`${await url()}/api/share/${share.id}/sync`, {
         method: "POST",
         headers: {
           "Content-Type": "application/json",
@@ -146,10 +146,9 @@ export namespace ShareNext {
 
   export async function remove(sessionID: string) {
     log.info("removing share", { sessionID })
-    const url = await Config.get().then((x) => x.enterprise!.url)
     const share = await get(sessionID)
     if (!share) return
-    await fetch(`${url}/api/share/${share.id}`, {
+    await fetch(`${await url()}/api/share/${share.id}`, {
       method: "DELETE",
       headers: {
         "Content-Type": "application/json",

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

@@ -83,7 +83,7 @@ export const BashTool = Tool.define("bash", async () => {
   log.info("bash tool using shell", { shell })
 
   return {
-    description: DESCRIPTION,
+    description: DESCRIPTION.replaceAll("${directory}", Instance.directory),
     parameters: z.object({
       command: z.string().describe("The command to execute"),
       timeout: z.number().describe("Optional timeout in milliseconds").optional(),
@@ -189,7 +189,7 @@ export const BashTool = Tool.define("bash", async () => {
           const action = Wildcard.allStructured({ head: command[0], tail: command.slice(1) }, permissions)
           if (action === "deny") {
             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") {

+ 43 - 8
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.
 
+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:
 
 1. Directory Verification:
@@ -17,14 +19,47 @@ Before executing the command, please follow these steps:
    - Capture the output of the command.
 
 Usage notes:
-  - The command argument is required.
-  - You can specify an optional timeout in milliseconds. If not specified, commands will timeout after 120000ms (2 minutes). Use the `timeout` parameter to control execution time.
-  - The `workdir` parameter specifies the working directory for the command. Defaults to the current working directory. Prefer setting `workdir` over using `cd` in your commands.
-  - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
-  - If the output exceeds 30000 characters, output will be truncated before being returned to you.
-  - VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and List to read files.
-  - If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed.
-  - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
+    - The command argument is required.
+    - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes).
+  If not specified, commands will timeout after 120000ms (2 minutes).
+    - The description argument is required. You must write a clear, concise description of what this command does in 5-10 words.
+    - If the output exceeds 30000 characters, output will be truncated before being
+  returned to you.
+    - You can use the `run_in_background` parameter to run the command in the background,
+  which allows you to continue working while the command runs. You can monitor the output
+  using the Bash tool as it becomes available. You do not need to use '&' at the end of
+  the command when using this parameter.
+
+    - Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or
+  `echo` commands, unless explicitly instructed or when these commands are truly necessary
+   for the task. Instead, always prefer using the dedicated tools for these commands:
+      - File search: Use Glob (NOT find or ls)
+      - Content search: Use Grep (NOT grep or rg)
+      - Read files: Use Read (NOT cat/head/tail)
+      - Edit files: Use Edit (NOT sed/awk)
+      - Write files: Use Write (NOT echo >/cat <<EOF)
+      - Communication: Output text directly (NOT echo/printf)
+    - When issuing multiple commands:
+      - If the commands are independent and can run in parallel, make multiple Bash tool
+  calls in a single message. For example, if you need to run "git status" and "git diff",
+  send a single message with two Bash tool calls in parallel.
+      - If the commands depend on each other and must run sequentially, use a single Bash
+  call with '&&' to chain them together (e.g., `git add . && git commit -m "message" &&
+  git push`). For instance, if one operation must complete before another starts (like
+  mkdir before cp, Write before Bash for git operations, or git add before git commit),
+  run these operations sequentially instead.
+      - Use ';' only when you need to run commands sequentially but don't care if earlier
+  commands fail
+      - DO NOT use newlines to separate commands (newlines are ok in quoted strings)
+    - Try to maintain your current working directory throughout the session by using
+  absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly
+  requests it.
+      <good-example>
+      pytest /foo/bar/tests
+      </good-example>
+      <bad-example>
+      cd /foo/bar && pytest tests
+      </bad-example>
 
 # Working Directory
 

Деякі файли не було показано, через те що забагато файлів було змінено