2
0
Эх сурвалжийг харах

Merge public/dev into current branch — resolve conflicts in bun.lock and server: mount /app

paviko 2 сар өмнө
parent
commit
d7c0e9fe4f
100 өөрчлөгдсөн 8818 нэмэгдсэн , 1759 устгасан
  1. 3 1
      .github/workflows/nix-desktop.yml
  2. 1 1
      .github/workflows/publish.yml
  3. 105 15
      .github/workflows/update-nix-hashes.yml
  4. 1 0
      .gitignore
  5. 2 2
      .opencode/agent/triage.md
  6. 24 0
      .opencode/command/ai-deps.md
  7. 3 3
      AGENTS.md
  8. 40 0
      SECURITY.md
  9. 202 197
      STATS.md
  10. 69 9
      STYLE_GUIDE.md
  11. 185 99
      bun.lock
  12. 3 3
      flake.lock
  13. 23 3
      flake.nix
  14. 6 5
      github/README.md
  15. 4 0
      infra/console.ts
  16. 1 1
      install
  17. 6 1
      nix/hashes.json
  18. 4 2
      nix/node-modules.nix
  19. 10 3
      nix/scripts/update-hashes.sh
  20. 3 2
      package.json
  21. 1 2
      packages/app/index.html
  22. 1 1
      packages/app/package.json
  23. 12 12
      packages/app/src/app.tsx
  24. 99 0
      packages/app/src/components/dialog-fork.tsx
  25. 153 25
      packages/app/src/components/dialog-select-file.tsx
  26. 81 14
      packages/app/src/components/dialog-select-server.tsx
  27. 35 8
      packages/app/src/components/prompt-input.tsx
  28. 175 231
      packages/app/src/components/session/session-header.tsx
  29. 6 3
      packages/app/src/components/terminal.tsx
  30. 118 0
      packages/app/src/components/titlebar.tsx
  31. 12 69
      packages/app/src/context/command.tsx
  32. 2 1
      packages/app/src/context/global-sdk.tsx
  33. 178 31
      packages/app/src/context/global-sync.tsx
  34. 34 2
      packages/app/src/context/layout.tsx
  35. 9 0
      packages/app/src/context/platform.tsx
  36. 1 4
      packages/app/src/context/server.tsx
  37. 1 1
      packages/app/src/context/sync.tsx
  38. 15 1
      packages/app/src/context/terminal.tsx
  39. 4 0
      packages/app/src/index.css
  40. 8 0
      packages/app/src/pages/directory-layout.tsx
  41. 793 434
      packages/app/src/pages/layout.tsx
  42. 27 4
      packages/app/src/pages/session.tsx
  43. 7 2
      packages/console/app/package.json
  44. 1 0
      packages/console/app/public/social-share-black.png
  45. 3 0
      packages/console/app/src/component/footer.tsx
  46. 15 0
      packages/console/app/src/component/spotlight.css
  47. 820 0
      packages/console/app/src/component/spotlight.tsx
  48. 3 3
      packages/console/app/src/config.ts
  49. 0 24
      packages/console/app/src/context/auth.session.ts
  50. 26 1
      packages/console/app/src/context/auth.ts
  51. 7 6
      packages/console/app/src/lib/github.ts
  52. 3 2
      packages/console/app/src/routes/auth/[...callback].ts
  53. 4 1
      packages/console/app/src/routes/auth/authorize.ts
  54. 1 1
      packages/console/app/src/routes/auth/logout.ts
  55. 1 1
      packages/console/app/src/routes/auth/status.ts
  56. 828 0
      packages/console/app/src/routes/black.css
  57. 235 0
      packages/console/app/src/routes/black.tsx
  58. 62 0
      packages/console/app/src/routes/black/common.tsx
  59. 0 221
      packages/console/app/src/routes/black/index.css
  60. 32 97
      packages/console/app/src/routes/black/index.tsx
  61. 451 0
      packages/console/app/src/routes/black/subscribe/[plan].tsx
  62. 477 0
      packages/console/app/src/routes/changelog/index.css
  63. 147 0
      packages/console/app/src/routes/changelog/index.tsx
  64. 1 1
      packages/console/app/src/routes/download/[platform].ts
  65. 0 1
      packages/console/app/src/routes/download/index.css
  66. 5 3
      packages/console/app/src/routes/download/index.tsx
  67. 1 1
      packages/console/app/src/routes/index.css
  68. 2 1
      packages/console/app/src/routes/index.tsx
  69. 1 1
      packages/console/app/src/routes/user-menu.tsx
  70. 54 0
      packages/console/app/src/routes/workspace/[id]/billing/black-section.module.css
  71. 76 1
      packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx
  72. 12 2
      packages/console/app/src/routes/workspace/common.tsx
  73. 38 28
      packages/console/app/src/routes/zen/util/handler.ts
  74. 161 56
      packages/console/app/src/routes/zen/util/provider/anthropic.ts
  75. 5 4
      packages/console/app/src/routes/zen/util/provider/google.ts
  76. 3 2
      packages/console/app/src/routes/zen/util/provider/openai-compatible.ts
  77. 3 2
      packages/console/app/src/routes/zen/util/provider/openai.ts
  78. 3 2
      packages/console/app/src/routes/zen/util/provider/provider.ts
  79. 1 1
      packages/console/app/src/routes/zen/util/stickyProviderTracker.ts
  80. 1 1
      packages/console/app/tsconfig.json
  81. 1 0
      packages/console/core/migrations/0051_jazzy_green_goblin.sql
  82. 1 0
      packages/console/core/migrations/0052_aromatic_agent_zero.sql
  83. 1228 0
      packages/console/core/migrations/meta/0051_snapshot.json
  84. 1235 0
      packages/console/core/migrations/meta/0052_snapshot.json
  85. 14 0
      packages/console/core/migrations/meta/_journal.json
  86. 2 1
      packages/console/core/package.json
  87. 41 0
      packages/console/core/script/black-cancel-waitlist.ts
  88. 163 0
      packages/console/core/script/black-transfer.ts
  89. 15 0
      packages/console/core/script/credit-workspace.ts
  90. 17 2
      packages/console/core/script/lookup-user.ts
  91. 14 22
      packages/console/core/script/promote-models.ts
  92. 15 22
      packages/console/core/script/pull-models.ts
  93. 18 36
      packages/console/core/script/update-models.ts
  94. 1 16
      packages/console/core/src/billing.ts
  95. 72 0
      packages/console/core/src/black.ts
  96. 3 2
      packages/console/core/src/model.ts
  97. 3 1
      packages/console/core/src/schema/billing.sql.ts
  98. 12 0
      packages/console/core/sst-env.d.ts
  99. 1 1
      packages/console/function/package.json
  100. 12 0
      packages/console/function/sst-env.d.ts

+ 3 - 1
.github/workflows/nix-desktop.yml

@@ -25,6 +25,8 @@ jobs:
       matrix:
         os:
           - blacksmith-4vcpu-ubuntu-2404
+          - blacksmith-4vcpu-ubuntu-2404-arm
+          - macos-15
           - macos-latest
     runs-on: ${{ matrix.os }}
     timeout-minutes: 60
@@ -33,7 +35,7 @@ jobs:
         uses: actions/checkout@v6
 
       - name: Setup Nix
-        uses: DeterminateSystems/nix-installer-action@v21
+        uses: nixbuild/nix-quick-install-action@v34
 
       - name: Build desktop via flake
         run: |

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

@@ -92,7 +92,7 @@ jobs:
 
   publish-tauri:
     needs: publish
-    continue-on-error: true
+    continue-on-error: false
     strategy:
       fail-fast: false
       matrix:

+ 105 - 15
.github/workflows/update-nix-hashes.yml

@@ -17,11 +17,11 @@ on:
       - "packages/*/package.json"
 
 jobs:
-  update:
+  update-flake:
     if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
     runs-on: blacksmith-4vcpu-ubuntu-2404
     env:
-      SYSTEM: x86_64-linux
+      TITLE: flake.lock
 
     steps:
       - name: Checkout repository
@@ -33,39 +33,129 @@ jobs:
           repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
 
       - name: Setup Nix
-        uses: DeterminateSystems/nix-installer-action@v20
+        uses: nixbuild/nix-quick-install-action@v34
 
       - name: Configure git
         run: |
           git config --global user.email "[email protected]"
           git config --global user.name "Github Action"
 
-      - name: Update flake.lock
+      - name: Update ${{ env.TITLE }}
         run: |
           set -euo pipefail
-          echo "📦 Updating flake.lock..."
+          echo "📦 Updating $TITLE..."
           nix flake update
-          echo "✅ flake.lock updated successfully"
+          echo "✅ $TITLE updated successfully"
 
-      - name: Update node_modules hash
+      - name: Commit ${{ env.TITLE }} changes
+        env:
+          TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
+        run: |
+          set -euo pipefail
+
+          echo "🔍 Checking for changes in tracked files..."
+
+          summarize() {
+            local status="$1"
+            {
+              echo "### Nix $TITLE"
+              echo ""
+              echo "- ref: ${GITHUB_REF_NAME}"
+              echo "- status: ${status}"
+            } >> "$GITHUB_STEP_SUMMARY"
+            if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
+              echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY"
+            fi
+            echo "" >> "$GITHUB_STEP_SUMMARY"
+          }
+          FILES=(flake.lock flake.nix)
+          STATUS="$(git status --short -- "${FILES[@]}" || true)"
+          if [ -z "$STATUS" ]; then
+            echo "✅ No changes detected."
+            summarize "no changes"
+            exit 0
+          fi
+
+          echo "📝 Changes detected:"
+          echo "$STATUS"
+          echo "🔗 Staging files..."
+          git add "${FILES[@]}"
+          echo "💾 Committing changes..."
+          git commit -m "Update $TITLE"
+          echo "✅ Changes committed"
+
+          BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
+          echo "🌳 Pulling latest from branch: $BRANCH"
+          git pull --rebase origin "$BRANCH"
+          echo "🚀 Pushing changes to branch: $BRANCH"
+          git push origin HEAD:"$BRANCH"
+          echo "✅ Changes pushed successfully"
+
+          summarize "committed $(git rev-parse --short HEAD)"
+
+  update-node-modules-hash:
+    needs: update-flake
+    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - system: x86_64-linux
+            host: blacksmith-4vcpu-ubuntu-2404
+          - system: aarch64-linux
+            host: blacksmith-4vcpu-ubuntu-2404-arm
+          - system: x86_64-darwin
+            host: macos-15-intel
+          - system: aarch64-darwin
+            host: macos-latest
+    runs-on: ${{ matrix.host }}
+    env:
+      SYSTEM: ${{ matrix.system }}
+      TITLE: node_modules hash (${{ matrix.system }})
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          fetch-depth: 0
+          ref: ${{ github.head_ref || github.ref_name }}
+          repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
+
+      - name: Setup Nix
+        uses: nixbuild/nix-quick-install-action@v34
+
+      - name: Configure git
+        run: |
+          git config --global user.email "[email protected]"
+          git config --global user.name "Github Action"
+
+      - name: Pull latest changes
+        env:
+          TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
+        run: |
+          BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
+          git pull origin "$BRANCH"
+
+      - name: Update ${{ env.TITLE }}
         run: |
           set -euo pipefail
-          echo "🔄 Updating node_modules hash..."
+          echo "🔄 Updating $TITLE..."
           nix/scripts/update-hashes.sh
-          echo "✅ node_modules hash updated successfully"
+          echo "✅ $TITLE updated successfully"
 
-      - name: Commit hash changes
+      - name: Commit ${{ env.TITLE }} changes
         env:
           TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
         run: |
           set -euo pipefail
 
-          echo "🔍 Checking for changes in tracked Nix files..."
+          echo "🔍 Checking for changes in tracked files..."
 
           summarize() {
             local status="$1"
             {
-              echo "### Nix Hash Update"
+              echo "### Nix $TITLE"
               echo ""
               echo "- ref: ${GITHUB_REF_NAME}"
               echo "- status: ${status}"
@@ -76,10 +166,10 @@ jobs:
             echo "" >> "$GITHUB_STEP_SUMMARY"
           }
 
-          FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
+          FILES=(nix/hashes.json)
           STATUS="$(git status --short -- "${FILES[@]}" || true)"
           if [ -z "$STATUS" ]; then
-            echo "✅ No changes detected. Hashes are already up to date."
+            echo "✅ No changes detected."
             summarize "no changes"
             exit 0
           fi
@@ -89,7 +179,7 @@ jobs:
           echo "🔗 Staging files..."
           git add "${FILES[@]}"
           echo "💾 Committing changes..."
-          git commit -m "Update Nix flake.lock and hashes"
+          git commit -m "Update $TITLE"
           echo "✅ Changes committed"
 
           BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"

+ 1 - 0
.gitignore

@@ -73,3 +73,4 @@ coverage.*
 # Local dev files
 opencode-dev
 logs/
+*.bun-build

+ 2 - 2
.opencode/agent/triage.md

@@ -45,9 +45,9 @@ Desktop app issues:
 
 #### zen
 
-**Only** add if the issue mentions "zen" or "opencode zen". Zen is our gateway for coding models. **Do not** add for other gateways or inference providers.
+**Only** add if the issue mentions "zen" or "opencode zen" or "opencode black".
 
-If the issue doesn't have "zen" in it then don't add zen label
+If the issue doesn't have "zen" or "opencode black" in it then don't add zen label
 
 #### docs
 

+ 24 - 0
.opencode/command/ai-deps.md

@@ -0,0 +1,24 @@
+---
+description: "Bump AI sdk dependencies minor / patch versions only"
+---
+
+Please read @package.json and @packages/opencode/package.json.
+
+Your job is to look into AI SDK dependencies, figure out if they have versions that can be upgraded (minor or patch versions ONLY no major ignore major changes).
+
+I want a report of every dependency and the version that can be upgraded to.
+What would be even better is if you can give me links to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added.
+
+Consider using subagents for each dep to save your context window.
+
+Here is a short list of some deps (please be comprehensive tho):
+
+- "ai"
+- "@ai-sdk/openai"
+- "@ai-sdk/anthropic"
+- "@openrouter/ai-sdk-provider"
+- etc, etc
+
+DO NOT upgrade the dependencies yet, just make a list of all dependencies and their versions that can be upgraded to minor or patch versions only.
+
+Write up your findings to ai-sdk-updates.md

+ 3 - 3
AGENTS.md

@@ -1,4 +1,4 @@
-- To test opencode in the `packages/opencode` directory you can run `bun dev`
-- To regenerate the javascript SDK, run ./packages/sdk/js/script/build.ts
+- To test opencode in `packages/opencode`, run `bun dev`.
+- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
 - ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
-- the default branch in this repo is `dev`
+- The default branch in this repo is `dev`.

+ 40 - 0
SECURITY.md

@@ -0,0 +1,40 @@
+# Security
+
+## Threat Model
+
+### Overview
+
+OpenCode is an AI-powered coding assistant that runs locally on your machine. It provides an agent system with access to powerful tools including shell execution, file operations, and web access.
+
+### No Sandbox
+
+OpenCode does **not** sandbox the agent. The permission system exists as a UX feature to help users stay aware of what actions the agent is taking - it prompts for confirmation before executing commands, writing files, etc. However, it is not designed to provide security isolation.
+
+If you need true isolation, run OpenCode inside a Docker container or VM.
+
+### Server Mode
+
+Server mode is opt-in only. When enabled, set `OPENCODE_SERVER_PASSWORD` to require HTTP Basic Auth. Without this, the server runs unauthenticated (with a warning). It is the end user's responsibility to secure the server - any functionality it provides is not a vulnerability.
+
+### Out of Scope
+
+| Category                        | Rationale                                                               |
+| ------------------------------- | ----------------------------------------------------------------------- |
+| **Server access when opted-in** | If you enable server mode, API access is expected behavior              |
+| **Sandbox escapes**             | The permission system is not a sandbox (see above)                      |
+| **LLM provider data handling**  | Data sent to your configured LLM provider is governed by their policies |
+| **MCP server behavior**         | External MCP servers you configure are outside our trust boundary       |
+
+---
+
+# Reporting Security Issues
+
+We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
+
+To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/anomalyco/opencode/security/advisories/new) tab.
+
+The team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
+
+## Escalation
+
+If you do not receive an acknowledgement of your report within 6 business days, you may send an email to [email protected]

+ 202 - 197
STATS.md

@@ -1,199 +1,204 @@
 # 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)  |
-| 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)  |
-| 2025-12-12 | 1,061,340 (+16,230)  | 1,030,838 (+20,279) | 2,092,178 (+36,509)  |
-| 2025-12-13 | 1,073,561 (+12,221)  | 1,044,608 (+13,770) | 2,118,169 (+25,991)  |
-| 2025-12-14 | 1,082,042 (+8,481)   | 1,052,425 (+7,817)  | 2,134,467 (+16,298)  |
-| 2025-12-15 | 1,093,632 (+11,590)  | 1,059,078 (+6,653)  | 2,152,710 (+18,243)  |
-| 2025-12-16 | 1,120,477 (+26,845)  | 1,078,022 (+18,944) | 2,198,499 (+45,789)  |
-| 2025-12-17 | 1,151,067 (+30,590)  | 1,097,661 (+19,639) | 2,248,728 (+50,229)  |
-| 2025-12-18 | 1,178,658 (+27,591)  | 1,113,418 (+15,757) | 2,292,076 (+43,348)  |
-| 2025-12-19 | 1,203,485 (+24,827)  | 1,129,698 (+16,280) | 2,333,183 (+41,107)  |
-| 2025-12-20 | 1,223,000 (+19,515)  | 1,146,258 (+16,560) | 2,369,258 (+36,075)  |
-| 2025-12-21 | 1,242,675 (+19,675)  | 1,158,909 (+12,651) | 2,401,584 (+32,326)  |
-| 2025-12-22 | 1,262,522 (+19,847)  | 1,169,121 (+10,212) | 2,431,643 (+30,059)  |
-| 2025-12-23 | 1,286,548 (+24,026)  | 1,186,439 (+17,318) | 2,472,987 (+41,344)  |
-| 2025-12-24 | 1,309,323 (+22,775)  | 1,203,767 (+17,328) | 2,513,090 (+40,103)  |
-| 2025-12-25 | 1,333,032 (+23,709)  | 1,217,283 (+13,516) | 2,550,315 (+37,225)  |
-| 2025-12-26 | 1,352,411 (+19,379)  | 1,227,615 (+10,332) | 2,580,026 (+29,711)  |
-| 2025-12-27 | 1,371,771 (+19,360)  | 1,238,236 (+10,621) | 2,610,007 (+29,981)  |
-| 2025-12-28 | 1,390,388 (+18,617)  | 1,245,690 (+7,454)  | 2,636,078 (+26,071)  |
-| 2025-12-29 | 1,415,560 (+25,172)  | 1,257,101 (+11,411) | 2,672,661 (+36,583)  |
-| 2025-12-30 | 1,445,450 (+29,890)  | 1,272,689 (+15,588) | 2,718,139 (+45,478)  |
-| 2025-12-31 | 1,479,598 (+34,148)  | 1,293,235 (+20,546) | 2,772,833 (+54,694)  |
-| 2026-01-01 | 1,508,883 (+29,285)  | 1,309,874 (+16,639) | 2,818,757 (+45,924)  |
-| 2026-01-02 | 1,563,474 (+54,591)  | 1,320,959 (+11,085) | 2,884,433 (+65,676)  |
-| 2026-01-03 | 1,618,065 (+54,591)  | 1,331,914 (+10,955) | 2,949,979 (+65,546)  |
-| 2026-01-04 | 1,672,656 (+39,702)  | 1,339,883 (+7,969)  | 3,012,539 (+62,560)  |
-| 2026-01-05 | 1,738,171 (+65,515)  | 1,353,043 (+13,160) | 3,091,214 (+78,675)  |
-| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
-| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
-| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
-| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
-| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
+| 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)  |
+| 2025-12-12 | 1,061,340 (+16,230)  | 1,030,838 (+20,279)  | 2,092,178 (+36,509)  |
+| 2025-12-13 | 1,073,561 (+12,221)  | 1,044,608 (+13,770)  | 2,118,169 (+25,991)  |
+| 2025-12-14 | 1,082,042 (+8,481)   | 1,052,425 (+7,817)   | 2,134,467 (+16,298)  |
+| 2025-12-15 | 1,093,632 (+11,590)  | 1,059,078 (+6,653)   | 2,152,710 (+18,243)  |
+| 2025-12-16 | 1,120,477 (+26,845)  | 1,078,022 (+18,944)  | 2,198,499 (+45,789)  |
+| 2025-12-17 | 1,151,067 (+30,590)  | 1,097,661 (+19,639)  | 2,248,728 (+50,229)  |
+| 2025-12-18 | 1,178,658 (+27,591)  | 1,113,418 (+15,757)  | 2,292,076 (+43,348)  |
+| 2025-12-19 | 1,203,485 (+24,827)  | 1,129,698 (+16,280)  | 2,333,183 (+41,107)  |
+| 2025-12-20 | 1,223,000 (+19,515)  | 1,146,258 (+16,560)  | 2,369,258 (+36,075)  |
+| 2025-12-21 | 1,242,675 (+19,675)  | 1,158,909 (+12,651)  | 2,401,584 (+32,326)  |
+| 2025-12-22 | 1,262,522 (+19,847)  | 1,169,121 (+10,212)  | 2,431,643 (+30,059)  |
+| 2025-12-23 | 1,286,548 (+24,026)  | 1,186,439 (+17,318)  | 2,472,987 (+41,344)  |
+| 2025-12-24 | 1,309,323 (+22,775)  | 1,203,767 (+17,328)  | 2,513,090 (+40,103)  |
+| 2025-12-25 | 1,333,032 (+23,709)  | 1,217,283 (+13,516)  | 2,550,315 (+37,225)  |
+| 2025-12-26 | 1,352,411 (+19,379)  | 1,227,615 (+10,332)  | 2,580,026 (+29,711)  |
+| 2025-12-27 | 1,371,771 (+19,360)  | 1,238,236 (+10,621)  | 2,610,007 (+29,981)  |
+| 2025-12-28 | 1,390,388 (+18,617)  | 1,245,690 (+7,454)   | 2,636,078 (+26,071)  |
+| 2025-12-29 | 1,415,560 (+25,172)  | 1,257,101 (+11,411)  | 2,672,661 (+36,583)  |
+| 2025-12-30 | 1,445,450 (+29,890)  | 1,272,689 (+15,588)  | 2,718,139 (+45,478)  |
+| 2025-12-31 | 1,479,598 (+34,148)  | 1,293,235 (+20,546)  | 2,772,833 (+54,694)  |
+| 2026-01-01 | 1,508,883 (+29,285)  | 1,309,874 (+16,639)  | 2,818,757 (+45,924)  |
+| 2026-01-02 | 1,563,474 (+54,591)  | 1,320,959 (+11,085)  | 2,884,433 (+65,676)  |
+| 2026-01-03 | 1,618,065 (+54,591)  | 1,331,914 (+10,955)  | 2,949,979 (+65,546)  |
+| 2026-01-04 | 1,672,656 (+39,702)  | 1,339,883 (+7,969)   | 3,012,539 (+62,560)  |
+| 2026-01-05 | 1,738,171 (+65,515)  | 1,353,043 (+13,160)  | 3,091,214 (+78,675)  |
+| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334)  | 3,338,365 (+247,151) |
+| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271)  | 3,521,887 (+183,522) |
+| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832)  | 3,705,110 (+183,223) |
+| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971)  | 3,913,016 (+207,906) |
+| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219)  | 4,135,693 (+222,677) |
+| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809)  | 4,366,873 (+231,180) |
+| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192)  | 4,607,265 (+240,392) |
+| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391)  | 4,892,140 (+284,875) |
+| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300)  | 5,214,290 (+322,150) |
+| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |

+ 69 - 9
STYLE_GUIDE.md

@@ -1,11 +1,71 @@
 ## Style Guide
 
-- Try to keep things in one function unless composable or reusable
-- AVOID unnecessary destructuring of variables. instead of doing `const { a, b }
-= obj` just reference it as obj.a and obj.b. this preserves context
-- AVOID `try`/`catch` where possible
-- AVOID `else` statements
-- AVOID using `any` type
-- AVOID `let` statements
-- PREFER single word variable names where possible
-- Use as many bun apis as possible like Bun.file()
+- Keep things in one function unless composable or reusable
+- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context
+- Avoid `try`/`catch` where possible
+- Avoid using the `any` type
+- Prefer single word variable names where possible
+- Use Bun APIs when possible, like `Bun.file()`
+
+# Avoid let statements
+
+We don't like `let` statements, especially combined with if/else statements.
+Prefer `const`.
+
+Good:
+
+```ts
+const foo = condition ? 1 : 2
+```
+
+Bad:
+
+```ts
+let foo
+
+if (condition) foo = 1
+else foo = 2
+```
+
+# Avoid else statements
+
+Prefer early returns or using an `iife` to avoid else statements.
+
+Good:
+
+```ts
+function foo() {
+  if (condition) return 1
+  return 2
+}
+```
+
+Bad:
+
+```ts
+function foo() {
+  if (condition) return 1
+  else return 2
+}
+```
+
+# Prefer single word naming
+
+Try your best to find a single word name for your variables, functions, etc.
+Only use multiple words if you cannot.
+
+Good:
+
+```ts
+const foo = 1
+const bar = 2
+const baz = 3
+```
+
+Bad:
+
+```ts
+const fooBar = 1
+const barBaz = 2
+const bazFoo = 3
+```

+ 185 - 99
bun.lock

@@ -23,7 +23,7 @@
     },
     "packages/app": {
       "name": "@opencode-ai/app",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -71,7 +71,7 @@
     },
     "packages/console/app": {
       "name": "@opencode-ai/console-app",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@cloudflare/vite-plugin": "1.15.2",
         "@ibm/plex": "6.4.1",
@@ -82,25 +82,30 @@
         "@opencode-ai/console-mail": "workspace:*",
         "@opencode-ai/console-resource": "workspace:*",
         "@opencode-ai/ui": "workspace:*",
+        "@smithy/eventstream-codec": "4.2.7",
+        "@smithy/util-utf8": "4.2.0",
         "@solidjs/meta": "catalog:",
         "@solidjs/router": "catalog:",
         "@solidjs/start": "catalog:",
+        "@stripe/stripe-js": "8.6.1",
         "chart.js": "4.5.1",
         "nitro": "3.0.1-alpha.1",
         "solid-js": "catalog:",
         "solid-list": "0.3.0",
+        "solid-stripe": "0.8.1",
         "vite": "catalog:",
         "zod": "catalog:",
       },
       "devDependencies": {
         "@typescript/native-preview": "catalog:",
+        "@webgpu/types": "0.1.54",
         "typescript": "catalog:",
         "wrangler": "4.50.0",
       },
     },
     "packages/console/core": {
       "name": "@opencode-ai/console-core",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "@jsx-email/render": "1.1.1",
@@ -127,7 +132,7 @@
     },
     "packages/console/function": {
       "name": "@opencode-ai/console-function",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
@@ -151,7 +156,7 @@
     },
     "packages/console/mail": {
       "name": "@opencode-ai/console-mail",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@jsx-email/all": "2.2.3",
         "@jsx-email/cli": "1.4.3",
@@ -175,7 +180,7 @@
     },
     "packages/desktop": {
       "name": "@opencode-ai/desktop",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@opencode-ai/app": "workspace:*",
         "@opencode-ai/ui": "workspace:*",
@@ -204,7 +209,7 @@
     },
     "packages/enterprise": {
       "name": "@opencode-ai/enterprise",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@opencode-ai/ui": "workspace:*",
         "@opencode-ai/util": "workspace:*",
@@ -233,7 +238,7 @@
     },
     "packages/function": {
       "name": "@opencode-ai/function",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "catalog:",
@@ -249,7 +254,7 @@
     },
     "packages/opencode": {
       "name": "opencode",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "bin": {
         "opencode": "./bin/opencode",
       },
@@ -257,26 +262,27 @@
         "@actions/core": "1.11.1",
         "@actions/github": "6.0.1",
         "@agentclientprotocol/sdk": "0.5.1",
-        "@ai-sdk/amazon-bedrock": "3.0.57",
-        "@ai-sdk/anthropic": "2.0.56",
-        "@ai-sdk/azure": "2.0.82",
-        "@ai-sdk/cerebras": "1.0.33",
-        "@ai-sdk/cohere": "2.0.21",
-        "@ai-sdk/deepinfra": "1.0.30",
-        "@ai-sdk/gateway": "2.0.23",
-        "@ai-sdk/google": "2.0.49",
-        "@ai-sdk/google-vertex": "3.0.81",
-        "@ai-sdk/groq": "2.0.33",
-        "@ai-sdk/mistral": "2.0.26",
-        "@ai-sdk/openai": "2.0.71",
-        "@ai-sdk/openai-compatible": "1.0.29",
-        "@ai-sdk/perplexity": "2.0.22",
-        "@ai-sdk/provider": "2.0.0",
-        "@ai-sdk/provider-utils": "3.0.19",
-        "@ai-sdk/togetherai": "1.0.30",
+        "@ai-sdk/amazon-bedrock": "3.0.73",
+        "@ai-sdk/anthropic": "2.0.57",
+        "@ai-sdk/azure": "2.0.91",
+        "@ai-sdk/cerebras": "1.0.34",
+        "@ai-sdk/cohere": "2.0.22",
+        "@ai-sdk/deepinfra": "1.0.31",
+        "@ai-sdk/gateway": "2.0.25",
+        "@ai-sdk/google": "2.0.52",
+        "@ai-sdk/google-vertex": "3.0.97",
+        "@ai-sdk/groq": "2.0.34",
+        "@ai-sdk/mistral": "2.0.27",
+        "@ai-sdk/openai": "2.0.89",
+        "@ai-sdk/openai-compatible": "1.0.30",
+        "@ai-sdk/perplexity": "2.0.23",
+        "@ai-sdk/provider": "2.0.1",
+        "@ai-sdk/provider-utils": "3.0.20",
+        "@ai-sdk/togetherai": "1.0.31",
         "@ai-sdk/vercel": "1.0.31",
-        "@ai-sdk/xai": "2.0.42",
+        "@ai-sdk/xai": "2.0.51",
         "@clack/prompts": "1.0.0-alpha.1",
+        "@gitlab/gitlab-ai-provider": "3.1.1",
         "@hono/standard-validator": "0.1.5",
         "@hono/zod-validator": "catalog:",
         "@iarna/toml": "2.2.5",
@@ -289,8 +295,8 @@
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/util": "workspace:*",
         "@openrouter/ai-sdk-provider": "1.5.2",
-        "@opentui/core": "0.1.72",
-        "@opentui/solid": "0.1.72",
+        "@opentui/core": "0.1.74",
+        "@opentui/solid": "0.1.74",
         "@parcel/watcher": "2.5.1",
         "@pierre/diffs": "catalog:",
         "@solid-primitives/event-bus": "1.1.2",
@@ -399,7 +405,7 @@
     },
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "zod": "catalog:",
@@ -419,9 +425,9 @@
     },
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "devDependencies": {
-        "@hey-api/openapi-ts": "0.88.1",
+        "@hey-api/openapi-ts": "0.90.4",
         "@tsconfig/node22": "catalog:",
         "@types/node": "catalog:",
         "@typescript/native-preview": "catalog:",
@@ -430,7 +436,7 @@
     },
     "packages/slack": {
       "name": "@opencode-ai/slack",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@slack/bolt": "^3.17.1",
@@ -443,7 +449,7 @@
     },
     "packages/ui": {
       "name": "@opencode-ai/ui",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -455,6 +461,7 @@
         "@solid-primitives/resize-observer": "2.1.3",
         "@solidjs/meta": "catalog:",
         "@typescript/native-preview": "catalog:",
+        "dompurify": "3.3.1",
         "fuzzysort": "catalog:",
         "katex": "0.16.27",
         "luxon": "catalog:",
@@ -482,7 +489,7 @@
     },
     "packages/util": {
       "name": "@opencode-ai/util",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "zod": "catalog:",
       },
@@ -493,7 +500,7 @@
     },
     "packages/web": {
       "name": "@opencode-ai/web",
-      "version": "1.1.11",
+      "version": "1.1.24",
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
@@ -549,12 +556,13 @@
     "@tailwindcss/vite": "4.1.11",
     "@tsconfig/bun": "1.0.9",
     "@tsconfig/node22": "22.0.2",
-    "@types/bun": "1.3.4",
+    "@types/bun": "1.3.5",
     "@types/luxon": "3.7.1",
     "@types/node": "22.13.9",
     "@typescript/native-preview": "7.0.0-dev.20251207.1",
-    "ai": "5.0.97",
+    "ai": "5.0.119",
     "diff": "8.0.2",
+    "dompurify": "3.3.1",
     "fuzzysort": "3.1.0",
     "hono": "4.10.7",
     "hono-openapi": "1.1.2",
@@ -592,48 +600,52 @@
 
     "@agentclientprotocol/sdk": ["@agentclientprotocol/[email protected]", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-9bq2TgjhLBSUSC5jE04MEe+Hqw8YePzKghhYZ9QcjOyonY3q2oJfX6GoSO83hURpEnsqEPIrex6VZN3+61fBJg=="],
 
-    "@ai-sdk/amazon-bedrock": ["@ai-sdk/[email protected].57", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.45", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mOUSLe+RgZzx0rtL1p9QXmSd/08z1EkBR+vQ1ydpd1t5P0Nx2kB8afiukEgM8nuDvmO9eYQlp7VTy1n5ffPs2g=="],
+    "@ai-sdk/amazon-bedrock": ["@ai-sdk/[email protected]3", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EAAGJ/dfbAZaqIhK3w52hq6cftSLZwXdC6uHKh8Cls1T0N4MxS6ykDf54UyFO3bZWkQxR+Mdw1B3qireGOxtJQ=="],
 
     "@ai-sdk/anthropic": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
 
-    "@ai-sdk/azure": ["@ai-sdk/[email protected].82", "", { "dependencies": { "@ai-sdk/openai": "2.0.80", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Bpab51ETBB4adZC1xGMYsryL/CB8j1sA+t5aDqhRv3t3WRLTxhaBDcFKtQTIuxiEQTFosz9Q2xQqdfBvQm5jHw=="],
+    "@ai-sdk/azure": ["@ai-sdk/[email protected].91", "", { "dependencies": { "@ai-sdk/openai": "2.0.89", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9tznVSs6LGQNKKxb8pKd7CkBV9yk+a/ENpFicHCj2CmBUKefxzwJ9JbUqrlK3VF6dGZw3LXq0dWxt7/Yekaj1w=="],
 
-    "@ai-sdk/cerebras": ["@ai-sdk/[email protected]3", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2gSSS/7kunIwMdC4td5oWsUAzoLw84ccGpz6wQbxVnrb1iWnrEnKa5tRBduaP6IXpzLWsu8wME3+dQhZy+gT7w=="],
+    "@ai-sdk/cerebras": ["@ai-sdk/[email protected]4", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XOK0dJsAGoPYi/lfR4KFBi8xhvJ46oCpAxUD6FmJAuJ4eh0qlj5zDt+myvzM8gvN7S6K7zHD+mdWlOPKGQT8Vg=="],
 
-    "@ai-sdk/cohere": ["@ai-sdk/[email protected]1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZjaZFvJlc5XOPi3QwTLEFZbHIgTJc6YGvxz+8zIMGVZi/hdynR8/f/C1A9x6mhzmBtAqi/dZ2h11oouAQH5z4g=="],
+    "@ai-sdk/cohere": ["@ai-sdk/[email protected]2", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="],
 
-    "@ai-sdk/deepinfra": ["@ai-sdk/[email protected]0", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XK8oRZFApzo6xnS5C+FhWUUkB2itA5Nfon3pU9dJVM0goViq8GwdleZTBRqhu4DE4KJURo5DGWpJr2hfV54cEg=="],
+    "@ai-sdk/deepinfra": ["@ai-sdk/[email protected]1", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-87qFcYNvDF/89hB//MQjYTb3tlsAfmgeZrZ34RESeBTZpSgs0EzYOMqPMwFTHUNp4wteoifikDJbaS/9Da8cfw=="],
 
-    "@ai-sdk/gateway": ["@ai-sdk/[email protected]3", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qmX7afPRszUqG5hryHF3UN8ITPIRSGmDW6VYCmByzjoUkgm3MekzSx2hMV1wr0P+llDeuXb378SjqUfpvWJulg=="],
+    "@ai-sdk/gateway": ["@ai-sdk/[email protected]5", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rq+FX55ne7lMiqai7NcvvDZj4HLsr+hg77WayqmySqc6zhw3tIOLxd4Ty6OpwNj0C0bVMi3iCl2zvJIEirh9XA=="],
 
-    "@ai-sdk/google": ["@ai-sdk/[email protected].49", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-efwKk4mOV0SpumUaQskeYABk37FJPmEYwoDJQEjyLRmGSjtHRe9P5Cwof5ffLvaFav2IaJpBGEz98pyTs7oNWA=="],
+    "@ai-sdk/google": ["@ai-sdk/[email protected].52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="],
 
-    "@ai-sdk/google-vertex": ["@ai-sdk/[email protected].81", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.50", "@ai-sdk/google": "2.0.44", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yrl5Ug0Mqwo9ya45oxczgy2RWgpEA/XQQCSFYP+3NZMQ4yA3Iim1vkOjVCsGaZZ8rjVk395abi1ZMZV0/6rqVA=="],
+    "@ai-sdk/google-vertex": ["@ai-sdk/[email protected].97", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/google": "2.0.52", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-s4tI7Z15i6FlbtCvS4SBRal8wRfkOXJzKxlS6cU4mJW/QfUfoVy4b22836NVNJwDvkG/HkDSfzwm/X8mn46MhA=="],
 
-    "@ai-sdk/groq": ["@ai-sdk/[email protected]3", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FWGl7xNr88NBveao3y9EcVWYUt9ABPrwLFY7pIutSNgaTf32vgvyhREobaMrLU4Scr5G/2tlNqOPZ5wkYMaZig=="],
+    "@ai-sdk/groq": ["@ai-sdk/[email protected]4", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wfCYkVgmVjxNA32T57KbLabVnv9aFUflJ4urJ7eWgTwbnmGQHElCTu+rJ3ydxkXSqxOkXPwMOttDm7XNrvPjmg=="],
 
-    "@ai-sdk/mistral": ["@ai-sdk/[email protected]6", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jxDB++4WI1wEx5ONNBI+VbkmYJOYIuS8UQY13/83UGRaiW7oB/WHiH4ETe6KzbKpQPB3XruwTJQjUMsMfKyTXA=="],
+    "@ai-sdk/mistral": ["@ai-sdk/[email protected]7", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gaptHgaXjMw3+eA0Q4FABcsj5nQNP6EpFaGUR+Pj5WJy7Kn6mApl975/x57224MfeJIShNpt8wFKK3tvh5ewKg=="],
 
     "@ai-sdk/openai": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="],
 
     "@ai-sdk/openai-compatible": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="],
 
-    "@ai-sdk/perplexity": ["@ai-sdk/[email protected]2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zwzcnk08R2J3mZcQPn4Ifl4wYGrvANR7jsBB0hCTUSbb+Rx3ybpikSWiGuXQXxdiRc1I5MWXgj70m+bZaLPvHw=="],
+    "@ai-sdk/perplexity": ["@ai-sdk/[email protected]3", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-aiaRvnc6mhQZKhTTSXPCjPH8Iqr5D/PfCN1hgVP/3RGTBbJtsd9HemIBSABeSdAKbsMH/PwJxgnqH75HEamcBA=="],
 
-    "@ai-sdk/provider": ["@ai-sdk/[email protected].0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
+    "@ai-sdk/provider": ["@ai-sdk/[email protected].1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
 
-    "@ai-sdk/provider-utils": ["@ai-sdk/[email protected].19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
+    "@ai-sdk/provider-utils": ["@ai-sdk/[email protected].20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
 
-    "@ai-sdk/togetherai": ["@ai-sdk/[email protected]0", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9bxQbIXnWSN4bNismrza3NvIo+ui/Y3pj3UN6e9vCszCWFCN45RgISi4oDe10RqmzaJ/X8cfO/Tem+K8MT3wGQ=="],
+    "@ai-sdk/togetherai": ["@ai-sdk/[email protected]1", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RlYubjStoZQxna4Ng91Vvo8YskvL7lW9zj68IwZfCnaDBSAp1u6Nhc5BR4ZtKnY6PA3XEtu4bATIQl7yiiQ+Lw=="],
 
     "@ai-sdk/vercel": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ggvwAMt/KsbqcdR6ILQrjwrRONLV/8aG6rOLbjcOGvV0Ai+WdZRRKQj5nOeQ06PvwVQtKdkp7S4IinpXIhCiHg=="],
 
-    "@ai-sdk/xai": ["@ai-sdk/[email protected].42", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wlwO4yRoZ/d+ca29vN8SDzxus7POdnL7GBTyRdSrt6icUF0hooLesauC8qRUC4aLxtqvMEc1YHtJOU7ZnLWbTQ=="],
+    "@ai-sdk/xai": ["@ai-sdk/[email protected].51", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AI3le03qiegkZvn9hpnpDwez49lOvQLj4QUBT8H41SMbrdTYOxn3ktTwrsSu90cNDdzKGMvoH0u2GHju1EdnCg=="],
 
     "@alloc/quick-lru": ["@alloc/[email protected]", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
 
     "@ampproject/remapping": ["@ampproject/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
 
+    "@anthropic-ai/sdk": ["@anthropic-ai/[email protected]", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="],
+
+    "@anycable/core": ["@anycable/[email protected]", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="],
+
     "@asamuzakjp/css-color": ["@asamuzakjp/[email protected]", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.4" } }, "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ=="],
 
     "@asamuzakjp/dom-selector": ["@asamuzakjp/[email protected]", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.4" } }, "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg=="],
@@ -992,13 +1004,17 @@
 
     "@fontsource/inter": ["@fontsource/[email protected]", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
 
+    "@gitlab/gitlab-ai-provider": ["@gitlab/[email protected]", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-7AtFrCflq2NzC99bj7YaqbQDCZyaScM1+L4ujllV5syiRTFE239Uhnd/yEkPXa7sUAnNRfN3CWusCkQ2zK/q9g=="],
+
+    "@graphql-typed-document-node/core": ["@graphql-typed-document-node/[email protected]", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
+
     "@happy-dom/global-registrator": ["@happy-dom/[email protected]", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
 
-    "@hey-api/codegen-core": ["@hey-api/[email protected]", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="],
+    "@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.2", "", { "dependencies": { "ansi-colors": "4.1.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-88cqrrB2cLXN8nMOHidQTcVOnZsJ5kebEbBefjMCifaUCwTA30ouSSWvTZqrOX4O104zjJyu7M8Gcv/NNYQuaA=="],
 
     "@hey-api/json-schema-ref-parser": ["@hey-api/[email protected]", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="],
 
-    "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.1", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-x/nDTupOnV9VuSeNIiJpgIpc915GHduhyseJeMTnI0JMsXaObmpa0rgPr3ASVEYMLgpvqozIEG1RTOOnal6zLQ=="],
+    "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.4", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.2", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-9l++kjcb0ui4JqPlueZ6OZ9zKn6eK/8//Z2jHcIXb5MRwDRgubOOSpTU5llEv3uvWfT10VzcMp99dySWq0AASw=="],
 
     "@hono/node-server": ["@hono/[email protected]", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
 
@@ -1344,21 +1360,21 @@
 
     "@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
-    "@opentui/core": ["@opentui/[email protected]2", "", { "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.72", "@opentui/core-darwin-x64": "0.1.72", "@opentui/core-linux-arm64": "0.1.72", "@opentui/core-linux-x64": "0.1.72", "@opentui/core-win32-arm64": "0.1.72", "@opentui/core-win32-x64": "0.1.72", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-l4WQzubBJ80Q0n77Lxuodjwwm8qj/sOa7IXxEAzzDDXY/7bsIhdSpVhRTt+KevBRlok5J+w/KMKYr8UzkA4/hA=="],
+    "@opentui/core": ["@opentui/[email protected]4", "", { "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.74", "@opentui/core-darwin-x64": "0.1.74", "@opentui/core-linux-arm64": "0.1.74", "@opentui/core-linux-x64": "0.1.74", "@opentui/core-win32-arm64": "0.1.74", "@opentui/core-win32-x64": "0.1.74", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g4W16ymv12JdgZ+9B4t7mpIICvzWy2+eHERfmDf80ALduOQCUedKQdULcBFhVCYUXIkDRtIy6CID5thMAah3FA=="],
 
-    "@opentui/core-darwin-arm64": ["@opentui/[email protected]2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RoU48kOrhLZYDBiXaDu1LXS2bwRdlJlFle8eUQiqJjLRbMIY34J/srBuL0JnAS3qKW4J34NepUQa0l0/S43Q3w=="],
+    "@opentui/core-darwin-arm64": ["@opentui/[email protected]4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rfmlDLtm/u17CnuhJgCxPeYMvOST+A2MOdVOk46IurtHO849bdYqK6iudKNlFRs1FOrymgSKF9GlWBHAOKeRjg=="],
 
-    "@opentui/core-darwin-x64": ["@opentui/[email protected]2", "", { "os": "darwin", "cpu": "x64" }, "sha512-hHUQw8i2LWPToRW1rjAiRqmNf34iJPS9ve9CJDygvFs5JOqUxN5yrfLfKfE+1bQjfFDHnpqW1HUk96iLhkPj8Q=="],
+    "@opentui/core-darwin-x64": ["@opentui/[email protected]4", "", { "os": "darwin", "cpu": "x64" }, "sha512-WAD8orsDV0ZdW/5GwjOOB4FY96772xbkz+rcV7WRzEFUVaqoBaC04IuqYzS9d5s+cjkbT5Cpj47hrVYkkVQKng=="],
 
-    "@opentui/core-linux-arm64": ["@opentui/[email protected]2", "", { "os": "linux", "cpu": "arm64" }, "sha512-63yml0OQ8tVa0JuDF9lBAWiChX6Q+iDO7lKv7c2n0352n/WyPr3iAgq4uSoH49HXuKeAXY/VwHGjvPzjXD/SDA=="],
+    "@opentui/core-linux-arm64": ["@opentui/[email protected]4", "", { "os": "linux", "cpu": "arm64" }, "sha512-lgmHzrzLy4e+rgBS+lhtsMLLgIMLbtLNMm6EzVPyYVDlLDGjM7+ulXMem7AtpaRrWrUUl4REiG9BoQUsCFDwYA=="],
 
-    "@opentui/core-linux-x64": ["@opentui/[email protected]2", "", { "os": "linux", "cpu": "x64" }, "sha512-51veiQXNLvzDsFzsEvt71uK7WhiRe2DnvlJSGBSe6aRRHHxjCFYHzYi7t6bitJqtDTUj+EaMPbH81oZ6xy7tyg=="],
+    "@opentui/core-linux-x64": ["@opentui/[email protected]4", "", { "os": "linux", "cpu": "x64" }, "sha512-8Mn2WbdBQ29xCThuPZezjDhd1N3+fXwKkGvCBOdTI0le6h2A/vCNbfUVjwfr/EGZSRXxCG+Yapol34BAULGpOA=="],
 
-    "@opentui/core-win32-arm64": ["@opentui/[email protected]2", "", { "os": "win32", "cpu": "arm64" }, "sha512-1Ep6OcaYTy1RlLOln+LNN7DL1iNyLwLjG2M8aO0pVJKFvxeD5P7rdRzY065E4uhkHeJIHuduUqxvUjD0dyuwbw=="],
+    "@opentui/core-win32-arm64": ["@opentui/[email protected]4", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvYUXz03avnI6ZluyLp00HPmR0UT/IE/6QS97XBsgJlUTtpnbKkBtB5jD1NHwWkElaRj1Qv2QP36ngFoJqbl9g=="],
 
-    "@opentui/core-win32-x64": ["@opentui/[email protected]2", "", { "os": "win32", "cpu": "x64" }, "sha512-5QUv91UkOINlkEaPky3kaxmJvshcJMBAX7LZtIroduaKBGpWRA1aogNhPZzp+30WkvgOU7aOtUktAZuFXb9WdQ=="],
+    "@opentui/core-win32-x64": ["@opentui/[email protected]4", "", { "os": "win32", "cpu": "x64" }, "sha512-3wfWXaAKOIlDQz6ZZIESf2M+YGZ7uFHijjTEM8w/STRlLw8Y6+QyGYi1myHSM4d6RSO+/s2EMDxvjDf899W9vQ=="],
 
-    "@opentui/solid": ["@opentui/[email protected]2", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.72", "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-hytoLPboL/MTY/BQUnf/HlBuNXTVONney0X+PIQI82wT7kMx7+HHI2wnowpM3dyvA7l6NfORSud2cs9kIUBFBw=="],
+    "@opentui/solid": ["@opentui/[email protected]4", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.74", "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-Vz82cI8T9YeJjGsVg4ULp6ral4N+xyt1j9A6Tbu3aaQgEKiB74LW03EXREehfjPr1irOFxtKfWPbx5NKH0Upag=="],
 
     "@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
 
@@ -1654,7 +1670,7 @@
 
     "@smithy/credential-provider-imds": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="],
 
-    "@smithy/eventstream-codec": ["@smithy/[email protected].5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
+    "@smithy/eventstream-codec": ["@smithy/[email protected].7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
 
     "@smithy/eventstream-serde-browser": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw=="],
 
@@ -1744,6 +1760,8 @@
 
     "@smithy/uuid": ["@smithy/[email protected]", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="],
 
+    "@socket.io/component-emitter": ["@socket.io/[email protected]", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
+
     "@solid-primitives/active-element": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="],
 
     "@solid-primitives/audio": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="],
@@ -1796,6 +1814,8 @@
 
     "@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
 
+    "@stripe/stripe-js": ["@stripe/[email protected]", "", {}, "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA=="],
+
     "@swc/helpers": ["@swc/[email protected]", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
 
     "@tailwindcss/node": ["@tailwindcss/[email protected]", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
@@ -1914,7 +1934,7 @@
 
     "@types/braces": ["@types/[email protected]", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
 
-    "@types/bun": ["@types/[email protected].4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
+    "@types/bun": ["@types/[email protected].5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
 
     "@types/chai": ["@types/[email protected]", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
 
@@ -1992,6 +2012,8 @@
 
     "@types/serve-static": ["@types/[email protected]", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
 
+    "@types/trusted-types": ["@types/[email protected]", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
+
     "@types/tsscmp": ["@types/[email protected]", "", {}, "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g=="],
 
     "@types/tunnel": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="],
@@ -2070,7 +2092,7 @@
 
     "@vitest/utils": ["@vitest/[email protected]", "", { "dependencies": { "@vitest/pretty-format": "4.0.13", "tinyrainbow": "^3.0.3" } }, "sha512-ydozWyQ4LZuu8rLp47xFUWis5VOKMdHjXCWhs1LuJsTNKww+pTHQNK4e0assIB9K80TxFyskENL6vCu3j34EYA=="],
 
-    "@webgpu/types": ["@webgpu/[email protected].66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
+    "@webgpu/types": ["@webgpu/[email protected].54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
 
     "@zip.js/zip.js": ["@zip.js/[email protected]", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
 
@@ -2090,7 +2112,7 @@
 
     "agentkeepalive": ["[email protected]", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
 
-    "ai": ["[email protected]7", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="],
+    "ai": ["[email protected].119", "", { "dependencies": { "@ai-sdk/gateway": "2.0.25", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HUOwhc17fl2SZTJGZyA/99aNu706qKfXaUBCy9vgZiXBwrxg2eTzn2BCz7kmYDsfx6Fg2ACBy2icm41bsDXCTw=="],
 
     "ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
 
@@ -2244,7 +2266,7 @@
 
     "bun-pty": ["[email protected]", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="],
 
-    "bun-types": ["[email protected].4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
+    "bun-types": ["[email protected].5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
 
     "bun-webgpu": ["[email protected]", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
 
@@ -2260,7 +2282,7 @@
 
     "bytes": ["[email protected]", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
 
-    "c12": ["[email protected].2", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A=="],
+    "c12": ["[email protected].3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
 
     "call-bind": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
 
@@ -2402,6 +2424,8 @@
 
     "csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
 
+    "data-uri-to-buffer": ["[email protected]", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
+
     "data-urls": ["[email protected]", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="],
 
     "data-view-buffer": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
@@ -2480,6 +2504,8 @@
 
     "domhandler": ["[email protected]", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
 
+    "dompurify": ["[email protected]", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="],
+
     "domutils": ["[email protected]", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
 
     "dot-case": ["[email protected]", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="],
@@ -2512,6 +2538,10 @@
 
     "encodeurl": ["[email protected]", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
 
+    "engine.io-client": ["[email protected]", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="],
+
+    "engine.io-parser": ["[email protected]", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
+
     "enhanced-resolve": ["[email protected]", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
 
     "entities": ["[email protected]", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
@@ -2646,6 +2676,8 @@
 
     "fdir": ["[email protected]", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
 
+    "fetch-blob": ["[email protected]", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
+
     "fflate": ["[email protected]", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
 
     "file-entry-cache": ["[email protected]", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -2686,6 +2718,8 @@
 
     "formdata-node": ["[email protected]", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
 
+    "formdata-polyfill": ["[email protected]", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
+
     "forwarded": ["[email protected]", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
 
     "fraction.js": ["[email protected]", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
@@ -2708,9 +2742,9 @@
 
     "fuzzysort": ["[email protected]", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
 
-    "gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
+    "gaxios": ["[email protected].3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
 
-    "gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
+    "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
 
     "gel": ["[email protected]", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ=="],
 
@@ -2758,17 +2792,21 @@
 
     "globby": ["[email protected]", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.1.1", "ignore": "^5.1.4", "merge2": "^1.3.0", "slash": "^3.0.0" } }, "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg=="],
 
-    "google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
+    "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
 
-    "google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
+    "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
 
     "gopd": ["[email protected]", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
 
     "graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
 
+    "graphql": ["[email protected]", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="],
+
+    "graphql-request": ["[email protected]", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw=="],
+
     "gray-matter": ["[email protected]", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
 
-    "gtoken": ["[email protected]", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
+    "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
 
     "h3": ["[email protected]", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="],
 
@@ -3010,6 +3048,8 @@
 
     "isexe": ["[email protected]", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
 
+    "isomorphic-ws": ["[email protected]", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
+
     "isomorphic.js": ["[email protected]", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="],
 
     "iterate-iterator": ["[email protected]", "", {}, "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw=="],
@@ -3048,6 +3088,8 @@
 
     "json-schema": ["[email protected]", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
 
+    "json-schema-to-ts": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
+
     "json-schema-traverse": ["[email protected]", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
 
     "json-schema-typed": ["[email protected]", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
@@ -3344,6 +3386,8 @@
 
     "named-placeholders": ["[email protected]", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
 
+    "nanoevents": ["[email protected]", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="],
+
     "nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
 
     "natural-compare": ["[email protected]", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
@@ -3720,6 +3764,8 @@
 
     "reusify": ["[email protected]", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
 
+    "rimraf": ["[email protected]", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
+
     "rollup": ["[email protected]", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="],
 
     "rou3": ["[email protected]", "", {}, "sha512-aoFj6f7MJZ5muJ+Of79nrhs9N3oLGqi2VEMe94Zbkjb6Wupha46EuoYgpWSOZlXww3bbd8ojgXTAA2mzimX5Ww=="],
@@ -3752,7 +3798,7 @@
 
     "selderee": ["[email protected]", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
 
-    "semver": ["[email protected].2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+    "semver": ["[email protected].3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
 
     "send": ["[email protected]", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
 
@@ -3810,6 +3856,10 @@
 
     "smol-toml": ["[email protected]", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="],
 
+    "socket.io-client": ["[email protected]", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
+
+    "socket.io-parser": ["[email protected]", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="],
+
     "solid-js": ["[email protected]", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="],
 
     "solid-list": ["[email protected]", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="],
@@ -3820,6 +3870,8 @@
 
     "solid-refresh": ["[email protected]", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
 
+    "solid-stripe": ["[email protected]", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="],
+
     "solid-use": ["[email protected]", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
 
     "source-map": ["[email protected]", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
@@ -3990,6 +4042,8 @@
 
     "trough": ["[email protected]", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
 
+    "ts-algebra": ["[email protected]", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
+
     "ts-api-utils": ["[email protected]", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg=="],
 
     "ts-interface-checker": ["[email protected]", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
@@ -4200,6 +4254,8 @@
 
     "xmlchars": ["[email protected]", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
 
+    "xmlhttprequest-ssl": ["[email protected]", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
+
     "xtend": ["[email protected]", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
 
     "xxhash-wasm": ["[email protected]", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
@@ -4254,35 +4310,33 @@
 
     "@agentclientprotocol/sdk/zod": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
 
-    "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/[email protected].45", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ipv62vavDCmrV/oE/lXehL9FzwQuZOnnlhPEftWizx464Wb6lvnBTJx8uhmEYruFSzOWTI95Z33ncZ4tA8E6RQ=="],
+    "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/[email protected]7", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="],
 
-    "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
+    "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
 
     "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
 
-    "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/[email protected]0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tNHuraF11db+8xJEDBoU9E3vMcpnHFKRhnLQ3DQX2LnEzfPB9DksZ8rE+yVuDN1WRW9cm2OWAhgHFgVKs7ICuw=="],
+    "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/[email protected]9", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
 
-    "@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected].29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
+    "@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected].30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
 
-    "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected].29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
+    "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected].30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
 
-    "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/[email protected]0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
+    "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/[email protected]7", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="],
 
-    "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
+    "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
 
     "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
 
+    "@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
+
     "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
 
-    "@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected].29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
+    "@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected].30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
 
     "@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
 
-    "@ai-sdk/vercel/@ai-sdk/provider": ["@ai-sdk/[email protected]", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
-
-    "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
-
-    "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
+    "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
 
     "@asamuzakjp/css-color/lru-cache": ["[email protected]", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
 
@@ -4372,6 +4426,8 @@
 
     "@expressive-code/plugin-shiki/shiki": ["[email protected]", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
 
+    "@gitlab/gitlab-ai-provider/zod": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+
     "@happy-dom/global-registrator/happy-dom": ["[email protected]", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g=="],
 
     "@hey-api/json-schema-ref-parser/js-yaml": ["[email protected]", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
@@ -4582,6 +4638,10 @@
 
     "@slack/web-api/p-queue": ["[email protected]", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
 
+    "@smithy/eventstream-codec/@smithy/types": ["@smithy/[email protected]", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="],
+
+    "@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/[email protected]", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
+
     "@solidjs/start/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
 
     "@solidjs/start/path-to-regexp": ["[email protected]", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
@@ -4622,14 +4682,12 @@
 
     "@typescript-eslint/typescript-estree/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
 
+    "@typescript-eslint/typescript-estree/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+
     "@vitejs/plugin-react/@babel/core": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "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-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
 
     "accepts/mime-types": ["[email protected]", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
 
-    "ai/@ai-sdk/gateway": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W+cB1sOWvPcz9qiIsNtD+HxUrBUva2vWv2K1EFukuImX+HA0uZx3EyyOjhYQ9gtf/teqEG80M6OvJ7xx/VLV2A=="],
-
-    "ai/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
-
     "ansi-align/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 
     "anymatch/picomatch": ["[email protected]", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -4642,6 +4700,8 @@
 
     "astro/diff": ["[email protected]", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
 
+    "astro/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+
     "astro/shiki": ["[email protected]", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
 
     "astro/unstorage": ["[email protected]", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q=="],
@@ -4664,6 +4724,10 @@
 
     "body-parser/qs": ["[email protected]", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
 
+    "bun-webgpu/@webgpu/types": ["@webgpu/[email protected]", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
+
+    "c12/chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
+
     "cheerio/parse5": ["[email protected]", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
 
     "clean-css/source-map": ["[email protected]", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@@ -4688,6 +4752,10 @@
 
     "editorconfig/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="],
 
+    "editorconfig/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+
+    "engine.io-client/ws": ["[email protected]", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
+
     "es-get-iterator/isarray": ["[email protected]", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
 
     "esbuild-plugin-copy/chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -4712,15 +4780,17 @@
 
     "fast-glob/glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
 
+    "fetch-blob/web-streams-polyfill": ["[email protected]", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
+
     "finalhandler/debug": ["[email protected]", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
 
     "form-data/mime-types": ["[email protected]", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
 
     "framer-motion/react-dom": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
 
-    "gaxios/is-stream": ["[email protected]", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
+    "gaxios/node-fetch": ["[email protected]", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
 
-    "gaxios/uuid": ["[email protected]", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
+    "gel/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
 
     "glob/minimatch": ["[email protected]", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
 
@@ -4748,6 +4818,8 @@
 
     "jsonwebtoken/jws": ["[email protected]", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
 
+    "jsonwebtoken/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+
     "katex/commander": ["[email protected]", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
 
     "lazystream/readable-stream": ["[email protected]", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
@@ -4776,11 +4848,11 @@
 
     "nypm/tinyexec": ["[email protected]", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
 
-    "opencode/@ai-sdk/anthropic": ["@ai-sdk/[email protected]6", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
+    "opencode/@ai-sdk/anthropic": ["@ai-sdk/[email protected]7", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="],
 
-    "opencode/@ai-sdk/openai": ["@ai-sdk/[email protected].71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
+    "opencode/@ai-sdk/openai": ["@ai-sdk/[email protected].89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
 
-    "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected].29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
+    "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected].30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
 
     "opencode/@octokit/rest": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
 
@@ -4838,6 +4910,8 @@
 
     "refractor/prismjs": ["[email protected]", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="],
 
+    "rimraf/glob": ["[email protected]", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
+
     "router/path-to-regexp": ["[email protected]", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
 
     "safe-array-concat/isarray": ["[email protected]", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
@@ -4852,6 +4926,8 @@
 
     "sharp/detect-libc": ["[email protected]", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
 
+    "sharp/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+
     "shiki/@shikijs/core": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
 
     "shiki/@shikijs/types": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
@@ -5432,6 +5508,8 @@
 
     "body-parser/debug/ms": ["[email protected]", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
 
+    "c12/chokidar/readdirp": ["[email protected]", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
+
     "cheerio/parse5/entities": ["[email protected]", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
 
     "cross-spawn/which/isexe": ["[email protected]", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@@ -5518,8 +5596,6 @@
 
     "node-fetch/whatwg-url/webidl-conversions": ["[email protected]", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
 
-    "opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
-
     "opencode/@octokit/rest/@octokit/core": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="],
 
     "opencode/@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
@@ -5572,6 +5648,12 @@
 
     "refractor/parse-entities/is-hexadecimal": ["[email protected]", "", {}, "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="],
 
+    "rimraf/glob/jackspeak": ["[email protected]", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
+
+    "rimraf/glob/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+
+    "rimraf/glob/path-scurry": ["[email protected]", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
+
     "send/debug/ms": ["[email protected]", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
 
     "string-width-cjs/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -5826,6 +5908,10 @@
 
     "refractor/parse-entities/is-alphanumerical/is-alphabetical": ["[email protected]", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="],
 
+    "rimraf/glob/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+    "rimraf/glob/path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+
     "tw-to-css/tailwindcss/chokidar/glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
 
     "tw-to-css/tailwindcss/chokidar/readdirp": ["[email protected]", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],

+ 3 - 3
flake.lock

@@ -2,11 +2,11 @@
   "nodes": {
     "nixpkgs": {
       "locked": {
-        "lastModified": 1767966113,
-        "narHash": "sha256-mSTsvXa4WveSRJexsmCbm9dY17B1fKp7NLpJxllpQw4=",
+        "lastModified": 1768395095,
+        "narHash": "sha256-ZhuYJbwbZT32QA95tSkXd9zXHcdZj90EzHpEXBMabaw=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "5f02c91314c8ba4afe83b256b023756412218535",
+        "rev": "13868c071cc73a5e9f610c47d7bb08e5da64fdd5",
         "type": "github"
       },
       "original": {

+ 23 - 3
flake.nix

@@ -27,11 +27,28 @@
         "aarch64-darwin" = "bun-darwin-arm64";
         "x86_64-darwin" = "bun-darwin-x64";
       };
-      defaultNodeModules = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
+
+      # Parse "bun-{os}-{cpu}" to {os, cpu}
+      parseBunTarget =
+        target:
+        let
+          parts = lib.splitString "-" target;
+        in
+        {
+          os = builtins.elemAt parts 1;
+          cpu = builtins.elemAt parts 2;
+        };
+
       hashesFile = "${./nix}/hashes.json";
       hashesData =
         if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { };
-      nodeModulesHash = hashesData.nodeModules or defaultNodeModules;
+      # Lookup hash: supports per-system ({system: hash}) or legacy single hash
+      nodeModulesHashFor =
+        system:
+        if builtins.isAttrs hashesData.nodeModules then
+          hashesData.nodeModules.${system}
+        else
+          hashesData.nodeModules;
       modelsDev = forEachSystem (
         system:
         let
@@ -63,8 +80,11 @@
         system:
         let
           pkgs = pkgsFor system;
+          bunPlatform = parseBunTarget bunTarget.${system};
           mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
-            hash = nodeModulesHash;
+            hash = nodeModulesHashFor system;
+            bunCpu = bunPlatform.cpu;
+            bunOs = bunPlatform.os;
           };
           mkOpencode = pkgs.callPackage ./nix/opencode.nix { };
           mkDesktop = pkgs.callPackage ./nix/desktop.nix { };

+ 6 - 5
github/README.md

@@ -81,12 +81,13 @@ This will walk you through installing the GitHub app, creating the workflow, and
        permissions:
          id-token: write
        steps:
-         - name: Checkout repository
-           uses: actions/checkout@v6
-           with:
-             fetch-depth: 1
+          - name: Checkout repository
+            uses: actions/checkout@v6
+            with:
+              fetch-depth: 1
+              persist-credentials: false
 
-         - name: Run opencode
+          - name: Run opencode
            uses: anomalyco/opencode/github@latest
            env:
              ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

+ 4 - 0
infra/console.ts

@@ -119,9 +119,11 @@ const ZEN_MODELS = [
   new sst.Secret("ZEN_MODELS5"),
   new sst.Secret("ZEN_MODELS6"),
   new sst.Secret("ZEN_MODELS7"),
+  new sst.Secret("ZEN_MODELS8"),
 ]
 const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
 const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
+const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
 const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
   properties: { value: auth.url.apply((url) => url!) },
 })
@@ -163,6 +165,7 @@ new sst.cloudflare.x.SolidStart("Console", {
     AWS_SES_ACCESS_KEY_ID,
     AWS_SES_SECRET_ACCESS_KEY,
     ZEN_BLACK,
+    new sst.Secret("ZEN_SESSION_SECRET"),
     ...ZEN_MODELS,
     ...($dev
       ? [
@@ -176,6 +179,7 @@ new sst.cloudflare.x.SolidStart("Console", {
     //VITE_DOCS_URL: web.url.apply((url) => url!),
     //VITE_API_URL: gateway.url.apply((url) => url!),
     VITE_AUTH_URL: auth.url.apply((url) => url!),
+    VITE_STRIPE_PUBLISHABLE_KEY: STRIPE_PUBLISHABLE_KEY.value,
   },
   transform: {
     server: {

+ 1 - 1
install

@@ -369,7 +369,7 @@ case $current_shell in
         config_files="$HOME/.config/fish/config.fish"
     ;;
     zsh)
-        config_files="$HOME/.zshrc $HOME/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
+        config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
     ;;
     bash)
         config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"

+ 6 - 1
nix/hashes.json

@@ -1,3 +1,8 @@
 {
-  "nodeModules": "sha256-aIP+P0hSnQ50+lhnKvfRzQ8DlOhLjrrwarckxI3LBmE="
+  "nodeModules": {
+    "x86_64-linux": "sha256-07XxcHLuToM4QfWVyaPLACxjPZ93ZM7gtpX2o08Lp18=",
+    "aarch64-linux": "sha256-0Im52dLeZ0ZtaPJr/U4m7+IRtOfziHNJI/Bu/V6cPho=",
+    "aarch64-darwin": "sha256-U2UvE70nM0OI0VhIku8qnX+ptPbA+Q/y1BGXbFMcyt4=",
+    "x86_64-darwin": "sha256-CpZFHBMPJSib2Vqs6oC8HQjQtviPUMa/qezHAe22N/A="
+  }
 }

+ 4 - 2
nix/node-modules.nix

@@ -5,6 +5,8 @@
   bun,
   cacert,
   curl,
+  bunCpu,
+  bunOs,
 }:
 args:
 stdenvNoCC.mkDerivation {
@@ -29,8 +31,8 @@ stdenvNoCC.mkDerivation {
     export HOME=$(mktemp -d)
     export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
     bun install \
-      --cpu="*" \
-      --os="*" \
+      --cpu="${bunCpu}" \
+      --os="${bunOs}" \
       --frozen-lockfile \
       --ignore-scripts \
       --no-progress \

+ 10 - 3
nix/scripts/update-hashes.sh

@@ -10,7 +10,7 @@ HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE}
 if [ ! -f "$HASH_FILE" ]; then
   cat >"$HASH_FILE" <<EOF
 {
-  "nodeModules": "$DUMMY"
+  "nodeModules": {}
 }
 EOF
 fi
@@ -33,9 +33,16 @@ trap cleanup EXIT
 
 write_node_modules_hash() {
   local value="$1"
+  local system="${2:-$SYSTEM}"
   local temp
   temp=$(mktemp)
-  jq --arg value "$value" '.nodeModules = $value' "$HASH_FILE" >"$temp"
+  
+  if jq -e '.nodeModules | type == "object"' "$HASH_FILE" >/dev/null 2>&1; then
+    jq --arg system "$system" --arg value "$value" '.nodeModules[$system] = $value' "$HASH_FILE" >"$temp"
+  else
+    jq --arg system "$system" --arg value "$value" '.nodeModules = {($system): $value}' "$HASH_FILE" >"$temp"
+  fi
+  
   mv "$temp" "$HASH_FILE"
 }
 
@@ -104,7 +111,7 @@ fi
 
 write_node_modules_hash "$CORRECT_HASH"
 
-jq -e --arg hash "$CORRECT_HASH" '.nodeModules == $hash' "$HASH_FILE" >/dev/null
+jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null
 
 echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH"
 

+ 3 - 2
package.json

@@ -22,7 +22,7 @@
       "packages/opencode/webgui"
     ],
     "catalog": {
-      "@types/bun": "1.3.4",
+      "@types/bun": "1.3.5",
       "@octokit/rest": "22.0.0",
       "@hono/zod-validator": "0.4.2",
       "ulid": "3.0.1",
@@ -37,7 +37,8 @@
       "@solid-primitives/storage": "4.3.3",
       "@tailwindcss/vite": "4.1.11",
       "diff": "8.0.2",
-      "ai": "5.0.97",
+      "dompurify": "3.3.1",
+      "ai": "5.0.119",
       "hono": "4.10.7",
       "hono-openapi": "1.1.2",
       "fuzzysort": "3.1.0",

+ 1 - 2
packages/app/index.html

@@ -13,12 +13,11 @@
     <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" />
-    <!-- Theme preload script - applies cached theme to avoid FOUC -->
     <script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
   </head>
   <body class="antialiased overscroll-none text-12-regular overflow-hidden">
     <noscript>You need to enable JavaScript to run this app.</noscript>
-    <div id="root" class="flex flex-col h-dvh"></div>
+    <div id="root" class="flex flex-col h-dvh p-px"></div>
     <script src="/src/entry.tsx" type="module"></script>
   </body>
 </html>

+ 1 - 1
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/app",
-  "version": "1.1.11",
+  "version": "1.1.24",
   "description": "",
   "type": "module",
   "exports": {

+ 12 - 12
packages/app/src/app.tsx

@@ -33,19 +33,10 @@ const Loading = () => <div class="size-full flex items-center justify-center tex
 
 declare global {
   interface Window {
-    __OPENCODE__?: { updaterEnabled?: boolean; port?: number; serverReady?: boolean }
+    __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string }
   }
 }
 
-const defaultServerUrl = iife(() => {
-  if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
-  if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}`
-  if (import.meta.env.DEV)
-    return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
-
-  return window.location.origin
-})
-
 export function AppBaseProviders(props: ParentProps) {
   return (
     <MetaProvider>
@@ -74,9 +65,18 @@ function ServerKey(props: ParentProps) {
   )
 }
 
-export function AppInterface() {
+export function AppInterface(props: { defaultUrl?: string }) {
+  const defaultServerUrl = () => {
+    if (props.defaultUrl) return props.defaultUrl
+    if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
+    if (import.meta.env.DEV)
+      return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
+
+    return window.location.origin
+  }
+
   return (
-    <ServerProvider defaultUrl={defaultServerUrl}>
+    <ServerProvider defaultUrl={defaultServerUrl()}>
       <ServerKey>
         <GlobalSDKProvider>
           <GlobalSyncProvider>

+ 99 - 0
packages/app/src/components/dialog-fork.tsx

@@ -0,0 +1,99 @@
+import { Component, createMemo } from "solid-js"
+import { useNavigate, useParams } from "@solidjs/router"
+import { useSync } from "@/context/sync"
+import { useSDK } from "@/context/sdk"
+import { usePrompt } from "@/context/prompt"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List } from "@opencode-ai/ui/list"
+import { extractPromptFromParts } from "@/utils/prompt"
+import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
+import { base64Encode } from "@opencode-ai/util/encode"
+
+interface ForkableMessage {
+  id: string
+  text: string
+  time: string
+}
+
+function formatTime(date: Date): string {
+  return date.toLocaleTimeString(undefined, { timeStyle: "short" })
+}
+
+export const DialogFork: Component = () => {
+  const params = useParams()
+  const navigate = useNavigate()
+  const sync = useSync()
+  const sdk = useSDK()
+  const prompt = usePrompt()
+  const dialog = useDialog()
+
+  const messages = createMemo((): ForkableMessage[] => {
+    const sessionID = params.id
+    if (!sessionID) return []
+
+    const msgs = sync.data.message[sessionID] ?? []
+    const result: ForkableMessage[] = []
+
+    for (const message of msgs) {
+      if (message.role !== "user") continue
+
+      const parts = sync.data.part[message.id] ?? []
+      const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored)
+      if (!textPart) continue
+
+      result.push({
+        id: message.id,
+        text: textPart.text.replace(/\n/g, " ").slice(0, 200),
+        time: formatTime(new Date(message.time.created)),
+      })
+    }
+
+    return result.reverse()
+  })
+
+  const handleSelect = (item: ForkableMessage | undefined) => {
+    if (!item) return
+
+    const sessionID = params.id
+    if (!sessionID) return
+
+    const parts = sync.data.part[item.id] ?? []
+    const restored = extractPromptFromParts(parts, { directory: sdk.directory })
+
+    dialog.close()
+
+    sdk.client.session.fork({ sessionID, messageID: item.id }).then((forked) => {
+      if (!forked.data) return
+      navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`)
+      requestAnimationFrame(() => {
+        prompt.set(restored)
+      })
+    })
+  }
+
+  return (
+    <Dialog title="Fork from message">
+      <List
+        class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
+        search={{ placeholder: "Search", autofocus: true }}
+        emptyMessage="No messages to fork from"
+        key={(x) => x.id}
+        items={messages}
+        filterKeys={["text"]}
+        onSelect={handleSelect}
+      >
+        {(item) => (
+          <div class="w-full flex items-center gap-2">
+            <span class="truncate flex-1 min-w-0 text-left" style={{ "font-weight": "400" }}>
+              {item.text}
+            </span>
+            <span class="text-text-weak shrink-0" style={{ "font-weight": "400" }}>
+              {item.time}
+            </span>
+          </div>
+        )}
+      </List>
+    </Dialog>
+  )
+}

+ 153 - 25
packages/app/src/components/dialog-select-file.tsx

@@ -4,11 +4,26 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
 import { List } from "@opencode-ai/ui/list"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { useParams } from "@solidjs/router"
-import { createMemo } from "solid-js"
+import { createMemo, createSignal, onCleanup, Show } from "solid-js"
+import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
 import { useLayout } from "@/context/layout"
 import { useFile } from "@/context/file"
 
+type EntryType = "command" | "file"
+
+type Entry = {
+  id: string
+  type: EntryType
+  title: string
+  description?: string
+  keybind?: string
+  category: "Commands" | "Files"
+  option?: CommandOption
+  path?: string
+}
+
 export function DialogSelectFile() {
+  const command = useCommand()
   const layout = useLayout()
   const file = useFile()
   const dialog = useDialog()
@@ -16,35 +31,148 @@ export function DialogSelectFile() {
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
   const tabs = createMemo(() => layout.tabs(sessionKey()))
   const view = createMemo(() => layout.view(sessionKey()))
+  const state = { cleanup: undefined as (() => void) | void, committed: false }
+  const [grouped, setGrouped] = createSignal(false)
+  const common = ["session.new", "session.previous", "session.next", "terminal.toggle", "review.toggle"]
+  const limit = 5
+
+  const allowed = createMemo(() =>
+    command.options.filter(
+      (option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
+    ),
+  )
+
+  const commandItem = (option: CommandOption): Entry => ({
+    id: "command:" + option.id,
+    type: "command",
+    title: option.title,
+    description: option.description,
+    keybind: option.keybind,
+    category: "Commands",
+    option,
+  })
+
+  const fileItem = (path: string): Entry => ({
+    id: "file:" + path,
+    type: "file",
+    title: path,
+    category: "Files",
+    path,
+  })
+
+  const list = createMemo(() => allowed().map(commandItem))
+
+  const picks = createMemo(() => {
+    const all = allowed()
+    const order = new Map(common.map((id, index) => [id, index]))
+    const picked = all.filter((option) => order.has(option.id))
+    const base = picked.length ? picked : all.slice(0, limit)
+    const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
+    return sorted.map(commandItem)
+  })
+
+  const recent = createMemo(() => {
+    const all = tabs().all()
+    const active = tabs().active()
+    const order = active ? [active, ...all.filter((item) => item !== active)] : all
+    const seen = new Set<string>()
+    const items: Entry[] = []
+
+    for (const item of order) {
+      const path = file.pathFromTab(item)
+      if (!path) continue
+      if (seen.has(path)) continue
+      seen.add(path)
+      items.push(fileItem(path))
+    }
+
+    return items.slice(0, limit)
+  })
+
+  const items = async (filter: string) => {
+    const query = filter.trim()
+    setGrouped(query.length > 0)
+    if (!query) return [...picks(), ...recent()]
+    const files = await file.searchFiles(query)
+    const entries = files.map(fileItem)
+    return [...list(), ...entries]
+  }
+
+  const handleMove = (item: Entry | undefined) => {
+    state.cleanup?.()
+    if (!item) return
+    if (item.type !== "command") return
+    state.cleanup = item.option?.onHighlight?.()
+  }
+
+  const open = (path: string) => {
+    const value = file.tab(path)
+    tabs().open(value)
+    file.load(path)
+    view().reviewPanel.open()
+  }
+
+  const handleSelect = (item: Entry | undefined) => {
+    if (!item) return
+    state.committed = true
+    state.cleanup = undefined
+    dialog.close()
+
+    if (item.type === "command") {
+      item.option?.onSelect?.("palette")
+      return
+    }
+
+    if (!item.path) return
+    open(item.path)
+  }
+
+  onCleanup(() => {
+    if (state.committed) return
+    state.cleanup?.()
+  })
+
   return (
-    <Dialog title="Select file">
+    <Dialog title="Search">
       <List
-        search={{ placeholder: "Search files", autofocus: true }}
-        emptyMessage="No files found"
-        items={file.searchFiles}
-        key={(x) => x}
-        onSelect={(path) => {
-          if (path) {
-            const value = file.tab(path)
-            tabs().open(value)
-            file.load(path)
-            view().reviewPanel.open()
-          }
-          dialog.close()
-        }}
+        search={{ placeholder: "Search files and commands", autofocus: true }}
+        emptyMessage="No results found"
+        items={items}
+        key={(item) => item.id}
+        filterKeys={["title", "description", "category"]}
+        groupBy={(item) => (grouped() ? item.category : "")}
+        onMove={handleMove}
+        onSelect={handleSelect}
       >
-        {(i) => (
-          <div class="w-full flex items-center justify-between rounded-md">
-            <div class="flex items-center gap-x-3 grow min-w-0">
-              <FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
-              <div class="flex items-center text-14-regular">
-                <span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
-                  {getDirectory(i)}
-                </span>
-                <span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
+        {(item) => (
+          <Show
+            when={item.type === "command"}
+            fallback={
+              <div class="w-full flex items-center justify-between rounded-md">
+                <div class="flex items-center gap-x-3 grow min-w-0">
+                  <FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
+                  <div class="flex items-center text-14-regular">
+                    <span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
+                      {getDirectory(item.path ?? "")}
+                    </span>
+                    <span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span>
+                  </div>
+                </div>
+              </div>
+            }
+          >
+            <div class="w-full flex items-center justify-between gap-4">
+              <div class="flex items-center gap-2 min-w-0">
+                <span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
+                <Show when={item.description}>
+                  <span class="text-14-regular text-text-weak truncate">{item.description}</span>
+                </Show>
               </div>
+              <Show when={item.keybind}>
+                <span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(item.keybind ?? "")}</span>
+              </Show>
             </div>
-          </div>
+          </Show>
         )}
       </List>
     </Dialog>

+ 81 - 14
packages/app/src/components/dialog-select-server.tsx

@@ -1,10 +1,11 @@
-import { createEffect, createMemo, onCleanup } from "solid-js"
+import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js"
 import { createStore, reconcile } from "solid-js/store"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { List } from "@opencode-ai/ui/list"
 import { TextField } from "@opencode-ai/ui/text-field"
 import { Button } from "@opencode-ai/ui/button"
+import { IconButton } from "@opencode-ai/ui/icon-button"
 import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
 import { usePlatform } from "@/context/platform"
 import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
@@ -35,6 +36,8 @@ export function DialogSelectServer() {
     error: "",
     status: {} as Record<string, ServerStatus | undefined>,
   })
+  const [defaultUrl, defaultUrlActions] = createResource(() => platform.getDefaultServerUrl?.())
+  const isDesktop = platform.platform === "desktop"
 
   const items = createMemo(() => {
     const current = server.url
@@ -114,6 +117,10 @@ export function DialogSelectServer() {
     select(value, true)
   }
 
+  async function handleRemove(url: string) {
+    server.remove(url)
+  }
+
   return (
     <Dialog title="Servers" description="Switch which OpenCode server this app connects to.">
       <div class="flex flex-col gap-4 pb-4">
@@ -128,20 +135,33 @@ export function DialogSelectServer() {
           }}
         >
           {(i) => (
-            <div
-              class="flex items-center gap-2 min-w-0 flex-1"
-              classList={{ "opacity-50": store.status[i]?.healthy === false }}
-            >
+            <div class="flex items-center gap-2 min-w-0 flex-1 group/item">
               <div
-                classList={{
-                  "size-1.5 rounded-full shrink-0": true,
-                  "bg-icon-success-base": store.status[i]?.healthy === true,
-                  "bg-icon-critical-base": store.status[i]?.healthy === false,
-                  "bg-border-weak-base": store.status[i] === undefined,
-                }}
-              />
-              <span class="truncate">{serverDisplayName(i)}</span>
-              <span class="text-text-weak">{store.status[i]?.version}</span>
+                class="flex items-center gap-2 min-w-0 flex-1"
+                classList={{ "opacity-50": store.status[i]?.healthy === false }}
+              >
+                <div
+                  classList={{
+                    "size-1.5 rounded-full shrink-0": true,
+                    "bg-icon-success-base": store.status[i]?.healthy === true,
+                    "bg-icon-critical-base": store.status[i]?.healthy === false,
+                    "bg-border-weak-base": store.status[i] === undefined,
+                  }}
+                />
+                <span class="truncate">{serverDisplayName(i)}</span>
+                <span class="text-text-weak">{store.status[i]?.version}</span>
+              </div>
+              <Show when={current() !== i && server.list.includes(i)}>
+                <IconButton
+                  icon="circle-x"
+                  variant="ghost"
+                  class="bg-transparent transition-opacity shrink-0 hover:scale-110"
+                  onClick={(e) => {
+                    e.stopPropagation()
+                    handleRemove(i)
+                  }}
+                />
+              </Show>
             </div>
           )}
         </List>
@@ -173,6 +193,53 @@ export function DialogSelectServer() {
             </div>
           </form>
         </div>
+
+        <Show when={isDesktop}>
+          <div class="mt-6 px-3 flex flex-col gap-1.5">
+            <div class="px-3">
+              <h3 class="text-14-regular text-text-weak">Default server</h3>
+              <p class="text-12-regular text-text-weak mt-1">
+                Connect to this server on app launch instead of starting a local server. Requires restart.
+              </p>
+            </div>
+            <div class="flex items-center gap-2 px-3 py-2">
+              <Show
+                when={defaultUrl()}
+                fallback={
+                  <Show
+                    when={server.url}
+                    fallback={<span class="text-14-regular text-text-weak">No server selected</span>}
+                  >
+                    <Button
+                      variant="secondary"
+                      size="small"
+                      onClick={async () => {
+                        await platform.setDefaultServerUrl?.(server.url)
+                        defaultUrlActions.refetch(server.url)
+                      }}
+                    >
+                      Set current server as default
+                    </Button>
+                  </Show>
+                }
+              >
+                <div class="flex items-center gap-2 flex-1 min-w-0">
+                  <span class="truncate text-14-regular">{serverDisplayName(defaultUrl()!)}</span>
+                </div>
+                <Button
+                  variant="ghost"
+                  size="small"
+                  onClick={async () => {
+                    await platform.setDefaultServerUrl?.(null)
+                    defaultUrlActions.refetch()
+                  }}
+                >
+                  Clear
+                </Button>
+              </Show>
+            </div>
+          </div>
+        </Show>
       </div>
     </Dialog>
   )

+ 35 - 8
packages/app/src/components/prompt-input.tsx

@@ -33,11 +33,14 @@ import { useSync } from "@/context/sync"
 import { FileIcon } from "@opencode-ai/ui/file-icon"
 import { Button } from "@opencode-ai/ui/button"
 import { Icon } from "@opencode-ai/ui/icon"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
+import type { IconName } from "@opencode-ai/ui/icons/provider"
 import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Select } from "@opencode-ai/ui/select"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { ImagePreview } from "@opencode-ai/ui/image-preview"
 import { ModelSelectorPopover } from "@/components/dialog-select-model"
 import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
 import { useProviders } from "@/hooks/use-providers"
@@ -361,6 +364,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     if (!isFocused()) setStore("popover", null)
   })
 
+  // Safety: reset composing state on focus change to prevent stuck state
+  // This handles edge cases where compositionend event may not fire
+  createEffect(() => {
+    if (!isFocused()) setComposing(false)
+  })
+
   type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string }
 
   const agentList = createMemo(() =>
@@ -386,6 +395,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   const {
     flat: atFlat,
     active: atActive,
+    setActive: setAtActive,
     onInput: atOnInput,
     onKeyDown: atOnKeyDown,
   } = useFilteredList<AtOption>({
@@ -452,6 +462,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   const {
     flat: slashFlat,
     active: slashActive,
+    setActive: setSlashActive,
     onInput: slashOnInput,
     onKeyDown: slashOnKeyDown,
     refetch: slashRefetch,
@@ -876,6 +887,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       }
     }
 
+    // Handle Shift+Enter BEFORE IME check - Shift+Enter is never used for IME input
+    // and should always insert a newline regardless of composition state
+    if (event.key === "Enter" && event.shiftKey) {
+      addPart({ type: "text", content: "\n", start: 0, end: 0 })
+      event.preventDefault()
+      return
+    }
+
     if (event.key === "Enter" && isImeComposing(event)) {
       return
     }
@@ -939,11 +958,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       return
     }
 
-    if (event.key === "Enter" && event.shiftKey) {
-      addPart({ type: "text", content: "\n", start: 0, end: 0 })
-      event.preventDefault()
-      return
-    }
+    // Note: Shift+Enter is handled earlier, before IME check
     if (event.key === "Enter" && !event.shiftKey) {
       handleSubmit(event)
     }
@@ -1299,6 +1314,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
           class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-80 min-h-10
                  overflow-auto no-scrollbar flex flex-col p-2 rounded-md
                  border border-border-base bg-surface-raised-stronger-non-alpha shadow-md"
+          onMouseDown={(e) => e.preventDefault()}
         >
           <Switch>
             <Match when={store.popover === "at"}>
@@ -1314,6 +1330,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                         "bg-surface-raised-base-hover": atActive() === atKey(item),
                       }}
                       onClick={() => handleAtSelect(item)}
+                      onMouseEnter={() => setAtActive(atKey(item))}
                     >
                       <Show
                         when={item.type === "agent"}
@@ -1360,6 +1377,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                         "bg-surface-raised-base-hover": slashActive() === cmd.id,
                       }}
                       onClick={() => handleSlashSelect(cmd)}
+                      onMouseEnter={() => setSlashActive(cmd.id)}
                     >
                       <div class="flex items-center gap-2 min-w-0">
                         <span class="text-14-regular text-text-strong whitespace-nowrap">/{cmd.trigger}</span>
@@ -1479,7 +1497,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                     <img
                       src={attachment.dataUrl}
                       alt={attachment.filename}
-                      class="size-16 rounded-md object-cover border border-border-base"
+                      class="size-16 rounded-md object-cover border border-border-base hover:border-border-strong-base transition-colors"
+                      onClick={() =>
+                        dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
+                      }
                     />
                   </Show>
                   <button
@@ -1551,6 +1572,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                   fallback={
                     <TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
                       <Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
+                        <Show when={local.model.current()?.provider?.id}>
+                          <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
+                        </Show>
                         {local.model.current()?.name ?? "Select model"}
                         <Icon name="chevron-down" size="small" />
                       </Button>
@@ -1560,6 +1584,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                   <ModelSelectorPopover>
                     <TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
                       <Button as="div" variant="ghost">
+                        <Show when={local.model.current()?.provider?.id}>
+                          <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
+                        </Show>
                         {local.model.current()?.name ?? "Select model"}
                         <Icon name="chevron-down" size="small" />
                       </Button>
@@ -1574,10 +1601,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                   >
                     <Button
                       variant="ghost"
-                      class="text-text-base _hidden group-hover/prompt-input:inline-block"
+                      class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
                       onClick={() => local.model.variant.cycle()}
                     >
-                      <span class="capitalize text-12-regular">{local.model.variant.current() ?? "Default"}</span>
+                      {local.model.variant.current() ?? "Default"}
                     </Button>
                   </TooltipKeybind>
                 </Show>

+ 175 - 231
packages/app/src/components/session/session-header.tsx

@@ -1,267 +1,211 @@
 import { createMemo, createResource, Show } from "solid-js"
-import { A, useNavigate, useParams } from "@solidjs/router"
+import { Portal } from "solid-js/web"
+import { useParams } from "@solidjs/router"
 import { useLayout } from "@/context/layout"
 import { useCommand } from "@/context/command"
-import { useServer } from "@/context/server"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
+// import { useServer } from "@/context/server"
+// import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useSync } from "@/context/sync"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { getFilename } from "@opencode-ai/util/path"
-import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
+import { base64Decode } from "@opencode-ai/util/encode"
 import { iife } from "@opencode-ai/util/iife"
 import { Icon } from "@opencode-ai/ui/icon"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Button } from "@opencode-ai/ui/button"
 import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
-import { Select } from "@opencode-ai/ui/select"
 import { Popover } from "@opencode-ai/ui/popover"
 import { TextField } from "@opencode-ai/ui/text-field"
-import { DialogSelectServer } from "@/components/dialog-select-server"
-import { SessionLspIndicator } from "@/components/session-lsp-indicator"
-import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
-import type { Session } from "@opencode-ai/sdk/v2/client"
-import { same } from "@/utils/same"
 
 export function SessionHeader() {
   const globalSDK = useGlobalSDK()
   const layout = useLayout()
   const params = useParams()
-  const navigate = useNavigate()
   const command = useCommand()
-  const server = useServer()
-  const dialog = useDialog()
+  // const server = useServer()
+  // const dialog = useDialog()
   const sync = useSync()
 
   const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
+  const project = createMemo(() => {
+    const directory = projectDirectory()
+    if (!directory) return
+    return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
+  })
+  const name = createMemo(() => {
+    const current = project()
+    if (current) return current.name || getFilename(current.worktree)
+    return getFilename(projectDirectory())
+  })
+  const hotkey = createMemo(() => command.keybind("file.open"))
 
-  const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
   const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
-  const parentSession = createMemo(() => {
-    const current = currentSession()
-    if (!current?.parentID) return undefined
-    return sync.data.session.find((s) => s.id === current.parentID)
-  })
   const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
-  const worktrees = createMemo(() => layout.projects.list().map((p) => p.worktree), [], { equals: same })
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
   const view = createMemo(() => layout.view(sessionKey()))
 
-  function navigateToProject(directory: string) {
-    navigate(`/${base64Encode(directory)}`)
-  }
-
-  function navigateToSession(session: Session | undefined) {
-    if (!session) return
-    // Only navigate if we're actually changing to a different session
-    if (session.id === params.id) return
-    navigate(`/${params.dir}/session/${session.id}`)
-  }
+  const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
+  const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
 
   return (
-    <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex">
-      <button
-        type="button"
-        class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
-        onClick={layout.mobileSidebar.toggle}
-      >
-        <Icon name="menu" size="small" />
-      </button>
-      <div class="px-4 flex items-center justify-between gap-4 w-full">
-        <div class="flex items-center gap-3 min-w-0">
-          <div class="flex items-center gap-2 min-w-0">
-            <div class="hidden xl:flex items-center gap-2">
-              <Select
-                options={worktrees()}
-                current={sync.project?.worktree ?? projectDirectory()}
-                label={(x) => getFilename(x)}
-                onSelect={(x) => (x ? navigateToProject(x) : undefined)}
-                class="text-14-regular text-text-base"
-                variant="ghost"
-              >
-                {/* @ts-ignore */}
-                {(i) => (
-                  <div class="flex items-center gap-2">
-                    <Icon name="folder" size="small" />
-                    <div class="text-text-strong">{getFilename(i)}</div>
-                  </div>
-                )}
-              </Select>
-              <div class="text-text-weaker">/</div>
-            </div>
-            <Show
-              when={parentSession()}
-              fallback={
-                <>
-                  <Select
-                    options={sessions()}
-                    current={currentSession()}
-                    placeholder="New session"
-                    label={(x) => x.title}
-                    value={(x) => x.id}
-                    onSelect={navigateToSession}
-                    class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
-                    variant="ghost"
-                  />
-                </>
-              }
+    <>
+      <Show when={centerMount()}>
+        {(mount) => (
+          <Portal mount={mount()}>
+            <button
+              type="button"
+              class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
+              onClick={() => command.trigger("file.open")}
             >
-              <div class="flex items-center gap-2 min-w-0">
-                <Select
-                  options={sessions()}
-                  current={parentSession()}
-                  placeholder="Back to parent session"
-                  label={(x) => x.title}
-                  value={(x) => x.id}
-                  onSelect={(session) => {
-                    // Only navigate if selecting a different session than current parent
-                    const currentParent = parentSession()
-                    if (session && currentParent && session.id !== currentParent.id) {
-                      navigateToSession(session)
-                    }
-                  }}
-                  class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
-                  variant="ghost"
-                />
-                <div class="text-text-weaker">/</div>
-                <div class="flex items-center gap-1.5 min-w-0">
-                  <Tooltip value="Back to parent session">
-                    <button
-                      type="button"
-                      class="flex items-center justify-center gap-1 p-1 rounded hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors flex-shrink-0"
-                      onClick={() => navigateToSession(parentSession())}
+              <div class="flex items-center gap-2">
+                <Icon name="magnifying-glass" size="normal" class="icon-base" />
+                <span class="flex-1 min-w-0 text-14-regular text-text-weak truncate" style={{ "line-height": 1 }}>
+                  Search {name()}
+                </span>
+              </div>
+
+              <Show when={hotkey()}>
+                {(keybind) => (
+                  <span
+                    class="shrink-0 flex items-center justify-center h-5 px-2 rounded-[2px] bg-surface-base text-12-medium text-text-weak"
+                    style={{ "box-shadow": "var(--shadow-xxs-border)" }}
+                  >
+                    {keybind()}
+                  </span>
+                )}
+              </Show>
+            </button>
+          </Portal>
+        )}
+      </Show>
+      <Show when={rightMount()}>
+        {(mount) => (
+          <Portal mount={mount()}>
+            <div class="flex items-center gap-3">
+              {/* <div class="hidden md:flex items-center gap-1"> */}
+              {/*   <Button */}
+              {/*     size="small" */}
+              {/*     variant="ghost" */}
+              {/*     onClick={() => { */}
+              {/*       dialog.show(() => <DialogSelectServer />) */}
+              {/*     }} */}
+              {/*   > */}
+              {/*     <div */}
+              {/*       classList={{ */}
+              {/*         "size-1.5 rounded-full": true, */}
+              {/*         "bg-icon-success-base": server.healthy() === true, */}
+              {/*         "bg-icon-critical-base": server.healthy() === false, */}
+              {/*         "bg-border-weak-base": server.healthy() === undefined, */}
+              {/*       }} */}
+              {/*     /> */}
+              {/*     <Icon name="server" size="small" class="text-icon-weak" /> */}
+              {/*     <span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span> */}
+              {/*   </Button> */}
+              {/*   <SessionLspIndicator /> */}
+              {/*   <SessionMcpIndicator /> */}
+              {/* </div> */}
+              <div class="flex items-center gap-1">
+                <Show when={currentSession()?.summary?.files}>
+                  <TooltipKeybind
+                    class="hidden md:block shrink-0"
+                    title="Toggle review"
+                    keybind={command.keybind("review.toggle")}
+                  >
+                    <Button
+                      variant="ghost"
+                      class="group/review-toggle size-6 p-0"
+                      onClick={() => view().reviewPanel.toggle()}
                     >
-                      <Icon name="arrow-left" size="small" class="text-icon-base" />
-                    </button>
-                  </Tooltip>
-                </div>
+                      <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
+                        <Icon
+                          name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
+                          size="small"
+                          class="group-hover/review-toggle:hidden"
+                        />
+                        <Icon
+                          name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
+                          size="small"
+                          class="hidden group-hover/review-toggle:inline-block"
+                        />
+                        <Icon
+                          name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
+                          size="small"
+                          class="hidden group-active/review-toggle:inline-block"
+                        />
+                      </div>
+                    </Button>
+                  </TooltipKeybind>
+                </Show>
+                <TooltipKeybind
+                  class="hidden md:block shrink-0"
+                  title="Toggle terminal"
+                  keybind={command.keybind("terminal.toggle")}
+                >
+                  <Button
+                    variant="ghost"
+                    class="group/terminal-toggle size-6 p-0"
+                    onClick={() => view().terminal.toggle()}
+                  >
+                    <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
+                      <Icon
+                        size="small"
+                        name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
+                        class="group-hover/terminal-toggle:hidden"
+                      />
+                      <Icon
+                        size="small"
+                        name="layout-bottom-partial"
+                        class="hidden group-hover/terminal-toggle:inline-block"
+                      />
+                      <Icon
+                        size="small"
+                        name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
+                        class="hidden group-active/terminal-toggle:inline-block"
+                      />
+                    </div>
+                  </Button>
+                </TooltipKeybind>
               </div>
-            </Show>
-          </div>
-          <Show when={currentSession() && !parentSession()}>
-            <TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
-              <IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
-            </TooltipKeybind>
-          </Show>
-        </div>
-        <div class="flex items-center gap-3">
-          <div class="hidden md:flex items-center gap-1">
-            <Button
-              size="small"
-              variant="ghost"
-              onClick={() => {
-                dialog.show(() => <DialogSelectServer />)
-              }}
-            >
-              <div
-                classList={{
-                  "size-1.5 rounded-full": true,
-                  "bg-icon-success-base": server.healthy() === true,
-                  "bg-icon-critical-base": server.healthy() === false,
-                  "bg-border-weak-base": server.healthy() === undefined,
-                }}
-              />
-              <Icon name="server" size="small" class="text-icon-weak" />
-              <span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
-            </Button>
-            <SessionLspIndicator />
-            <SessionMcpIndicator />
-          </div>
-          <div class="flex items-center gap-1">
-            <Show when={currentSession()?.summary?.files}>
-              <TooltipKeybind
-                class="hidden md:block shrink-0"
-                title="Toggle review"
-                keybind={command.keybind("review.toggle")}
-              >
-                <Button
-                  variant="ghost"
-                  class="group/review-toggle size-6 p-0"
-                  onClick={() => view().reviewPanel.toggle()}
+              <Show when={shareEnabled() && currentSession()}>
+                <Popover
+                  title="Share session"
+                  trigger={
+                    <Tooltip class="shrink-0" value="Share session">
+                      <IconButton icon="share" variant="ghost" class="" />
+                    </Tooltip>
+                  }
                 >
-                  <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
-                    <Icon
-                      name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
-                      size="small"
-                      class="group-hover/review-toggle:hidden"
-                    />
-                    <Icon
-                      name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
-                      size="small"
-                      class="hidden group-hover/review-toggle:inline-block"
-                    />
-                    <Icon
-                      name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
-                      size="small"
-                      class="hidden group-active/review-toggle:inline-block"
-                    />
-                  </div>
-                </Button>
-              </TooltipKeybind>
-            </Show>
-            <TooltipKeybind
-              class="hidden md:block shrink-0"
-              title="Toggle terminal"
-              keybind={command.keybind("terminal.toggle")}
-            >
-              <Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={() => view().terminal.toggle()}>
-                <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
-                  <Icon
-                    size="small"
-                    name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
-                    class="group-hover/terminal-toggle:hidden"
-                  />
-                  <Icon
-                    size="small"
-                    name="layout-bottom-partial"
-                    class="hidden group-hover/terminal-toggle:inline-block"
-                  />
-                  <Icon
-                    size="small"
-                    name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
-                    class="hidden group-active/terminal-toggle:inline-block"
-                  />
-                </div>
-              </Button>
-            </TooltipKeybind>
-          </div>
-          <Show when={shareEnabled() && currentSession()}>
-            <Popover
-              title="Share session"
-              trigger={
-                <Tooltip class="shrink-0" value="Share session">
-                  <IconButton icon="share" variant="ghost" class="" />
-                </Tooltip>
-              }
-            >
-              {iife(() => {
-                const [url] = createResource(
-                  () => currentSession(),
-                  async (session) => {
-                    if (!session) return
-                    let shareURL = session.share?.url
-                    if (!shareURL) {
-                      shareURL = await globalSDK.client.session
-                        .share({ sessionID: session.id, directory: projectDirectory() })
-                        .then((r) => r.data?.share?.url)
-                        .catch((e) => {
-                          console.error("Failed to share session", e)
-                          return undefined
-                        })
-                    }
-                    return shareURL
-                  },
-                  { initialValue: "" },
-                )
-                return (
-                  <Show when={url.latest}>
-                    {(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
-                  </Show>
-                )
-              })}
-            </Popover>
-          </Show>
-        </div>
-      </div>
-    </header>
+                  {iife(() => {
+                    const [url] = createResource(
+                      () => currentSession(),
+                      async (session) => {
+                        if (!session) return
+                        let shareURL = session.share?.url
+                        if (!shareURL) {
+                          shareURL = await globalSDK.client.session
+                            .share({ sessionID: session.id, directory: projectDirectory() })
+                            .then((r) => r.data?.share?.url)
+                            .catch((e) => {
+                              console.error("Failed to share session", e)
+                              return undefined
+                            })
+                        }
+                        return shareURL
+                      },
+                      { initialValue: "" },
+                    )
+                    return (
+                      <Show when={url.latest}>
+                        {(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
+                      </Show>
+                    )
+                  })}
+                </Popover>
+              </Show>
+            </div>
+          </Portal>
+        )}
+      </Show>
+    </>
   )
 }

+ 6 - 3
packages/app/src/components/terminal.tsx

@@ -100,9 +100,12 @@ export const Terminal = (props: TerminalProps) => {
     const mod = await import("ghostty-web")
     ghostty = await mod.Ghostty.load()
 
-    const socket = new WebSocket(
-      sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`,
-    )
+    const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
+    if (window.__OPENCODE__?.serverPassword) {
+      url.username = "opencode"
+      url.password = window.__OPENCODE__?.serverPassword
+    }
+    const socket = new WebSocket(url)
     ws = socket
 
     const t = new mod.Terminal({

+ 118 - 0
packages/app/src/components/titlebar.tsx

@@ -0,0 +1,118 @@
+import { createEffect, createMemo, Show } from "solid-js"
+import { IconButton } from "@opencode-ai/ui/icon-button"
+import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
+import { useTheme } from "@opencode-ai/ui/theme"
+
+import { useLayout } from "@/context/layout"
+import { usePlatform } from "@/context/platform"
+import { useCommand } from "@/context/command"
+
+export function Titlebar() {
+  const layout = useLayout()
+  const platform = usePlatform()
+  const command = useCommand()
+  const theme = useTheme()
+
+  const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
+  const reserve = createMemo(
+    () => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"),
+  )
+  const web = createMemo(() => platform.platform === "web")
+
+  const getWin = () => {
+    if (platform.platform !== "desktop") return
+
+    const tauri = (
+      window as unknown as {
+        __TAURI__?: { window?: { getCurrentWindow?: () => { startDragging?: () => Promise<void> } } }
+      }
+    ).__TAURI__
+    if (!tauri?.window?.getCurrentWindow) return
+
+    return tauri.window.getCurrentWindow()
+  }
+
+  createEffect(() => {
+    if (platform.platform !== "desktop") return
+
+    const scheme = theme.colorScheme()
+    const value = scheme === "system" ? null : scheme
+
+    const tauri = (window as unknown as { __TAURI__?: { webviewWindow?: { getCurrentWebviewWindow?: () => unknown } } })
+      .__TAURI__
+    const get = tauri?.webviewWindow?.getCurrentWebviewWindow
+    if (!get) return
+
+    const win = get() as { setTheme?: (theme?: "light" | "dark" | null) => Promise<void> }
+    if (!win.setTheme) return
+
+    void win.setTheme(value).catch(() => undefined)
+  })
+
+  const interactive = (target: EventTarget | null) => {
+    if (!(target instanceof Element)) return false
+
+    const selector =
+      "button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']"
+
+    return !!target.closest(selector)
+  }
+
+  const drag = (e: MouseEvent) => {
+    if (platform.platform !== "desktop") return
+    if (e.buttons !== 1) return
+    if (interactive(e.target)) return
+
+    const win = getWin()
+    if (!win?.startDragging) return
+
+    e.preventDefault()
+    void win.startDragging().catch(() => undefined)
+  }
+
+  return (
+    <header class="h-10 shrink-0 bg-background-base flex items-center relative">
+      <div
+        classList={{
+          "flex items-center w-full min-w-0 pr-2": true,
+          "pl-2": !mac(),
+        }}
+        onMouseDown={drag}
+      >
+        <Show when={mac()}>
+          <div class="w-[72px] h-full shrink-0" data-tauri-drag-region />
+          <div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
+            <IconButton icon="menu" variant="ghost" class="size-8 rounded-md" onClick={layout.mobileSidebar.toggle} />
+          </div>
+        </Show>
+        <Show when={!mac()}>
+          <div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
+            <IconButton icon="menu" variant="ghost" class="size-8 rounded-md" onClick={layout.mobileSidebar.toggle} />
+          </div>
+        </Show>
+        <TooltipKeybind
+          class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0"}
+          placement="bottom"
+          title="Toggle sidebar"
+          keybind={command.keybind("sidebar.toggle")}
+        >
+          <IconButton
+            icon={layout.sidebar.opened() ? "layout-left" : "layout-right"}
+            variant="ghost"
+            class="size-8 rounded-md"
+            onClick={layout.sidebar.toggle}
+          />
+        </TooltipKeybind>
+        <div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
+        <div class="flex-1 h-full" data-tauri-drag-region />
+        <div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0" />
+        <Show when={reserve()}>
+          <div class="w-[120px] h-full shrink-0" data-tauri-drag-region />
+        </Show>
+      </div>
+      <div class="absolute inset-0 flex items-center justify-center pointer-events-none">
+        <div id="opencode-titlebar-center" class="pointer-events-auto" />
+      </div>
+    </header>
+  )
+}

+ 12 - 69
packages/app/src/context/command.tsx

@@ -1,8 +1,5 @@
-import { createMemo, createSignal, onCleanup, onMount, Show, type Accessor } from "solid-js"
+import { createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
 import { createSimpleContext } from "@opencode-ai/ui/context"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
-import { Dialog } from "@opencode-ai/ui/dialog"
-import { List } from "@opencode-ai/ui/list"
 
 const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
 
@@ -114,67 +111,11 @@ export function formatKeybind(config: string): string {
   return IS_MAC ? parts.join("") : parts.join("+")
 }
 
-function DialogCommand(props: { options: CommandOption[] }) {
-  const dialog = useDialog()
-  let cleanup: (() => void) | void
-  let committed = false
-
-  const handleMove = (option: CommandOption | undefined) => {
-    cleanup?.()
-    cleanup = option?.onHighlight?.()
-  }
-
-  const handleSelect = (option: CommandOption | undefined) => {
-    if (option) {
-      committed = true
-      cleanup = undefined
-      dialog.close()
-      option.onSelect?.("palette")
-    }
-  }
-
-  onCleanup(() => {
-    if (!committed) {
-      cleanup?.()
-    }
-  })
-
-  return (
-    <Dialog title="Commands">
-      <List
-        search={{ placeholder: "Search commands", autofocus: true }}
-        emptyMessage="No commands found"
-        items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
-        key={(x) => x?.id}
-        filterKeys={["title", "description", "category"]}
-        groupBy={(x) => x.category ?? ""}
-        onMove={handleMove}
-        onSelect={handleSelect}
-      >
-        {(option) => (
-          <div class="w-full flex items-center justify-between gap-4">
-            <div class="flex items-center gap-2 min-w-0">
-              <span class="text-14-regular text-text-strong whitespace-nowrap">{option.title}</span>
-              <Show when={option.description}>
-                <span class="text-14-regular text-text-weak truncate">{option.description}</span>
-              </Show>
-            </div>
-            <Show when={option.keybind}>
-              <span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(option.keybind!)}</span>
-            </Show>
-          </div>
-        )}
-      </List>
-    </Dialog>
-  )
-}
-
 export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
   name: "Command",
   init: () => {
     const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
     const [suspendCount, setSuspendCount] = createSignal(0)
-    const dialog = useDialog()
 
     const options = createMemo(() => {
       const seen = new Set<string>()
@@ -202,12 +143,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
 
     const suspended = () => suspendCount() > 0
 
-    const showPalette = () => {
-      if (!dialog.active) {
-        dialog.show(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
+    const run = (id: string, source?: "palette" | "keybind" | "slash") => {
+      for (const option of options()) {
+        if (option.id === id || option.id === "suggested." + id) {
+          option.onSelect?.(source)
+          return
+        }
       }
     }
 
+    const showPalette = () => {
+      run("file.open", "palette")
+    }
+
     const handleKeyDown = (event: KeyboardEvent) => {
       if (suspended()) return
 
@@ -248,12 +196,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
         })
       },
       trigger(id: string, source?: "palette" | "keybind" | "slash") {
-        for (const option of options()) {
-          if (option.id === id || option.id === "suggested." + id) {
-            option.onSelect?.(source)
-            return
-          }
-        }
+        run(id, source)
       },
       keybind(id: string) {
         const option = options().find((x) => x.id === id || x.id === "suggested." + id)

+ 2 - 1
packages/app/src/context/global-sdk.tsx

@@ -9,11 +9,13 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
   name: "GlobalSDK",
   init: () => {
     const server = useServer()
+    const platform = usePlatform()
     const abort = new AbortController()
 
     const eventSdk = createOpencodeClient({
       baseUrl: server.url,
       signal: abort.signal,
+      fetch: platform.fetch,
     })
     const emitter = createGlobalEmitter<{
       [key: string]: Event
@@ -93,7 +95,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
       stop()
     })
 
-    const platform = usePlatform()
     const sdk = createOpencodeClient({
       baseUrl: server.url,
       fetch: platform.fetch,

+ 178 - 31
packages/app/src/context/global-sync.tsx

@@ -16,16 +16,32 @@ import {
   type LspStatus,
   type VcsInfo,
   type PermissionRequest,
+  type QuestionRequest,
   createOpencodeClient,
 } from "@opencode-ai/sdk/v2/client"
-import { createStore, produce, reconcile } from "solid-js/store"
+import { createStore, produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
 import { Binary } from "@opencode-ai/util/binary"
 import { retry } from "@opencode-ai/util/retry"
 import { useGlobalSDK } from "./global-sdk"
 import { ErrorPage, type InitError } from "../pages/error"
-import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js"
+import {
+  batch,
+  createContext,
+  createEffect,
+  getOwner,
+  runWithOwner,
+  useContext,
+  onCleanup,
+  onMount,
+  type Accessor,
+  type ParentProps,
+  Switch,
+  Match,
+} from "solid-js"
 import { showToast } from "@opencode-ai/ui/toast"
 import { getFilename } from "@opencode-ai/util/path"
+import { usePlatform } from "./platform"
+import { Persist, persisted } from "@/utils/persist"
 
 type State = {
   status: "loading" | "partial" | "complete"
@@ -36,6 +52,7 @@ type State = {
   config: Config
   path: Path
   session: Session[]
+  sessionTotal: number
   session_status: {
     [sessionID: string]: SessionStatus
   }
@@ -48,6 +65,9 @@ type State = {
   permission: {
     [sessionID: string]: PermissionRequest[]
   }
+  question: {
+    [sessionID: string]: QuestionRequest[]
+  }
   mcp: {
     [name: string]: McpStatus
   }
@@ -62,8 +82,18 @@ type State = {
   }
 }
 
+type VcsCache = {
+  store: Store<{ value: VcsInfo | undefined }>
+  setStore: SetStoreFunction<{ value: VcsInfo | undefined }>
+  ready: Accessor<boolean>
+}
+
 function createGlobalSync() {
   const globalSDK = useGlobalSDK()
+  const platform = usePlatform()
+  const owner = getOwner()
+  if (!owner) throw new Error("GlobalSync must be created within owner")
+  const vcsCache = new Map<string, VcsCache>()
   const [globalStore, setGlobalStore] = createStore<{
     ready: boolean
     error?: InitError
@@ -79,52 +109,81 @@ function createGlobalSync() {
     provider_auth: {},
   })
 
-  const children: Record<string, ReturnType<typeof createStore<State>>> = {}
+  const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
+
   function child(directory: string) {
     if (!directory) console.error("No directory provided")
     if (!children[directory]) {
-      children[directory] = createStore<State>({
-        project: "",
-        provider: { all: [], connected: [], default: {} },
-        config: {},
-        path: { state: "", config: "", worktree: "", directory: "", home: "" },
-        status: "loading" as const,
-        agent: [],
-        command: [],
-        session: [],
-        session_status: {},
-        session_diff: {},
-        todo: {},
-        permission: {},
-        mcp: {},
-        lsp: [],
-        vcs: undefined,
-        limit: 5,
-        message: {},
-        part: {},
-      })
-      bootstrapInstance(directory)
+      const cache = runWithOwner(owner, () =>
+        persisted(
+          Persist.workspace(directory, "vcs", ["vcs.v1"]),
+          createStore({ value: undefined as VcsInfo | undefined }),
+        ),
+      )
+      if (!cache) throw new Error("Failed to create persisted cache")
+      vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })
+
+      const init = () => {
+        children[directory] = createStore<State>({
+          project: "",
+          provider: { all: [], connected: [], default: {} },
+          config: {},
+          path: { state: "", config: "", worktree: "", directory: "", home: "" },
+          status: "loading" as const,
+          agent: [],
+          command: [],
+          session: [],
+          sessionTotal: 0,
+          session_status: {},
+          session_diff: {},
+          todo: {},
+          permission: {},
+          question: {},
+          mcp: {},
+          lsp: [],
+          vcs: cache[0].value,
+          limit: 5,
+          message: {},
+          part: {},
+        })
+        bootstrapInstance(directory)
+      }
+
+      runWithOwner(owner, init)
     }
-    return children[directory]
+    const childStore = children[directory]
+    if (!childStore) throw new Error("Failed to create store")
+    return childStore
   }
 
   async function loadSessions(directory: string) {
     const [store, setStore] = child(directory)
-    globalSDK.client.session
-      .list({ directory })
+    const limit = store.limit
+
+    return globalSDK.client.session
+      .list({ directory, roots: true })
       .then((x) => {
-        const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
         const nonArchived = (x.data ?? [])
           .filter((s) => !!s?.id)
           .filter((s) => !s.time?.archived)
           .slice()
           .sort((a, b) => a.id.localeCompare(b.id))
+
+        const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory))
+        if (sandboxWorkspace) {
+          setStore("session", reconcile(nonArchived, { key: "id" }))
+          return
+        }
+
+        const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
         // Include up to the limit, plus any updated in the last 4 hours
         const sessions = nonArchived.filter((s, i) => {
-          if (i < store.limit) return true
+          if (i < limit) return true
           const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
           return updated > fourHoursAgo
         })
+        // Store total session count (used for "load more" pagination)
+        setStore("sessionTotal", nonArchived.length)
         setStore("session", reconcile(sessions, { key: "id" }))
       })
       .catch((err) => {
@@ -137,12 +196,22 @@ function createGlobalSync() {
   async function bootstrapInstance(directory: string) {
     if (!directory) return
     const [store, setStore] = child(directory)
+    const cache = vcsCache.get(directory)
+    if (!cache) return
     const sdk = createOpencodeClient({
       baseUrl: globalSDK.url,
+      fetch: platform.fetch,
       directory,
       throwOnError: true,
     })
 
+    createEffect(() => {
+      if (!cache.ready()) return
+      const cached = cache.store.value
+      if (!cached?.branch) return
+      setStore("vcs", (value) => value ?? cached)
+    })
+
     const blockingRequests = {
       project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
       provider: () =>
@@ -172,7 +241,11 @@ function createGlobalSync() {
           loadSessions(directory),
           sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
           sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
-          sdk.vcs.get().then((x) => setStore("vcs", x.data)),
+          sdk.vcs.get().then((x) => {
+            const next = x.data ?? store.vcs
+            setStore("vcs", next)
+            if (next?.branch) cache.setStore("value", next)
+          }),
           sdk.permission.list().then((x) => {
             const grouped: Record<string, PermissionRequest[]> = {}
             for (const perm of x.data ?? []) {
@@ -205,6 +278,38 @@ function createGlobalSync() {
               }
             })
           }),
+          sdk.question.list().then((x) => {
+            const grouped: Record<string, QuestionRequest[]> = {}
+            for (const question of x.data ?? []) {
+              if (!question?.id || !question.sessionID) continue
+              const existing = grouped[question.sessionID]
+              if (existing) {
+                existing.push(question)
+                continue
+              }
+              grouped[question.sessionID] = [question]
+            }
+
+            batch(() => {
+              for (const sessionID of Object.keys(store.question)) {
+                if (grouped[sessionID]) continue
+                setStore("question", sessionID, [])
+              }
+              for (const [sessionID, questions] of Object.entries(grouped)) {
+                setStore(
+                  "question",
+                  sessionID,
+                  reconcile(
+                    questions
+                      .filter((q) => !!q?.id)
+                      .slice()
+                      .sort((a, b) => a.id.localeCompare(b.id)),
+                    { key: "id" },
+                  ),
+                )
+              }
+            })
+          }),
         ]).then(() => {
           setStore("status", "complete")
         })
@@ -353,7 +458,10 @@ function createGlobalSync() {
         break
       }
       case "vcs.branch.updated": {
-        setStore("vcs", { branch: event.properties.branch })
+        const next = { branch: event.properties.branch }
+        setStore("vcs", next)
+        const cache = vcsCache.get(directory)
+        if (cache) cache.setStore("value", next)
         break
       }
       case "permission.asked": {
@@ -393,9 +501,48 @@ function createGlobalSync() {
         )
         break
       }
+      case "question.asked": {
+        const sessionID = event.properties.sessionID
+        const questions = store.question[sessionID]
+        if (!questions) {
+          setStore("question", sessionID, [event.properties])
+          break
+        }
+
+        const result = Binary.search(questions, event.properties.id, (q) => q.id)
+        if (result.found) {
+          setStore("question", sessionID, result.index, reconcile(event.properties))
+          break
+        }
+
+        setStore(
+          "question",
+          sessionID,
+          produce((draft) => {
+            draft.splice(result.index, 0, event.properties)
+          }),
+        )
+        break
+      }
+      case "question.replied":
+      case "question.rejected": {
+        const questions = store.question[event.properties.sessionID]
+        if (!questions) break
+        const result = Binary.search(questions, event.properties.requestID, (q) => q.id)
+        if (!result.found) break
+        setStore(
+          "question",
+          event.properties.sessionID,
+          produce((draft) => {
+            draft.splice(result.index, 1)
+          }),
+        )
+        break
+      }
       case "lsp.updated": {
         const sdk = createOpencodeClient({
           baseUrl: globalSDK.url,
+          fetch: platform.fetch,
           directory,
           throwOnError: true,
         })

+ 34 - 2
packages/app/src/context/layout.tsx

@@ -47,12 +47,34 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
     const globalSdk = useGlobalSDK()
     const globalSync = useGlobalSync()
     const server = useServer()
+
+    const isRecord = (value: unknown): value is Record<string, unknown> =>
+      typeof value === "object" && value !== null && !Array.isArray(value)
+
+    const migrate = (value: unknown) => {
+      if (!isRecord(value)) return value
+      const sidebar = value.sidebar
+      if (!isRecord(sidebar)) return value
+      if (typeof sidebar.workspaces !== "boolean") return value
+      return {
+        ...value,
+        sidebar: {
+          ...sidebar,
+          workspaces: {},
+          workspacesDefault: sidebar.workspaces,
+        },
+      }
+    }
+
+    const target = Persist.global("layout", ["layout.v6"])
     const [store, setStore, _, ready] = persisted(
-      Persist.global("layout", ["layout.v6"]),
+      { ...target, migrate },
       createStore({
         sidebar: {
           opened: false,
-          width: 280,
+          width: 344,
+          workspaces: {} as Record<string, boolean>,
+          workspacesDefault: false,
         },
         terminal: {
           height: 280,
@@ -304,6 +326,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         resize(width: number) {
           setStore("sidebar", "width", width)
         },
+        workspaces(directory: string) {
+          return createMemo(() => store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false)
+        },
+        setWorkspaces(directory: string, value: boolean) {
+          setStore("sidebar", "workspaces", directory, value)
+        },
+        toggleWorkspaces(directory: string) {
+          const current = store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false
+          setStore("sidebar", "workspaces", directory, !current)
+        },
       },
       terminal: {
         height: createMemo(() => store.terminal.height),

+ 9 - 0
packages/app/src/context/platform.tsx

@@ -5,6 +5,9 @@ export type Platform = {
   /** Platform discriminator */
   platform: "web" | "desktop"
 
+  /** Desktop OS (Tauri only) */
+  os?: "macos" | "windows" | "linux"
+
   /** App version */
   version?: string
 
@@ -37,6 +40,12 @@ export type Platform = {
 
   /** Fetch override */
   fetch?: typeof fetch
+
+  /** Get the configured default server URL (desktop only) */
+  getDefaultServerUrl?(): Promise<string | null>
+
+  /** Set the default server URL to use on app startup (desktop only) */
+  setDefaultServerUrl?(url: string | null): Promise<void>
 }
 
 export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({

+ 1 - 4
packages/app/src/context/server.tsx

@@ -16,10 +16,7 @@ export function normalizeServerUrl(input: string) {
 
 export function serverDisplayName(url: string) {
   if (!url) return ""
-  return url
-    .replace(/^https?:\/\//, "")
-    .replace(/\/+$/, "")
-    .split("/")[0]
+  return url.replace(/^https?:\/\//, "").replace(/\/+$/, "")
 }
 
 function projectsKey(url: string) {

+ 1 - 1
packages/app/src/context/sync.tsx

@@ -14,7 +14,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
     const sdk = useSDK()
     const [store, setStore] = globalSync.child(sdk.directory)
     const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
-    const chunk = 200
+    const chunk = 400
     const inflight = new Map<string, Promise<void>>()
     const inflightDiff = new Map<string, Promise<void>>()
     const inflightTodo = new Map<string, Promise<void>>()

+ 15 - 1
packages/app/src/context/terminal.tsx

@@ -8,6 +8,7 @@ import { Persist, persisted } from "@/utils/persist"
 export type LocalPTY = {
   id: string
   title: string
+  titleNumber: number
   rows?: number
   cols?: number
   buffer?: string
@@ -42,8 +43,20 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
     all: createMemo(() => Object.values(store.all)),
     active: createMemo(() => store.active),
     new() {
+      const existingTitleNumbers = new Set(
+        store.all.map((pty) => {
+          const match = pty.titleNumber
+          return match
+        }),
+      )
+
+      let nextNumber = 1
+      while (existingTitleNumbers.has(nextNumber)) {
+        nextNumber++
+      }
+
       sdk.client.pty
-        .create({ title: `Terminal ${store.all.length + 1}` })
+        .create({ title: `Terminal ${nextNumber}` })
         .then((pty) => {
           const id = pty.data?.id
           if (!id) return
@@ -52,6 +65,7 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
             {
               id,
               title: pty.data?.title ?? "Terminal",
+              titleNumber: nextNumber,
             },
           ])
           setStore("active", id)

+ 4 - 0
packages/app/src/index.css

@@ -5,3 +5,7 @@
     cursor: default;
   }
 }
+
+*[data-tauri-drag-region] {
+  app-region: drag;
+}

+ 8 - 0
packages/app/src/pages/directory-layout.tsx

@@ -7,6 +7,7 @@ import { LocalProvider } from "@/context/local"
 import { base64Decode } from "@opencode-ai/util/encode"
 import { DataProvider } from "@opencode-ai/ui/context"
 import { iife } from "@opencode-ai/util/iife"
+import type { QuestionAnswer } from "@opencode-ai/sdk/v2"
 
 export default function Layout(props: ParentProps) {
   const params = useParams()
@@ -27,6 +28,11 @@ export default function Layout(props: ParentProps) {
               response: "once" | "always" | "reject"
             }) => sdk.client.permission.respond(input)
 
+            const replyToQuestion = (input: { requestID: string; answers: QuestionAnswer[] }) =>
+              sdk.client.question.reply(input)
+
+            const rejectQuestion = (input: { requestID: string }) => sdk.client.question.reject(input)
+
             const navigateToSession = (sessionID: string) => {
               navigate(`/${params.dir}/session/${sessionID}`)
             }
@@ -36,6 +42,8 @@ export default function Layout(props: ParentProps) {
                 data={sync.data}
                 directory={directory()}
                 onPermissionRespond={respond}
+                onQuestionReply={replyToQuestion}
+                onQuestionReject={rejectQuestion}
                 onNavigateToSession={navigateToSession}
               >
                 <LocalProvider>{props.children}</LocalProvider>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 793 - 434
packages/app/src/pages/layout.tsx


+ 27 - 4
packages/app/src/pages/session.tsx

@@ -31,6 +31,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { DialogSelectFile } from "@/components/dialog-select-file"
 import { DialogSelectModel } from "@/components/dialog-select-model"
 import { DialogSelectMcp } from "@/components/dialog-select-mcp"
+import { DialogFork } from "@/components/dialog-fork"
 import { useCommand } from "@/context/command"
 import { useNavigate, useParams } from "@solidjs/router"
 import { UserMessage } from "@opencode-ai/sdk/v2"
@@ -427,7 +428,7 @@ export default function Page() {
     {
       id: "file.open",
       title: "Open file",
-      description: "Search and open a file",
+      description: "Search files and commands",
       category: "File",
       keybind: "mod+p",
       slash: "open",
@@ -645,6 +646,15 @@ export default function Page() {
         })
       },
     },
+    {
+      id: "session.fork",
+      title: "Fork from message",
+      description: "Create a new session from a previous message",
+      category: "Session",
+      slash: "fork",
+      disabled: !params.id || visibleUserMessages().length === 0,
+      onSelect: () => dialog.show(() => <DialogFork />),
+    },
   ])
 
   const handleKeyDown = (event: KeyboardEvent) => {
@@ -875,6 +885,19 @@ export default function Page() {
     window.history.replaceState(null, "", `#${anchor(id)}`)
   }
 
+  const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
+    const root = scroller
+    if (!root) {
+      el.scrollIntoView({ behavior, block: "start" })
+      return
+    }
+
+    const a = el.getBoundingClientRect()
+    const b = root.getBoundingClientRect()
+    const top = a.top - b.top + root.scrollTop
+    root.scrollTo({ top, behavior })
+  }
+
   const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
     setActiveMessage(message)
 
@@ -886,7 +909,7 @@ export default function Page() {
 
       requestAnimationFrame(() => {
         const el = document.getElementById(anchor(message.id))
-        if (el) el.scrollIntoView({ behavior, block: "start" })
+        if (el) scrollToElement(el, behavior)
       })
 
       updateHash(message.id)
@@ -894,7 +917,7 @@ export default function Page() {
     }
 
     const el = document.getElementById(anchor(message.id))
-    if (el) el.scrollIntoView({ behavior, block: "start" })
+    if (el) scrollToElement(el, behavior)
     updateHash(message.id)
   }
 
@@ -946,7 +969,7 @@ export default function Page() {
 
       const hashTarget = document.getElementById(hash)
       if (hashTarget) {
-        hashTarget.scrollIntoView({ behavior: "auto", block: "start" })
+        scrollToElement(hashTarget, "auto")
         return
       }
 

+ 7 - 2
packages/console/app/package.json

@@ -1,12 +1,12 @@
 {
   "name": "@opencode-ai/console-app",
-  "version": "1.1.11",
+  "version": "1.1.24",
   "type": "module",
   "license": "MIT",
   "scripts": {
     "typecheck": "tsgo --noEmit",
     "dev": "vite dev --host 0.0.0.0",
-    "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
+    "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev",
     "build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json",
     "start": "vite start"
   },
@@ -20,18 +20,23 @@
     "@opencode-ai/console-mail": "workspace:*",
     "@opencode-ai/console-resource": "workspace:*",
     "@opencode-ai/ui": "workspace:*",
+    "@smithy/eventstream-codec": "4.2.7",
+    "@smithy/util-utf8": "4.2.0",
     "@solidjs/meta": "catalog:",
     "@solidjs/router": "catalog:",
     "@solidjs/start": "catalog:",
+    "@stripe/stripe-js": "8.6.1",
     "chart.js": "4.5.1",
     "nitro": "3.0.1-alpha.1",
     "solid-js": "catalog:",
     "solid-list": "0.3.0",
+    "solid-stripe": "0.8.1",
     "vite": "catalog:",
     "zod": "catalog:"
   },
   "devDependencies": {
     "@typescript/native-preview": "catalog:",
+    "@webgpu/types": "0.1.54",
     "typescript": "catalog:",
     "wrangler": "4.50.0"
   },

+ 1 - 0
packages/console/app/public/social-share-black.png

@@ -0,0 +1 @@
+../../../ui/src/assets/images/social-share-black.png

+ 3 - 0
packages/console/app/src/component/footer.tsx

@@ -24,6 +24,9 @@ export function Footer() {
       <div data-slot="cell">
         <a href="/docs">Docs</a>
       </div>
+      <div data-slot="cell">
+        <a href="/changelog">Changelog</a>
+      </div>
       <div data-slot="cell">
         <a href="/discord">Discord</a>
       </div>

+ 15 - 0
packages/console/app/src/component/spotlight.css

@@ -0,0 +1,15 @@
+.spotlight-container {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 50dvh;
+  pointer-events: none;
+  overflow: hidden;
+}
+
+.spotlight-container canvas {
+  display: block;
+  width: 100%;
+  height: 100%;
+}

+ 820 - 0
packages/console/app/src/component/spotlight.tsx

@@ -0,0 +1,820 @@
+import { createSignal, createEffect, onMount, onCleanup, Accessor } from "solid-js"
+import "./spotlight.css"
+
+export interface ParticlesConfig {
+  enabled: boolean
+  amount: number
+  size: [number, number]
+  speed: number
+  opacity: number
+  drift: number
+}
+
+export interface SpotlightConfig {
+  placement: [number, number]
+  color: string
+  speed: number
+  spread: number
+  length: number
+  width: number
+  pulsating: false | [number, number]
+  distance: number
+  saturation: number
+  noiseAmount: number
+  distortion: number
+  opacity: number
+  particles: ParticlesConfig
+}
+
+export const defaultConfig: SpotlightConfig = {
+  placement: [0.5, -0.15],
+  color: "#ffffff",
+  speed: 0.8,
+  spread: 0.5,
+  length: 4.0,
+  width: 0.15,
+  pulsating: [0.95, 1.1],
+  distance: 3.5,
+  saturation: 0.35,
+  noiseAmount: 0.15,
+  distortion: 0.05,
+  opacity: 0.325,
+  particles: {
+    enabled: true,
+    amount: 70,
+    size: [1.25, 1.5],
+    speed: 0.75,
+    opacity: 0.9,
+    drift: 1.5,
+  },
+}
+
+export interface SpotlightAnimationState {
+  time: number
+  intensity: number
+  pulseValue: number
+}
+
+interface SpotlightProps {
+  config: Accessor<SpotlightConfig>
+  class?: string
+  onAnimationFrame?: (state: SpotlightAnimationState) => void
+}
+
+const hexToRgb = (hex: string): [number, number, number] => {
+  const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
+  return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1]
+}
+
+const getAnchorAndDir = (
+  placement: [number, number],
+  w: number,
+  h: number,
+): { anchor: [number, number]; dir: [number, number] } => {
+  const [px, py] = placement
+  const outside = 0.2
+
+  let anchorX = px * w
+  let anchorY = py * h
+  let dirX = 0
+  let dirY = 0
+
+  const centerX = 0.5
+  const centerY = 0.5
+
+  if (py <= 0.25) {
+    anchorY = -outside * h + py * h
+    dirY = 1
+    dirX = (centerX - px) * 0.5
+  } else if (py >= 0.75) {
+    anchorY = (1 + outside) * h - (1 - py) * h
+    dirY = -1
+    dirX = (centerX - px) * 0.5
+  } else if (px <= 0.25) {
+    anchorX = -outside * w + px * w
+    dirX = 1
+    dirY = (centerY - py) * 0.5
+  } else if (px >= 0.75) {
+    anchorX = (1 + outside) * w - (1 - px) * w
+    dirX = -1
+    dirY = (centerY - py) * 0.5
+  } else {
+    dirY = 1
+  }
+
+  const len = Math.sqrt(dirX * dirX + dirY * dirY)
+  if (len > 0) {
+    dirX /= len
+    dirY /= len
+  }
+
+  return { anchor: [anchorX, anchorY], dir: [dirX, dirY] }
+}
+
+interface UniformData {
+  iTime: number
+  iResolution: [number, number]
+  lightPos: [number, number]
+  lightDir: [number, number]
+  color: [number, number, number]
+  speed: number
+  lightSpread: number
+  lightLength: number
+  sourceWidth: number
+  pulsating: number
+  pulsatingMin: number
+  pulsatingMax: number
+  fadeDistance: number
+  saturation: number
+  noiseAmount: number
+  distortion: number
+  particlesEnabled: number
+  particleAmount: number
+  particleSizeMin: number
+  particleSizeMax: number
+  particleSpeed: number
+  particleOpacity: number
+  particleDrift: number
+}
+
+const WGSL_SHADER = `
+  struct Uniforms {
+    iTime: f32,
+    _pad0: f32,
+    iResolution: vec2<f32>,
+    lightPos: vec2<f32>,
+    lightDir: vec2<f32>,
+    color: vec3<f32>, 
+    speed: f32,
+    lightSpread: f32,
+    lightLength: f32,
+    sourceWidth: f32,
+    pulsating: f32,
+    pulsatingMin: f32,
+    pulsatingMax: f32,
+    fadeDistance: f32,
+    saturation: f32,
+    noiseAmount: f32,
+    distortion: f32,
+    particlesEnabled: f32,
+    particleAmount: f32,
+    particleSizeMin: f32,
+    particleSizeMax: f32,
+    particleSpeed: f32,
+    particleOpacity: f32,
+    particleDrift: f32,
+    _pad1: f32,
+    _pad2: f32,
+  };
+
+  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
+
+  struct VertexOutput {
+    @builtin(position) position: vec4<f32>,
+    @location(0) vUv: vec2<f32>,
+  };
+
+  @vertex
+  fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
+    var positions = array<vec2<f32>, 3>(
+      vec2<f32>(-1.0, -1.0),
+      vec2<f32>(3.0, -1.0),
+      vec2<f32>(-1.0, 3.0)
+    );
+    
+    var output: VertexOutput;
+    let pos = positions[vertexIndex];
+    output.position = vec4<f32>(pos, 0.0, 1.0);
+    output.vUv = pos * 0.5 + 0.5;
+    return output;
+  }
+
+  fn hash(p: vec2<f32>) -> f32 {
+    let p3 = fract(p.xyx * 0.1031);
+    return fract((p3.x + p3.y) * p3.z + dot(p3, p3.yzx + 33.33));
+  }
+
+  fn hash2(p: vec2<f32>) -> vec2<f32> {
+    let n = sin(dot(p, vec2<f32>(41.0, 289.0)));
+    return fract(vec2<f32>(n * 262144.0, n * 32768.0));
+  }
+
+  fn fastNoise(st: vec2<f32>) -> f32 {
+    return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453);
+  }
+
+  fn lightStrengthCombined(lightSource: vec2<f32>, lightRefDirection: vec2<f32>, coord: vec2<f32>) -> f32 {
+    let sourceToCoord = coord - lightSource;
+    let distSq = dot(sourceToCoord, sourceToCoord);
+    let distance = sqrt(distSq);
+    
+    let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
+    let maxDistance = max(baseSize * uniforms.lightLength, 0.001);
+    if (distance > maxDistance) {
+      return 0.0;
+    }
+    
+    let invDist = 1.0 / max(distance, 0.001);
+    let dirNorm = sourceToCoord * invDist;
+    let cosAngle = dot(dirNorm, lightRefDirection);
+    
+    if (cosAngle < 0.0) {
+      return 0.0;
+    }
+
+    let side = dot(dirNorm, vec2<f32>(-lightRefDirection.y, lightRefDirection.x));
+    let time = uniforms.iTime;
+    let speed = uniforms.speed;
+    
+    let asymNoise = fastNoise(vec2<f32>(side * 6.0 + time * 0.12, distance * 0.004 + cosAngle * 2.0));
+    let asymShift = (asymNoise - 0.5) * uniforms.distortion * 0.6;
+    
+    let distortPhase = time * 1.4 + distance * 0.006 + cosAngle * 4.5 + side * 1.7;
+    let distortedAngle = cosAngle + uniforms.distortion * sin(distortPhase) * 0.22 + asymShift;
+    
+    let flickerSeed = cosAngle * 9.0 + side * 4.0 + time * speed * 0.35;
+    let flicker = 0.86 + fastNoise(vec2<f32>(flickerSeed, distance * 0.01)) * 0.28;
+    
+    let asymSpread = max(uniforms.lightSpread * (0.9 + (asymNoise - 0.5) * 0.25), 0.001);
+    let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / asymSpread);
+    let lengthFalloff = clamp(1.0 - distance / maxDistance, 0.0, 1.0);
+    
+    let fadeMaxDist = max(baseSize * uniforms.fadeDistance, 0.001);
+    let fadeFalloff = clamp((fadeMaxDist - distance) / fadeMaxDist, 0.0, 1.0);
+    
+    var pulse: f32 = 1.0;
+    if (uniforms.pulsating > 0.5) {
+      let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5;
+      let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5;
+      pulse = pulseCenter + pulseAmplitude * sin(time * speed * 3.0);
+    }
+
+    let timeSpeed = time * speed;
+    let wave = 0.5
+      + 0.25 * sin(cosAngle * 28.0 + side * 8.0 + timeSpeed * 1.2)
+      + 0.18 * cos(cosAngle * 22.0 - timeSpeed * 0.95 + side * 6.0)
+      + 0.12 * sin(cosAngle * 35.0 + timeSpeed * 1.6 + asymNoise * 3.0);
+    let minStrength = 0.14 + asymNoise * 0.06;
+    let baseStrength = max(clamp(wave * (0.85 + asymNoise * 0.3), 0.0, 1.0), minStrength);
+
+    let lightStrength = baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse * flicker;
+    let ambientLight = (0.06 + asymNoise * 0.04) * lengthFalloff * fadeFalloff * spreadFactor;
+
+    return max(lightStrength, ambientLight);
+  }
+
+  fn particle(coord: vec2<f32>, particlePos: vec2<f32>, size: f32) -> f32 {
+    let delta = coord - particlePos;
+    let distSq = dot(delta, delta);
+    let sizeSq = size * size;
+    
+    if (distSq > sizeSq * 9.0) {
+      return 0.0;
+    }
+    
+    let d = sqrt(distSq);
+    let core = smoothstep(size, size * 0.35, d);
+    let glow = smoothstep(size * 3.0, 0.0, d) * 0.55;
+    return core + glow;
+  }
+
+  fn renderParticles(coord: vec2<f32>, lightSource: vec2<f32>, lightDir: vec2<f32>) -> f32 {
+    if (uniforms.particlesEnabled < 0.5 || uniforms.particleAmount < 1.0) {
+      return 0.0;
+    }
+
+    var particleSum: f32 = 0.0;
+    let particleCount = i32(uniforms.particleAmount);
+    let time = uniforms.iTime * uniforms.particleSpeed;
+    let perpDir = vec2<f32>(-lightDir.y, lightDir.x);
+    let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
+    let maxDist = max(baseSize * uniforms.lightLength, 1.0);
+    let spreadScale = uniforms.lightSpread * baseSize * 0.65;
+    let coneHalfWidth = uniforms.lightSpread * baseSize * 0.55;
+    
+    for (var i: i32 = 0; i < particleCount; i = i + 1) {
+      let fi = f32(i);
+      let seed = vec2<f32>(fi * 127.1, fi * 311.7);
+      let rnd = hash2(seed);
+      
+      let lifeDuration = 2.0 + hash(seed + vec2<f32>(19.0, 73.0)) * 3.0;
+      let lifeOffset = hash(seed + vec2<f32>(91.0, 37.0)) * lifeDuration;
+      let lifeProgress = fract((time + lifeOffset) / lifeDuration);
+      
+      let fadeIn = smoothstep(0.0, 0.2, lifeProgress);
+      let fadeOut = 1.0 - smoothstep(0.8, 1.0, lifeProgress);
+      let lifeFade = fadeIn * fadeOut;
+      if (lifeFade < 0.01) {
+        continue;
+      }
+      
+      let alongLight = rnd.x * maxDist * 0.8;
+      let perpOffset = (rnd.y - 0.5) * spreadScale;
+      
+      let floatPhase = rnd.y * 6.28318 + fi * 0.37;
+      let floatSpeed = 0.35 + rnd.x * 0.9;
+      let drift = vec2<f32>(
+        sin(time * floatSpeed + floatPhase),
+        cos(time * floatSpeed * 0.85 + floatPhase * 1.3)
+      ) * uniforms.particleDrift * baseSize * 0.08;
+      
+      let wobble = vec2<f32>(
+        sin(time * 1.4 + floatPhase * 2.1),
+        cos(time * 1.1 + floatPhase * 1.6)
+      ) * uniforms.particleDrift * baseSize * 0.03;
+      
+      let flowOffset = (rnd.x - 0.5) * baseSize * 0.12 + fract(time * 0.06 + rnd.y) * baseSize * 0.1;
+      
+      let basePos = lightSource + lightDir * (alongLight + flowOffset) + perpDir * perpOffset + drift + wobble;
+      
+      let toParticle = basePos - lightSource;
+      let projLen = dot(toParticle, lightDir);
+      if (projLen < 0.0 || projLen > maxDist) {
+        continue;
+      }
+      
+      let sideDist = abs(dot(toParticle, perpDir));
+      if (sideDist > coneHalfWidth) {
+        continue;
+      }
+      
+      let size = mix(uniforms.particleSizeMin, uniforms.particleSizeMax, rnd.x);
+      let twinkle = 0.7 + 0.3 * sin(time * (1.5 + rnd.y * 2.0) + floatPhase);
+      let distFade = 1.0 - smoothstep(maxDist * 0.2, maxDist * 0.95, projLen);
+      if (distFade < 0.01) {
+        continue;
+      }
+      
+      let p = particle(coord, basePos, size);
+      if (p > 0.0) {
+        particleSum = particleSum + p * lifeFade * twinkle * distFade * uniforms.particleOpacity;
+        if (particleSum >= 1.0) {
+          break;
+        }
+      }
+    }
+    
+    return min(particleSum, 1.0);
+  }
+
+  @fragment
+  fn fragmentMain(@builtin(position) fragCoord: vec4<f32>, @location(0) vUv: vec2<f32>) -> @location(0) vec4<f32> {
+    let coord = vec2<f32>(fragCoord.x, fragCoord.y);
+    
+    let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5;
+    let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x;
+    
+    let perpDir = vec2<f32>(-uniforms.lightDir.y, uniforms.lightDir.x);
+    let adjustedLightPos = uniforms.lightPos + perpDir * widthOffset;
+    
+    let lightValue = lightStrengthCombined(adjustedLightPos, uniforms.lightDir, coord);
+    
+    if (lightValue < 0.001) {
+      let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
+      if (particles < 0.001) {
+        return vec4<f32>(0.0, 0.0, 0.0, 0.0);
+      }
+      let particleBrightness = particles * 1.8;
+      return vec4<f32>(uniforms.color * particleBrightness, particles * 0.9);
+    }
+
+    var fragColor = vec4<f32>(lightValue, lightValue, lightValue, lightValue);
+
+    if (uniforms.noiseAmount > 0.01) {
+      let n = fastNoise(coord * 0.5 + uniforms.iTime * 0.5);
+      let grain = mix(1.0, n, uniforms.noiseAmount * 0.5);
+      fragColor = vec4<f32>(fragColor.rgb * grain, fragColor.a);
+    }
+
+    let brightness = 1.0 - (coord.y / uniforms.iResolution.y);
+    fragColor = vec4<f32>(
+      fragColor.x * (0.15 + brightness * 0.85),
+      fragColor.y * (0.35 + brightness * 0.65),
+      fragColor.z * (0.55 + brightness * 0.45),
+      fragColor.a
+    );
+
+    if (abs(uniforms.saturation - 1.0) > 0.01) {
+      let gray = dot(fragColor.rgb, vec3<f32>(0.299, 0.587, 0.114));
+      fragColor = vec4<f32>(mix(vec3<f32>(gray), fragColor.rgb, uniforms.saturation), fragColor.a);
+    }
+
+    fragColor = vec4<f32>(fragColor.rgb * uniforms.color, fragColor.a);
+    
+    let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
+    if (particles > 0.001) {
+      let particleBrightness = particles * 1.8;
+      fragColor = vec4<f32>(fragColor.rgb + uniforms.color * particleBrightness, max(fragColor.a, particles * 0.9));
+    }
+    
+    return fragColor;
+  }
+`
+
+const UNIFORM_BUFFER_SIZE = 144
+
+function updateUniformBuffer(buffer: Float32Array, data: UniformData): void {
+  buffer[0] = data.iTime
+  buffer[2] = data.iResolution[0]
+  buffer[3] = data.iResolution[1]
+  buffer[4] = data.lightPos[0]
+  buffer[5] = data.lightPos[1]
+  buffer[6] = data.lightDir[0]
+  buffer[7] = data.lightDir[1]
+  buffer[8] = data.color[0]
+  buffer[9] = data.color[1]
+  buffer[10] = data.color[2]
+  buffer[11] = data.speed
+  buffer[12] = data.lightSpread
+  buffer[13] = data.lightLength
+  buffer[14] = data.sourceWidth
+  buffer[15] = data.pulsating
+  buffer[16] = data.pulsatingMin
+  buffer[17] = data.pulsatingMax
+  buffer[18] = data.fadeDistance
+  buffer[19] = data.saturation
+  buffer[20] = data.noiseAmount
+  buffer[21] = data.distortion
+  buffer[22] = data.particlesEnabled
+  buffer[23] = data.particleAmount
+  buffer[24] = data.particleSizeMin
+  buffer[25] = data.particleSizeMax
+  buffer[26] = data.particleSpeed
+  buffer[27] = data.particleOpacity
+  buffer[28] = data.particleDrift
+}
+
+export default function Spotlight(props: SpotlightProps) {
+  let containerRef: HTMLDivElement | undefined
+  let canvasRef: HTMLCanvasElement | null = null
+  let deviceRef: GPUDevice | null = null
+  let contextRef: GPUCanvasContext | null = null
+  let pipelineRef: GPURenderPipeline | null = null
+  let uniformBufferRef: GPUBuffer | null = null
+  let bindGroupRef: GPUBindGroup | null = null
+  let animationIdRef: number | null = null
+  let cleanupFunctionRef: (() => void) | null = null
+  let uniformDataRef: UniformData | null = null
+  let uniformArrayRef: Float32Array | null = null
+  let configRef: SpotlightConfig = props.config()
+  let frameCount = 0
+
+  const [isVisible, setIsVisible] = createSignal(false)
+
+  createEffect(() => {
+    configRef = props.config()
+  })
+
+  onMount(() => {
+    if (!containerRef) return
+
+    const observer = new IntersectionObserver(
+      (entries) => {
+        const entry = entries[0]
+        setIsVisible(entry.isIntersecting)
+      },
+      { threshold: 0.1 },
+    )
+
+    observer.observe(containerRef)
+
+    onCleanup(() => {
+      observer.disconnect()
+    })
+  })
+
+  createEffect(() => {
+    const visible = isVisible()
+    const config = props.config()
+    if (!visible || !containerRef) {
+      return
+    }
+
+    if (cleanupFunctionRef) {
+      cleanupFunctionRef()
+      cleanupFunctionRef = null
+    }
+
+    const initializeWebGPU = async () => {
+      if (!containerRef) {
+        return
+      }
+
+      await new Promise((resolve) => setTimeout(resolve, 10))
+
+      if (!containerRef) {
+        return
+      }
+
+      if (!navigator.gpu) {
+        console.warn("WebGPU is not supported in this browser")
+        return
+      }
+
+      const adapter = await navigator.gpu.requestAdapter({
+        powerPreference: "high-performance",
+      })
+      if (!adapter) {
+        console.warn("Failed to get WebGPU adapter")
+        return
+      }
+
+      const device = await adapter.requestDevice()
+      deviceRef = device
+
+      const canvas = document.createElement("canvas")
+      canvas.style.width = "100%"
+      canvas.style.height = "100%"
+      canvasRef = canvas
+
+      while (containerRef.firstChild) {
+        containerRef.removeChild(containerRef.firstChild)
+      }
+      containerRef.appendChild(canvas)
+
+      const context = canvas.getContext("webgpu")
+      if (!context) {
+        console.warn("Failed to get WebGPU context")
+        return
+      }
+      contextRef = context
+
+      const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
+      context.configure({
+        device,
+        format: presentationFormat,
+        alphaMode: "premultiplied",
+      })
+
+      const shaderModule = device.createShaderModule({
+        code: WGSL_SHADER,
+      })
+
+      const uniformBuffer = device.createBuffer({
+        size: UNIFORM_BUFFER_SIZE,
+        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+      })
+      uniformBufferRef = uniformBuffer
+
+      const bindGroupLayout = device.createBindGroupLayout({
+        entries: [
+          {
+            binding: 0,
+            visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
+            buffer: { type: "uniform" },
+          },
+        ],
+      })
+
+      const bindGroup = device.createBindGroup({
+        layout: bindGroupLayout,
+        entries: [
+          {
+            binding: 0,
+            resource: { buffer: uniformBuffer },
+          },
+        ],
+      })
+      bindGroupRef = bindGroup
+
+      const pipelineLayout = device.createPipelineLayout({
+        bindGroupLayouts: [bindGroupLayout],
+      })
+
+      const pipeline = device.createRenderPipeline({
+        layout: pipelineLayout,
+        vertex: {
+          module: shaderModule,
+          entryPoint: "vertexMain",
+        },
+        fragment: {
+          module: shaderModule,
+          entryPoint: "fragmentMain",
+          targets: [
+            {
+              format: presentationFormat,
+              blend: {
+                color: {
+                  srcFactor: "src-alpha",
+                  dstFactor: "one-minus-src-alpha",
+                  operation: "add",
+                },
+                alpha: {
+                  srcFactor: "one",
+                  dstFactor: "one-minus-src-alpha",
+                  operation: "add",
+                },
+              },
+            },
+          ],
+        },
+        primitive: {
+          topology: "triangle-list",
+        },
+      })
+      pipelineRef = pipeline
+
+      const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
+      const dpr = Math.min(window.devicePixelRatio, 2)
+      const w = wCSS * dpr
+      const h = hCSS * dpr
+      const { anchor, dir } = getAnchorAndDir(config.placement, w, h)
+
+      uniformDataRef = {
+        iTime: 0,
+        iResolution: [w, h],
+        lightPos: anchor,
+        lightDir: dir,
+        color: hexToRgb(config.color),
+        speed: config.speed,
+        lightSpread: config.spread,
+        lightLength: config.length,
+        sourceWidth: config.width,
+        pulsating: config.pulsating !== false ? 1.0 : 0.0,
+        pulsatingMin: config.pulsating !== false ? config.pulsating[0] : 1.0,
+        pulsatingMax: config.pulsating !== false ? config.pulsating[1] : 1.0,
+        fadeDistance: config.distance,
+        saturation: config.saturation,
+        noiseAmount: config.noiseAmount,
+        distortion: config.distortion,
+        particlesEnabled: config.particles.enabled ? 1.0 : 0.0,
+        particleAmount: config.particles.amount,
+        particleSizeMin: config.particles.size[0],
+        particleSizeMax: config.particles.size[1],
+        particleSpeed: config.particles.speed,
+        particleOpacity: config.particles.opacity,
+        particleDrift: config.particles.drift,
+      }
+
+      const updatePlacement = () => {
+        if (!containerRef || !canvasRef || !uniformDataRef) {
+          return
+        }
+
+        const dpr = Math.min(window.devicePixelRatio, 2)
+        const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
+        const w = Math.floor(wCSS * dpr)
+        const h = Math.floor(hCSS * dpr)
+
+        canvasRef.width = w
+        canvasRef.height = h
+
+        uniformDataRef.iResolution = [w, h]
+
+        const { anchor, dir } = getAnchorAndDir(configRef.placement, w, h)
+        uniformDataRef.lightPos = anchor
+        uniformDataRef.lightDir = dir
+      }
+
+      const loop = (t: number) => {
+        if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) {
+          return
+        }
+
+        const timeSeconds = t * 0.001
+        uniformDataRef.iTime = timeSeconds
+        frameCount++
+
+        if (props.onAnimationFrame && frameCount % 2 === 0) {
+          const pulsatingMin = configRef.pulsating !== false ? configRef.pulsating[0] : 1.0
+          const pulsatingMax = configRef.pulsating !== false ? configRef.pulsating[1] : 1.0
+          const pulseCenter = (pulsatingMin + pulsatingMax) * 0.5
+          const pulseAmplitude = (pulsatingMax - pulsatingMin) * 0.5
+          const pulseValue =
+            configRef.pulsating !== false
+              ? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * configRef.speed * 3.0)
+              : 1.0
+
+          const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * configRef.speed * 1.5)
+          const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * configRef.speed * 1.1)
+          const intensity = Math.max((baseIntensity1 + baseIntensity2) * pulseValue, 0.55)
+
+          props.onAnimationFrame({
+            time: timeSeconds,
+            intensity,
+            pulseValue: Math.max(pulseValue, 0.9),
+          })
+        }
+
+        try {
+          if (!uniformArrayRef) {
+            uniformArrayRef = new Float32Array(36)
+          }
+          updateUniformBuffer(uniformArrayRef, uniformDataRef)
+          deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformArrayRef.buffer)
+
+          const commandEncoder = deviceRef.createCommandEncoder()
+
+          const textureView = contextRef.getCurrentTexture().createView()
+
+          const renderPass = commandEncoder.beginRenderPass({
+            colorAttachments: [
+              {
+                view: textureView,
+                clearValue: { r: 0, g: 0, b: 0, a: 0 },
+                loadOp: "clear",
+                storeOp: "store",
+              },
+            ],
+          })
+
+          renderPass.setPipeline(pipelineRef)
+          renderPass.setBindGroup(0, bindGroupRef)
+          renderPass.draw(3)
+          renderPass.end()
+
+          deviceRef.queue.submit([commandEncoder.finish()])
+
+          animationIdRef = requestAnimationFrame(loop)
+        } catch (error) {
+          console.warn("WebGPU rendering error:", error)
+          return
+        }
+      }
+
+      window.addEventListener("resize", updatePlacement)
+      updatePlacement()
+      animationIdRef = requestAnimationFrame(loop)
+
+      cleanupFunctionRef = () => {
+        if (animationIdRef) {
+          cancelAnimationFrame(animationIdRef)
+          animationIdRef = null
+        }
+
+        window.removeEventListener("resize", updatePlacement)
+
+        if (uniformBufferRef) {
+          uniformBufferRef.destroy()
+          uniformBufferRef = null
+        }
+
+        if (deviceRef) {
+          deviceRef.destroy()
+          deviceRef = null
+        }
+
+        if (canvasRef && canvasRef.parentNode) {
+          canvasRef.parentNode.removeChild(canvasRef)
+        }
+
+        canvasRef = null
+        contextRef = null
+        pipelineRef = null
+        bindGroupRef = null
+        uniformDataRef = null
+      }
+    }
+
+    initializeWebGPU()
+
+    onCleanup(() => {
+      if (cleanupFunctionRef) {
+        cleanupFunctionRef()
+        cleanupFunctionRef = null
+      }
+    })
+  })
+
+  createEffect(() => {
+    if (!uniformDataRef || !containerRef) {
+      return
+    }
+
+    const config = props.config()
+
+    uniformDataRef.color = hexToRgb(config.color)
+    uniformDataRef.speed = config.speed
+    uniformDataRef.lightSpread = config.spread
+    uniformDataRef.lightLength = config.length
+    uniformDataRef.sourceWidth = config.width
+    uniformDataRef.pulsating = config.pulsating !== false ? 1.0 : 0.0
+    uniformDataRef.pulsatingMin = config.pulsating !== false ? config.pulsating[0] : 1.0
+    uniformDataRef.pulsatingMax = config.pulsating !== false ? config.pulsating[1] : 1.0
+    uniformDataRef.fadeDistance = config.distance
+    uniformDataRef.saturation = config.saturation
+    uniformDataRef.noiseAmount = config.noiseAmount
+    uniformDataRef.distortion = config.distortion
+    uniformDataRef.particlesEnabled = config.particles.enabled ? 1.0 : 0.0
+    uniformDataRef.particleAmount = config.particles.amount
+    uniformDataRef.particleSizeMin = config.particles.size[0]
+    uniformDataRef.particleSizeMax = config.particles.size[1]
+    uniformDataRef.particleSpeed = config.particles.speed
+    uniformDataRef.particleOpacity = config.particles.opacity
+    uniformDataRef.particleDrift = config.particles.drift
+
+    const dpr = Math.min(window.devicePixelRatio, 2)
+    const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
+    const { anchor, dir } = getAnchorAndDir(config.placement, wCSS * dpr, hCSS * dpr)
+    uniformDataRef.lightPos = anchor
+    uniformDataRef.lightDir = dir
+  })
+
+  return (
+    <div
+      ref={containerRef}
+      class={`spotlight-container ${props.class ?? ""}`.trim()}
+      style={{ opacity: props.config().opacity }}
+    />
+  )
+}

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

@@ -9,8 +9,8 @@ export const config = {
   github: {
     repoUrl: "https://github.com/anomalyco/opencode",
     starsFormatted: {
-      compact: "50K",
-      full: "50,000",
+      compact: "70K",
+      full: "70,000",
     },
   },
 
@@ -23,7 +23,7 @@ export const config = {
   // Static stats (used on landing page)
   stats: {
     contributors: "500",
-    commits: "6,500",
+    commits: "7,000",
     monthlyUsers: "650,000",
   },
 } as const

+ 0 - 24
packages/console/app/src/context/auth.session.ts

@@ -1,24 +0,0 @@
-import { useSession } from "@solidjs/start/http"
-
-export interface AuthSession {
-  account?: Record<
-    string,
-    {
-      id: string
-      email: string
-    }
-  >
-  current?: string
-}
-
-export function useAuthSession() {
-  return useSession<AuthSession>({
-    password: "0".repeat(32),
-    name: "auth",
-    maxAge: 60 * 60 * 24 * 365,
-    cookie: {
-      secure: false,
-      httpOnly: true,
-    },
-  })
-}

+ 26 - 1
packages/console/app/src/context/auth.ts

@@ -5,13 +5,38 @@ import { redirect } from "@solidjs/router"
 import { Actor } from "@opencode-ai/console-core/actor.js"
 
 import { createClient } from "@openauthjs/openauth/client"
-import { useAuthSession } from "./auth.session"
 
 export const AuthClient = createClient({
   clientID: "app",
   issuer: import.meta.env.VITE_AUTH_URL,
 })
 
+import { useSession } from "@solidjs/start/http"
+import { Resource } from "@opencode-ai/console-resource"
+
+export interface AuthSession {
+  account?: Record<
+    string,
+    {
+      id: string
+      email: string
+    }
+  >
+  current?: string
+}
+
+export function useAuthSession() {
+  return useSession<AuthSession>({
+    password: Resource.ZEN_SESSION_SECRET.value,
+    name: "auth",
+    maxAge: 60 * 60 * 24 * 365,
+    cookie: {
+      secure: false,
+      httpOnly: true,
+    },
+  })
+}
+
 export const getActor = async (workspace?: string): Promise<Actor.Info> => {
   "use server"
   const evt = getRequestEvent()

+ 7 - 6
packages/console/app/src/lib/github.ts

@@ -14,13 +14,14 @@ export const github = query(async () => {
       fetch(`${apiBaseUrl}/releases`, { headers }).then((res) => res.json()),
       fetch(`${apiBaseUrl}/contributors?per_page=1`, { headers }),
     ])
+    if (!Array.isArray(releases) || releases.length === 0) {
+      return undefined
+    }
     const [release] = releases
-    const contributorCount = Number.parseInt(
-      contributors.headers
-        .get("Link")!
-        .match(/&page=(\d+)>; rel="last"/)!
-        .at(1)!,
-    )
+    const linkHeader = contributors.headers.get("Link")
+    const contributorCount = linkHeader
+      ? Number.parseInt(linkHeader.match(/&page=(\d+)>; rel="last"/)?.at(1) ?? "0")
+      : 0
     return {
       stars: meta.stargazers_count,
       release: {

+ 3 - 2
packages/console/app/src/routes/auth/callback.ts → packages/console/app/src/routes/auth/[...callback].ts

@@ -1,10 +1,11 @@
 import { redirect } from "@solidjs/router"
 import type { APIEvent } from "@solidjs/start/server"
 import { AuthClient } from "~/context/auth"
-import { useAuthSession } from "~/context/auth.session"
+import { useAuthSession } from "~/context/auth"
 
 export async function GET(input: APIEvent) {
   const url = new URL(input.request.url)
+
   try {
     const code = url.searchParams.get("code")
     if (!code) throw new Error("No code found")
@@ -27,7 +28,7 @@ export async function GET(input: APIEvent) {
         current: id,
       }
     })
-    return redirect("/auth")
+    return redirect(url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", ""))
   } catch (e: any) {
     return new Response(
       JSON.stringify({

+ 4 - 1
packages/console/app/src/routes/auth/authorize.ts

@@ -2,6 +2,9 @@ import type { APIEvent } from "@solidjs/start/server"
 import { AuthClient } from "~/context/auth"
 
 export async function GET(input: APIEvent) {
-  const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code")
+  const url = new URL(input.request.url)
+  const cont = url.searchParams.get("continue") ?? ""
+  const callbackUrl = new URL(`./callback${cont}`, input.request.url)
+  const result = await AuthClient.authorize(callbackUrl.toString(), "code")
   return Response.redirect(result.url, 302)
 }

+ 1 - 1
packages/console/app/src/routes/auth/logout.ts

@@ -1,6 +1,6 @@
 import { redirect } from "@solidjs/router"
 import { APIEvent } from "@solidjs/start"
-import { useAuthSession } from "~/context/auth.session"
+import { useAuthSession } from "~/context/auth"
 
 export async function GET(event: APIEvent) {
   const auth = await useAuthSession()

+ 1 - 1
packages/console/app/src/routes/auth/status.ts

@@ -1,5 +1,5 @@
 import { APIEvent } from "@solidjs/start"
-import { useAuthSession } from "~/context/auth.session"
+import { useAuthSession } from "~/context/auth"
 
 export async function GET(input: APIEvent) {
   const session = await useAuthSession()

+ 828 - 0
packages/console/app/src/routes/black.css

@@ -0,0 +1,828 @@
+::view-transition-group(*) {
+  animation-duration: 250ms;
+  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+::view-transition-old(root),
+::view-transition-new(root) {
+  animation-duration: 250ms;
+  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+::view-transition-image-pair(root) {
+  isolation: isolate;
+}
+
+::view-transition-old(root) {
+  animation: none;
+  mix-blend-mode: normal;
+}
+
+::view-transition-new(root) {
+  animation: none;
+  mix-blend-mode: normal;
+}
+
+@keyframes fade-in {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes fade-out {
+  from {
+    opacity: 1;
+  }
+  to {
+    opacity: 0;
+  }
+}
+
+@keyframes fade-in-up {
+  from {
+    opacity: 0;
+    transform: translateY(8px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes reveal-terms {
+  from {
+    mask-position: 0% 200%;
+  }
+  to {
+    mask-position: 0% 50%;
+  }
+}
+
+@keyframes hide-terms {
+  from {
+    mask-position: 0% 50%;
+  }
+  to {
+    mask-position: 0% 200%;
+  }
+}
+
+::view-transition-old(terms-20),
+::view-transition-old(terms-100),
+::view-transition-old(terms-200) {
+  mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent);
+  mask-repeat: no-repeat;
+  mask-size: 100% 200%;
+  animation: hide-terms 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
+}
+
+::view-transition-new(terms-20),
+::view-transition-new(terms-100),
+::view-transition-new(terms-200) {
+  mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent);
+  mask-repeat: no-repeat;
+  mask-position: 0% 200%;
+  mask-size: 100% 200%;
+  animation: reveal-terms 300ms cubic-bezier(0.25, 0, 0.5, 1) 50ms forwards;
+}
+
+::view-transition-old(actions-20),
+::view-transition-old(actions-100),
+::view-transition-old(actions-200) {
+  animation: fade-out 80ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
+}
+
+::view-transition-new(actions-20),
+::view-transition-new(actions-100),
+::view-transition-new(actions-200) {
+  animation: fade-in-up 300ms cubic-bezier(0.16, 1, 0.3, 1) 300ms forwards;
+  opacity: 0;
+}
+
+::view-transition-group(card-20),
+::view-transition-group(card-100),
+::view-transition-group(card-200) {
+  animation-duration: 250ms;
+  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+[data-page="black"] {
+  background: #000;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: stretch;
+  font-family: var(--font-mono);
+  color: #fff;
+
+  [data-component="header-logo"] {
+    filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.25)) drop-shadow(0 4px 16px rgba(0, 0, 0, 0.1));
+    position: relative;
+    z-index: 1;
+  }
+
+  .header-light-rays {
+    position: absolute;
+    inset: 0 0 auto 0;
+    height: 30dvh;
+    pointer-events: none;
+    z-index: 0;
+  }
+
+  [data-component="header"] {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding-top: 40px;
+    flex-shrink: 0;
+  }
+
+  [data-component="content"] {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    width: 100%;
+    flex-grow: 1;
+
+    [data-slot="hero"] {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      text-align: center;
+      gap: 8px;
+      margin-top: 40px;
+      padding: 0 20px;
+
+      @media (min-width: 768px) {
+        margin-top: 60px;
+      }
+
+      h1 {
+        color: rgba(255, 255, 255, 0.92);
+        font-size: 16px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: 1.45;
+        margin: 0;
+
+        @media (min-width: 768px) {
+          font-size: 20px;
+        }
+
+        @media (max-width: 480px) {
+          font-size: 14px;
+        }
+      }
+
+      p {
+        color: rgba(255, 255, 255, 0.59);
+        font-size: 16px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: 1.45;
+        margin: 0;
+
+        @media (min-width: 768px) {
+          font-size: 20px;
+        }
+
+        @media (max-width: 480px) {
+          font-size: 14px;
+        }
+      }
+    }
+
+    [data-slot="hero-black"] {
+      margin-top: 40px;
+      padding: 0 20px;
+      position: relative;
+
+      @media (min-width: 768px) {
+        margin-top: 60px;
+      }
+
+      svg {
+        width: 100%;
+        max-width: 590px;
+        height: auto;
+        overflow: visible;
+        filter: drop-shadow(0 0 20px rgba(255, 255, 255, calc(0.1 + var(--hero-black-glow-intensity, 0) * 0.15)))
+          drop-shadow(0 -5px 30px rgba(255, 255, 255, calc(var(--hero-black-glow-intensity, 0) * 0.2)));
+        mask-image: linear-gradient(to bottom, black, transparent);
+        stroke-width: 1.5;
+
+        [data-slot="black-base"] {
+          fill: url(#hero-black-fill-gradient);
+          stroke: url(#hero-black-stroke-gradient);
+        }
+
+        [data-slot="black-glow"] {
+          fill: url(#hero-black-top-glow);
+          pointer-events: none;
+        }
+
+        [data-slot="black-shimmer"] {
+          fill: url(#hero-black-shimmer-gradient);
+          pointer-events: none;
+          mix-blend-mode: overlay;
+        }
+      }
+    }
+
+    [data-slot="cta"] {
+      display: flex;
+      flex-direction: column;
+      gap: 16px;
+      align-items: center;
+      text-align: center;
+      margin-top: -40px;
+      width: 100%;
+
+      @media (min-width: 768px) {
+        margin-top: -20px;
+      }
+
+      [data-slot="heading"] {
+        color: rgba(255, 255, 255, 0.92);
+        text-align: center;
+        font-size: 18px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: 160%;
+
+        span {
+          display: inline-block;
+        }
+      }
+      [data-slot="subheading"] {
+        color: rgba(255, 255, 255, 0.59);
+        font-size: 15px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: 160%;
+
+        @media (min-width: 768px) {
+          font-size: 18px;
+          line-height: 160%;
+        }
+      }
+      [data-slot="button"] {
+        display: inline-flex;
+        height: 40px;
+        padding: 0 12px;
+        justify-content: center;
+        align-items: center;
+        gap: 8px;
+        border-radius: 4px;
+        background: rgba(255, 255, 255, 0.92);
+        text-decoration: none;
+        color: #000;
+        font-family: "JetBrains Mono Nerd Font";
+        font-size: 16px;
+        font-style: normal;
+        font-weight: 500;
+        line-height: normal;
+
+        &:hover {
+          background: #e0e0e0;
+        }
+
+        &:active {
+          transform: scale(0.98);
+        }
+      }
+      [data-slot="back-soon"] {
+        color: rgba(255, 255, 255, 0.59);
+        text-align: center;
+        font-size: 13px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: 160%; /* 20.8px */
+      }
+      [data-slot="follow-us"] {
+        display: inline-flex;
+        height: 40px;
+        padding: 0 12px;
+        justify-content: center;
+        align-items: center;
+        gap: 8px;
+        border-radius: 4px;
+        border: 1px solid rgba(255, 255, 255, 0.17);
+        color: rgba(255, 255, 255, 0.59);
+        font-family: var(--font-mono);
+        font-size: 14px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: normal;
+        text-decoration: none;
+      }
+
+      [data-slot="pricing"] {
+        display: flex;
+        flex-direction: column;
+        gap: 16px;
+        width: 100%;
+        max-width: 660px;
+        padding: 0 20px;
+
+        @media (min-width: 768px) {
+          padding: 0;
+        }
+      }
+
+      [data-slot="pricing-card"] {
+        display: flex;
+        flex-direction: column;
+        gap: 12px;
+        padding: 24px;
+        border: 1px solid rgba(255, 255, 255, 0.17);
+        background: black;
+        background-clip: padding-box;
+        border-radius: 4px;
+        text-decoration: none;
+        transition: border-color 0.15s ease;
+        cursor: pointer;
+        text-align: left;
+
+        @media (max-width: 480px) {
+          padding: 16px;
+        }
+
+        &:hover:not(:active) {
+          border-color: rgba(255, 255, 255, 0.35);
+        }
+
+        [data-slot="icon"] {
+          color: rgba(255, 255, 255, 0.59);
+        }
+
+        [data-slot="price"] {
+          display: flex;
+          flex-wrap: wrap;
+          align-items: baseline;
+          gap: 8px;
+        }
+
+        [data-slot="amount"] {
+          color: rgba(255, 255, 255, 0.92);
+          font-size: 24px;
+          font-weight: 500;
+        }
+
+        [data-slot="period"] {
+          color: rgba(255, 255, 255, 0.59);
+          font-size: 14px;
+        }
+
+        [data-slot="multiplier"] {
+          color: rgba(255, 255, 255, 0.39);
+          font-size: 14px;
+
+          &::before {
+            content: "·";
+            margin-right: 8px;
+          }
+        }
+      }
+
+      [data-slot="selected-plan"] {
+        display: flex;
+        flex-direction: column;
+        gap: 32px;
+        width: 100%;
+        max-width: 660px;
+        margin: 0 auto;
+        position: relative;
+        background-color: rgba(0, 0, 0, 0.75);
+        z-index: 1;
+
+        @media (max-width: 480px) {
+          margin: 0 20px;
+          width: calc(100% - 40px);
+        }
+      }
+
+      [data-slot="selected-card"] {
+        display: flex;
+        flex-direction: column;
+        gap: 12px;
+        padding: 24px;
+        border: 1px solid rgba(255, 255, 255, 0.17);
+        border-radius: 4px;
+        width: 100%;
+
+        [data-slot="icon"] {
+          color: rgba(255, 255, 255, 0.59);
+        }
+
+        [data-slot="price"] {
+          display: flex;
+          flex-wrap: wrap;
+          align-items: baseline;
+          gap: 8px;
+        }
+
+        [data-slot="amount"] {
+          color: rgba(255, 255, 255, 0.92);
+          font-size: 24px;
+          font-weight: 500;
+        }
+
+        [data-slot="period"] {
+          color: rgba(255, 255, 255, 0.59);
+          font-size: 14px;
+        }
+
+        [data-slot="multiplier"] {
+          color: rgba(255, 255, 255, 0.39);
+          font-size: 14px;
+
+          &::before {
+            content: "·";
+            margin-right: 8px;
+          }
+        }
+
+        [data-slot="terms"] {
+          list-style: none;
+          padding: 0;
+          margin: 0;
+          display: flex;
+          flex-direction: column;
+          gap: 8px;
+          text-align: left;
+
+          li {
+            color: rgba(255, 255, 255, 0.59);
+            font-size: 14px;
+            line-height: 1.5;
+            padding-left: 16px;
+            position: relative;
+
+            &::before {
+              content: "▪";
+              position: absolute;
+              left: 0;
+              color: rgba(255, 255, 255, 0.39);
+            }
+
+            @media (max-width: 768px) {
+              font-size: 12px;
+            }
+          }
+        }
+
+        [data-slot="actions"] {
+          display: flex;
+          gap: 16px;
+          margin-top: 8px;
+
+          button,
+          a {
+            flex: 1;
+            display: inline-flex;
+            height: 48px;
+            padding: 0 16px;
+            justify-content: center;
+            align-items: center;
+            border-radius: 4px;
+            font-family: var(--font-mono);
+            font-size: 16px;
+            font-weight: 400;
+            text-decoration: none;
+            cursor: pointer;
+          }
+
+          [data-slot="cancel"] {
+            background: rgba(255, 255, 255, 0.05);
+            border: 1px solid rgba(255, 255, 255, 0.17);
+            color: rgba(255, 255, 255, 0.92);
+            transition-property: background-color, border-color;
+            transition-duration: 150ms;
+            transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
+
+            &:hover {
+              background-color: rgba(255, 255, 255, 0.08);
+              border-color: rgba(255, 255, 255, 0.25);
+            }
+          }
+
+          [data-slot="continue"] {
+            background: rgb(255, 255, 255);
+            color: rgb(0, 0, 0);
+            transition: background-color 150ms cubic-bezier(0.25, 0, 0.5, 1);
+
+            &:hover {
+              background: rgba(255, 255, 255, 0.9);
+            }
+          }
+        }
+      }
+
+      [data-slot="fine-print"] {
+        color: rgba(255, 255, 255, 0.39);
+        text-align: center;
+        font-size: 13px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: 160%; /* 20.8px */
+        font-style: italic;
+
+        a {
+          color: rgba(255, 255, 255, 0.39);
+          text-decoration: underline;
+        }
+      }
+    }
+
+    /* Subscribe page styles */
+    [data-slot="subscribe-form"] {
+      display: flex;
+      flex-direction: column;
+      gap: 32px;
+      align-items: center;
+      margin-top: -18px;
+      width: 100%;
+      max-width: 660px;
+      padding: 0 20px;
+
+      @media (min-width: 768px) {
+        margin-top: 40px;
+        padding: 0;
+      }
+
+      [data-slot="form-card"] {
+        width: 100%;
+        border: 1px solid rgba(255, 255, 255, 0.17);
+        border-radius: 4px;
+        padding: 24px;
+        display: flex;
+        flex-direction: column;
+        gap: 20px;
+      }
+
+      [data-slot="plan-header"] {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+      }
+
+      [data-slot="title"] {
+        color: rgba(255, 255, 255, 0.92);
+        font-size: 16px;
+        font-weight: 400;
+        margin-bottom: 8px;
+      }
+
+      [data-slot="icon"] {
+        color: rgba(255, 255, 255, 0.59);
+        isolation: isolate;
+        transform: translateZ(0);
+      }
+
+      [data-slot="price"] {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: baseline;
+        gap: 8px;
+      }
+
+      [data-slot="amount"] {
+        color: rgba(255, 255, 255, 0.92);
+        font-size: 24px;
+        font-weight: 500;
+      }
+
+      [data-slot="period"] {
+        color: rgba(255, 255, 255, 0.59);
+        font-size: 14px;
+      }
+
+      [data-slot="multiplier"] {
+        color: rgba(255, 255, 255, 0.39);
+        font-size: 14px;
+
+        &::before {
+          content: "·";
+          margin: 0 8px;
+        }
+      }
+
+      [data-slot="divider"] {
+        height: 1px;
+        background: rgba(255, 255, 255, 0.17);
+      }
+
+      [data-slot="section-title"] {
+        color: rgba(255, 255, 255, 0.92);
+        font-size: 16px;
+        font-weight: 400;
+      }
+
+      [data-slot="checkout-form"] {
+        display: flex;
+        flex-direction: column;
+        gap: 20px;
+      }
+
+      [data-slot="error"] {
+        color: #ff6b6b;
+        font-size: 14px;
+      }
+
+      [data-slot="submit-button"] {
+        width: 100%;
+        height: 48px;
+        background: rgba(255, 255, 255, 0.92);
+        border: none;
+        border-radius: 4px;
+        color: #000;
+        font-family: var(--font-mono);
+        font-size: 16px;
+        font-weight: 500;
+        cursor: pointer;
+        transition: background 0.15s ease;
+
+        &:hover:not(:disabled) {
+          background: #e0e0e0;
+        }
+
+        &:disabled {
+          opacity: 0.5;
+          cursor: not-allowed;
+        }
+      }
+
+      [data-slot="charge-notice"] {
+        color: #d4a500;
+        font-size: 14px;
+        text-align: center;
+      }
+
+      [data-slot="loading"] {
+        display: flex;
+        justify-content: center;
+        padding: 40px 0;
+
+        p {
+          color: rgba(255, 255, 255, 0.59);
+          font-size: 14px;
+        }
+      }
+
+      [data-slot="fine-print"] {
+        color: rgba(255, 255, 255, 0.39);
+        text-align: center;
+        font-size: 13px;
+        font-style: italic;
+        view-transition-name: fine-print;
+
+        a {
+          color: rgba(255, 255, 255, 0.39);
+          text-decoration: underline;
+        }
+      }
+
+      [data-slot="workspace-picker"] {
+        [data-slot="workspace-list"] {
+          width: 100%;
+          padding: 0;
+          margin: 0;
+          list-style: none;
+          display: flex;
+          flex-direction: column;
+          align-items: flex-start;
+          gap: 8px;
+          align-self: stretch;
+          outline: none;
+          overflow-y: auto;
+          max-height: 240px;
+          scrollbar-width: none;
+
+          &::-webkit-scrollbar {
+            display: none;
+          }
+
+          [data-slot="workspace-item"] {
+            width: 100%;
+            display: flex;
+            padding: 8px 12px;
+            align-items: center;
+            gap: 8px;
+            align-self: stretch;
+            cursor: pointer;
+
+            [data-slot="selected-icon"] {
+              visibility: hidden;
+              color: rgba(255, 255, 255, 0.39);
+              font-family: "IBM Plex Mono", monospace;
+              font-size: 16px;
+              font-style: normal;
+              font-weight: 400;
+              line-height: 160%;
+            }
+
+            span:last-child {
+              color: rgba(255, 255, 255, 0.92);
+              font-size: 16px;
+              font-style: normal;
+              font-weight: 400;
+              line-height: 160%;
+            }
+
+            &:hover,
+            &[data-active="true"] {
+              background: #161616;
+
+              [data-slot="selected-icon"] {
+                visibility: visible;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  [data-component="footer"] {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    justify-content: center;
+    align-items: center;
+    gap: 24px;
+    flex-shrink: 0;
+
+    @media (min-width: 768px) {
+      height: 120px;
+    }
+
+    [data-slot="footer-content"] {
+      display: flex;
+      gap: 24px;
+      align-items: center;
+      justify-content: center;
+
+      @media (min-width: 768px) {
+        gap: 40px;
+      }
+
+      span,
+      a {
+        color: rgba(255, 255, 255, 0.39);
+        font-family: "JetBrains Mono Nerd Font";
+        font-size: 16px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: normal;
+        text-decoration: none;
+      }
+
+      [data-slot="github-stars"] {
+        color: rgba(255, 255, 255, 0.25);
+        font-family: "JetBrains Mono Nerd Font";
+        font-size: 16px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: normal;
+      }
+
+      [data-slot="anomaly"] {
+        display: none;
+
+        @media (min-width: 768px) {
+          display: block;
+        }
+      }
+    }
+    [data-slot="anomaly-alt"] {
+      color: rgba(255, 255, 255, 0.39);
+      font-family: "JetBrains Mono Nerd Font";
+      font-size: 16px;
+      font-style: normal;
+      font-weight: 400;
+      line-height: normal;
+      text-decoration: none;
+      margin-bottom: 24px;
+
+      a {
+        color: rgba(255, 255, 255, 0.39);
+        font-family: "JetBrains Mono Nerd Font";
+        font-size: 16px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: normal;
+        text-decoration: none;
+      }
+
+      @media (min-width: 768px) {
+        display: none;
+      }
+    }
+  }
+}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 235 - 0
packages/console/app/src/routes/black.tsx


+ 62 - 0
packages/console/app/src/routes/black/common.tsx

@@ -0,0 +1,62 @@
+import { Match, Switch } from "solid-js"
+
+export const plans = [
+  { id: "20", multiplier: null },
+  { id: "100", multiplier: "5x more usage than Black 20" },
+  { id: "200", multiplier: "20x more usage than Black 20" },
+] as const
+
+export type PlanID = (typeof plans)[number]["id"]
+export type Plan = (typeof plans)[number]
+
+export function PlanIcon(props: { plan: string }) {
+  return (
+    <Switch>
+      <Match when={props.plan === "20"}>
+        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <title>Black 20 plan</title>
+          <rect x="0.5" y="0.5" width="23" height="23" stroke="currentColor" />
+        </svg>
+      </Match>
+      <Match when={props.plan === "100"}>
+        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <title>Black 100 plan</title>
+          <rect x="0.5" y="0.5" width="9" height="9" stroke="currentColor" />
+          <rect x="0.5" y="14.5" width="9" height="9" stroke="currentColor" />
+          <rect x="14.5" y="0.5" width="9" height="9" stroke="currentColor" />
+          <rect x="14.5" y="14.5" width="9" height="9" stroke="currentColor" />
+        </svg>
+      </Match>
+      <Match when={props.plan === "200"}>
+        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <title>Black 200 plan</title>
+          <rect x="0.5" y="0.5" width="3" height="3" stroke="currentColor" />
+          <rect x="0.5" y="5.5" width="3" height="3" stroke="currentColor" />
+          <rect x="0.5" y="10.5" width="3" height="3" stroke="currentColor" />
+          <rect x="0.5" y="15.5" width="3" height="3" stroke="currentColor" />
+          <rect x="0.5" y="20.5" width="3" height="3" stroke="currentColor" />
+          <rect x="5.5" y="0.5" width="3" height="3" stroke="currentColor" />
+          <rect x="5.5" y="5.5" width="3" height="3" stroke="currentColor" />
+          <rect x="5.5" y="10.5" width="3" height="3" stroke="currentColor" />
+          <rect x="5.5" y="15.5" width="3" height="3" stroke="currentColor" />
+          <rect x="5.5" y="20.5" width="3" height="3" stroke="currentColor" />
+          <rect x="10.5" y="0.5" width="3" height="3" stroke="currentColor" />
+          <rect x="10.5" y="5.5" width="3" height="3" stroke="currentColor" />
+          <rect x="10.5" y="10.5" width="3" height="3" stroke="currentColor" />
+          <rect x="10.5" y="15.5" width="3" height="3" stroke="currentColor" />
+          <rect x="10.5" y="20.5" width="3" height="3" stroke="currentColor" />
+          <rect x="15.5" y="0.5" width="3" height="3" stroke="currentColor" />
+          <rect x="15.5" y="5.5" width="3" height="3" stroke="currentColor" />
+          <rect x="15.5" y="10.5" width="3" height="3" stroke="currentColor" />
+          <rect x="15.5" y="15.5" width="3" height="3" stroke="currentColor" />
+          <rect x="15.5" y="20.5" width="3" height="3" stroke="currentColor" />
+          <rect x="20.5" y="0.5" width="3" height="3" stroke="currentColor" />
+          <rect x="20.5" y="5.5" width="3" height="3" stroke="currentColor" />
+          <rect x="20.5" y="10.5" width="3" height="3" stroke="currentColor" />
+          <rect x="20.5" y="15.5" width="3" height="3" stroke="currentColor" />
+          <rect x="20.5" y="20.5" width="3" height="3" stroke="currentColor" />
+        </svg>
+      </Match>
+    </Switch>
+  )
+}

+ 0 - 221
packages/console/app/src/routes/black/index.css

@@ -1,221 +0,0 @@
-[data-page="black"] {
-  background: #000;
-  min-height: 100vh;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: stretch;
-  font-family: var(--font-mono);
-  color: #fff;
-
-  [data-component="header-gradient"] {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 288px;
-    background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%);
-  }
-
-  [data-component="header"] {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    padding-top: 40px;
-    flex-shrink: 0;
-
-    /* [data-component="header-logo"] { */
-    /* } */
-  }
-
-  [data-component="content"] {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    width: 100%;
-    flex-grow: 1;
-
-    [data-slot="hero-black"] {
-      margin-top: 110px;
-
-      @media (min-width: 768px) {
-        margin-top: 150px;
-      }
-    }
-
-    [data-slot="cta"] {
-      display: flex;
-      flex-direction: column;
-      gap: 32px;
-      align-items: center;
-      text-align: center;
-      margin-top: -18px;
-
-      @media (min-width: 768px) {
-        margin-top: 40px;
-      }
-
-      [data-slot="heading"] {
-        color: rgba(255, 255, 255, 0.92);
-        text-align: center;
-        font-size: 18px;
-        font-style: normal;
-        font-weight: 400;
-        line-height: 160%; /* 28.8px */
-
-        span {
-          display: inline-block;
-        }
-      }
-      [data-slot="subheading"] {
-        color: rgba(255, 255, 255, 0.59);
-        font-size: 15px;
-        font-style: normal;
-        font-weight: 400;
-        line-height: 160%;
-
-        @media (min-width: 768px) {
-          font-size: 18px;
-          line-height: 160%;
-        }
-      }
-      [data-slot="button"] {
-        display: inline-flex;
-        height: 40px;
-        padding: 0 12px;
-        justify-content: center;
-        align-items: center;
-        gap: 8px;
-        border-radius: 4px;
-        background: rgba(255, 255, 255, 0.92);
-        text-decoration: none;
-        color: #000;
-        font-family: "JetBrains Mono Nerd Font";
-        font-size: 16px;
-        font-style: normal;
-        font-weight: 500;
-        line-height: normal;
-
-        &:hover {
-          background: #e0e0e0;
-        }
-
-        &:active {
-          transform: scale(0.98);
-        }
-      }
-      [data-slot="back-soon"] {
-        color: rgba(255, 255, 255, 0.59);
-        text-align: center;
-        font-size: 13px;
-        font-style: normal;
-        font-weight: 400;
-        line-height: 160%; /* 20.8px */
-      }
-      [data-slot="follow-us"] {
-        display: inline-flex;
-        height: 40px;
-        padding: 0 12px;
-        justify-content: center;
-        align-items: center;
-        gap: 8px;
-        border-radius: 4px;
-        border: 1px solid rgba(255, 255, 255, 0.17);
-        color: rgba(255, 255, 255, 0.59);
-        font-family: "JetBrains Mono Nerd Font";
-        font-size: 14px;
-        font-style: normal;
-        font-weight: 400;
-        line-height: normal;
-        text-decoration: none;
-      }
-
-      [data-slot="fine-print"] {
-        color: rgba(255, 255, 255, 0.39);
-        text-align: center;
-        font-size: 13px;
-        font-style: normal;
-        font-weight: 400;
-        line-height: 160%; /* 20.8px */
-      }
-    }
-  }
-
-  [data-component="footer"] {
-    display: flex;
-    flex-direction: column;
-    width: 100%;
-    justify-content: center;
-    align-items: center;
-    gap: 24px;
-    flex-shrink: 0;
-
-    @media (min-width: 768px) {
-      height: 120px;
-    }
-
-    [data-slot="footer-content"] {
-      display: flex;
-      gap: 24px;
-      align-items: center;
-      justify-content: center;
-
-      @media (min-width: 768px) {
-        gap: 40px;
-      }
-
-      span,
-      a {
-        color: rgba(255, 255, 255, 0.39);
-        font-family: "JetBrains Mono Nerd Font";
-        font-size: 16px;
-        font-style: normal;
-        font-weight: 400;
-        line-height: normal;
-        text-decoration: none;
-      }
-
-      [data-slot="github-stars"] {
-        color: rgba(255, 255, 255, 0.25);
-        font-family: "JetBrains Mono Nerd Font";
-        font-size: 16px;
-        font-style: normal;
-        font-weight: 400;
-        line-height: normal;
-      }
-
-      [data-slot="anomaly"] {
-        display: none;
-
-        @media (min-width: 768px) {
-          display: block;
-        }
-      }
-    }
-    [data-slot="anomaly-alt"] {
-      color: rgba(255, 255, 255, 0.39);
-      font-family: "JetBrains Mono Nerd Font";
-      font-size: 16px;
-      font-style: normal;
-      font-weight: 400;
-      line-height: normal;
-      text-decoration: none;
-      margin-bottom: 24px;
-
-      a {
-        color: rgba(255, 255, 255, 0.39);
-        font-family: "JetBrains Mono Nerd Font";
-        font-size: 16px;
-        font-style: normal;
-        font-weight: 400;
-        line-height: normal;
-        text-decoration: none;
-      }
-
-      @media (min-width: 768px) {
-        display: none;
-      }
-    }
-  }
-}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 32 - 97
packages/console/app/src/routes/black/index.tsx


+ 451 - 0
packages/console/app/src/routes/black/subscribe/[plan].tsx

@@ -0,0 +1,451 @@
+import { A, createAsync, query, redirect, useParams } from "@solidjs/router"
+import { Title } from "@solidjs/meta"
+import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js"
+import { type Stripe, type PaymentMethod, loadStripe } from "@stripe/stripe-js"
+import { Elements, PaymentElement, useStripe, useElements, AddressElement } from "solid-stripe"
+import { PlanID, plans } from "../common"
+import { getActor, useAuthSession } from "~/context/auth"
+import { withActor } from "~/context/auth.withActor"
+import { Actor } from "@opencode-ai/console-core/actor.js"
+import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
+import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
+import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
+import { createList } from "solid-list"
+import { Modal } from "~/component/modal"
+import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
+import { Billing } from "@opencode-ai/console-core/billing.js"
+
+const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record<PlanID, (typeof plans)[number]>
+const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!)
+
+const getWorkspaces = query(async (plan: string) => {
+  "use server"
+  const actor = await getActor()
+  if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe/" + plan)
+  return withActor(async () => {
+    return Database.use((tx) =>
+      tx
+        .select({
+          id: WorkspaceTable.id,
+          name: WorkspaceTable.name,
+          slug: WorkspaceTable.slug,
+          billing: {
+            customerID: BillingTable.customerID,
+            paymentMethodID: BillingTable.paymentMethodID,
+            paymentMethodType: BillingTable.paymentMethodType,
+            paymentMethodLast4: BillingTable.paymentMethodLast4,
+            subscriptionID: BillingTable.subscriptionID,
+            timeSubscriptionBooked: BillingTable.timeSubscriptionBooked,
+          },
+        })
+        .from(UserTable)
+        .innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
+        .innerJoin(BillingTable, eq(WorkspaceTable.id, BillingTable.workspaceID))
+        .where(
+          and(
+            eq(UserTable.accountID, Actor.account()),
+            isNull(WorkspaceTable.timeDeleted),
+            isNull(UserTable.timeDeleted),
+          ),
+        ),
+    )
+  })
+}, "black.subscribe.workspaces")
+
+const createSetupIntent = async (input: { plan: string; workspaceID: string }) => {
+  "use server"
+  const { plan, workspaceID } = input
+
+  if (!plan || !["20", "100", "200"].includes(plan)) return { error: "Invalid plan" }
+  if (!workspaceID) return { error: "Workspace ID is required" }
+
+  return withActor(async () => {
+    const session = await useAuthSession()
+    const account = session.data.account?.[session.data.current ?? ""]
+    const email = account?.email
+
+    const customer = await Database.use((tx) =>
+      tx
+        .select({
+          customerID: BillingTable.customerID,
+          subscriptionID: BillingTable.subscriptionID,
+        })
+        .from(BillingTable)
+        .where(eq(BillingTable.workspaceID, workspaceID))
+        .then((rows) => rows[0]),
+    )
+    if (customer?.subscriptionID) {
+      return { error: "This workspace already has a subscription" }
+    }
+
+    let customerID = customer?.customerID
+    if (!customerID) {
+      const customer = await Billing.stripe().customers.create({
+        email,
+        metadata: {
+          workspaceID,
+        },
+      })
+      customerID = customer.id
+      await Database.use((tx) =>
+        tx
+          .update(BillingTable)
+          .set({
+            customerID,
+          })
+          .where(eq(BillingTable.workspaceID, workspaceID)),
+      )
+    }
+
+    const intent = await Billing.stripe().setupIntents.create({
+      customer: customerID,
+      payment_method_types: ["card"],
+      metadata: {
+        workspaceID,
+      },
+    })
+
+    return { clientSecret: intent.client_secret ?? undefined }
+  }, workspaceID)
+}
+
+const bookSubscription = async (input: {
+  workspaceID: string
+  plan: PlanID
+  paymentMethodID: string
+  paymentMethodType: string
+  paymentMethodLast4?: string
+}) => {
+  "use server"
+  return withActor(
+    () =>
+      Database.use((tx) =>
+        tx
+          .update(BillingTable)
+          .set({
+            paymentMethodID: input.paymentMethodID,
+            paymentMethodType: input.paymentMethodType,
+            paymentMethodLast4: input.paymentMethodLast4,
+            subscriptionPlan: input.plan,
+            timeSubscriptionBooked: new Date(),
+          })
+          .where(eq(BillingTable.workspaceID, input.workspaceID)),
+      ),
+    input.workspaceID,
+  )
+}
+
+interface SuccessData {
+  plan: string
+  paymentMethodType: string
+  paymentMethodLast4?: string
+}
+
+function Failure(props: { message: string }) {
+  return (
+    <div data-slot="failure">
+      <p data-slot="message">Uh oh! {props.message}</p>
+    </div>
+  )
+}
+
+function Success(props: SuccessData) {
+  return (
+    <div data-slot="success">
+      <p data-slot="title">You're on the OpenCode Black waitlist</p>
+      <dl data-slot="details">
+        <div>
+          <dt>Subscription plan</dt>
+          <dd>OpenCode Black {props.plan}</dd>
+        </div>
+        <div>
+          <dt>Amount</dt>
+          <dd>${props.plan} per month</dd>
+        </div>
+        <div>
+          <dt>Payment method</dt>
+          <dd>
+            <Show when={props.paymentMethodLast4} fallback={<span>{props.paymentMethodType}</span>}>
+              <span>
+                {props.paymentMethodType} - {props.paymentMethodLast4}
+              </span>
+            </Show>
+          </dd>
+        </div>
+        <div>
+          <dt>Date joined</dt>
+          <dd>{new Date().toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}</dd>
+        </div>
+      </dl>
+      <p data-slot="charge-notice">Your card will be charged when your subscription is activated</p>
+    </div>
+  )
+}
+
+function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data: SuccessData) => void }) {
+  const stripe = useStripe()
+  const elements = useElements()
+  const [error, setError] = createSignal<string | undefined>(undefined)
+  const [loading, setLoading] = createSignal(false)
+
+  const handleSubmit = async (e: Event) => {
+    e.preventDefault()
+    if (!stripe() || !elements()) return
+
+    setLoading(true)
+    setError(undefined)
+
+    const result = await elements()!.submit()
+    if (result.error) {
+      setError(result.error.message ?? "An error occurred")
+      setLoading(false)
+      return
+    }
+
+    const { error: confirmError, setupIntent } = await stripe()!.confirmSetup({
+      elements: elements()!,
+      confirmParams: {
+        expand: ["payment_method"],
+        payment_method_data: {
+          allow_redisplay: "always",
+        },
+      },
+      redirect: "if_required",
+    })
+
+    if (confirmError) {
+      setError(confirmError.message ?? "An error occurred")
+      setLoading(false)
+      return
+    }
+
+    // TODO
+    console.log(setupIntent)
+    if (setupIntent?.status === "succeeded") {
+      const pm = setupIntent.payment_method as PaymentMethod
+
+      await bookSubscription({
+        workspaceID: props.workspaceID,
+        plan: props.plan,
+        paymentMethodID: pm.id,
+        paymentMethodType: pm.type,
+        paymentMethodLast4: pm.card?.last4,
+      })
+
+      props.onSuccess({
+        plan: props.plan,
+        paymentMethodType: pm.type,
+        paymentMethodLast4: pm.card?.last4,
+      })
+    }
+
+    setLoading(false)
+  }
+
+  return (
+    <form onSubmit={handleSubmit} data-slot="checkout-form">
+      <PaymentElement />
+      <AddressElement options={{ mode: "billing" }} />
+      <Show when={error()}>
+        <p data-slot="error">{error()}</p>
+      </Show>
+      <button type="submit" disabled={loading() || !stripe() || !elements()} data-slot="submit-button">
+        {loading() ? "Processing..." : `Subscribe $${props.plan}`}
+      </button>
+      <p data-slot="charge-notice">You will only be charged when your subscription is activated</p>
+    </form>
+  )
+}
+
+export default function BlackSubscribe() {
+  const params = useParams()
+  const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
+  const plan = planData.id
+
+  const workspaces = createAsync(() => getWorkspaces(plan))
+  const [selectedWorkspace, setSelectedWorkspace] = createSignal<string | undefined>(undefined)
+  const [success, setSuccess] = createSignal<SuccessData | undefined>(undefined)
+  const [failure, setFailure] = createSignal<string | undefined>(undefined)
+  const [clientSecret, setClientSecret] = createSignal<string | undefined>(undefined)
+  const [stripe, setStripe] = createSignal<Stripe | undefined>(undefined)
+
+  // Resolve stripe promise once
+  createEffect(() => {
+    stripePromise.then((s) => {
+      if (s) setStripe(s)
+    })
+  })
+
+  // Auto-select if only one workspace
+  createEffect(() => {
+    const ws = workspaces()
+    if (ws?.length === 1 && !selectedWorkspace()) {
+      setSelectedWorkspace(ws[0].id)
+    }
+  })
+
+  // Fetch setup intent when workspace is selected (unless workspace already has payment method)
+  createEffect(async () => {
+    const id = selectedWorkspace()
+    if (!id) return
+
+    const ws = workspaces()?.find((w) => w.id === id)
+    if (ws?.billing?.subscriptionID) {
+      setFailure("This workspace already has a subscription")
+      return
+    }
+    if (ws?.billing?.paymentMethodID) {
+      if (!ws?.billing?.timeSubscriptionBooked) {
+        await bookSubscription({
+          workspaceID: id,
+          plan: planData.id,
+          paymentMethodID: ws.billing.paymentMethodID!,
+          paymentMethodType: ws.billing.paymentMethodType!,
+          paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
+        })
+      }
+      setSuccess({
+        plan: planData.id,
+        paymentMethodType: ws.billing.paymentMethodType!,
+        paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
+      })
+      return
+    }
+
+    const result = await createSetupIntent({ plan, workspaceID: id })
+    if (result.error) {
+      setFailure(result.error)
+    } else if ("clientSecret" in result) {
+      setClientSecret(result.clientSecret)
+    }
+  })
+
+  // Keyboard navigation for workspace picker
+  const { active, setActive, onKeyDown } = createList({
+    items: () => workspaces()?.map((w) => w.id) ?? [],
+    initialActive: null,
+  })
+
+  const handleSelectWorkspace = (id: string) => {
+    setSelectedWorkspace(id)
+  }
+
+  let listRef: HTMLUListElement | undefined
+
+  // Show workspace picker if multiple workspaces and none selected
+  const showWorkspacePicker = () => {
+    const ws = workspaces()
+    return ws && ws.length > 1 && !selectedWorkspace()
+  }
+
+  return (
+    <>
+      <Title>Subscribe to OpenCode Black</Title>
+      <section data-slot="subscribe-form">
+        <div data-slot="form-card">
+          <Switch>
+            <Match when={success()}>{(data) => <Success {...data()} />}</Match>
+            <Match when={failure()}>{(data) => <Failure message={data()} />}</Match>
+            <Match when={true}>
+              <>
+                <div data-slot="plan-header">
+                  <p data-slot="title">Subscribe to OpenCode Black</p>
+                  <p data-slot="price">
+                    <span data-slot="amount">${planData.id}</span> <span data-slot="period">per month</span>
+                    <Show when={planData.multiplier}>
+                      <span data-slot="multiplier">{planData.multiplier}</span>
+                    </Show>
+                  </p>
+                </div>
+                <div data-slot="divider" />
+                <p data-slot="section-title">Payment method</p>
+
+                <Show
+                  when={clientSecret() && selectedWorkspace() && stripe()}
+                  fallback={
+                    <div data-slot="loading">
+                      <p>{selectedWorkspace() ? "Loading payment form..." : "Select a workspace to continue"}</p>
+                    </div>
+                  }
+                >
+                  <Elements
+                    stripe={stripe()!}
+                    options={{
+                      clientSecret: clientSecret()!,
+                      appearance: {
+                        theme: "night",
+                        variables: {
+                          colorPrimary: "#ffffff",
+                          colorBackground: "#1a1a1a",
+                          colorText: "#ffffff",
+                          colorTextSecondary: "#999999",
+                          colorDanger: "#ff6b6b",
+                          fontFamily: "JetBrains Mono, monospace",
+                          borderRadius: "4px",
+                          spacingUnit: "4px",
+                        },
+                        rules: {
+                          ".Input": {
+                            backgroundColor: "#1a1a1a",
+                            border: "1px solid rgba(255, 255, 255, 0.17)",
+                            color: "#ffffff",
+                          },
+                          ".Input:focus": {
+                            borderColor: "rgba(255, 255, 255, 0.35)",
+                            boxShadow: "none",
+                          },
+                          ".Label": {
+                            color: "rgba(255, 255, 255, 0.59)",
+                            fontSize: "14px",
+                            marginBottom: "8px",
+                          },
+                        },
+                      },
+                    }}
+                  >
+                    <IntentForm plan={plan} workspaceID={selectedWorkspace()!} onSuccess={setSuccess} />
+                  </Elements>
+                </Show>
+              </>
+            </Match>
+          </Switch>
+        </div>
+
+        {/* Workspace picker modal */}
+        <Modal open={showWorkspacePicker() ?? false} onClose={() => {}} title="Select a workspace for this plan">
+          <div data-slot="workspace-picker">
+            <ul
+              ref={listRef}
+              data-slot="workspace-list"
+              tabIndex={0}
+              onKeyDown={(e) => {
+                if (e.key === "Enter" && active()) {
+                  handleSelectWorkspace(active()!)
+                } else {
+                  onKeyDown(e)
+                }
+              }}
+            >
+              <For each={workspaces()}>
+                {(workspace) => (
+                  <li
+                    data-slot="workspace-item"
+                    data-active={active() === workspace.id}
+                    onMouseEnter={() => setActive(workspace.id)}
+                    onClick={() => handleSelectWorkspace(workspace.id)}
+                  >
+                    <span data-slot="selected-icon">[*]</span>
+                    <span>{workspace.name || workspace.slug}</span>
+                  </li>
+                )}
+              </For>
+            </ul>
+          </div>
+        </Modal>
+        <p data-slot="fine-print">
+          Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
+        </p>
+      </section>
+    </>
+  )
+}

+ 477 - 0
packages/console/app/src/routes/changelog/index.css

@@ -0,0 +1,477 @@
+::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="changelog"] {
+  --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-border: hsl(30, 2%, 81%);
+  --color-border-weak: hsl(0, 1%, 85%);
+
+  --color-icon: hsl(0, 1%, 55%);
+
+  background: var(--color-background);
+  font-family: var(--font-mono);
+  color: var(--color-text);
+  padding-bottom: 5rem;
+
+  @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-border: hsl(0, 3%, 28%);
+    --color-border-weak: hsl(0, 4%, 23%);
+
+    --color-icon: hsl(10, 3%, 43%);
+  }
+
+  /* Header styles - copied from download */
+  [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;
+    margin-top: 4rem;
+
+    @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;
+    }
+  }
+
+  /* Changelog Hero */
+  [data-component="changelog-hero"] {
+    margin-bottom: 4rem;
+    padding-bottom: 2rem;
+    border-bottom: 1px solid var(--color-border-weak);
+
+    @media (max-width: 50rem) {
+      margin-bottom: 2rem;
+      padding-bottom: 1.5rem;
+    }
+
+    h1 {
+      font-size: 1.5rem;
+      font-weight: 700;
+      color: var(--color-text-strong);
+      margin-bottom: 8px;
+    }
+
+    p {
+      color: var(--color-text);
+    }
+  }
+
+  /* Releases */
+  [data-component="releases"] {
+    display: flex;
+    flex-direction: column;
+    gap: 0;
+  }
+
+  [data-component="release"] {
+    display: grid;
+    grid-template-columns: 180px 1fr;
+    gap: 3rem;
+    padding: 2rem 0;
+    border-bottom: 1px solid var(--color-border-weak);
+
+    @media (max-width: 50rem) {
+      grid-template-columns: 1fr;
+      gap: 1rem;
+    }
+
+    &:first-child {
+      padding-top: 0;
+    }
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    header {
+      display: flex;
+      flex-direction: column;
+      gap: 4px;
+
+      @media (max-width: 50rem) {
+        flex-direction: row;
+        align-items: center;
+        gap: 12px;
+      }
+
+      [data-slot="version"] {
+        a {
+          font-weight: 600;
+          color: var(--color-text-strong);
+          text-decoration: none;
+
+          &:hover {
+            text-decoration: underline;
+            text-underline-offset: 2px;
+            text-decoration-thickness: 1px;
+          }
+        }
+      }
+
+      time {
+        color: var(--color-text-weak);
+        font-size: 14px;
+      }
+    }
+
+    [data-slot="content"] {
+      display: flex;
+      flex-direction: column;
+      gap: 1.5rem;
+    }
+
+    [data-component="section"] {
+      h3 {
+        font-size: 14px;
+        font-weight: 600;
+        color: var(--color-text-strong);
+        margin-bottom: 8px;
+      }
+
+      ul {
+        list-style: none;
+        padding: 0;
+        margin: 0;
+        display: flex;
+        flex-direction: column;
+        gap: 6px;
+
+        li {
+          color: var(--color-text);
+          line-height: 1.5;
+          padding-left: 16px;
+          position: relative;
+
+          &::before {
+            content: "-";
+            position: absolute;
+            left: 0;
+            color: var(--color-text-weak);
+          }
+
+          [data-slot="author"] {
+            color: var(--color-text-weak);
+            font-size: 13px;
+            margin-left: 4px;
+            text-decoration: none;
+
+            &:hover {
+              text-decoration: underline;
+              text-underline-offset: 2px;
+              text-decoration-thickness: 1px;
+            }
+          }
+        }
+      }
+    }
+
+    [data-component="contributors"] {
+      font-size: 13px;
+      color: var(--color-text-weak);
+      padding-top: 0.5rem;
+
+      span {
+        color: var(--color-text-weak);
+      }
+
+      a {
+        color: var(--color-text);
+        text-decoration: none;
+
+        &:hover {
+          text-decoration: underline;
+          text-underline-offset: 2px;
+          text-decoration-thickness: 1px;
+        }
+      }
+    }
+  }
+
+  a {
+    color: var(--color-text-strong);
+    text-decoration: underline;
+    text-underline-offset: 2px;
+    text-decoration-thickness: 1px;
+
+    &:hover {
+      text-decoration-thickness: 2px;
+    }
+  }
+}

+ 147 - 0
packages/console/app/src/routes/changelog/index.tsx

@@ -0,0 +1,147 @@
+import "./index.css"
+import { Title, Meta, Link } from "@solidjs/meta"
+import { createAsync, query } from "@solidjs/router"
+import { Header } from "~/component/header"
+import { Footer } from "~/component/footer"
+import { Legal } from "~/component/legal"
+import { config } from "~/config"
+import { For, Show } from "solid-js"
+
+type Release = {
+  tag_name: string
+  name: string
+  body: string
+  published_at: string
+  html_url: string
+}
+
+const getReleases = query(async () => {
+  "use server"
+  const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", {
+    headers: {
+      Accept: "application/vnd.github.v3+json",
+      "User-Agent": "OpenCode-Console",
+    },
+    cf: {
+      cacheTtl: 60 * 5,
+      cacheEverything: true,
+    },
+  } as any)
+  if (!response.ok) return []
+  return response.json() as Promise<Release[]>
+}, "releases.get")
+
+function formatDate(dateString: string) {
+  const date = new Date(dateString)
+  return date.toLocaleDateString("en-US", {
+    year: "numeric",
+    month: "short",
+    day: "numeric",
+  })
+}
+
+function parseMarkdown(body: string) {
+  const lines = body.split("\n")
+  const sections: { title: string; items: string[] }[] = []
+  let current: { title: string; items: string[] } | null = null
+  let skip = false
+
+  for (const line of lines) {
+    if (line.startsWith("## ")) {
+      if (current) sections.push(current)
+      const title = line.slice(3).trim()
+      current = { title, items: [] }
+      skip = false
+    } else if (line.startsWith("**Thank you")) {
+      skip = true
+    } else if (line.startsWith("- ") && !skip) {
+      current?.items.push(line.slice(2).trim())
+    }
+  }
+  if (current) sections.push(current)
+
+  return { sections }
+}
+
+function ReleaseItem(props: { item: string }) {
+  const parts = () => {
+    const match = props.item.match(/^(.+?)(\s*\(@([\w-]+)\))?$/)
+    if (match) {
+      return {
+        text: match[1],
+        username: match[3],
+      }
+    }
+    return { text: props.item, username: undefined }
+  }
+
+  return (
+    <li>
+      <span>{parts().text}</span>
+      <Show when={parts().username}>
+        <a data-slot="author" href={`https://github.com/${parts().username}`} target="_blank" rel="noopener noreferrer">
+          (@{parts().username})
+        </a>
+      </Show>
+    </li>
+  )
+}
+
+export default function Changelog() {
+  const releases = createAsync(() => getReleases())
+
+  return (
+    <main data-page="changelog">
+      <Title>OpenCode | Changelog</Title>
+      <Link rel="canonical" href={`${config.baseUrl}/changelog`} />
+      <Meta name="description" content="OpenCode release notes and changelog" />
+
+      <div data-component="container">
+        <Header hideGetStarted />
+
+        <div data-component="content">
+          <section data-component="changelog-hero">
+            <h1>Changelog</h1>
+            <p>New updates and improvements to OpenCode</p>
+          </section>
+
+          <section data-component="releases">
+            <For each={releases()}>
+              {(release) => {
+                const parsed = () => parseMarkdown(release.body || "")
+                return (
+                  <article data-component="release">
+                    <header>
+                      <div data-slot="version">
+                        <a href={release.html_url} target="_blank" rel="noopener noreferrer">
+                          {release.tag_name}
+                        </a>
+                      </div>
+                      <time dateTime={release.published_at}>{formatDate(release.published_at)}</time>
+                    </header>
+                    <div data-slot="content">
+                      <For each={parsed().sections}>
+                        {(section) => (
+                          <div data-component="section">
+                            <h3>{section.title}</h3>
+                            <ul>
+                              <For each={section.items}>{(item) => <ReleaseItem item={item} />}</For>
+                            </ul>
+                          </div>
+                        )}
+                      </For>
+                    </div>
+                  </article>
+                )
+              }}
+            </For>
+          </section>
+
+          <Footer />
+        </div>
+      </div>
+
+      <Legal />
+    </main>
+  )
+}

+ 1 - 1
packages/console/app/src/routes/download/[platform].ts

@@ -24,7 +24,7 @@ export async function GET({ params: { platform } }: APIEvent) {
   const resp = await fetch(`https://github.com/anomalyco/opencode/releases/latest/download/${assetName}`, {
     cf: {
       // in case gh releases has rate limits
-      cacheTtl: 60 * 60 * 24,
+      cacheTtl: 60 * 5,
       cacheEverything: true,
     },
   } as any)

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

@@ -34,7 +34,6 @@
   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%);

+ 5 - 3
packages/console/app/src/routes/download/index.tsx

@@ -244,7 +244,8 @@ export default function Download() {
                   Download
                 </a>
               </div>
-              <div data-component="download-row">
+              {/* Disabled temporarily as it doesn't work */}
+              {/*<div data-component="download-row">
                 <div data-component="download-info">
                   <span data-slot="icon">
                     <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -259,7 +260,7 @@ export default function Download() {
                 <a href={getDownloadHref("linux-x64-appimage")} data-component="action-button">
                   Download
                 </a>
-              </div>
+              </div>*/}
             </div>
           </section>
 
@@ -440,7 +441,8 @@ export default function Download() {
             </li>
             <li>
               <Faq question="Can I only use OpenCode in the terminal?">
-                Not anymore! OpenCode is now available as an app for your desktop.
+                Not anymore! OpenCode is now available as an app for your <a href="/download">desktop</a> and{" "}
+                <a href="/docs/cli/#web">web</a>!
               </Faq>
             </li>
             <li>

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

@@ -522,7 +522,7 @@ body {
     [data-slot="content"] {
       display: flex;
       align-items: center;
-      gap: 4px;
+      gap: 1ch;
     }
 
     [data-slot="text"] {

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

@@ -692,7 +692,8 @@ export default function Home() {
               </li>
               <li>
                 <Faq question="Can I only use OpenCode in the terminal?">
-                  Not anymore! OpenCode is now available as an app for your desktop.
+                  Not anymore! OpenCode is now available as an app for your <a href="/download">desktop</a> and{" "}
+                  <a href="/docs/web">web</a>!
                 </Faq>
               </li>
               <li>

+ 1 - 1
packages/console/app/src/routes/user-menu.tsx

@@ -1,6 +1,6 @@
 import { action } from "@solidjs/router"
 import { getRequestEvent } from "solid-js/web"
-import { useAuthSession } from "~/context/auth.session"
+import { useAuthSession } from "~/context/auth"
 import { Dropdown } from "~/component/dropdown"
 import "./user-menu.css"
 

+ 54 - 0
packages/console/app/src/routes/workspace/[id]/billing/black-section.module.css

@@ -5,4 +5,58 @@
     align-items: center;
     gap: var(--space-4);
   }
+
+  [data-slot="usage"] {
+    display: flex;
+    gap: var(--space-6);
+    margin-top: var(--space-4);
+
+    @media (max-width: 40rem) {
+      flex-direction: column;
+      gap: var(--space-4);
+    }
+  }
+
+  [data-slot="usage-item"] {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-2);
+  }
+
+  [data-slot="usage-header"] {
+    display: flex;
+    justify-content: space-between;
+    align-items: baseline;
+  }
+
+  [data-slot="usage-label"] {
+    font-size: var(--font-size-md);
+    font-weight: 500;
+    color: var(--color-text);
+  }
+
+  [data-slot="usage-value"] {
+    font-size: var(--font-size-sm);
+    color: var(--color-text-muted);
+  }
+
+  [data-slot="progress"] {
+    height: 8px;
+    background-color: var(--color-bg-surface);
+    border-radius: var(--border-radius-sm);
+    overflow: hidden;
+  }
+
+  [data-slot="progress-bar"] {
+    height: 100%;
+    background-color: var(--color-accent);
+    border-radius: var(--border-radius-sm);
+    transition: width 0.3s ease;
+  }
+
+  [data-slot="reset-time"] {
+    font-size: var(--font-size-sm);
+    color: var(--color-text-muted);
+  }
 }

+ 76 - 1
packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx

@@ -1,10 +1,58 @@
-import { action, useParams, useAction, useSubmission, json } from "@solidjs/router"
+import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router"
 import { createStore } from "solid-js/store"
+import { Show } from "solid-js"
 import { Billing } from "@opencode-ai/console-core/billing.js"
+import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
+import { SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
+import { Actor } from "@opencode-ai/console-core/actor.js"
+import { Black } from "@opencode-ai/console-core/black.js"
 import { withActor } from "~/context/auth.withActor"
 import { queryBillingInfo } from "../../common"
 import styles from "./black-section.module.css"
 
+const querySubscription = query(async (workspaceID: string) => {
+  "use server"
+  return withActor(async () => {
+    const row = await Database.use((tx) =>
+      tx
+        .select({
+          rollingUsage: SubscriptionTable.rollingUsage,
+          fixedUsage: SubscriptionTable.fixedUsage,
+          timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
+          timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
+        })
+        .from(SubscriptionTable)
+        .where(and(eq(SubscriptionTable.workspaceID, Actor.workspace()), isNull(SubscriptionTable.timeDeleted)))
+        .then((r) => r[0]),
+    )
+    if (!row) return null
+
+    return {
+      rollingUsage: Black.analyzeRollingUsage({
+        usage: row.rollingUsage ?? 0,
+        timeUpdated: row.timeRollingUpdated ?? new Date(),
+      }),
+      weeklyUsage: Black.analyzeWeeklyUsage({
+        usage: row.fixedUsage ?? 0,
+        timeUpdated: row.timeFixedUpdated ?? new Date(),
+      }),
+    }
+  }, workspaceID)
+}, "subscription.get")
+
+function formatResetTime(seconds: number) {
+  const days = Math.floor(seconds / 86400)
+  if (days >= 1) {
+    const hours = Math.floor((seconds % 86400) / 3600)
+    return `${days} ${days === 1 ? "day" : "days"} ${hours} ${hours === 1 ? "hour" : "hours"}`
+  }
+  const hours = Math.floor(seconds / 3600)
+  const minutes = Math.floor((seconds % 3600) / 60)
+  if (hours >= 1) return `${hours} ${hours === 1 ? "hour" : "hours"} ${minutes} ${minutes === 1 ? "minute" : "minutes"}`
+  if (minutes === 0) return "a few seconds"
+  return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`
+}
+
 const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
   "use server"
   return json(
@@ -26,6 +74,7 @@ export function BlackSection() {
   const params = useParams()
   const sessionAction = useAction(createSessionUrl)
   const sessionSubmission = useSubmission(createSessionUrl)
+  const subscription = createAsync(() => querySubscription(params.id!))
   const [store, setStore] = createStore({
     sessionRedirecting: false,
   })
@@ -53,6 +102,32 @@ export function BlackSection() {
           </button>
         </div>
       </div>
+      <Show when={subscription()}>
+        {(sub) => (
+          <div data-slot="usage">
+            <div data-slot="usage-item">
+              <div data-slot="usage-header">
+                <span data-slot="usage-label">5-hour Usage</span>
+                <span data-slot="usage-value">{sub().rollingUsage.usagePercent}%</span>
+              </div>
+              <div data-slot="progress">
+                <div data-slot="progress-bar" style={{ width: `${sub().rollingUsage.usagePercent}%` }} />
+              </div>
+              <span data-slot="reset-time">Resets in {formatResetTime(sub().rollingUsage.resetInSec)}</span>
+            </div>
+            <div data-slot="usage-item">
+              <div data-slot="usage-header">
+                <span data-slot="usage-label">Weekly Usage</span>
+                <span data-slot="usage-value">{sub().weeklyUsage.usagePercent}%</span>
+              </div>
+              <div data-slot="progress">
+                <div data-slot="progress-bar" style={{ width: `${sub().weeklyUsage.usagePercent}%` }} />
+              </div>
+              <span data-slot="reset-time">Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}</span>
+            </div>
+          </div>
+        )}
+      </Show>
     </section>
   )
 }

+ 12 - 2
packages/console/app/src/routes/workspace/common.tsx

@@ -3,7 +3,6 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
 import { action, json, query } from "@solidjs/router"
 import { withActor } from "~/context/auth.withActor"
 import { Billing } from "@opencode-ai/console-core/billing.js"
-import { User } from "@opencode-ai/console-core/user.js"
 import { and, Database, desc, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
 import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
 import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
@@ -96,11 +95,22 @@ export const queryBillingInfo = query(async (workspaceID: string) => {
   return withActor(async () => {
     const billing = await Billing.get()
     return {
-      ...billing,
+      customerID: billing.customerID,
+      paymentMethodID: billing.paymentMethodID,
+      paymentMethodType: billing.paymentMethodType,
+      paymentMethodLast4: billing.paymentMethodLast4,
+      balance: billing.balance,
+      reload: billing.reload,
       reloadAmount: billing.reloadAmount ?? Billing.RELOAD_AMOUNT,
       reloadAmountMin: Billing.RELOAD_AMOUNT_MIN,
       reloadTrigger: billing.reloadTrigger ?? Billing.RELOAD_TRIGGER,
       reloadTriggerMin: Billing.RELOAD_TRIGGER_MIN,
+      monthlyLimit: billing.monthlyLimit,
+      monthlyUsage: billing.monthlyUsage,
+      timeMonthlyUsageUpdated: billing.timeMonthlyUsageUpdated,
+      reloadError: billing.reloadError,
+      timeReloadError: billing.timeReloadError,
+      subscriptionID: billing.subscriptionID,
     }
   }, workspaceID)
 }, "billing.get")

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

@@ -9,7 +9,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js"
 import { Actor } from "@opencode-ai/console-core/actor.js"
 import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
 import { ZenData } from "@opencode-ai/console-core/model.js"
-import { BlackData } from "@opencode-ai/console-core/black.js"
+import { Black, BlackData } from "@opencode-ai/console-core/black.js"
 import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
 import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
 import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
@@ -81,12 +81,13 @@ export async function handler(
     const isTrial = await trialLimiter?.isTrial()
     const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
     await rateLimiter?.check()
-    const stickyTracker = createStickyTracker(modelInfo.stickyProvider ?? false, sessionId)
+    const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
     const stickyProvider = await stickyTracker?.get()
     const authInfo = await authenticate(modelInfo)
 
     const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
       const providerInfo = selectProvider(
+        model,
         zenData,
         authInfo,
         modelInfo,
@@ -101,7 +102,7 @@ export async function handler(
       logger.metric({ provider: providerInfo.id })
 
       const startTimestamp = Date.now()
-      const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream)
+      const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)
       const reqBody = JSON.stringify(
         providerInfo.modifyBody({
           ...createBodyConverter(opts.format, providerInfo.format)(body),
@@ -135,7 +136,7 @@ export async function handler(
         // ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
         res.status !== 404 &&
         // ie. cannot change codex model providers mid-session
-        !modelInfo.stickyProvider &&
+        modelInfo.stickyProvider !== "strict" &&
         modelInfo.fallbackProvider &&
         providerInfo.id !== modelInfo.fallbackProvider
       ) {
@@ -194,17 +195,19 @@ export async function handler(
     // Handle streaming response
     const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
     const usageParser = providerInfo.createUsageParser()
+    const binaryDecoder = providerInfo.createBinaryStreamDecoder()
     const stream = new ReadableStream({
       start(c) {
         const reader = res.body?.getReader()
         const decoder = new TextDecoder()
         const encoder = new TextEncoder()
+
         let buffer = ""
         let responseLength = 0
 
         function pump(): Promise<void> {
           return (
-            reader?.read().then(async ({ done, value }) => {
+            reader?.read().then(async ({ done, value: rawValue }) => {
               if (done) {
                 logger.metric({
                   response_length: responseLength,
@@ -230,6 +233,10 @@ export async function handler(
                   "timestamp.first_byte": now,
                 })
               }
+
+              const value = binaryDecoder ? binaryDecoder(rawValue) : rawValue
+              if (!value) return
+
               responseLength += value.length
               buffer += decoder.decode(value, { stream: true })
               dataDumper?.provideStream(buffer)
@@ -331,6 +338,7 @@ export async function handler(
   }
 
   function selectProvider(
+    reqModel: string,
     zenData: ZenData,
     authInfo: AuthInfo,
     modelInfo: ModelInfo,
@@ -339,7 +347,7 @@ export async function handler(
     retry: RetryOptions,
     stickyProvider: string | undefined,
   ) {
-    const provider = (() => {
+    const modelProvider = (() => {
       if (authInfo?.provider?.credentials) {
         return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
       }
@@ -372,18 +380,19 @@ export async function handler(
       return providers[index || 0]
     })()
 
-    if (!provider) throw new ModelError("No provider available")
-    if (!(provider.id in zenData.providers)) throw new ModelError(`Provider ${provider.id} not supported`)
+    if (!modelProvider) throw new ModelError("No provider available")
+    if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`)
 
     return {
-      ...provider,
-      ...zenData.providers[provider.id],
+      ...modelProvider,
+      ...zenData.providers[modelProvider.id],
       ...(() => {
-        const format = zenData.providers[provider.id].format
-        if (format === "anthropic") return anthropicHelper
-        if (format === "google") return googleHelper
-        if (format === "openai") return openaiHelper
-        return oaCompatHelper
+        const format = zenData.providers[modelProvider.id].format
+        const providerModel = modelProvider.model
+        if (format === "anthropic") return anthropicHelper({ reqModel, providerModel })
+        if (format === "google") return googleHelper({ reqModel, providerModel })
+        if (format === "openai") return openaiHelper({ reqModel, providerModel })
+        return oaCompatHelper({ reqModel, providerModel })
       })(),
     }
   }
@@ -495,27 +504,28 @@ export async function handler(
 
       // Check weekly limit
       if (sub.fixedUsage && sub.timeFixedUpdated) {
-        const week = getWeekBounds(now)
-        if (sub.timeFixedUpdated >= week.start && sub.fixedUsage >= centsToMicroCents(black.fixedLimit * 100)) {
-          const retryAfter = Math.ceil((week.end.getTime() - now.getTime()) / 1000)
+        const result = Black.analyzeWeeklyUsage({
+          usage: sub.fixedUsage,
+          timeUpdated: sub.timeFixedUpdated,
+        })
+        if (result.status === "rate-limited")
           throw new SubscriptionError(
-            `Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
-            retryAfter,
+            `Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
+            result.resetInSec,
           )
-        }
       }
 
       // Check rolling limit
       if (sub.rollingUsage && sub.timeRollingUpdated) {
-        const rollingWindowMs = black.rollingWindow * 3600 * 1000
-        const windowStart = new Date(now.getTime() - rollingWindowMs)
-        if (sub.timeRollingUpdated >= windowStart && sub.rollingUsage >= centsToMicroCents(black.rollingLimit * 100)) {
-          const retryAfter = Math.ceil((sub.timeRollingUpdated.getTime() + rollingWindowMs - now.getTime()) / 1000)
+        const result = Black.analyzeRollingUsage({
+          usage: sub.rollingUsage,
+          timeUpdated: sub.timeRollingUpdated,
+        })
+        if (result.status === "rate-limited")
           throw new SubscriptionError(
-            `Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
-            retryAfter,
+            `Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
+            result.resetInSec,
           )
-        }
       }
 
       return

+ 161 - 56
packages/console/app/src/routes/zen/util/provider/anthropic.ts

@@ -1,4 +1,6 @@
+import { EventStreamCodec } from "@smithy/eventstream-codec"
 import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider"
+import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
 
 type Usage = {
   cache_creation?: {
@@ -14,65 +16,168 @@ type Usage = {
   }
 }
 
-export const anthropicHelper = {
-  format: "anthropic",
-  modifyUrl: (providerApi: string) => providerApi + "/messages",
-  modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
-    headers.set("x-api-key", apiKey)
-    headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
-    if (body.model.startsWith("claude-sonnet-")) {
-      headers.set("anthropic-beta", "context-1m-2025-08-07")
-    }
-  },
-  modifyBody: (body: Record<string, any>) => {
-    return {
+export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => {
+  const isBedrockModelArn = providerModel.startsWith("arn:aws:bedrock:")
+  const isBedrockModelID = providerModel.startsWith("global.anthropic.")
+  const isBedrock = isBedrockModelArn || isBedrockModelID
+  const isSonnet = reqModel.includes("sonnet")
+  return {
+    format: "anthropic",
+    modifyUrl: (providerApi: string, isStream?: boolean) =>
+      isBedrock
+        ? `${providerApi}/model/${isBedrockModelArn ? encodeURIComponent(providerModel) : providerModel}/${isStream ? "invoke-with-response-stream" : "invoke"}`
+        : providerApi + "/messages",
+    modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
+      if (isBedrock) {
+        headers.set("Authorization", `Bearer ${apiKey}`)
+      } else {
+        headers.set("x-api-key", apiKey)
+        headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
+        if (body.model.startsWith("claude-sonnet-")) {
+          headers.set("anthropic-beta", "context-1m-2025-08-07")
+        }
+      }
+    },
+    modifyBody: (body: Record<string, any>) => ({
       ...body,
-      service_tier: "standard_only",
-    }
-  },
-  streamSeparator: "\n\n",
-  createUsageParser: () => {
-    let usage: Usage
-
-    return {
-      parse: (chunk: string) => {
-        const data = chunk.split("\n")[1]
-        if (!data.startsWith("data: ")) return
-
-        let json
-        try {
-          json = JSON.parse(data.slice(6))
-        } catch (e) {
-          return
+      ...(isBedrock
+        ? {
+            anthropic_version: "bedrock-2023-05-31",
+            anthropic_beta: isSonnet ? "context-1m-2025-08-07" : undefined,
+            model: undefined,
+            stream: undefined,
+          }
+        : {
+            service_tier: "standard_only",
+          }),
+    }),
+    createBinaryStreamDecoder: () => {
+      if (!isBedrock) return undefined
+
+      const decoder = new TextDecoder()
+      const encoder = new TextEncoder()
+      const codec = new EventStreamCodec(toUtf8, fromUtf8)
+      let buffer = new Uint8Array(0)
+      return (value: Uint8Array) => {
+        const newBuffer = new Uint8Array(buffer.length + value.length)
+        newBuffer.set(buffer)
+        newBuffer.set(value, buffer.length)
+        buffer = newBuffer
+
+        const messages = []
+
+        while (buffer.length >= 4) {
+          // first 4 bytes are the total length (big-endian)
+          const totalLength = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint32(0, false)
+
+          // wait for more chunks
+          if (buffer.length < totalLength) break
+
+          try {
+            const subView = buffer.subarray(0, totalLength)
+            const decoded = codec.decode(subView)
+            buffer = buffer.slice(totalLength)
+
+            /* Example of Bedrock data
+      ```
+        {
+          bytes: 'eyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtb3B1cy00LTUtMjAyNTExMDEiLCJpZCI6Im1zZ19iZHJrXzAxMjVGdHRGb2lkNGlwWmZ4SzZMbktxeCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo0LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjEsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMTk2MywiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MSwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjF9fX0=',
+          p: '...'
         }
-
-        const usageUpdate = json.usage ?? json.message?.usage
-        if (!usageUpdate) return
-        usage = {
-          ...usage,
-          ...usageUpdate,
-          cache_creation: {
-            ...usage?.cache_creation,
-            ...usageUpdate.cache_creation,
-          },
-          server_tool_use: {
-            ...usage?.server_tool_use,
-            ...usageUpdate.server_tool_use,
-          },
+      ```
+
+      Decoded bytes
+      ```
+        {
+          type: 'message_start',
+          message: {
+            model: 'claude-opus-4-5-20251101',
+            id: 'msg_bdrk_0125FttFoid4ipZfxK6LnKqx',
+            type: 'message',
+            role: 'assistant',
+            content: [],
+            stop_reason: null,
+            stop_sequence: null,
+            usage: {
+              input_tokens: 4,
+              cache_creation_input_tokens: 1,
+              cache_read_input_tokens: 11963,
+              cache_creation: [Object],
+              output_tokens: 1
+            }
+          }
         }
-      },
-      retrieve: () => usage,
-    }
-  },
-  normalizeUsage: (usage: Usage) => ({
-    inputTokens: usage.input_tokens ?? 0,
-    outputTokens: usage.output_tokens ?? 0,
-    reasoningTokens: undefined,
-    cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
-    cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
-    cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
-  }),
-} satisfies ProviderHelper
+      ```
+      */
+
+            /* Example of Anthropic data
+      ```
+        event: message_delta
+        data: {"type":"message_start","message":{"model":"claude-opus-4-5-20251101","id":"msg_01ETvwVWSKULxzPdkQ1xAnk2","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11543,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":11543,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}}}
+      ```
+      */
+            if (decoded.headers[":message-type"]?.value === "event") {
+              const data = decoder.decode(decoded.body, { stream: true })
+
+              const parsedDataResult = JSON.parse(data)
+              delete parsedDataResult.p
+              const bytes = atob(parsedDataResult.bytes)
+              const eventName = JSON.parse(bytes).type
+              messages.push([`event: ${eventName}`, "\n", `data: ${bytes}`, "\n\n"].join(""))
+            }
+          } catch (e) {
+            console.log("@@@EE@@@")
+            console.log(e)
+            break
+          }
+        }
+        return encoder.encode(messages.join(""))
+      }
+    },
+    streamSeparator: "\n\n",
+    createUsageParser: () => {
+      let usage: Usage
+
+      return {
+        parse: (chunk: string) => {
+          const data = chunk.split("\n")[1]
+          if (!data.startsWith("data: ")) return
+
+          let json
+          try {
+            json = JSON.parse(data.slice(6))
+          } catch (e) {
+            return
+          }
+
+          const usageUpdate = json.usage ?? json.message?.usage
+          if (!usageUpdate) return
+          usage = {
+            ...usage,
+            ...usageUpdate,
+            cache_creation: {
+              ...usage?.cache_creation,
+              ...usageUpdate.cache_creation,
+            },
+            server_tool_use: {
+              ...usage?.server_tool_use,
+              ...usageUpdate.server_tool_use,
+            },
+          }
+        },
+        retrieve: () => usage,
+      }
+    },
+    normalizeUsage: (usage: Usage) => ({
+      inputTokens: usage.input_tokens ?? 0,
+      outputTokens: usage.output_tokens ?? 0,
+      reasoningTokens: undefined,
+      cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
+      cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
+      cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
+    }),
+  }
+}
 
 export function fromAnthropicRequest(body: any): CommonRequest {
   if (!body || typeof body !== "object") return body

+ 5 - 4
packages/console/app/src/routes/zen/util/provider/google.ts

@@ -26,16 +26,17 @@ type Usage = {
   thoughtsTokenCount?: number
 }
 
-export const googleHelper = {
+export const googleHelper: ProviderHelper = ({ providerModel }) => ({
   format: "google",
-  modifyUrl: (providerApi: string, model?: string, isStream?: boolean) =>
-    `${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
+  modifyUrl: (providerApi: string, isStream?: boolean) =>
+    `${providerApi}/models/${providerModel}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
   modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
     headers.set("x-goog-api-key", apiKey)
   },
   modifyBody: (body: Record<string, any>) => {
     return body
   },
+  createBinaryStreamDecoder: () => undefined,
   streamSeparator: "\r\n\r\n",
   createUsageParser: () => {
     let usage: Usage
@@ -71,4 +72,4 @@ export const googleHelper = {
       cacheWrite1hTokens: undefined,
     }
   },
-} satisfies ProviderHelper
+})

+ 3 - 2
packages/console/app/src/routes/zen/util/provider/openai-compatible.ts

@@ -21,7 +21,7 @@ type Usage = {
   }
 }
 
-export const oaCompatHelper = {
+export const oaCompatHelper: ProviderHelper = () => ({
   format: "oa-compat",
   modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
   modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
@@ -33,6 +33,7 @@ export const oaCompatHelper = {
       ...(body.stream ? { stream_options: { include_usage: true } } : {}),
     }
   },
+  createBinaryStreamDecoder: () => undefined,
   streamSeparator: "\n\n",
   createUsageParser: () => {
     let usage: Usage
@@ -68,7 +69,7 @@ export const oaCompatHelper = {
       cacheWrite1hTokens: undefined,
     }
   },
-} satisfies ProviderHelper
+})
 
 export function fromOaCompatibleRequest(body: any): CommonRequest {
   if (!body || typeof body !== "object") return body

+ 3 - 2
packages/console/app/src/routes/zen/util/provider/openai.ts

@@ -12,7 +12,7 @@ type Usage = {
   total_tokens?: number
 }
 
-export const openaiHelper = {
+export const openaiHelper: ProviderHelper = () => ({
   format: "openai",
   modifyUrl: (providerApi: string) => providerApi + "/responses",
   modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
@@ -21,6 +21,7 @@ export const openaiHelper = {
   modifyBody: (body: Record<string, any>) => {
     return body
   },
+  createBinaryStreamDecoder: () => undefined,
   streamSeparator: "\n\n",
   createUsageParser: () => {
     let usage: Usage
@@ -58,7 +59,7 @@ export const openaiHelper = {
       cacheWrite1hTokens: undefined,
     }
   },
-} satisfies ProviderHelper
+})
 
 export function fromOpenaiRequest(body: any): CommonRequest {
   if (!body || typeof body !== "object") return body

+ 3 - 2
packages/console/app/src/routes/zen/util/provider/provider.ts

@@ -33,11 +33,12 @@ export type UsageInfo = {
   cacheWrite1hTokens?: number
 }
 
-export type ProviderHelper = {
+export type ProviderHelper = (input: { reqModel: string; providerModel: string }) => {
   format: ZenData.Format
-  modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string
+  modifyUrl: (providerApi: string, isStream?: boolean) => string
   modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
   modifyBody: (body: Record<string, any>) => Record<string, any>
+  createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined
   streamSeparator: string
   createUsageParser: () => {
     parse: (chunk: string) => void

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

@@ -1,6 +1,6 @@
 import { Resource } from "@opencode-ai/console-resource"
 
-export function createStickyTracker(stickyProvider: boolean, session: string) {
+export function createStickyTracker(stickyProvider: "strict" | "prefer" | undefined, session: string) {
   if (!stickyProvider) return
   if (!session) return
   const key = `sticky:${session}`

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

@@ -12,7 +12,7 @@
     "allowJs": true,
     "strict": true,
     "noEmit": true,
-    "types": ["vite/client"],
+    "types": ["vite/client", "@webgpu/types"],
     "isolatedModules": true,
     "paths": {
       "~/*": ["./src/*"]

+ 1 - 0
packages/console/core/migrations/0051_jazzy_green_goblin.sql

@@ -0,0 +1 @@
+ALTER TABLE `billing` ADD `time_subscription_booked` timestamp(3);

+ 1 - 0
packages/console/core/migrations/0052_aromatic_agent_zero.sql

@@ -0,0 +1 @@
+ALTER TABLE `billing` ADD `subscription_plan` enum('20','100','200');

+ 1228 - 0
packages/console/core/migrations/meta/0051_snapshot.json

@@ -0,0 +1,1228 @@
+{
+  "version": "5",
+  "dialect": "mysql",
+  "id": "14cbf4c8-55f1-4488-956f-56fb5ccb3a5a",
+  "prevId": "a0d18802-c390-47d4-98f1-d1f83869cf0c",
+  "tables": {
+    "account": {
+      "name": "account",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "account_id_pk": {
+          "name": "account_id_pk",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "auth": {
+      "name": "auth",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "enum('email','github','google')",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "subject": {
+          "name": "subject",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "account_id": {
+          "name": "account_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "provider": {
+          "name": "provider",
+          "columns": ["provider", "subject"],
+          "isUnique": true
+        },
+        "account_id": {
+          "name": "account_id",
+          "columns": ["account_id"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "auth_id_pk": {
+          "name": "auth_id_pk",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "benchmark": {
+      "name": "benchmark",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "agent": {
+          "name": "agent",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "result": {
+          "name": "result",
+          "type": "mediumtext",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "time_created": {
+          "name": "time_created",
+          "columns": ["time_created"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "benchmark_id_pk": {
+          "name": "benchmark_id_pk",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "billing": {
+      "name": "billing",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "customer_id": {
+          "name": "customer_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_method_id": {
+          "name": "payment_method_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_method_type": {
+          "name": "payment_method_type",
+          "type": "varchar(32)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_method_last4": {
+          "name": "payment_method_last4",
+          "type": "varchar(4)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "balance": {
+          "name": "balance",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "monthly_limit": {
+          "name": "monthly_limit",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "monthly_usage": {
+          "name": "monthly_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_monthly_usage_updated": {
+          "name": "time_monthly_usage_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload": {
+          "name": "reload",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload_trigger": {
+          "name": "reload_trigger",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload_amount": {
+          "name": "reload_amount",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload_error": {
+          "name": "reload_error",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_reload_error": {
+          "name": "time_reload_error",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_reload_locked_till": {
+          "name": "time_reload_locked_till",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "subscription_id": {
+          "name": "subscription_id",
+          "type": "varchar(28)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "subscription_coupon_id": {
+          "name": "subscription_coupon_id",
+          "type": "varchar(28)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_subscription_booked": {
+          "name": "time_subscription_booked",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "global_customer_id": {
+          "name": "global_customer_id",
+          "columns": ["customer_id"],
+          "isUnique": true
+        },
+        "global_subscription_id": {
+          "name": "global_subscription_id",
+          "columns": ["subscription_id"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "billing_workspace_id_id_pk": {
+          "name": "billing_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "payment": {
+      "name": "payment",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "customer_id": {
+          "name": "customer_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "invoice_id": {
+          "name": "invoice_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_id": {
+          "name": "payment_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "amount": {
+          "name": "amount",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_refunded": {
+          "name": "time_refunded",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "enrichment": {
+          "name": "enrichment",
+          "type": "json",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "payment_workspace_id_id_pk": {
+          "name": "payment_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "subscription": {
+      "name": "subscription",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "rolling_usage": {
+          "name": "rolling_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "fixed_usage": {
+          "name": "fixed_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_rolling_updated": {
+          "name": "time_rolling_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_fixed_updated": {
+          "name": "time_fixed_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "workspace_user_id": {
+          "name": "workspace_user_id",
+          "columns": ["workspace_id", "user_id"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "subscription_workspace_id_id_pk": {
+          "name": "subscription_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "usage": {
+      "name": "usage",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "reasoning_tokens": {
+          "name": "reasoning_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_read_tokens": {
+          "name": "cache_read_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_write_5m_tokens": {
+          "name": "cache_write_5m_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_write_1h_tokens": {
+          "name": "cache_write_1h_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cost": {
+          "name": "cost",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_id": {
+          "name": "key_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "enrichment": {
+          "name": "enrichment",
+          "type": "json",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "usage_time_created": {
+          "name": "usage_time_created",
+          "columns": ["workspace_id", "time_created"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "usage_workspace_id_id_pk": {
+          "name": "usage_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "ip_rate_limit": {
+      "name": "ip_rate_limit",
+      "columns": {
+        "ip": {
+          "name": "ip",
+          "type": "varchar(45)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "interval": {
+          "name": "interval",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "count": {
+          "name": "count",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "ip_rate_limit_ip_interval_pk": {
+          "name": "ip_rate_limit_ip_interval_pk",
+          "columns": ["ip", "interval"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "ip": {
+      "name": "ip",
+      "columns": {
+        "ip": {
+          "name": "ip",
+          "type": "varchar(45)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "usage": {
+          "name": "usage",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "ip_ip_pk": {
+          "name": "ip_ip_pk",
+          "columns": ["ip"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "key": {
+      "name": "key",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_used": {
+          "name": "time_used",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "global_key": {
+          "name": "global_key",
+          "columns": ["key"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "key_workspace_id_id_pk": {
+          "name": "key_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "model": {
+      "name": "model",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "model_workspace_model": {
+          "name": "model_workspace_model",
+          "columns": ["workspace_id", "model"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "model_workspace_id_id_pk": {
+          "name": "model_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "provider": {
+      "name": "provider",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "credentials": {
+          "name": "credentials",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "workspace_provider": {
+          "name": "workspace_provider",
+          "columns": ["workspace_id", "provider"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "provider_workspace_id_id_pk": {
+          "name": "provider_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "user": {
+      "name": "user",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "account_id": {
+          "name": "account_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "email": {
+          "name": "email",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_seen": {
+          "name": "time_seen",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "color": {
+          "name": "color",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "role": {
+          "name": "role",
+          "type": "enum('admin','member')",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "monthly_limit": {
+          "name": "monthly_limit",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "monthly_usage": {
+          "name": "monthly_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_monthly_usage_updated": {
+          "name": "time_monthly_usage_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "user_account_id": {
+          "name": "user_account_id",
+          "columns": ["workspace_id", "account_id"],
+          "isUnique": true
+        },
+        "user_email": {
+          "name": "user_email",
+          "columns": ["workspace_id", "email"],
+          "isUnique": true
+        },
+        "global_account_id": {
+          "name": "global_account_id",
+          "columns": ["account_id"],
+          "isUnique": false
+        },
+        "global_email": {
+          "name": "global_email",
+          "columns": ["email"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "user_workspace_id_id_pk": {
+          "name": "user_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "workspace": {
+      "name": "workspace",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "slug": {
+          "name": "slug",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "slug": {
+          "name": "slug",
+          "columns": ["slug"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "workspace_id": {
+          "name": "workspace_id",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    }
+  },
+  "views": {},
+  "_meta": {
+    "schemas": {},
+    "tables": {},
+    "columns": {}
+  },
+  "internal": {
+    "tables": {},
+    "indexes": {}
+  }
+}

+ 1235 - 0
packages/console/core/migrations/meta/0052_snapshot.json

@@ -0,0 +1,1235 @@
+{
+  "version": "5",
+  "dialect": "mysql",
+  "id": "00774acd-a1e5-49c0-b296-cacc9506a566",
+  "prevId": "14cbf4c8-55f1-4488-956f-56fb5ccb3a5a",
+  "tables": {
+    "account": {
+      "name": "account",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "account_id_pk": {
+          "name": "account_id_pk",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "auth": {
+      "name": "auth",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "enum('email','github','google')",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "subject": {
+          "name": "subject",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "account_id": {
+          "name": "account_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "provider": {
+          "name": "provider",
+          "columns": ["provider", "subject"],
+          "isUnique": true
+        },
+        "account_id": {
+          "name": "account_id",
+          "columns": ["account_id"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "auth_id_pk": {
+          "name": "auth_id_pk",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "benchmark": {
+      "name": "benchmark",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "agent": {
+          "name": "agent",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "result": {
+          "name": "result",
+          "type": "mediumtext",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "time_created": {
+          "name": "time_created",
+          "columns": ["time_created"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "benchmark_id_pk": {
+          "name": "benchmark_id_pk",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "billing": {
+      "name": "billing",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "customer_id": {
+          "name": "customer_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_method_id": {
+          "name": "payment_method_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_method_type": {
+          "name": "payment_method_type",
+          "type": "varchar(32)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_method_last4": {
+          "name": "payment_method_last4",
+          "type": "varchar(4)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "balance": {
+          "name": "balance",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "monthly_limit": {
+          "name": "monthly_limit",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "monthly_usage": {
+          "name": "monthly_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_monthly_usage_updated": {
+          "name": "time_monthly_usage_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload": {
+          "name": "reload",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload_trigger": {
+          "name": "reload_trigger",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload_amount": {
+          "name": "reload_amount",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload_error": {
+          "name": "reload_error",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_reload_error": {
+          "name": "time_reload_error",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_reload_locked_till": {
+          "name": "time_reload_locked_till",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "subscription_id": {
+          "name": "subscription_id",
+          "type": "varchar(28)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "subscription_coupon_id": {
+          "name": "subscription_coupon_id",
+          "type": "varchar(28)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "subscription_plan": {
+          "name": "subscription_plan",
+          "type": "enum('20','100','200')",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_subscription_booked": {
+          "name": "time_subscription_booked",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "global_customer_id": {
+          "name": "global_customer_id",
+          "columns": ["customer_id"],
+          "isUnique": true
+        },
+        "global_subscription_id": {
+          "name": "global_subscription_id",
+          "columns": ["subscription_id"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "billing_workspace_id_id_pk": {
+          "name": "billing_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "payment": {
+      "name": "payment",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "customer_id": {
+          "name": "customer_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "invoice_id": {
+          "name": "invoice_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_id": {
+          "name": "payment_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "amount": {
+          "name": "amount",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_refunded": {
+          "name": "time_refunded",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "enrichment": {
+          "name": "enrichment",
+          "type": "json",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "payment_workspace_id_id_pk": {
+          "name": "payment_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "subscription": {
+      "name": "subscription",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "rolling_usage": {
+          "name": "rolling_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "fixed_usage": {
+          "name": "fixed_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_rolling_updated": {
+          "name": "time_rolling_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_fixed_updated": {
+          "name": "time_fixed_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "workspace_user_id": {
+          "name": "workspace_user_id",
+          "columns": ["workspace_id", "user_id"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "subscription_workspace_id_id_pk": {
+          "name": "subscription_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "usage": {
+      "name": "usage",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "reasoning_tokens": {
+          "name": "reasoning_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_read_tokens": {
+          "name": "cache_read_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_write_5m_tokens": {
+          "name": "cache_write_5m_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_write_1h_tokens": {
+          "name": "cache_write_1h_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cost": {
+          "name": "cost",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_id": {
+          "name": "key_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "enrichment": {
+          "name": "enrichment",
+          "type": "json",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "usage_time_created": {
+          "name": "usage_time_created",
+          "columns": ["workspace_id", "time_created"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "usage_workspace_id_id_pk": {
+          "name": "usage_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "ip_rate_limit": {
+      "name": "ip_rate_limit",
+      "columns": {
+        "ip": {
+          "name": "ip",
+          "type": "varchar(45)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "interval": {
+          "name": "interval",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "count": {
+          "name": "count",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "ip_rate_limit_ip_interval_pk": {
+          "name": "ip_rate_limit_ip_interval_pk",
+          "columns": ["ip", "interval"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "ip": {
+      "name": "ip",
+      "columns": {
+        "ip": {
+          "name": "ip",
+          "type": "varchar(45)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "usage": {
+          "name": "usage",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "ip_ip_pk": {
+          "name": "ip_ip_pk",
+          "columns": ["ip"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "key": {
+      "name": "key",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_used": {
+          "name": "time_used",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "global_key": {
+          "name": "global_key",
+          "columns": ["key"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "key_workspace_id_id_pk": {
+          "name": "key_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "model": {
+      "name": "model",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "model_workspace_model": {
+          "name": "model_workspace_model",
+          "columns": ["workspace_id", "model"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "model_workspace_id_id_pk": {
+          "name": "model_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "provider": {
+      "name": "provider",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "credentials": {
+          "name": "credentials",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "workspace_provider": {
+          "name": "workspace_provider",
+          "columns": ["workspace_id", "provider"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "provider_workspace_id_id_pk": {
+          "name": "provider_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "user": {
+      "name": "user",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "account_id": {
+          "name": "account_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "email": {
+          "name": "email",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_seen": {
+          "name": "time_seen",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "color": {
+          "name": "color",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "role": {
+          "name": "role",
+          "type": "enum('admin','member')",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "monthly_limit": {
+          "name": "monthly_limit",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "monthly_usage": {
+          "name": "monthly_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_monthly_usage_updated": {
+          "name": "time_monthly_usage_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "user_account_id": {
+          "name": "user_account_id",
+          "columns": ["workspace_id", "account_id"],
+          "isUnique": true
+        },
+        "user_email": {
+          "name": "user_email",
+          "columns": ["workspace_id", "email"],
+          "isUnique": true
+        },
+        "global_account_id": {
+          "name": "global_account_id",
+          "columns": ["account_id"],
+          "isUnique": false
+        },
+        "global_email": {
+          "name": "global_email",
+          "columns": ["email"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "user_workspace_id_id_pk": {
+          "name": "user_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "workspace": {
+      "name": "workspace",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "slug": {
+          "name": "slug",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "slug": {
+          "name": "slug",
+          "columns": ["slug"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "workspace_id": {
+          "name": "workspace_id",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    }
+  },
+  "views": {},
+  "_meta": {
+    "schemas": {},
+    "tables": {},
+    "columns": {}
+  },
+  "internal": {
+    "tables": {},
+    "indexes": {}
+  }
+}

+ 14 - 0
packages/console/core/migrations/meta/_journal.json

@@ -358,6 +358,20 @@
       "when": 1767931290031,
       "tag": "0050_bumpy_mephistopheles",
       "breakpoints": true
+    },
+    {
+      "idx": 51,
+      "version": "5",
+      "when": 1768341152722,
+      "tag": "0051_jazzy_green_goblin",
+      "breakpoints": true
+    },
+    {
+      "idx": 52,
+      "version": "5",
+      "when": 1768343920467,
+      "tag": "0052_aromatic_agent_zero",
+      "breakpoints": true
     }
   ]
 }

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

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/console-core",
-  "version": "1.1.11",
+  "version": "1.1.24",
   "private": true,
   "type": "module",
   "license": "MIT",
@@ -32,6 +32,7 @@
     "promote-models-to-dev": "script/promote-models.ts dev",
     "promote-models-to-prod": "script/promote-models.ts production",
     "pull-models-from-dev": "script/pull-models.ts dev",
+    "pull-models-from-prod": "script/pull-models.ts production",
     "update-black": "script/update-black.ts",
     "promote-black-to-dev": "script/promote-black.ts dev",
     "promote-black-to-prod": "script/promote-black.ts production",

+ 41 - 0
packages/console/core/script/black-cancel-waitlist.ts

@@ -0,0 +1,41 @@
+import { subscribe } from "diagnostics_channel"
+import { Billing } from "../src/billing.js"
+import { and, Database, eq } from "../src/drizzle/index.js"
+import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
+
+const workspaceID = process.argv[2]
+
+if (!workspaceID) {
+  console.error("Usage: bun script/foo.ts <workspaceID>")
+  process.exit(1)
+}
+
+console.log(`Removing from Black waitlist`)
+
+const billing = await Database.use((tx) =>
+  tx
+    .select({
+      subscriptionPlan: BillingTable.subscriptionPlan,
+      timeSubscriptionBooked: BillingTable.timeSubscriptionBooked,
+    })
+    .from(BillingTable)
+    .where(eq(BillingTable.workspaceID, workspaceID))
+    .then((rows) => rows[0]),
+)
+
+if (!billing?.timeSubscriptionBooked) {
+  console.error(`Error: Workspace is not on the waitlist`)
+  process.exit(1)
+}
+
+await Database.use((tx) =>
+  tx
+    .update(BillingTable)
+    .set({
+      subscriptionPlan: null,
+      timeSubscriptionBooked: null,
+    })
+    .where(eq(BillingTable.workspaceID, workspaceID)),
+)
+
+console.log(`Done`)

+ 163 - 0
packages/console/core/script/black-transfer.ts

@@ -0,0 +1,163 @@
+import { Billing } from "../src/billing.js"
+import { and, Database, desc, eq, isNotNull, lt, sql } from "../src/drizzle/index.js"
+import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
+
+const fromWrkID = process.argv[2]
+const toWrkID = process.argv[3]
+
+if (!fromWrkID || !toWrkID) {
+  console.error("Usage: bun foo.ts <fromWrkID> <toWrkID>")
+  process.exit(1)
+}
+
+console.log(`Transferring subscription from ${fromWrkID} to ${toWrkID}`)
+
+// Look up the FROM workspace billing
+const fromBilling = await Database.use((tx) =>
+  tx
+    .select({
+      customerID: BillingTable.customerID,
+      subscriptionID: BillingTable.subscriptionID,
+      subscriptionCouponID: BillingTable.subscriptionCouponID,
+      paymentMethodID: BillingTable.paymentMethodID,
+      paymentMethodType: BillingTable.paymentMethodType,
+      paymentMethodLast4: BillingTable.paymentMethodLast4,
+    })
+    .from(BillingTable)
+    .where(eq(BillingTable.workspaceID, fromWrkID))
+    .then((rows) => rows[0]),
+)
+if (!fromBilling) throw new Error(`Error: FROM workspace has no billing record`)
+if (!fromBilling.customerID) throw new Error(`Error: FROM workspace has no Stripe customer ID`)
+if (!fromBilling.subscriptionID) throw new Error(`Error: FROM workspace has no subscription`)
+
+const fromSubscription = await Database.use((tx) =>
+  tx
+    .select({ userID: SubscriptionTable.userID })
+    .from(SubscriptionTable)
+    .where(eq(SubscriptionTable.workspaceID, fromWrkID))
+    .then((rows) => rows[0]),
+)
+if (!fromSubscription) throw new Error(`Error: FROM workspace has no subscription`)
+
+// Look up the previous customer ID in FROM workspace
+const subscriptionPayment = await Database.use((tx) =>
+  tx
+    .select({
+      customerID: PaymentTable.customerID,
+      timeCreated: PaymentTable.timeCreated,
+    })
+    .from(PaymentTable)
+    .where(and(eq(PaymentTable.workspaceID, fromWrkID), sql`JSON_EXTRACT(enrichment, '$.type') = 'subscription'`))
+    .then((rows) => {
+      if (rows.length > 1) {
+        console.error(`Error: Multiple subscription payments found for workspace ${fromWrkID}`)
+        process.exit(1)
+      }
+      return rows[0]
+    }),
+)
+const fromPrevPayment = await Database.use((tx) =>
+  tx
+    .select({ customerID: PaymentTable.customerID })
+    .from(PaymentTable)
+    .where(
+      and(
+        eq(PaymentTable.workspaceID, fromWrkID),
+        isNotNull(PaymentTable.customerID),
+        lt(PaymentTable.timeCreated, subscriptionPayment.timeCreated),
+      ),
+    )
+    .orderBy(desc(PaymentTable.timeCreated))
+    .limit(1)
+    .then((rows) => rows[0]),
+)
+if (!fromPrevPayment?.customerID) throw new Error(`Error: FROM workspace has no previous Stripe customer to revert to`)
+if (fromPrevPayment.customerID === fromBilling.customerID)
+  throw new Error(`Error: FROM workspace has the same Stripe customer ID as the current one`)
+
+const fromPrevPaymentMethods = await Billing.stripe().customers.listPaymentMethods(fromPrevPayment.customerID, {})
+if (fromPrevPaymentMethods.data.length === 0)
+  throw new Error(`Error: FROM workspace has no previous Stripe payment methods`)
+
+// Look up the TO workspace billing
+const toBilling = await Database.use((tx) =>
+  tx
+    .select({
+      customerID: BillingTable.customerID,
+      subscriptionID: BillingTable.subscriptionID,
+    })
+    .from(BillingTable)
+    .where(eq(BillingTable.workspaceID, toWrkID))
+    .then((rows) => rows[0]),
+)
+if (!toBilling) throw new Error(`Error: TO workspace has no billing record`)
+if (toBilling.subscriptionID) throw new Error(`Error: TO workspace already has a subscription`)
+
+console.log(`FROM:`)
+console.log(`  Old Customer ID: ${fromBilling.customerID}`)
+console.log(`  New Customer ID: ${fromPrevPayment.customerID}`)
+console.log(`TO:`)
+console.log(`  Old Customer ID: ${toBilling.customerID}`)
+console.log(`  New Customer ID: ${fromBilling.customerID}`)
+
+// Clear workspaceID from Stripe customer metadata
+await Billing.stripe().customers.update(fromPrevPayment.customerID, {
+  metadata: {
+    workspaceID: fromWrkID,
+  },
+})
+await Billing.stripe().customers.update(fromBilling.customerID, {
+  metadata: {
+    workspaceID: toWrkID,
+  },
+})
+
+await Database.transaction(async (tx) => {
+  await tx
+    .update(BillingTable)
+    .set({
+      customerID: fromPrevPayment.customerID,
+      subscriptionID: null,
+      subscriptionCouponID: null,
+      paymentMethodID: fromPrevPaymentMethods.data[0].id,
+      paymentMethodLast4: fromPrevPaymentMethods.data[0].card?.last4 ?? null,
+      paymentMethodType: fromPrevPaymentMethods.data[0].type,
+    })
+    .where(eq(BillingTable.workspaceID, fromWrkID))
+
+  await tx
+    .update(BillingTable)
+    .set({
+      customerID: fromBilling.customerID,
+      subscriptionID: fromBilling.subscriptionID,
+      subscriptionCouponID: fromBilling.subscriptionCouponID,
+      paymentMethodID: fromBilling.paymentMethodID,
+      paymentMethodLast4: fromBilling.paymentMethodLast4,
+      paymentMethodType: fromBilling.paymentMethodType,
+    })
+    .where(eq(BillingTable.workspaceID, toWrkID))
+
+  await tx
+    .update(SubscriptionTable)
+    .set({
+      workspaceID: toWrkID,
+      userID: fromSubscription.userID,
+    })
+    .where(eq(SubscriptionTable.workspaceID, fromWrkID))
+
+  await tx
+    .update(PaymentTable)
+    .set({
+      workspaceID: toWrkID,
+    })
+    .where(
+      and(
+        eq(PaymentTable.workspaceID, fromWrkID),
+        sql`JSON_EXTRACT(enrichment, '$.type') = 'subscription'`,
+        eq(PaymentTable.amount, 20000000000),
+      ),
+    )
+})
+
+console.log(`done`)

+ 15 - 0
packages/console/core/script/credit-workspace.ts

@@ -1,4 +1,6 @@
 import { Billing } from "../src/billing.js"
+import { Database, eq } from "../src/drizzle/index.js"
+import { WorkspaceTable } from "../src/schema/workspace.sql.js"
 
 // get input from command line
 const workspaceID = process.argv[2]
@@ -9,6 +11,19 @@ if (!workspaceID || !dollarAmount) {
   process.exit(1)
 }
 
+// check workspace exists
+const workspace = await Database.use((tx) =>
+  tx
+    .select()
+    .from(WorkspaceTable)
+    .where(eq(WorkspaceTable.id, workspaceID))
+    .then((rows) => rows[0]),
+)
+if (!workspace) {
+  console.error("Error: Workspace not found")
+  process.exit(1)
+}
+
 const amountInDollars = parseFloat(dollarAmount)
 if (isNaN(amountInDollars) || amountInDollars <= 0) {
   console.error("Error: dollarAmount must be a positive number")

+ 17 - 2
packages/console/core/script/lookup-user.ts

@@ -41,8 +41,8 @@ if (identifier.startsWith("wrk_")) {
         subscribed: SubscriptionTable.timeCreated,
       })
       .from(UserTable)
-      .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
-      .innerJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
+      .rightJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
+      .leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
       .where(eq(UserTable.accountID, accountID))
       .then((rows) =>
         rows.map((row) => ({
@@ -113,6 +113,13 @@ async function printWorkspace(workspaceID: string) {
       .select({
         balance: BillingTable.balance,
         customerID: BillingTable.customerID,
+        reload: BillingTable.reload,
+        subscription: {
+          id: BillingTable.subscriptionID,
+          couponID: BillingTable.subscriptionCouponID,
+          plan: BillingTable.subscriptionPlan,
+          booked: BillingTable.timeSubscriptionBooked,
+        },
       })
       .from(BillingTable)
       .where(eq(BillingTable.workspaceID, workspace.id))
@@ -121,6 +128,11 @@ async function printWorkspace(workspaceID: string) {
           rows.map((row) => ({
             ...row,
             balance: `$${(row.balance / 100000000).toFixed(2)}`,
+            subscription: row.subscription.id
+              ? `Subscribed ${row.subscription.couponID ? `(coupon: ${row.subscription.couponID}) ` : ""}`
+              : row.subscription.booked
+                ? `Waitlist ${row.subscription.plan} plan`
+                : undefined,
           }))[0],
       ),
   )
@@ -131,6 +143,7 @@ async function printWorkspace(workspaceID: string) {
         amount: PaymentTable.amount,
         paymentID: PaymentTable.paymentID,
         invoiceID: PaymentTable.invoiceID,
+        customerID: PaymentTable.customerID,
         timeCreated: PaymentTable.timeCreated,
         timeRefunded: PaymentTable.timeRefunded,
       })
@@ -149,6 +162,7 @@ async function printWorkspace(workspaceID: string) {
       ),
   )
 
+  /*
   await printTable("Usage", (tx) =>
     tx
       .select({
@@ -174,6 +188,7 @@ async function printWorkspace(workspaceID: string) {
         })),
       ),
   )
+        */
 }
 
 function formatMicroCents(value: number | null | undefined) {

+ 14 - 22
packages/console/core/script/promote-models.ts

@@ -8,33 +8,25 @@ const stage = process.argv[2]
 if (!stage) throw new Error("Stage is required")
 
 const root = path.resolve(process.cwd(), "..", "..", "..")
+const PARTS = 8
 
 // read the secret
 const ret = await $`bun sst secret list`.cwd(root).text()
 const lines = ret.split("\n")
-const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
-const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
-const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
-const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
-const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
-const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
-const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
-if (!value1) throw new Error("ZEN_MODELS1 not found")
-if (!value2) throw new Error("ZEN_MODELS2 not found")
-if (!value3) throw new Error("ZEN_MODELS3 not found")
-if (!value4) throw new Error("ZEN_MODELS4 not found")
-if (!value5) throw new Error("ZEN_MODELS5 not found")
-if (!value6) throw new Error("ZEN_MODELS6 not found")
-if (!value7) throw new Error("ZEN_MODELS7 not found")
+const values = Array.from({ length: PARTS }, (_, i) => {
+  const value = lines
+    .find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
+    ?.split("=")
+    .slice(1)
+    .join("=")
+  if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
+  return value
+})
 
 // validate value
-ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
+ZenData.validate(JSON.parse(values.join("")))
 
 // update the secret
-await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
-await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
-await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
-await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
-await $`bun sst secret set ZEN_MODELS5 ${value5} --stage ${stage}`
-await $`bun sst secret set ZEN_MODELS6 ${value6} --stage ${stage}`
-await $`bun sst secret set ZEN_MODELS7 ${value7} --stage ${stage}`
+for (let i = 0; i < PARTS; i++) {
+  await $`bun sst secret set ZEN_MODELS${i + 1} --stage ${stage} -- ${values[i]}`
+}

+ 15 - 22
packages/console/core/script/pull-models.ts

@@ -8,32 +8,25 @@ const stage = process.argv[2]
 if (!stage) throw new Error("Stage is required")
 
 const root = path.resolve(process.cwd(), "..", "..", "..")
+const PARTS = 8
 
 // read the secret
 const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()
 const lines = ret.split("\n")
-const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
-const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
-const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
-const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
-const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
-const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
-const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
-if (!value1) throw new Error("ZEN_MODELS1 not found")
-if (!value2) throw new Error("ZEN_MODELS2 not found")
-if (!value3) throw new Error("ZEN_MODELS3 not found")
-if (!value4) throw new Error("ZEN_MODELS4 not found")
-if (!value5) throw new Error("ZEN_MODELS5 not found")
-if (!value6) throw new Error("ZEN_MODELS6 not found")
-if (!value7) throw new Error("ZEN_MODELS7 not found")
+const values = Array.from({ length: PARTS }, (_, i) => {
+  const value = lines
+    .find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
+    ?.split("=")
+    .slice(1)
+    .join("=")
+  if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
+  return value
+})
+
 // validate value
-ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
+ZenData.validate(JSON.parse(values.join("")))
 
 // update the secret
-await $`bun sst secret set ZEN_MODELS1 ${value1}`
-await $`bun sst secret set ZEN_MODELS2 ${value2}`
-await $`bun sst secret set ZEN_MODELS3 ${value3}`
-await $`bun sst secret set ZEN_MODELS4 ${value4}`
-await $`bun sst secret set ZEN_MODELS5 ${value5}`
-await $`bun sst secret set ZEN_MODELS6 ${value6}`
-await $`bun sst secret set ZEN_MODELS7 ${value7}`
+for (let i = 0; i < PARTS; i++) {
+  await $`bun sst secret set ZEN_MODELS${i + 1} -- ${values[i]}`
+}

+ 18 - 36
packages/console/core/script/update-models.ts

@@ -7,34 +7,24 @@ import { ZenData } from "../src/model"
 
 const root = path.resolve(process.cwd(), "..", "..", "..")
 const models = await $`bun sst secret list`.cwd(root).text()
+const PARTS = 8
 
 // read the line starting with "ZEN_MODELS"
 const lines = models.split("\n")
-const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
-const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
-const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
-const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
-const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
-const oldValue6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
-const oldValue7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
-if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
-if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
-if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
-if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
-if (!oldValue5) throw new Error("ZEN_MODELS5 not found")
-if (!oldValue6) throw new Error("ZEN_MODELS6 not found")
-if (!oldValue7) throw new Error("ZEN_MODELS7 not found")
+const oldValues = Array.from({ length: PARTS }, (_, i) => {
+  const value = lines
+    .find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
+    ?.split("=")
+    .slice(1)
+    .join("=")
+  if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
+  return value
+})
 
 // store the prettified json to a temp file
 const filename = `models-${Date.now()}.json`
 const tempFile = Bun.file(path.join(os.tmpdir(), filename))
-await tempFile.write(
-  JSON.stringify(
-    JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5 + oldValue6 + oldValue7),
-    null,
-    2,
-  ),
-)
+await tempFile.write(JSON.stringify(JSON.parse(oldValues.join("")), null, 2))
 console.log("tempFile", tempFile.name)
 
 // open temp file in vim and read the file on close
@@ -43,19 +33,11 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
 ZenData.validate(JSON.parse(newValue))
 
 // update the secret
-const chunk = Math.ceil(newValue.length / 7)
-const newValue1 = newValue.slice(0, chunk)
-const newValue2 = newValue.slice(chunk, chunk * 2)
-const newValue3 = newValue.slice(chunk * 2, chunk * 3)
-const newValue4 = newValue.slice(chunk * 3, chunk * 4)
-const newValue5 = newValue.slice(chunk * 4, chunk * 5)
-const newValue6 = newValue.slice(chunk * 5, chunk * 6)
-const newValue7 = newValue.slice(chunk * 6)
+const chunk = Math.ceil(newValue.length / PARTS)
+const newValues = Array.from({ length: PARTS }, (_, i) =>
+  newValue.slice(chunk * i, i === PARTS - 1 ? undefined : chunk * (i + 1)),
+)
 
-await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
-await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
-await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
-await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
-await $`bun sst secret set ZEN_MODELS5 ${newValue5}`
-await $`bun sst secret set ZEN_MODELS6 ${newValue6}`
-await $`bun sst secret set ZEN_MODELS7 ${newValue7}`
+for (let i = 0; i < PARTS; i++) {
+  await $`bun sst secret set ZEN_MODELS${i + 1} -- ${newValues[i]}`
+}

+ 1 - 16
packages/console/core/src/billing.ts

@@ -25,22 +25,7 @@ export namespace Billing {
   export const get = async () => {
     return Database.use(async (tx) =>
       tx
-        .select({
-          customerID: BillingTable.customerID,
-          subscriptionID: BillingTable.subscriptionID,
-          paymentMethodID: BillingTable.paymentMethodID,
-          paymentMethodType: BillingTable.paymentMethodType,
-          paymentMethodLast4: BillingTable.paymentMethodLast4,
-          balance: BillingTable.balance,
-          reload: BillingTable.reload,
-          reloadAmount: BillingTable.reloadAmount,
-          reloadTrigger: BillingTable.reloadTrigger,
-          monthlyLimit: BillingTable.monthlyLimit,
-          monthlyUsage: BillingTable.monthlyUsage,
-          timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
-          reloadError: BillingTable.reloadError,
-          timeReloadError: BillingTable.timeReloadError,
-        })
+        .select()
         .from(BillingTable)
         .where(eq(BillingTable.workspaceID, Actor.workspace()))
         .then((r) => r[0]),

+ 72 - 0
packages/console/core/src/black.ts

@@ -1,6 +1,8 @@
 import { z } from "zod"
 import { fn } from "./util/fn"
 import { Resource } from "@opencode-ai/console-resource"
+import { centsToMicroCents } from "./util/price"
+import { getWeekBounds } from "./util/date"
 
 export namespace BlackData {
   const Schema = z.object({
@@ -18,3 +20,73 @@ export namespace BlackData {
     return Schema.parse(json)
   })
 }
+
+export namespace Black {
+  export const analyzeRollingUsage = fn(
+    z.object({
+      usage: z.number().int(),
+      timeUpdated: z.date(),
+    }),
+    ({ usage, timeUpdated }) => {
+      const now = new Date()
+      const black = BlackData.get()
+      const rollingWindowMs = black.rollingWindow * 3600 * 1000
+      const rollingLimitInMicroCents = centsToMicroCents(black.rollingLimit * 100)
+      const windowStart = new Date(now.getTime() - rollingWindowMs)
+      if (timeUpdated < windowStart) {
+        return {
+          status: "ok" as const,
+          resetInSec: black.rollingWindow * 3600,
+          usagePercent: 0,
+        }
+      }
+
+      const windowEnd = new Date(timeUpdated.getTime() + rollingWindowMs)
+      if (usage < rollingLimitInMicroCents) {
+        return {
+          status: "ok" as const,
+          resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000),
+          usagePercent: Math.ceil(Math.min(100, (usage / rollingLimitInMicroCents) * 100)),
+        }
+      }
+      return {
+        status: "rate-limited" as const,
+        resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000),
+        usagePercent: 100,
+      }
+    },
+  )
+
+  export const analyzeWeeklyUsage = fn(
+    z.object({
+      usage: z.number().int(),
+      timeUpdated: z.date(),
+    }),
+    ({ usage, timeUpdated }) => {
+      const black = BlackData.get()
+      const now = new Date()
+      const week = getWeekBounds(now)
+      const fixedLimitInMicroCents = centsToMicroCents(black.fixedLimit * 100)
+      if (timeUpdated < week.start) {
+        return {
+          status: "ok" as const,
+          resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
+          usagePercent: 0,
+        }
+      }
+      if (usage < fixedLimitInMicroCents) {
+        return {
+          status: "ok" as const,
+          resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
+          usagePercent: Math.ceil(Math.min(100, (usage / fixedLimitInMicroCents) * 100)),
+        }
+      }
+
+      return {
+        status: "rate-limited" as const,
+        resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
+        usagePercent: 100,
+      }
+    },
+  )
+}

+ 3 - 2
packages/console/core/src/model.ts

@@ -35,7 +35,7 @@ export namespace ZenData {
     cost200K: ModelCostSchema.optional(),
     allowAnonymous: z.boolean().optional(),
     byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
-    stickyProvider: z.boolean().optional(),
+    stickyProvider: z.enum(["strict", "prefer"]).optional(),
     trial: TrialSchema.optional(),
     rateLimit: z.number().optional(),
     fallbackProvider: z.string().optional(),
@@ -74,7 +74,8 @@ export namespace ZenData {
         Resource.ZEN_MODELS4.value +
         Resource.ZEN_MODELS5.value +
         Resource.ZEN_MODELS6.value +
-        Resource.ZEN_MODELS7.value,
+        Resource.ZEN_MODELS7.value +
+        Resource.ZEN_MODELS8.value,
     )
     return ModelsSchema.parse(json)
   })

+ 3 - 1
packages/console/core/src/schema/billing.sql.ts

@@ -1,4 +1,4 @@
-import { bigint, boolean, index, int, json, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
+import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
 import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
 import { workspaceIndexes } from "./workspace.sql"
 
@@ -23,6 +23,8 @@ export const BillingTable = mysqlTable(
     timeReloadLockedTill: utc("time_reload_locked_till"),
     subscriptionID: varchar("subscription_id", { length: 28 }),
     subscriptionCouponID: varchar("subscription_coupon_id", { length: 28 }),
+    subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const),
+    timeSubscriptionBooked: utc("time_subscription_booked"),
   },
   (table) => [
     ...workspaceIndexes(table),

+ 12 - 0
packages/console/core/sst-env.d.ts

@@ -78,6 +78,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
+    "STRIPE_PUBLISHABLE_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
     "STRIPE_SECRET_KEY": {
       "type": "sst.sst.Secret"
       "value": string
@@ -130,6 +134,14 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
+    "ZEN_MODELS8": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
+    "ZEN_SESSION_SECRET": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
 }
 // cloudflare 

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

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

+ 12 - 0
packages/console/function/sst-env.d.ts

@@ -78,6 +78,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
+    "STRIPE_PUBLISHABLE_KEY": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
     "STRIPE_SECRET_KEY": {
       "type": "sst.sst.Secret"
       "value": string
@@ -130,6 +134,14 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
+    "ZEN_MODELS8": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
+    "ZEN_SESSION_SECRET": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
 }
 // cloudflare 

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно