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

Merge public/dev into current branch — resolve conflicts: server: mount /app, add fetch RPC, update deps (@modelcontextprotocol/[email protected], keep @iarna/toml)

paviko 3 месяцев назад
Родитель
Сommit
be32d4cf76
100 измененных файлов с 14622 добавлено и 1370 удалено
  1. 3 0
      .github/pull_request_template.md
  2. 2 0
      .github/workflows/duplicate-prs.yml
  3. 7 0
      .github/workflows/nix-desktop.yml
  4. 139 0
      .github/workflows/pr-standards.yml
  5. 16 12
      .github/workflows/publish.yml
  6. 1 1
      .github/workflows/stats.yml
  7. 3 1
      .opencode/agent/duplicate-pr.md
  8. 5 0
      .opencode/tool/github-pr-search.ts
  9. 75 5
      CONTRIBUTING.md
  10. 3 7
      README.md
  11. 116 0
      README.zh-CN.md
  12. 2 1
      README.zh-TW.md
  13. 197 189
      STATS.md
  14. 2 3
      STYLE_GUIDE.md
  15. 43 36
      bun.lock
  16. 3 3
      flake.lock
  17. 1 1
      github/README.md
  18. 16 0
      infra/console.ts
  19. 134 107
      install
  20. 1 1
      nix/hashes.json
  21. 6 1
      nix/scripts/bun-build.ts
  22. 2 2
      packages/app/README.md
  23. 1 30
      packages/app/index.html
  24. 1 1
      packages/app/package.json
  25. 28 0
      packages/app/public/oc-theme-preload.js
  26. 47 0
      packages/app/src/addons/serialize.test.ts
  27. 19 23
      packages/app/src/addons/serialize.ts
  28. 71 59
      packages/app/src/app.tsx
  29. 1 5
      packages/app/src/components/dialog-edit-project.tsx
  30. 2 0
      packages/app/src/components/dialog-select-file.tsx
  31. 1 1
      packages/app/src/components/dialog-select-model.tsx
  32. 62 42
      packages/app/src/components/prompt-input.tsx
  33. 2 1
      packages/app/src/components/session-context-usage.tsx
  34. 80 22
      packages/app/src/components/session/session-header.tsx
  35. 14 4
      packages/app/src/components/session/session-new-view.tsx
  36. 1 0
      packages/app/src/components/session/session-sortable-tab.tsx
  37. 118 68
      packages/app/src/components/terminal.tsx
  38. 12 1
      packages/app/src/context/command.tsx
  39. 149 38
      packages/app/src/context/file.tsx
  40. 69 4
      packages/app/src/context/global-sdk.tsx
  41. 3 2
      packages/app/src/context/global-sync.tsx
  42. 73 0
      packages/app/src/context/layout-scroll.test.ts
  43. 118 0
      packages/app/src/context/layout-scroll.ts
  44. 225 55
      packages/app/src/context/layout.tsx
  45. 12 5
      packages/app/src/context/local.tsx
  46. 30 5
      packages/app/src/context/notification.tsx
  47. 65 20
      packages/app/src/context/permission.tsx
  48. 133 61
      packages/app/src/context/prompt.tsx
  49. 3 1
      packages/app/src/context/sdk.tsx
  50. 44 22
      packages/app/src/context/server.tsx
  51. 162 56
      packages/app/src/context/sync.tsx
  52. 164 97
      packages/app/src/context/terminal.tsx
  53. 4 2
      packages/app/src/entry.tsx
  54. 1 1
      packages/app/src/index.ts
  55. 12 2
      packages/app/src/pages/directory-layout.tsx
  56. 224 24
      packages/app/src/pages/layout.tsx
  57. 593 209
      packages/app/src/pages/session.tsx
  58. 135 0
      packages/app/src/utils/perf.ts
  59. 223 5
      packages/app/src/utils/persist.ts
  60. 184 29
      packages/app/src/utils/prompt.ts
  61. 6 0
      packages/app/src/utils/same.ts
  62. 2 1
      packages/console/app/package.json
  63. BIN
      packages/console/app/src/asset/black/hero.png
  64. 2 2
      packages/console/app/src/config.ts
  65. 221 0
      packages/console/app/src/routes/black/index.css
  66. 104 0
      packages/console/app/src/routes/black/index.tsx
  67. 214 0
      packages/console/app/src/routes/black/workspace.css
  68. 134 0
      packages/console/app/src/routes/black/workspace.tsx
  69. 2 2
      packages/console/app/src/routes/download/index.tsx
  70. 7 1
      packages/console/app/src/routes/index.tsx
  71. 370 3
      packages/console/app/src/routes/stripe/webhook.ts
  72. 3 0
      packages/console/app/src/routes/temp.tsx
  73. 8 0
      packages/console/app/src/routes/workspace/[id]/billing/black-section.module.css
  74. 58 0
      packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx
  75. 6 2
      packages/console/app/src/routes/workspace/[id]/billing/index.tsx
  76. 19 2
      packages/console/app/src/routes/workspace/[id]/billing/payment-section.module.css
  77. 23 13
      packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx
  78. 1 0
      packages/console/app/src/routes/workspace/[id]/graph-section.tsx
  79. 3 1
      packages/console/app/src/routes/workspace/[id]/usage-section.tsx
  80. 7 0
      packages/console/app/src/routes/zen/util/error.ts
  81. 178 64
      packages/console/app/src/routes/zen/util/handler.ts
  82. 20 14
      packages/console/app/src/routes/zen/util/rateLimiter.ts
  83. 6 0
      packages/console/core/migrations/0041_odd_misty_knight.sql
  84. 7 0
      packages/console/core/migrations/0042_flat_nightmare.sql
  85. 2 0
      packages/console/core/migrations/0043_lame_calypso.sql
  86. 1 0
      packages/console/core/migrations/0044_tiny_captain_midlands.sql
  87. 1 0
      packages/console/core/migrations/0045_cuddly_diamondback.sql
  88. 13 0
      packages/console/core/migrations/0046_charming_black_bolt.sql
  89. 6 0
      packages/console/core/migrations/0047_huge_omega_red.sql
  90. 2 0
      packages/console/core/migrations/0048_mean_frank_castle.sql
  91. 1 0
      packages/console/core/migrations/0049_noisy_domino.sql
  92. 1 0
      packages/console/core/migrations/0050_bumpy_mephistopheles.sql
  93. 1095 0
      packages/console/core/migrations/meta/0041_snapshot.json
  94. 1144 0
      packages/console/core/migrations/meta/0042_snapshot.json
  95. 1147 0
      packages/console/core/migrations/meta/0043_snapshot.json
  96. 1146 0
      packages/console/core/migrations/meta/0044_snapshot.json
  97. 1149 0
      packages/console/core/migrations/meta/0045_snapshot.json
  98. 1236 0
      packages/console/core/migrations/meta/0046_snapshot.json
  99. 1207 0
      packages/console/core/migrations/meta/0047_snapshot.json
  100. 1207 0
      packages/console/core/migrations/meta/0048_snapshot.json

+ 3 - 0
.github/pull_request_template.md

@@ -0,0 +1,3 @@
+### What does this PR do?
+
+### How did you verify your code works?

+ 2 - 0
.github/workflows/duplicate-prs.yml

@@ -44,6 +44,8 @@ jobs:
           {
             echo "Check for duplicate PRs related to this new PR:"
             echo ""
+            echo "CURRENT_PR_NUMBER: $PR_NUMBER"
+            echo ""
             echo "Title: $(gh pr view "$PR_NUMBER" --json title --jq .title)"
             echo ""
             echo "Description:"

+ 7 - 0
.github/workflows/nix-desktop.yml

@@ -9,6 +9,13 @@ on:
       - "nix/**"
       - "packages/app/**"
       - "packages/desktop/**"
+  pull_request:
+    paths:
+      - "flake.nix"
+      - "flake.lock"
+      - "nix/**"
+      - "packages/app/**"
+      - "packages/desktop/**"
   workflow_dispatch:
 
 jobs:

+ 139 - 0
.github/workflows/pr-standards.yml

@@ -0,0 +1,139 @@
+name: PR Standards
+
+on:
+  pull_request_target:
+    types: [opened, edited, synchronize]
+
+jobs:
+  check-standards:
+    if: |
+      github.event.pull_request.user.login != 'actions-user' &&
+      github.event.pull_request.user.login != 'opencode' &&
+      github.event.pull_request.user.login != 'rekram1-node' &&
+      github.event.pull_request.user.login != 'thdxr' &&
+      github.event.pull_request.user.login != 'kommander' &&
+      github.event.pull_request.user.login != 'jayair' &&
+      github.event.pull_request.user.login != 'fwang' &&
+      github.event.pull_request.user.login != 'adamdotdevin' &&
+      github.event.pull_request.user.login != 'iamdavidhill' &&
+      github.event.pull_request.user.login != 'opencode-agent[bot]'
+    runs-on: ubuntu-latest
+    permissions:
+      pull-requests: write
+    steps:
+      - name: Check PR standards
+        uses: actions/github-script@v7
+        with:
+          script: |
+            const pr = context.payload.pull_request;
+            const title = pr.title;
+
+            async function addLabel(label) {
+              await github.rest.issues.addLabels({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                issue_number: pr.number,
+                labels: [label]
+              });
+            }
+
+            async function removeLabel(label) {
+              try {
+                await github.rest.issues.removeLabel({
+                  owner: context.repo.owner,
+                  repo: context.repo.repo,
+                  issue_number: pr.number,
+                  name: label
+                });
+              } catch (e) {
+                // Label wasn't present, ignore
+              }
+            }
+
+            async function comment(marker, body) {
+              const markerText = `<!-- pr-standards:${marker} -->`;
+              const { data: comments } = await github.rest.issues.listComments({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                issue_number: pr.number
+              });
+              
+              const existing = comments.find(c => c.body.includes(markerText));
+              if (existing) return;
+              
+              await github.rest.issues.createComment({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                issue_number: pr.number,
+                body: markerText + '\n' + body
+              });
+            }
+
+            // Step 1: Check title format
+            // Matches: feat:, feat(scope):, feat (scope):, etc.
+            const titlePattern = /^(feat|fix|docs|chore|refactor|test)\s*(\([a-zA-Z0-9-]+\))?\s*:/;
+            const hasValidTitle = titlePattern.test(title);
+
+            if (!hasValidTitle) {
+              await addLabel('needs:title');
+              await comment('title', `Hey! Your PR title \`${title}\` doesn't follow conventional commit format.
+
+            Please update it to start with one of:
+            - \`feat:\` or \`feat(scope):\` new feature
+            - \`fix:\` or \`fix(scope):\` bug fix
+            - \`docs:\` or \`docs(scope):\` documentation changes
+            - \`chore:\` or \`chore(scope):\` maintenance tasks
+            - \`refactor:\` or \`refactor(scope):\` code refactoring
+            - \`test:\` or \`test(scope):\` adding or updating tests
+
+            Where \`scope\` is the package name (e.g., \`app\`, \`desktop\`, \`opencode\`).
+
+            See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#pr-titles) for details.`);
+              return;
+            }
+
+            await removeLabel('needs:title');
+
+            // Step 2: Check for linked issue (skip for docs/refactor PRs)
+            const skipIssueCheck = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
+            if (skipIssueCheck) {
+              await removeLabel('needs:issue');
+              console.log('Skipping issue check for docs/refactor PR');
+              return;
+            }
+            const query = `
+              query($owner: String!, $repo: String!, $number: Int!) {
+                repository(owner: $owner, name: $repo) {
+                  pullRequest(number: $number) {
+                    closingIssuesReferences(first: 1) {
+                      totalCount
+                    }
+                  }
+                }
+              }
+            `;
+
+            const result = await github.graphql(query, {
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              number: pr.number
+            });
+
+            const linkedIssues = result.repository.pullRequest.closingIssuesReferences.totalCount;
+
+            if (linkedIssues === 0) {
+              await addLabel('needs:issue');
+              await comment('issue', `Thanks for your contribution!
+
+            This PR doesn't have a linked issue. All PRs must reference an existing issue.
+
+            Please:
+            1. Open an issue describing the bug/feature (if one doesn't exist)
+            2. Add \`Fixes #<number>\` or \`Closes #<number>\` to this PR description
+
+            See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#issue-first-policy) for details.`);
+              return;
+            }
+
+            await removeLabel('needs:issue');
+            console.log('PR meets all standards');

+ 16 - 12
.github/workflows/publish.yml

@@ -177,8 +177,22 @@ jobs:
           cargo tauri --version
 
       - name: Build and upload artifacts
-        timeout-minutes: 20
-        uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
+        uses: Wandalen/wretry.action@v3
+        timeout-minutes: 60
+        with:
+          attempt_limit: 3
+          attempt_delay: 10000
+          action: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
+          with: |
+            projectPath: packages/desktop
+            uploadWorkflowArtifacts: true
+            tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
+            args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
+            updaterJsonPreferNsis: true
+            releaseId: ${{ needs.publish.outputs.release }}
+            tagName: ${{ needs.publish.outputs.tag }}
+            releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
+            releaseDraft: true
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
@@ -190,16 +204,6 @@ jobs:
           APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
           APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
           APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
-        with:
-          projectPath: packages/desktop
-          uploadWorkflowArtifacts: true
-          tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
-          args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
-          updaterJsonPreferNsis: true
-          releaseId: ${{ needs.publish.outputs.release }}
-          tagName: ${{ needs.publish.outputs.tag }}
-          releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
-          releaseDraft: true
 
   publish-release:
     needs:

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

@@ -9,7 +9,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
 
 jobs:
   stats:
-    if: github.repository == 'sst/opencode'
+    if: github.repository == 'anomalyco/opencode'
     runs-on: blacksmith-4vcpu-ubuntu-2404
     permissions:
       contents: write

+ 3 - 1
.opencode/agent/duplicate-pr.md

@@ -12,6 +12,8 @@ You are a duplicate PR detection agent. When a PR is opened, your job is to sear
 
 Use the github-pr-search tool to search for PRs that might be addressing the same issue or feature.
 
+IMPORTANT: The input will contain a line `CURRENT_PR_NUMBER: NNNN`. This is the current PR number, you should not mark that the current PR as a duplicate of itself.
+
 Search using keywords from the PR title and description. Try multiple searches with different relevant terms.
 
 If you find potential duplicates:
@@ -19,6 +21,6 @@ If you find potential duplicates:
 - List them with their titles and URLs
 - Briefly explain why they might be related
 
-If no duplicates are found, say so clearly.
+If no duplicates are found, say so clearly. BUT ONLY SAY "No duplicate PRs found" (don't say anything else if no dups)
 
 Keep your response concise and actionable.

+ 5 - 0
.opencode/tool/github-pr-search.ts

@@ -45,6 +45,11 @@ export default tool({
     }
 
     const prs = result.items as PR[]
+
+    if (prs.length === 0) {
+      return `No other PRs found matching "${args.query}"`
+    }
+
     const formatted = prs.map((pr) => `${pr.title}\n${pr.html_url}`).join("\n\n")
 
     return `Found ${result.total_count} PRs (showing ${prs.length}):\n\n${formatted}`

+ 75 - 5
CONTRIBUTING.md

@@ -83,12 +83,30 @@ This starts a local dev server at http://localhost:5173 (or similar port shown i
 
 ### Running the Desktop App
 
-The desktop app is a native Tauri application that wraps the web UI. To run it:
+The desktop app is a native Tauri application that wraps the web UI.
+
+To run the native desktop app:
+
+```bash
+bun run --cwd packages/desktop tauri dev
+```
+
+This starts the web dev server on http://localhost:1420 and opens the native window.
+
+If you only want the web dev server (no native shell):
 
 ```bash
 bun run --cwd packages/desktop dev
 ```
 
+To create a production `dist/` and build the native app bundle:
+
+```bash
+bun run --cwd packages/desktop tauri build
+```
+
+This runs `bun run --cwd packages/desktop build` automatically via Tauri’s `beforeBuildCommand`.
+
 > [!NOTE]
 > Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions.
 
@@ -131,11 +149,63 @@ With that said, you may want to try these methods, as they might work for you.
 
 ## Pull Request Expectations
 
-- Try to keep pull requests small and focused.
-- Link relevant issue(s) in the description
+### Issue First Policy
+
+**All PRs must reference an existing issue.** Before opening a PR, open an issue describing the bug or feature. This helps maintainers triage and prevents duplicate work. PRs without a linked issue may be closed without review.
+
+- Use `Fixes #123` or `Closes #123` in your PR description to link the issue
+- For small fixes, a brief issue is fine - just enough context for maintainers to understand the problem
+
+### General Requirements
+
+- Keep pull requests small and focused
 - Explain the issue and why your change fixes it
-- Avoid having verbose LLM generated PR descriptions
-- Before adding new functions or functionality, ensure that such behavior doesn't already exist elsewhere in the codebase.
+- Before adding new functionality, ensure it doesn't already exist elsewhere in the codebase
+
+### UI Changes
+
+If your PR includes UI changes, please include screenshots or videos showing the before and after. This helps maintainers review faster and gives you quicker feedback.
+
+### Logic Changes
+
+For non-UI changes (bug fixes, new features, refactors), explain **how you verified it works**:
+
+- What did you test?
+- How can a reviewer reproduce/confirm the fix?
+
+### No AI-Generated Walls of Text
+
+Long, AI-generated PR descriptions and issues are not acceptable and may be ignored. Respect the maintainers' time:
+
+- Write short, focused descriptions
+- Explain what changed and why in your own words
+- If you can't explain it briefly, your PR might be too large
+
+### PR Titles
+
+PR titles should follow conventional commit standards:
+
+- `feat:` new feature or functionality
+- `fix:` bug fix
+- `docs:` documentation or README changes
+- `chore:` maintenance tasks, dependency updates, etc.
+- `refactor:` code refactoring without changing behavior
+- `test:` adding or updating tests
+
+You can optionally include a scope to indicate which package is affected:
+
+- `feat(app):` feature in the app package
+- `fix(desktop):` bug fix in the desktop package
+- `chore(opencode):` maintenance in the opencode package
+
+Examples:
+
+- `docs: update contributing guidelines`
+- `fix: resolve crash on startup`
+- `feat: add dark mode support`
+- `feat(app): add dark mode support`
+- `fix(desktop): resolve crash on startup`
+- `chore: bump dependency versions`
 
 ### Style Preferences
 

+ 3 - 7
README.md

@@ -38,7 +38,8 @@ curl -fsSL https://opencode.ai/install | bash
 npm i -g opencode-ai@latest        # or bun/pnpm/yarn
 scoop bucket add extras; scoop install extras/opencode  # Windows
 choco install opencode             # Windows
-brew install opencode              # macOS and Linux
+brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date)
+brew install opencode              # macOS and Linux (official brew formula, updated less)
 paru -S opencode-bin               # Arch Linux
 mise use -g opencode               # Any OS
 nix run nixpkgs#opencode           # or github:anomalyco/opencode for latest dev branch
@@ -80,8 +81,7 @@ XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
 
 ### Agents
 
-OpenCode includes two built-in agents you can switch between,
-you can switch between these using the `Tab` key.
+OpenCode includes two built-in agents you can switch between with the `Tab` key.
 
 - **build** - Default, full access agent for development work
 - **plan** - Read-only agent for analysis and code exploration
@@ -118,10 +118,6 @@ It's very similar to Claude Code in terms of capability. Here are the key differ
 - A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
 - A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
 
-#### What's the other repo?
-
-The other confusingly named repo has no relation to this one. You can [read the story behind it here](https://x.com/thdxr/status/1933561254481666466).
-
 ---
 
 **Join our community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

+ 116 - 0
README.zh-CN.md

@@ -0,0 +1,116 @@
+<p align="center">
+  <a href="https://opencode.ai">
+    <picture>
+      <source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
+      <source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
+      <img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
+    </picture>
+  </a>
+</p>
+<p align="center">开源的 AI Coding Agent。</p>
+<p align="center">
+  <a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
+  <a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
+  <a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
+</p>
+
+[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+
+---
+
+### 安装
+
+```bash
+# 直接安装 (YOLO)
+curl -fsSL https://opencode.ai/install | bash
+
+# 软件包管理器
+npm i -g opencode-ai@latest        # 也可使用 bun/pnpm/yarn
+scoop bucket add extras; scoop install extras/opencode  # Windows
+choco install opencode             # Windows
+brew install anomalyco/tap/opencode # macOS 和 Linux(推荐,始终保持最新)
+brew install opencode              # macOS 和 Linux(官方 brew formula,更新频率较低)
+paru -S opencode-bin               # Arch Linux
+mise use -g opencode               # 任意系统
+nix run nixpkgs#opencode           # 或用 github:anomalyco/opencode 获取最新 dev 分支
+```
+
+> [!TIP]
+> 安装前请先移除 0.1.x 之前的旧版本。
+
+### 桌面应用程序 (BETA)
+
+OpenCode 也提供桌面版应用。可直接从 [发布页 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下载。
+
+| 平台                  | 下载文件                              |
+| --------------------- | ------------------------------------- |
+| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
+| macOS (Intel)         | `opencode-desktop-darwin-x64.dmg`     |
+| Windows               | `opencode-desktop-windows-x64.exe`    |
+| Linux                 | `.deb`、`.rpm` 或 AppImage            |
+
+```bash
+# macOS (Homebrew Cask)
+brew install --cask opencode-desktop
+```
+
+#### 安装目录
+
+安装脚本按照以下优先级决定安装路径:
+
+1. `$OPENCODE_INSTALL_DIR` - 自定义安装目录
+2. `$XDG_BIN_DIR` - 符合 XDG 基础目录规范的路径
+3. `$HOME/bin` - 如果存在或可创建的用户二进制目录
+4. `$HOME/.opencode/bin` - 默认备用路径
+
+```bash
+# 示例
+OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
+XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
+```
+
+### Agents
+
+OpenCode 内置两种 Agent,可用 `Tab` 键快速切换:
+
+- **build** - 默认模式,具备完整权限,适合开发工作
+- **plan** - 只读模式,适合代码分析与探索
+  - 默认拒绝修改文件
+  - 运行 bash 命令前会询问
+  - 便于探索未知代码库或规划改动
+
+另外还包含一个 **general** 子 Agent,用于复杂搜索和多步任务,内部使用,也可在消息中输入 `@general` 调用。
+
+了解更多 [Agents](https://opencode.ai/docs/agents) 相关信息。
+
+### 文档
+
+更多配置说明请查看我们的 [**官方文档**](https://opencode.ai/docs)。
+
+### 参与贡献
+
+如有兴趣贡献代码,请在提交 PR 前阅读 [贡献指南 (Contributing Docs)](./CONTRIBUTING.md)。
+
+### 基于 OpenCode 进行开发
+
+如果你在项目名中使用了 “opencode”(如 “opencode-dashboard” 或 “opencode-mobile”),请在 README 里注明该项目不是 OpenCode 团队官方开发,且不存在隶属关系。
+
+### 常见问题 (FAQ)
+
+#### 这和 Claude Code 有什么不同?
+
+功能上很相似,关键差异:
+
+- 100% 开源。
+- 不绑定特定提供商。推荐使用 [OpenCode Zen](https://opencode.ai/zen) 的模型,但也可搭配 Claude、OpenAI、Google 甚至本地模型。模型迭代会缩小差异、降低成本,因此保持 provider-agnostic 很重要。
+- 内置 LSP 支持。
+- 聚焦终端界面 (TUI)。OpenCode 由 Neovim 爱好者和 [terminal.shop](https://terminal.shop) 的创建者打造,会持续探索终端的极限。
+- 客户端/服务器架构。可在本机运行,同时用移动设备远程驱动。TUI 只是众多潜在客户端之一。
+
+#### 另一个同名的仓库是什么?
+
+另一个名字相近的仓库与本项目无关。[点击这里了解背后故事](https://x.com/thdxr/status/1933561254481666466)。
+
+---
+
+**加入我们的社区** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

+ 2 - 1
README.zh-TW.md

@@ -28,7 +28,8 @@ curl -fsSL https://opencode.ai/install | bash
 npm i -g opencode-ai@latest        # 也可使用 bun/pnpm/yarn
 scoop bucket add extras; scoop install extras/opencode  # Windows
 choco install opencode             # Windows
-brew install opencode              # macOS 與 Linux
+brew install anomalyco/tap/opencode # macOS 與 Linux(推薦,始終保持最新)
+brew install opencode              # macOS 與 Linux(官方 brew formula,更新頻率較低)
 paru -S opencode-bin               # Arch Linux
 mise use -g github:anomalyco/opencode    # 任何作業系統
 nix run nixpkgs#opencode           # 或使用 github:anomalyco/opencode 以取得最新開發分支

+ 197 - 189
STATS.md

@@ -1,191 +1,199 @@
 # 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) |
+| 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) |

+ 2 - 3
STYLE_GUIDE.md

@@ -1,9 +1,8 @@
 ## Style Guide
 
 - Try to keep things in one function unless composable or reusable
-- DO NOT do unnecessary destructuring of variables
-- DO NOT use `else` statements unless necessary
-- DO NOT use `try`/`catch` if it can be avoided
+- 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

+ 43 - 36
bun.lock

@@ -23,7 +23,7 @@
     },
     "packages/app": {
       "name": "@opencode-ai/app",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -71,7 +71,7 @@
     },
     "packages/console/app": {
       "name": "@opencode-ai/console-app",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@cloudflare/vite-plugin": "1.15.2",
         "@ibm/plex": "6.4.1",
@@ -88,6 +88,7 @@
         "chart.js": "4.5.1",
         "nitro": "3.0.1-alpha.1",
         "solid-js": "catalog:",
+        "solid-list": "0.3.0",
         "vite": "catalog:",
         "zod": "catalog:",
       },
@@ -99,7 +100,7 @@
     },
     "packages/console/core": {
       "name": "@opencode-ai/console-core",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "@jsx-email/render": "1.1.1",
@@ -126,7 +127,7 @@
     },
     "packages/console/function": {
       "name": "@opencode-ai/console-function",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
@@ -150,7 +151,7 @@
     },
     "packages/console/mail": {
       "name": "@opencode-ai/console-mail",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@jsx-email/all": "2.2.3",
         "@jsx-email/cli": "1.4.3",
@@ -174,9 +175,10 @@
     },
     "packages/desktop": {
       "name": "@opencode-ai/desktop",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@opencode-ai/app": "workspace:*",
+        "@opencode-ai/ui": "workspace:*",
         "@solid-primitives/storage": "catalog:",
         "@tauri-apps/api": "^2",
         "@tauri-apps/plugin-dialog": "~2",
@@ -202,7 +204,7 @@
     },
     "packages/enterprise": {
       "name": "@opencode-ai/enterprise",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@opencode-ai/ui": "workspace:*",
         "@opencode-ai/util": "workspace:*",
@@ -231,7 +233,7 @@
     },
     "packages/function": {
       "name": "@opencode-ai/function",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "catalog:",
@@ -247,7 +249,7 @@
     },
     "packages/opencode": {
       "name": "opencode",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "bin": {
         "opencode": "./bin/opencode",
       },
@@ -278,7 +280,7 @@
         "@hono/standard-validator": "0.1.5",
         "@hono/zod-validator": "catalog:",
         "@iarna/toml": "2.2.5",
-        "@modelcontextprotocol/sdk": "1.15.1",
+        "@modelcontextprotocol/sdk": "1.25.2",
         "@octokit/graphql": "9.0.2",
         "@octokit/rest": "catalog:",
         "@openauthjs/openauth": "catalog:",
@@ -287,8 +289,8 @@
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/util": "workspace:*",
         "@openrouter/ai-sdk-provider": "1.5.2",
-        "@opentui/core": "0.1.68",
-        "@opentui/solid": "0.1.68",
+        "@opentui/core": "0.1.72",
+        "@opentui/solid": "0.1.72",
         "@parcel/watcher": "2.5.1",
         "@pierre/diffs": "catalog:",
         "@solid-primitives/event-bus": "1.1.2",
@@ -397,7 +399,7 @@
     },
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "zod": "catalog:",
@@ -417,7 +419,7 @@
     },
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "devDependencies": {
         "@hey-api/openapi-ts": "0.88.1",
         "@tsconfig/node22": "catalog:",
@@ -428,7 +430,7 @@
     },
     "packages/slack": {
       "name": "@opencode-ai/slack",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@slack/bolt": "^3.17.1",
@@ -441,7 +443,7 @@
     },
     "packages/ui": {
       "name": "@opencode-ai/ui",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -449,6 +451,7 @@
         "@pierre/diffs": "catalog:",
         "@shikijs/transformers": "3.9.2",
         "@solid-primitives/bounds": "0.1.3",
+        "@solid-primitives/media": "2.3.3",
         "@solid-primitives/resize-observer": "2.1.3",
         "@solidjs/meta": "catalog:",
         "@typescript/native-preview": "catalog:",
@@ -479,7 +482,7 @@
     },
     "packages/util": {
       "name": "@opencode-ai/util",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "zod": "catalog:",
       },
@@ -490,7 +493,7 @@
     },
     "packages/web": {
       "name": "@opencode-ai/web",
-      "version": "1.1.2",
+      "version": "1.1.11",
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
@@ -997,6 +1000,8 @@
 
     "@hey-api/openapi-ts": ["@hey-api/[email protected]", "", { "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=="],
 
+    "@hono/node-server": ["@hono/[email protected]", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
+
     "@hono/standard-validator": ["@hono/[email protected]", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="],
 
     "@hono/zod-validator": ["@hono/[email protected]", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="],
@@ -1235,7 +1240,7 @@
 
     "@mixmark-io/domino": ["@mixmark-io/[email protected]", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
 
-    "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.15.1", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w=="],
+    "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="],
 
     "@motionone/animation": ["@motionone/[email protected]", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
 
@@ -1339,21 +1344,21 @@
 
     "@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
-    "@opentui/core": ["@opentui/[email protected].68", "", { "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.68", "@opentui/core-darwin-x64": "0.1.68", "@opentui/core-linux-arm64": "0.1.68", "@opentui/core-linux-x64": "0.1.68", "@opentui/core-win32-arm64": "0.1.68", "@opentui/core-win32-x64": "0.1.68", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-SZz5qNO+2lJ8jDEoTSieyXH23t49myu6NetLex+xzqOf67XsU6QKlDcw5oMmc3zrKvETXhgbBvlSnbyJNQoBMg=="],
+    "@opentui/core": ["@opentui/[email protected].72", "", { "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-darwin-arm64": ["@opentui/[email protected].68", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ipPX2gavBLVtw3d8L4ZPJDLlEwIjIRNdlNlxu07rqSEGSfxD5s29yc+33wLAlYXbmnJDajOqm0Dx6HnlY1Y9Fg=="],
+    "@opentui/core-darwin-arm64": ["@opentui/[email protected].72", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RoU48kOrhLZYDBiXaDu1LXS2bwRdlJlFle8eUQiqJjLRbMIY34J/srBuL0JnAS3qKW4J34NepUQa0l0/S43Q3w=="],
 
-    "@opentui/core-darwin-x64": ["@opentui/[email protected].68", "", { "os": "darwin", "cpu": "x64" }, "sha512-9dW0S9HINnuVjvC9QLj+S+329H7qEBQQtyJ9WHpykemokiJ5k4rnuDkfws5FxgTHIf/ddoYYTyPoGCS7WN5gsQ=="],
+    "@opentui/core-darwin-x64": ["@opentui/[email protected].72", "", { "os": "darwin", "cpu": "x64" }, "sha512-hHUQw8i2LWPToRW1rjAiRqmNf34iJPS9ve9CJDygvFs5JOqUxN5yrfLfKfE+1bQjfFDHnpqW1HUk96iLhkPj8Q=="],
 
-    "@opentui/core-linux-arm64": ["@opentui/[email protected].68", "", { "os": "linux", "cpu": "arm64" }, "sha512-/el6TbSQriBUfPhIa6SBfCCc7tjU98Bnhf2+w0zKwQFBjf3F3kmnI42++YxedMGFmL7bRt3EUawGOkQRZZzFAg=="],
+    "@opentui/core-linux-arm64": ["@opentui/[email protected].72", "", { "os": "linux", "cpu": "arm64" }, "sha512-63yml0OQ8tVa0JuDF9lBAWiChX6Q+iDO7lKv7c2n0352n/WyPr3iAgq4uSoH49HXuKeAXY/VwHGjvPzjXD/SDA=="],
 
-    "@opentui/core-linux-x64": ["@opentui/[email protected].68", "", { "os": "linux", "cpu": "x64" }, "sha512-9NzVI3GZzmICoIu3YhWBdkEt0KvY27m++tu/MqW+xb6fnvN74jZkRWzlgjTdM70obL4eUGQdvU08sDHgZjsIJw=="],
+    "@opentui/core-linux-x64": ["@opentui/[email protected].72", "", { "os": "linux", "cpu": "x64" }, "sha512-51veiQXNLvzDsFzsEvt71uK7WhiRe2DnvlJSGBSe6aRRHHxjCFYHzYi7t6bitJqtDTUj+EaMPbH81oZ6xy7tyg=="],
 
-    "@opentui/core-win32-arm64": ["@opentui/[email protected].68", "", { "os": "win32", "cpu": "arm64" }, "sha512-wrAeotyotOplUjQVBSxOGA8GCr9FWXSd6xCEo1PEGo/NjuAOtvHmKoENzyFEP0GzFsjvoUOyy2dZb987oFAn9A=="],
+    "@opentui/core-win32-arm64": ["@opentui/[email protected].72", "", { "os": "win32", "cpu": "arm64" }, "sha512-1Ep6OcaYTy1RlLOln+LNN7DL1iNyLwLjG2M8aO0pVJKFvxeD5P7rdRzY065E4uhkHeJIHuduUqxvUjD0dyuwbw=="],
 
-    "@opentui/core-win32-x64": ["@opentui/[email protected].68", "", { "os": "win32", "cpu": "x64" }, "sha512-w0yBjvzs/oMIwVdWICL4XlUrfsPoVXd4+RDqiuu+Xi/zD0UgANSTRY2asXca+gPe5zPHLsxvz1bAG0Z7uGtmyw=="],
+    "@opentui/core-win32-x64": ["@opentui/[email protected].72", "", { "os": "win32", "cpu": "x64" }, "sha512-5QUv91UkOINlkEaPky3kaxmJvshcJMBAX7LZtIroduaKBGpWRA1aogNhPZzp+30WkvgOU7aOtUktAZuFXb9WdQ=="],
 
-    "@opentui/solid": ["@opentui/[email protected].68", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.68", "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-S1oHvCQaY+gCQu2kiiksPIScP8i0FiDOlAlLjtfwcRlgeSjzT0wRwFkvoh4uVUPuAlyigox7vMCE3j04SYSGKg=="],
+    "@opentui/solid": ["@opentui/[email protected].72", "", { "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=="],
 
     "@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
 
@@ -2087,7 +2092,9 @@
 
     "ai": ["[email protected]", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="],
 
-    "ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
+    "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=="],
+
+    "ajv-formats": ["[email protected]", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
 
     "ansi-align": ["[email protected]", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
 
@@ -2627,10 +2634,10 @@
 
     "fast-glob": ["[email protected]", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
 
-    "fast-json-stable-stringify": ["[email protected]", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
-
     "fast-levenshtein": ["[email protected]", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
 
+    "fast-uri": ["[email protected]", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
+
     "fast-xml-parser": ["[email protected]", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
 
     "fastq": ["[email protected]", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
@@ -3041,7 +3048,9 @@
 
     "json-schema": ["[email protected]", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
 
-    "json-schema-traverse": ["[email protected]", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+    "json-schema-traverse": ["[email protected]", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+    "json-schema-typed": ["[email protected]", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
 
     "json-stable-stringify-without-jsonify": ["[email protected]", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
 
@@ -4087,8 +4096,6 @@
 
     "update-browserslist-db": ["[email protected]", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
 
-    "uri-js": ["[email protected]", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
-
     "url": ["[email protected]", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
 
     "use-callback-ref": ["[email protected]", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
@@ -4427,9 +4434,11 @@
 
     "@modelcontextprotocol/sdk/express": ["[email protected]", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
 
+    "@modelcontextprotocol/sdk/jose": ["[email protected]", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
+
     "@modelcontextprotocol/sdk/raw-body": ["[email protected]", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
 
-    "@modelcontextprotocol/sdk/zod": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+    "@modelcontextprotocol/sdk/zod-to-json-schema": ["[email protected]", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
 
     "@octokit/auth-app/@octokit/request": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="],
 
@@ -4881,8 +4890,6 @@
 
     "unifont/ofetch": ["[email protected]", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
 
-    "uri-js/punycode": ["[email protected]", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
-
     "utif2/pako": ["[email protected]", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
 
     "vitest/vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],

+ 3 - 3
flake.lock

@@ -2,11 +2,11 @@
   "nodes": {
     "nixpkgs": {
       "locked": {
-        "lastModified": 1767364772,
-        "narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
+        "lastModified": 1767966113,
+        "narHash": "sha256-mSTsvXa4WveSRJexsmCbm9dY17B1fKp7NLpJxllpQw4=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
+        "rev": "5f02c91314c8ba4afe83b256b023756412218535",
         "type": "github"
       },
       "original": {

+ 1 - 1
github/README.md

@@ -82,7 +82,7 @@ This will walk you through installing the GitHub app, creating the workflow, and
          id-token: write
        steps:
          - name: Checkout repository
-           uses: actions/checkout@v4
+           uses: actions/checkout@v6
            with:
              fetch-depth: 1
 

+ 16 - 0
infra/console.ts

@@ -76,6 +76,7 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
     "checkout.session.completed",
     "checkout.session.expired",
     "charge.refunded",
+    "invoice.payment_succeeded",
     "customer.created",
     "customer.deleted",
     "customer.updated",
@@ -97,6 +98,19 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
   ],
 })
 
+const zenProduct = new stripe.Product("ZenBlack", {
+  name: "OpenCode Black",
+})
+const zenPrice = new stripe.Price("ZenBlackPrice", {
+  product: zenProduct.id,
+  unitAmount: 20000,
+  currency: "usd",
+  recurring: {
+    interval: "month",
+    intervalCount: 1,
+  },
+})
+
 const ZEN_MODELS = [
   new sst.Secret("ZEN_MODELS1"),
   new sst.Secret("ZEN_MODELS2"),
@@ -106,6 +120,7 @@ const ZEN_MODELS = [
   new sst.Secret("ZEN_MODELS6"),
   new sst.Secret("ZEN_MODELS7"),
 ]
+const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
 const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
 const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
   properties: { value: auth.url.apply((url) => url!) },
@@ -147,6 +162,7 @@ new sst.cloudflare.x.SolidStart("Console", {
     EMAILOCTOPUS_API_KEY,
     AWS_SES_ACCESS_KEY_ID,
     AWS_SES_SECRET_ACCESS_KEY,
+    ZEN_BLACK,
     ...ZEN_MODELS,
     ...($dev
       ? [

+ 134 - 107
install

@@ -16,16 +16,19 @@ Usage: install.sh [options]
 Options:
     -h, --help              Display this help message
     -v, --version <version> Install a specific version (e.g., 1.0.180)
+    -b, --binary <path>     Install from a local binary instead of downloading
         --no-modify-path    Don't modify shell config files (.zshrc, .bashrc, etc.)
 
 Examples:
     curl -fsSL https://opencode.ai/install | bash
     curl -fsSL https://opencode.ai/install | bash -s -- --version 1.0.180
+    ./install --binary /path/to/opencode
 EOF
 }
 
 requested_version=${VERSION:-}
 no_modify_path=false
+binary_path=""
 
 while [[ $# -gt 0 ]]; do
     case "$1" in
@@ -42,6 +45,15 @@ while [[ $# -gt 0 ]]; do
                 exit 1
             fi
             ;;
+        -b|--binary)
+            if [[ -n "${2:-}" ]]; then
+                binary_path="$2"
+                shift 2
+            else
+                echo -e "${RED}Error: --binary requires a path argument${NC}"
+                exit 1
+            fi
+            ;;
         --no-modify-path)
             no_modify_path=true
             shift
@@ -53,119 +65,128 @@ while [[ $# -gt 0 ]]; do
     esac
 done
 
-raw_os=$(uname -s)
-os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
-case "$raw_os" in
-  Darwin*) os="darwin" ;;
-  Linux*) os="linux" ;;
-  MINGW*|MSYS*|CYGWIN*) os="windows" ;;
-esac
-
-arch=$(uname -m)
-if [[ "$arch" == "aarch64" ]]; then
-  arch="arm64"
-fi
-if [[ "$arch" == "x86_64" ]]; then
-  arch="x64"
-fi
+INSTALL_DIR=$HOME/.opencode/bin
+mkdir -p "$INSTALL_DIR"
 
-if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
-  rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
-  if [ "$rosetta_flag" = "1" ]; then
-    arch="arm64"
-  fi
-fi
+# If --binary is provided, skip all download/detection logic
+if [ -n "$binary_path" ]; then
+    if [ ! -f "$binary_path" ]; then
+        echo -e "${RED}Error: Binary not found at ${binary_path}${NC}"
+        exit 1
+    fi
+    specific_version="local"
+else
+    raw_os=$(uname -s)
+    os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
+    case "$raw_os" in
+      Darwin*) os="darwin" ;;
+      Linux*) os="linux" ;;
+      MINGW*|MSYS*|CYGWIN*) os="windows" ;;
+    esac
 
-combo="$os-$arch"
-case "$combo" in
-  linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
-    ;;
-  *)
-    echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
-    exit 1
-    ;;
-esac
+    arch=$(uname -m)
+    if [[ "$arch" == "aarch64" ]]; then
+      arch="arm64"
+    fi
+    if [[ "$arch" == "x86_64" ]]; then
+      arch="x64"
+    fi
 
-archive_ext=".zip"
-if [ "$os" = "linux" ]; then
-  archive_ext=".tar.gz"
-fi
+    if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
+      rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
+      if [ "$rosetta_flag" = "1" ]; then
+        arch="arm64"
+      fi
+    fi
 
-is_musl=false
-if [ "$os" = "linux" ]; then
-  if [ -f /etc/alpine-release ]; then
-    is_musl=true
-  fi
+    combo="$os-$arch"
+    case "$combo" in
+      linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
+        ;;
+      *)
+        echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
+        exit 1
+        ;;
+    esac
 
-  if command -v ldd >/dev/null 2>&1; then
-    if ldd --version 2>&1 | grep -qi musl; then
-      is_musl=true
+    archive_ext=".zip"
+    if [ "$os" = "linux" ]; then
+      archive_ext=".tar.gz"
     fi
-  fi
-fi
 
-needs_baseline=false
-if [ "$arch" = "x64" ]; then
-  if [ "$os" = "linux" ]; then
-    if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
-      needs_baseline=true
-    fi
-  fi
+    is_musl=false
+    if [ "$os" = "linux" ]; then
+      if [ -f /etc/alpine-release ]; then
+        is_musl=true
+      fi
 
-  if [ "$os" = "darwin" ]; then
-    avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
-    if [ "$avx2" != "1" ]; then
-      needs_baseline=true
+      if command -v ldd >/dev/null 2>&1; then
+        if ldd --version 2>&1 | grep -qi musl; then
+          is_musl=true
+        fi
+      fi
     fi
-  fi
-fi
-
-target="$os-$arch"
-if [ "$needs_baseline" = "true" ]; then
-  target="$target-baseline"
-fi
-if [ "$is_musl" = "true" ]; then
-  target="$target-musl"
-fi
 
-filename="$APP-$target$archive_ext"
+    needs_baseline=false
+    if [ "$arch" = "x64" ]; then
+      if [ "$os" = "linux" ]; then
+        if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
+          needs_baseline=true
+        fi
+      fi
 
+      if [ "$os" = "darwin" ]; then
+        avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
+        if [ "$avx2" != "1" ]; then
+          needs_baseline=true
+        fi
+      fi
+    fi
 
-if [ "$os" = "linux" ]; then
-    if ! command -v tar >/dev/null 2>&1; then
-         echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
-         exit 1
+    target="$os-$arch"
+    if [ "$needs_baseline" = "true" ]; then
+      target="$target-baseline"
     fi
-else
-    if ! command -v unzip >/dev/null 2>&1; then
-        echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
-        exit 1
+    if [ "$is_musl" = "true" ]; then
+      target="$target-musl"
     fi
-fi
 
-INSTALL_DIR=$HOME/.opencode/bin
-mkdir -p "$INSTALL_DIR"
+    filename="$APP-$target$archive_ext"
 
-if [ -z "$requested_version" ]; then
-    url="https://github.com/anomalyco/opencode/releases/latest/download/$filename"
-    specific_version=$(curl -s https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
 
-    if [[ $? -ne 0 || -z "$specific_version" ]]; then
-        echo -e "${RED}Failed to fetch version information${NC}"
-        exit 1
+    if [ "$os" = "linux" ]; then
+        if ! command -v tar >/dev/null 2>&1; then
+             echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
+             exit 1
+        fi
+    else
+        if ! command -v unzip >/dev/null 2>&1; then
+            echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
+            exit 1
+        fi
     fi
-else
-    # Strip leading 'v' if present
-    requested_version="${requested_version#v}"
-    url="https://github.com/anomalyco/opencode/releases/download/v${requested_version}/$filename"
-    specific_version=$requested_version
-    
-    # Verify the release exists before downloading
-    http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/tag/v${requested_version}")
-    if [ "$http_status" = "404" ]; then
-        echo -e "${RED}Error: Release v${requested_version} not found${NC}"
-        echo -e "${MUTED}Available releases: https://github.com/anomalyco/opencode/releases${NC}"
-        exit 1
+
+    if [ -z "$requested_version" ]; then
+        url="https://github.com/anomalyco/opencode/releases/latest/download/$filename"
+        specific_version=$(curl -s https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
+
+        if [[ $? -ne 0 || -z "$specific_version" ]]; then
+            echo -e "${RED}Failed to fetch version information${NC}"
+            exit 1
+        fi
+    else
+        # Strip leading 'v' if present
+        requested_version="${requested_version#v}"
+        url="https://github.com/anomalyco/opencode/releases/download/v${requested_version}/$filename"
+        specific_version=$requested_version
+
+        # Verify the release exists before downloading
+        http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/tag/v${requested_version}")
+        if [ "$http_status" = "404" ]; then
+            echo -e "${RED}Error: Release v${requested_version} not found${NC}"
+            echo -e "${MUTED}Available releases: https://github.com/anomalyco/opencode/releases${NC}"
+            exit 1
+        fi
     fi
 fi
 
@@ -187,11 +208,8 @@ check_version() {
     if command -v opencode >/dev/null 2>&1; then
         opencode_path=$(which opencode)
 
-
-        ## TODO: check if version is installed
-        # installed_version=$(opencode version)
-        installed_version="0.0.1"
-        installed_version=$(echo $installed_version | awk '{print $2}')
+        ## Check the installed version
+        installed_version=$(opencode --version 2>/dev/null || echo "")
 
         if [[ "$installed_version" != "$specific_version" ]]; then
             print_message info "${MUTED}Installed version: ${NC}$installed_version."
@@ -267,11 +285,11 @@ download_with_progress() {
     {
         local length=0
         local bytes=0
-        
+
         while IFS=" " read -r -a line; do
             [ "${#line[@]}" -lt 2 ] && continue
             local tag="${line[0]} ${line[1]}"
-            
+
             if [ "$tag" = "0000: content-length:" ]; then
                 length="${line[2]}"
                 length=$(echo "$length" | tr -d '\r')
@@ -296,7 +314,7 @@ download_and_install() {
     print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
     local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$"
     mkdir -p "$tmp_dir"
-    
+
     if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then
         # Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails
         curl -# -L -o "$tmp_dir/$filename" "$url"
@@ -307,14 +325,24 @@ download_and_install() {
     else
         unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
     fi
-    
+
     mv "$tmp_dir/opencode" "$INSTALL_DIR"
     chmod 755 "${INSTALL_DIR}/opencode"
     rm -rf "$tmp_dir"
 }
 
-check_version
-download_and_install
+install_from_binary() {
+    print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}from: ${NC}$binary_path"
+    cp "$binary_path" "${INSTALL_DIR}/opencode"
+    chmod 755 "${INSTALL_DIR}/opencode"
+}
+
+if [ -n "$binary_path" ]; then
+    install_from_binary
+else
+    check_version
+    download_and_install
+fi
 
 
 add_to_path() {
@@ -416,4 +444,3 @@ echo -e ""
 echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
 echo -e ""
 echo -e ""
-

+ 1 - 1
nix/hashes.json

@@ -1,3 +1,3 @@
 {
-  "nodeModules": "sha256-OJ3C4RMzfbbG1Fwa/5yru0rlISj+28UPITMNBEU5AeM="
+  "nodeModules": "sha256-aIP+P0hSnQ50+lhnKvfRzQ8DlOhLjrrwarckxI3LBmE="
 }

+ 6 - 1
nix/scripts/bun-build.ts

@@ -60,7 +60,12 @@ const result = await Bun.build({
   compile: {
     target,
     outfile: "opencode",
-    execArgv: ["--user-agent=opencode/" + version, '--env-file=""', "--"],
+    autoloadBunfig: false,
+    autoloadDotenv: false,
+    //@ts-ignore (bun types aren't up to date)
+    autoloadTsconfig: true,
+    autoloadPackageJson: true,
+    execArgv: ["--user-agent=opencode/" + version, "--use-system-ca", "--"],
     windows: {},
   },
 })

+ 2 - 2
packages/app/README.md

@@ -1,8 +1,8 @@
 ## Usage
 
-Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
+Dependencies for these templates are managed with [pnpm](https://pnpm.io) using `pnpm up -Lri`.
 
-This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
+This is the reason you see a `pnpm-lock.yaml`. That said, any package manager will work. This file can safely be removed once you clone a template.
 
 ```bash
 $ npm install # or pnpm install or yarn install

+ 1 - 30
packages/app/index.html

@@ -14,36 +14,7 @@
     <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">
-      ;(function () {
-        var themeId = localStorage.getItem("opencode-theme-id")
-        if (!themeId) return
-
-        var scheme = localStorage.getItem("opencode-color-scheme") || "system"
-        var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
-        var mode = isDark ? "dark" : "light"
-
-        document.documentElement.dataset.theme = themeId
-        document.documentElement.dataset.colorScheme = mode
-
-        if (themeId === "oc-1") return
-
-        var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
-        if (css) {
-          var style = document.createElement("style")
-          style.id = "oc-theme-preload"
-          style.textContent =
-            ":root{color-scheme:" +
-            mode +
-            ";--text-mix-blend-mode:" +
-            (isDark ? "plus-lighter" : "multiply") +
-            ";" +
-            css +
-            "}"
-          document.head.appendChild(style)
-        }
-      })()
-    </script>
+    <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>

+ 1 - 1
packages/app/package.json

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

+ 28 - 0
packages/app/public/oc-theme-preload.js

@@ -0,0 +1,28 @@
+;(function () {
+  var themeId = localStorage.getItem("opencode-theme-id")
+  if (!themeId) return
+
+  var scheme = localStorage.getItem("opencode-color-scheme") || "system"
+  var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
+  var mode = isDark ? "dark" : "light"
+
+  document.documentElement.dataset.theme = themeId
+  document.documentElement.dataset.colorScheme = mode
+
+  if (themeId === "oc-1") return
+
+  var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
+  if (css) {
+    var style = document.createElement("style")
+    style.id = "oc-theme-preload"
+    style.textContent =
+      ":root{color-scheme:" +
+      mode +
+      ";--text-mix-blend-mode:" +
+      (isDark ? "plus-lighter" : "multiply") +
+      ";" +
+      css +
+      "}"
+    document.head.appendChild(style)
+  }
+})()

+ 47 - 0
packages/app/src/addons/serialize.test.ts

@@ -242,6 +242,53 @@ describe("SerializeAddon", () => {
       expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42")
     })
 
+    test("serialized output should restore after Terminal.reset()", async () => {
+      const { term, addon } = createTerminal()
+
+      const content = [
+        "\x1b[1;32m❯\x1b[0m \x1b[34mcd\x1b[0m /some/path",
+        "\x1b[1;32m❯\x1b[0m \x1b[34mls\x1b[0m -la",
+        "total 42",
+      ].join("\r\n")
+
+      await writeAndWait(term, content)
+
+      const serialized = addon.serialize()
+
+      const { term: term2 } = createTerminal()
+      terminals.push(term2)
+      term2.reset()
+      await writeAndWait(term2, serialized)
+
+      expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path")
+      expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la")
+      expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42")
+    })
+
+    test("alternate buffer should round-trip without garbage", async () => {
+      const { term, addon } = createTerminal(20, 5)
+
+      await writeAndWait(term, "normal\r\n")
+      await writeAndWait(term, "\x1b[?1049h\x1b[HALT")
+
+      expect(term.buffer.active.type).toBe("alternate")
+
+      const serialized = addon.serialize()
+
+      const { term: term2 } = createTerminal(20, 5)
+      terminals.push(term2)
+      await writeAndWait(term2, serialized)
+
+      expect(term2.buffer.active.type).toBe("alternate")
+
+      const line = term2.buffer.active.getLine(0)
+      expect(line?.translateToString(true)).toBe("ALT")
+
+      // Ensure a cell beyond content isn't garbage
+      const cellCode = line?.getCell(10)?.getCode()
+      expect(cellCode === 0 || cellCode === 32).toBe(true)
+    })
+
     test("serialized output written to new terminal should match original colors", async () => {
       const { term, addon } = createTerminal(40, 5)
 

+ 19 - 23
packages/app/src/addons/serialize.ts

@@ -157,23 +157,6 @@ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
 abstract class BaseSerializeHandler {
   constructor(protected readonly _buffer: IBuffer) {}
 
-  private _isRealContent(codepoint: number): boolean {
-    if (codepoint === 0) return false
-    if (codepoint >= 0xf000) return false
-    return true
-  }
-
-  private _findLastContentColumn(line: IBufferLine): number {
-    let lastContent = -1
-    for (let col = 0; col < line.length; col++) {
-      const cell = line.getCell(col)
-      if (cell && this._isRealContent(cell.getCode())) {
-        lastContent = col
-      }
-    }
-    return lastContent + 1
-  }
-
   public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {
     let oldCell = this._buffer.getNullCell()
 
@@ -182,14 +165,14 @@ abstract class BaseSerializeHandler {
     const startColumn = range.start.x
     const endColumn = range.end.x
 
-    this._beforeSerialize(endRow - startRow, startRow, endRow)
+    this._beforeSerialize(endRow - startRow + 1, startRow, endRow)
 
     for (let row = startRow; row <= endRow; row++) {
       const line = this._buffer.getLine(row)
       if (line) {
         const startLineColumn = row === range.start.y ? startColumn : 0
-        const maxColumn = row === range.end.y ? endColumn : this._findLastContentColumn(line)
-        const endLineColumn = Math.min(maxColumn, line.length)
+        const endLineColumn = Math.min(endColumn, line.length)
+
         for (let col = startLineColumn; col < endLineColumn; col++) {
           const c = line.getCell(col)
           if (!c) {
@@ -243,6 +226,13 @@ class StringSerializeHandler extends BaseSerializeHandler {
 
   protected _beforeSerialize(rows: number, start: number, _end: number): void {
     this._allRows = new Array<string>(rows)
+    this._allRowSeparators = new Array<string>(rows)
+    this._rowIndex = 0
+
+    this._currentRow = ""
+    this._nullCellCount = 0
+    this._cursorStyle = this._buffer.getNullCell()
+
     this._lastContentCursorRow = start
     this._lastCursorRow = start
     this._firstRow = start
@@ -251,6 +241,11 @@ class StringSerializeHandler extends BaseSerializeHandler {
   protected _rowEnd(row: number, isLastRow: boolean): void {
     let rowSeparator = ""
 
+    if (this._nullCellCount > 0) {
+      this._currentRow += " ".repeat(this._nullCellCount)
+      this._nullCellCount = 0
+    }
+
     if (!isLastRow) {
       const nextLine = this._buffer.getLine(row + 1)
 
@@ -388,7 +383,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
     }
 
     const codepoint = cell.getCode()
-    const isGarbage = codepoint >= 0xf000
+    const isInvalidCodepoint = codepoint > 0x10ffff || (codepoint >= 0xd800 && codepoint <= 0xdfff)
+    const isGarbage = isInvalidCodepoint || (codepoint >= 0xf000 && cell.getWidth() === 1)
     const isEmptyCell = codepoint === 0 || cell.getChars() === "" || isGarbage
 
     const sgrSeq = this._diffStyle(cell, this._cursorStyle)
@@ -397,7 +393,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
 
     if (styleChanged) {
       if (this._nullCellCount > 0) {
-        this._currentRow += `\u001b[${this._nullCellCount}C`
+        this._currentRow += " ".repeat(this._nullCellCount)
         this._nullCellCount = 0
       }
 
@@ -417,7 +413,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
       this._nullCellCount += cell.getWidth()
     } else {
       if (this._nullCellCount > 0) {
-        this._currentRow += `\u001b[${this._nullCellCount}C`
+        this._currentRow += " ".repeat(this._nullCellCount)
         this._nullCellCount = 0
       }
 

+ 71 - 59
packages/app/src/app.tsx

@@ -1,5 +1,5 @@
 import "@/index.css"
-import { ErrorBoundary, Show, type ParentProps } from "solid-js"
+import { ErrorBoundary, Show, lazy, type ParentProps } from "solid-js"
 import { Router, Route, Navigate } from "@solidjs/router"
 import { MetaProvider } from "@solidjs/meta"
 import { Font } from "@opencode-ai/ui/font"
@@ -20,23 +20,24 @@ import { FileProvider } from "@/context/file"
 import { NotificationProvider } from "@/context/notification"
 import { DialogProvider } from "@opencode-ai/ui/context/dialog"
 import { CommandProvider } from "@/context/command"
+import { Logo } from "@opencode-ai/ui/logo"
 import Layout from "@/pages/layout"
-import Home from "@/pages/home"
 import DirectoryLayout from "@/pages/directory-layout"
-import Session from "@/pages/session"
 import { ErrorPage } from "./pages/error"
 import { iife } from "@opencode-ai/util/iife"
+import { Suspense } from "solid-js"
+
+const Home = lazy(() => import("@/pages/home"))
+const Session = lazy(() => import("@/pages/session"))
+const Loading = () => <div class="size-full flex items-center justify-center text-text-weak">Loading...</div>
 
 declare global {
   interface Window {
-    __OPENCODE__?: { updaterEnabled?: boolean; port?: number }
+    __OPENCODE__?: { updaterEnabled?: boolean; port?: number; serverReady?: boolean }
   }
 }
 
 const defaultServerUrl = iife(() => {
-  const param = new URLSearchParams(document.location.search).get("url")
-  if (param) return param
-
   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)
@@ -45,16 +46,7 @@ const defaultServerUrl = iife(() => {
   return window.location.origin
 })
 
-function ServerKey(props: ParentProps) {
-  const server = useServer()
-  return (
-    <Show when={server.url} keyed>
-      {props.children}
-    </Show>
-  )
-}
-
-export function App() {
+export function AppBaseProviders(props: ParentProps) {
   return (
     <MetaProvider>
       <Font />
@@ -63,48 +55,7 @@ export function App() {
           <DialogProvider>
             <MarkedProvider>
               <DiffComponentProvider component={Diff}>
-                <CodeComponentProvider component={Code}>
-                  <ServerProvider defaultUrl={defaultServerUrl}>
-                    <ServerKey>
-                      <GlobalSDKProvider>
-                        <GlobalSyncProvider>
-                          <Router
-                            root={(props) => (
-                              <PermissionProvider>
-                                <LayoutProvider>
-                                  <NotificationProvider>
-                                    <CommandProvider>
-                                      <Layout>{props.children}</Layout>
-                                    </CommandProvider>
-                                  </NotificationProvider>
-                                </LayoutProvider>
-                              </PermissionProvider>
-                            )}
-                          >
-                            <Route path="/" component={Home} />
-                            <Route path="/:dir" component={DirectoryLayout}>
-                              <Route path="/" component={() => <Navigate href="session" />} />
-                              <Route
-                                path="/session/:id?"
-                                component={(p) => (
-                                  <Show when={p.params.id ?? "new"} keyed>
-                                    <TerminalProvider>
-                                      <FileProvider>
-                                        <PromptProvider>
-                                          <Session />
-                                        </PromptProvider>
-                                      </FileProvider>
-                                    </TerminalProvider>
-                                  </Show>
-                                )}
-                              />
-                            </Route>
-                          </Router>
-                        </GlobalSyncProvider>
-                      </GlobalSDKProvider>
-                    </ServerKey>
-                  </ServerProvider>
-                </CodeComponentProvider>
+                <CodeComponentProvider component={Code}>{props.children}</CodeComponentProvider>
               </DiffComponentProvider>
             </MarkedProvider>
           </DialogProvider>
@@ -113,3 +64,64 @@ export function App() {
     </MetaProvider>
   )
 }
+
+function ServerKey(props: ParentProps) {
+  const server = useServer()
+  return (
+    <Show when={server.url} keyed>
+      {props.children}
+    </Show>
+  )
+}
+
+export function AppInterface() {
+  return (
+    <ServerProvider defaultUrl={defaultServerUrl}>
+      <ServerKey>
+        <GlobalSDKProvider>
+          <GlobalSyncProvider>
+            <Router
+              root={(props) => (
+                <PermissionProvider>
+                  <LayoutProvider>
+                    <NotificationProvider>
+                      <CommandProvider>
+                        <Layout>{props.children}</Layout>
+                      </CommandProvider>
+                    </NotificationProvider>
+                  </LayoutProvider>
+                </PermissionProvider>
+              )}
+            >
+              <Route
+                path="/"
+                component={() => (
+                  <Suspense fallback={<Loading />}>
+                    <Home />
+                  </Suspense>
+                )}
+              />
+              <Route path="/:dir" component={DirectoryLayout}>
+                <Route path="/" component={() => <Navigate href="session" />} />
+                <Route
+                  path="/session/:id?"
+                  component={() => (
+                    <TerminalProvider>
+                      <FileProvider>
+                        <PromptProvider>
+                          <Suspense fallback={<Loading />}>
+                            <Session />
+                          </Suspense>
+                        </PromptProvider>
+                      </FileProvider>
+                    </TerminalProvider>
+                  )}
+                />
+              </Route>
+            </Router>
+          </GlobalSyncProvider>
+        </GlobalSDKProvider>
+      </ServerKey>
+    </ServerProvider>
+  )
+}

+ 1 - 5
packages/app/src/components/dialog-edit-project.tsx

@@ -7,15 +7,11 @@ import { createMemo, createSignal, For, Show } from "solid-js"
 import { createStore } from "solid-js/store"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { type LocalProject, getAvatarColors } from "@/context/layout"
+import { getFilename } from "@opencode-ai/util/path"
 import { Avatar } from "@opencode-ai/ui/avatar"
 
 const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
 
-function getFilename(input: string) {
-  const parts = input.split("/")
-  return parts[parts.length - 1] || input
-}
-
 export function DialogEditProject(props: { project: LocalProject }) {
   const dialog = useDialog()
   const globalSDK = useGlobalSDK()

+ 2 - 0
packages/app/src/components/dialog-select-file.tsx

@@ -15,6 +15,7 @@ export function DialogSelectFile() {
   const params = useParams()
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
   const tabs = createMemo(() => layout.tabs(sessionKey()))
+  const view = createMemo(() => layout.view(sessionKey()))
   return (
     <Dialog title="Select file">
       <List
@@ -27,6 +28,7 @@ export function DialogSelectFile() {
             const value = file.tab(path)
             tabs().open(value)
             file.load(path)
+            view().reviewPanel.open()
           }
           dialog.close()
         }}

+ 1 - 1
packages/app/src/components/dialog-select-model.tsx

@@ -76,7 +76,7 @@ export const ModelSelectorPopover: Component<{
     <Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
       <Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
       <Kobalte.Portal>
-        <Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none">
+        <Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
           <Kobalte.Title class="sr-only">Select model</Kobalte.Title>
           <ModelList provider={props.provider} onSelect={() => setOpen(false)} class="p-1" />
         </Kobalte.Content>

+ 62 - 42
packages/app/src/components/prompt-input.tsx

@@ -1,5 +1,17 @@
 import { useFilteredList } from "@opencode-ai/ui/hooks"
-import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createMemo } from "solid-js"
+import {
+  createEffect,
+  on,
+  Component,
+  Show,
+  For,
+  onMount,
+  onCleanup,
+  Switch,
+  Match,
+  createMemo,
+  createSignal,
+} from "solid-js"
 import { createStore, produce } from "solid-js/store"
 import { createFocusSignal } from "@solid-primitives/active-element"
 import { useLocal } from "@/context/local"
@@ -30,7 +42,7 @@ import { ModelSelectorPopover } from "@/components/dialog-select-model"
 import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
 import { useProviders } from "@/hooks/use-providers"
 import { useCommand } from "@/context/command"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
 import { Identifier } from "@/utils/id"
 import { SessionContextUsage } from "@/components/session-context-usage"
 import { usePermission } from "@/context/permission"
@@ -153,6 +165,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       },
   )
   const working = createMemo(() => status()?.type !== "idle")
+  const imageAttachments = createMemo(
+    () => prompt.current().filter((part) => part.type === "image") as ImageAttachmentPart[],
+  )
 
   const [store, setStore] = createStore<{
     popover: "at" | "slash" | null
@@ -160,7 +175,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     savedPrompt: Prompt | null
     placeholder: number
     dragging: boolean
-    imageAttachments: ImageAttachmentPart[]
     mode: "normal" | "shell"
     applyingHistory: boolean
   }>({
@@ -169,14 +183,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     savedPrompt: null,
     placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
     dragging: false,
-    imageAttachments: [],
     mode: "normal",
     applyingHistory: false,
   })
 
   const MAX_HISTORY = 100
   const [history, setHistory] = persisted(
-    "prompt-history.v1",
+    Persist.global("prompt-history", ["prompt-history.v1"]),
     createStore<{
       entries: Prompt[]
     }>({
@@ -184,7 +197,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     }),
   )
   const [shellHistory, setShellHistory] = persisted(
-    "prompt-history-shell.v1",
+    Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]),
     createStore<{
       entries: Prompt[]
     }>({
@@ -235,6 +248,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     }
   }
 
+  const isFocused = createFocusSignal(() => editorRef)
+
   createEffect(() => {
     params.id
     editorRef.focus()
@@ -245,7 +260,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     onCleanup(() => clearInterval(interval))
   })
 
-  const isFocused = createFocusSignal(() => editorRef)
+  const [composing, setComposing] = createSignal(false)
+  const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
 
   const addImageAttachment = async (file: File) => {
     if (!ACCEPTED_FILE_TYPES.includes(file.type)) return
@@ -260,21 +276,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
         mime: file.type,
         dataUrl,
       }
-      setStore(
-        produce((draft) => {
-          draft.imageAttachments.push(attachment)
-        }),
-      )
+      const cursorPosition = prompt.cursor() ?? getCursorPosition(editorRef)
+      prompt.set([...prompt.current(), attachment], cursorPosition)
     }
     reader.readAsDataURL(file)
   }
 
   const removeImageAttachment = (id: string) => {
-    setStore(
-      produce((draft) => {
-        draft.imageAttachments = draft.imageAttachments.filter((a) => a.id !== id)
-      }),
-    )
+    const current = prompt.current()
+    const next = current.filter((part) => part.type !== "image" || part.id !== id)
+    prompt.set(next, prompt.cursor())
   }
 
   const handlePaste = async (event: ClipboardEvent) => {
@@ -282,12 +293,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     const clipboardData = event.clipboardData
     if (!clipboardData) return
 
+    event.preventDefault()
+    event.stopPropagation()
+
     const items = Array.from(clipboardData.items)
     const imageItems = items.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type))
 
     if (imageItems.length > 0) {
-      event.preventDefault()
-      event.stopPropagation()
       for (const item of imageItems) {
         const file = item.getAsFile()
         if (file) await addImageAttachment(file)
@@ -295,8 +307,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       return
     }
 
-    event.preventDefault()
-    event.stopPropagation()
     const plainText = clipboardData.getData("text/plain") ?? ""
     addPart({ type: "text", content: plainText, start: 0, end: 0 })
   }
@@ -337,13 +347,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   }
 
   onMount(() => {
-    editorRef.addEventListener("paste", handlePaste)
     document.addEventListener("dragover", handleGlobalDragOver)
     document.addEventListener("dragleave", handleGlobalDragLeave)
     document.addEventListener("drop", handleGlobalDrop)
   })
   onCleanup(() => {
-    editorRef.removeEventListener("paste", handlePaste)
     document.removeEventListener("dragover", handleGlobalDragOver)
     document.removeEventListener("dragleave", handleGlobalDragLeave)
     document.removeEventListener("drop", handleGlobalDrop)
@@ -524,8 +532,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     on(
       () => prompt.current(),
       (currentParts) => {
+        const inputParts = currentParts.filter((part) => part.type !== "image") as Prompt
         const domParts = parseFromDOM()
-        if (isNormalizedEditor() && isPromptEqual(currentParts, domParts)) return
+        if (isNormalizedEditor() && isPromptEqual(inputParts, domParts)) return
 
         const selection = window.getSelection()
         let cursorPosition: number | null = null
@@ -533,7 +542,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
           cursorPosition = getCursorPosition(editorRef)
         }
 
-        renderEditor(currentParts)
+        renderEditor(inputParts)
 
         if (cursorPosition !== null) {
           setCursorPosition(editorRef, cursorPosition)
@@ -624,11 +633,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
 
   const handleInput = () => {
     const rawParts = parseFromDOM()
+    const images = imageAttachments()
     const cursorPosition = getCursorPosition(editorRef)
     const rawText = rawParts.map((p) => ("content" in p ? p.content : "")).join("")
     const trimmed = rawText.replace(/\u200B/g, "").trim()
     const hasNonText = rawParts.some((part) => part.type !== "text")
-    const shouldReset = trimmed.length === 0 && !hasNonText
+    const shouldReset = trimmed.length === 0 && !hasNonText && images.length === 0
 
     if (shouldReset) {
       setStore("popover", null)
@@ -667,7 +677,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       setStore("savedPrompt", null)
     }
 
-    prompt.set(rawParts, cursorPosition)
+    prompt.set([...rawParts, ...images], cursorPosition)
     queueScroll()
   }
 
@@ -770,16 +780,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       .map((p) => ("content" in p ? p.content : ""))
       .join("")
       .trim()
-    if (!text) return
+    const hasImages = prompt.some((part) => part.type === "image")
+    if (!text && !hasImages) return
 
     const entry = clonePromptParts(prompt)
     const currentHistory = mode === "shell" ? shellHistory : history
     const setCurrentHistory = mode === "shell" ? setShellHistory : setHistory
     const lastEntry = currentHistory.entries[0]
-    if (lastEntry) {
-      const lastText = lastEntry.map((p) => ("content" in p ? p.content : "")).join("")
-      if (lastText === text) return
-    }
+    if (lastEntry && isPromptEqual(lastEntry, entry)) return
 
     setCurrentHistory("entries", (entries) => [entry, ...entries].slice(0, MAX_HISTORY))
   }
@@ -868,6 +876,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       }
     }
 
+    if (event.key === "Enter" && isImeComposing(event)) {
+      return
+    }
+
     if (store.popover && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) {
       if (store.popover === "at") {
         atOnKeyDown(event)
@@ -949,7 +961,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
 
     const currentPrompt = prompt.current()
     const text = currentPrompt.map((part) => ("content" in part ? part.content : "")).join("")
-    const images = store.imageAttachments.slice()
+    const images = imageAttachments().slice()
     const mode = store.mode
 
     if (text.trim().length === 0 && images.length === 0) {
@@ -1043,14 +1055,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
 
     const clearInput = () => {
       prompt.reset()
-      setStore("imageAttachments", [])
       setStore("mode", "normal")
       setStore("popover", null)
     }
 
     const restoreInput = () => {
       prompt.set(currentPrompt, promptLength(currentPrompt))
-      setStore("imageAttachments", images)
       setStore("mode", mode)
       setStore("popover", null)
       requestAnimationFrame(() => {
@@ -1093,6 +1103,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             agent,
             model: `${model.providerID}/${model.modelID}`,
             variant,
+            parts: images.map((attachment) => ({
+              id: Identifier.ascending("part"),
+              type: "file" as const,
+              mime: attachment.mime,
+              url: attachment.dataUrl,
+              filename: attachment.filename,
+            })),
           })
           .catch((err) => {
             showToast({
@@ -1446,9 +1463,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             </For>
           </div>
         </Show>
-        <Show when={store.imageAttachments.length > 0}>
+        <Show when={imageAttachments().length > 0}>
           <div class="flex flex-wrap gap-2 px-3 pt-3">
-            <For each={store.imageAttachments}>
+            <For each={imageAttachments()}>
               {(attachment) => (
                 <div class="relative group">
                   <Show
@@ -1489,6 +1506,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             }}
             contenteditable="true"
             onInput={handleInput}
+            onPaste={handlePaste}
+            onCompositionStart={() => setComposing(true)}
+            onCompositionEnd={() => setComposing(false)}
             onKeyDown={handleKeyDown}
             classList={{
               "select-text": true,
@@ -1498,7 +1518,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
               "font-mono!": store.mode === "shell",
             }}
           />
-          <Show when={!prompt.dirty() && store.imageAttachments.length === 0}>
+          <Show when={!prompt.dirty()}>
             <div class="absolute top-0 inset-x-0 px-5 py-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
               {store.mode === "shell"
                 ? "Enter shell command..."
@@ -1572,14 +1592,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                       onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
                       classList={{
                         "_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
-                        "text-text-base": !permission.isAutoAccepting(params.id!),
-                        "hover:bg-surface-success-base": permission.isAutoAccepting(params.id!),
+                        "text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
+                        "hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
                       }}
                     >
                       <Icon
                         name="chevron-double-right"
                         size="small"
-                        classList={{ "text-icon-success-base": permission.isAutoAccepting(params.id!) }}
+                        classList={{ "text-icon-success-base": permission.isAutoAccepting(params.id!, sdk.directory) }}
                       />
                     </Button>
                   </TooltipKeybind>
@@ -1631,7 +1651,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             >
               <IconButton
                 type="submit"
-                disabled={!prompt.dirty() && store.imageAttachments.length === 0 && !working()}
+                disabled={!prompt.dirty() && !working()}
                 icon={working() ? "stop" : "arrow-up"}
                 variant="primary"
                 class="h-6 w-4.5"

+ 2 - 1
packages/app/src/components/session-context-usage.tsx

@@ -20,6 +20,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
   const variant = createMemo(() => props.variant ?? "button")
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
   const tabs = createMemo(() => layout.tabs(sessionKey()))
+  const view = createMemo(() => layout.view(sessionKey()))
   const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
 
   const cost = createMemo(() => {
@@ -48,7 +49,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
 
   const openContext = () => {
     if (!params.id) return
-    layout.review.open()
+    view().reviewPanel.open()
     tabs().open("context")
     tabs().setActive("context")
   }

+ 80 - 22
packages/app/src/components/session/session-header.tsx

@@ -1,4 +1,4 @@
-import { createEffect, createMemo, createResource, Show } from "solid-js"
+import { createMemo, createResource, Show } from "solid-js"
 import { A, useNavigate, useParams } from "@solidjs/router"
 import { useLayout } from "@/context/layout"
 import { useCommand } from "@/context/command"
@@ -20,6 +20,7 @@ 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()
@@ -34,8 +35,16 @@ export function SessionHeader() {
   const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
 
   const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
-  const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
+  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)}`)
@@ -43,6 +52,8 @@ export function SessionHeader() {
 
   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}`)
   }
 
@@ -60,7 +71,7 @@ export function SessionHeader() {
           <div class="flex items-center gap-2 min-w-0">
             <div class="hidden xl:flex items-center gap-2">
               <Select
-                options={layout.projects.list().map((project) => project.worktree)}
+                options={worktrees()}
                 current={sync.project?.worktree ?? projectDirectory()}
                 label={(x) => getFilename(x)}
                 onSelect={(x) => (x ? navigateToProject(x) : undefined)}
@@ -77,18 +88,56 @@ export function SessionHeader() {
               </Select>
               <div class="text-text-weaker">/</div>
             </div>
-            <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={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"
+                  />
+                </>
+              }
+            >
+              <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())}
+                    >
+                      <Icon name="arrow-left" size="small" class="text-icon-base" />
+                    </button>
+                  </Tooltip>
+                </div>
+              </div>
+            </Show>
           </div>
-          <Show when={currentSession()}>
+          <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>
@@ -124,20 +173,24 @@ export function SessionHeader() {
                 title="Toggle review"
                 keybind={command.keybind("review.toggle")}
               >
-                <Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
+                <Button
+                  variant="ghost"
+                  class="group/review-toggle size-6 p-0"
+                  onClick={() => view().reviewPanel.toggle()}
+                >
                   <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
                     <Icon
-                      name={layout.review.opened() ? "layout-right" : "layout-left"}
+                      name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
                       size="small"
                       class="group-hover/review-toggle:hidden"
                     />
                     <Icon
-                      name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
+                      name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
                       size="small"
                       class="hidden group-hover/review-toggle:inline-block"
                     />
                     <Icon
-                      name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
+                      name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
                       size="small"
                       class="hidden group-active/review-toggle:inline-block"
                     />
@@ -150,11 +203,11 @@ export function SessionHeader() {
               title="Toggle terminal"
               keybind={command.keybind("terminal.toggle")}
             >
-              <Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.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={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
+                    name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
                     class="group-hover/terminal-toggle:hidden"
                   />
                   <Icon
@@ -164,7 +217,7 @@ export function SessionHeader() {
                   />
                   <Icon
                     size="small"
-                    name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
+                    name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
                     class="hidden group-active/terminal-toggle:inline-block"
                   />
                 </div>
@@ -197,8 +250,13 @@ export function SessionHeader() {
                     }
                     return shareURL
                   },
+                  { initialValue: "" },
+                )
+                return (
+                  <Show when={url.latest}>
+                    {(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
+                  </Show>
                 )
-                return <Show when={url()}>{(url) => <TextField value={url()} readOnly copyable class="w-72" />}</Show>
               })}
             </Popover>
           </Show>

+ 14 - 4
packages/app/src/components/session/session-new-view.tsx

@@ -23,11 +23,18 @@ export function NewSessionView(props: NewSessionViewProps) {
     if (options().includes(selection)) return selection
     return MAIN_WORKTREE
   })
+  const projectRoot = createMemo(() => sync.project?.worktree ?? sync.data.path.directory)
+  const isWorktree = createMemo(() => {
+    const project = sync.project
+    if (!project) return false
+    return sync.data.path.directory !== project.worktree
+  })
 
   const label = (value: string) => {
     if (value === MAIN_WORKTREE) {
+      if (isWorktree()) return "Main branch"
       const branch = sync.data.vcs?.branch
-      if (branch) return `Current branch (${branch})`
+      if (branch) return `Main branch (${branch})`
       return "Main branch"
     }
 
@@ -37,13 +44,16 @@ export function NewSessionView(props: NewSessionViewProps) {
   }
 
   return (
-    <div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
+    <div
+      class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6"
+      style={{ "padding-bottom": "calc(var(--prompt-height, 11.25rem) + 64px)" }}
+    >
       <div class="text-20-medium text-text-weaker">New session</div>
       <div class="flex justify-center items-center gap-3">
         <Icon name="folder" size="small" />
         <div class="text-12-medium text-text-weak">
-          {getDirectory(sync.data.path.directory)}
-          <span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
+          {getDirectory(projectRoot())}
+          <span class="text-text-strong">{getFilename(projectRoot())}</span>
         </div>
       </div>
       <div class="flex justify-center items-center gap-1">

+ 1 - 0
packages/app/src/components/session/session-sortable-tab.tsx

@@ -39,6 +39,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
             </Tooltip>
           }
           hideCloseButton
+          onMiddleClick={() => props.onTabClose(props.tab)}
         >
           <Show when={path()}>{(p) => <FileVisual path={p()} />}</Show>
         </Tabs.Trigger>

+ 118 - 68
packages/app/src/components/terminal.tsx

@@ -1,9 +1,9 @@
-import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
+import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
 import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
 import { useSDK } from "@/context/sdk"
 import { SerializeAddon } from "@/addons/serialize"
 import { LocalPTY } from "@/context/terminal"
-import { resolveThemeVariant, useTheme } from "@opencode-ai/ui/theme"
+import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme"
 
 export interface TerminalProps extends ComponentProps<"div"> {
   pty: LocalPTY
@@ -16,6 +16,7 @@ type TerminalColors = {
   background: string
   foreground: string
   cursor: string
+  selectionBackground: string
 }
 
 const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
@@ -23,11 +24,13 @@ const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
     background: "#fcfcfc",
     foreground: "#211e1e",
     cursor: "#211e1e",
+    selectionBackground: withAlpha("#211e1e", 0.2),
   },
   dark: {
     background: "#191515",
     foreground: "#d4d4d4",
     cursor: "#d4d4d4",
+    selectionBackground: withAlpha("#d4d4d4", 0.25),
   },
 }
 
@@ -36,12 +39,16 @@ export const Terminal = (props: TerminalProps) => {
   const theme = useTheme()
   let container!: HTMLDivElement
   const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
-  let ws: WebSocket
-  let term: Term
+  let ws: WebSocket | undefined
+  let term: Term | undefined
   let ghostty: Ghostty
   let serializeAddon: SerializeAddon
   let fitAddon: FitAddon
   let handleResize: () => void
+  let handleTextareaFocus: () => void
+  let handleTextareaBlur: () => void
+  let reconnect: number | undefined
+  let disposed = false
 
   const getTerminalColors = (): TerminalColors => {
     const mode = theme.mode()
@@ -51,12 +58,16 @@ export const Terminal = (props: TerminalProps) => {
     const variant = mode === "dark" ? currentTheme.dark : currentTheme.light
     if (!variant?.seeds) return fallback
     const resolved = resolveThemeVariant(variant, mode === "dark")
-    const text = resolved["text-base"] ?? fallback.foreground
+    const text = resolved["text-stronger"] ?? fallback.foreground
     const background = resolved["background-stronger"] ?? fallback.background
+    const alpha = mode === "dark" ? 0.25 : 0.2
+    const base = text.startsWith("#") ? (text as HexColor) : (fallback.foreground as HexColor)
+    const selectionBackground = withAlpha(base, alpha)
     return {
       background,
       foreground: text,
       cursor: text,
+      selectionBackground,
     }
   }
 
@@ -71,27 +82,11 @@ export const Terminal = (props: TerminalProps) => {
     setOption("theme", colors)
   })
 
-  const focusTerminal = () => term?.focus()
-  const copySelection = () => {
-    if (!term || !term.hasSelection()) return false
-    const selection = term.getSelection()
-    if (!selection) return false
-    const clipboard = navigator.clipboard
-    if (clipboard?.writeText) {
-      clipboard.writeText(selection).catch(() => {})
-      return true
-    }
-    if (!document.body) return false
-    const textarea = document.createElement("textarea")
-    textarea.value = selection
-    textarea.setAttribute("readonly", "")
-    textarea.style.position = "fixed"
-    textarea.style.opacity = "0"
-    document.body.appendChild(textarea)
-    textarea.select()
-    const copied = document.execCommand("copy")
-    document.body.removeChild(textarea)
-    return copied
+  const focusTerminal = () => {
+    const t = term
+    if (!t) return
+    t.focus()
+    setTimeout(() => t.textarea?.focus(), 0)
   }
   const handlePointerDown = () => {
     const activeElement = document.activeElement
@@ -102,11 +97,17 @@ export const Terminal = (props: TerminalProps) => {
   }
 
   onMount(async () => {
-    ghostty = await Ghostty.load()
+    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)}`,
+    )
+    ws = socket
 
-    ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
-    term = new Term({
+    const t = new mod.Terminal({
       cursorBlink: true,
+      cursorStyle: "bar",
       fontSize: 14,
       fontFamily: "IBM Plex Mono, monospace",
       allowTransparency: true,
@@ -114,50 +115,94 @@ export const Terminal = (props: TerminalProps) => {
       scrollback: 10_000,
       ghostty,
     })
-    term.attachCustomKeyEventHandler((event) => {
+    term = t
+
+    const copy = () => {
+      const selection = t.getSelection()
+      if (!selection) return false
+
+      const body = document.body
+      if (body) {
+        const textarea = document.createElement("textarea")
+        textarea.value = selection
+        textarea.setAttribute("readonly", "")
+        textarea.style.position = "fixed"
+        textarea.style.opacity = "0"
+        body.appendChild(textarea)
+        textarea.select()
+        const copied = document.execCommand("copy")
+        body.removeChild(textarea)
+        if (copied) return true
+      }
+
+      const clipboard = navigator.clipboard
+      if (clipboard?.writeText) {
+        clipboard.writeText(selection).catch(() => {})
+        return true
+      }
+
+      return false
+    }
+
+    t.attachCustomKeyEventHandler((event) => {
       const key = event.key.toLowerCase()
-      if (key === "c") {
-        const macCopy = event.metaKey && !event.ctrlKey && !event.altKey
-        const linuxCopy = event.ctrlKey && event.shiftKey && !event.metaKey
-        if ((macCopy || linuxCopy) && copySelection()) {
-          event.preventDefault()
-          return true
-        }
+
+      if (event.ctrlKey && event.shiftKey && !event.metaKey && key === "c") {
+        copy()
+        return true
       }
+
+      if (event.metaKey && !event.ctrlKey && !event.altKey && key === "c") {
+        if (!t.hasSelection()) return true
+        copy()
+        return true
+      }
+
       // allow for ctrl-` to toggle terminal in parent
       if (event.ctrlKey && key === "`") {
-        event.preventDefault()
         return true
       }
+
       return false
     })
 
-    fitAddon = new FitAddon()
+    fitAddon = new mod.FitAddon()
     serializeAddon = new SerializeAddon()
-    term.loadAddon(serializeAddon)
-    term.loadAddon(fitAddon)
+    t.loadAddon(serializeAddon)
+    t.loadAddon(fitAddon)
 
-    term.open(container)
+    t.open(container)
     container.addEventListener("pointerdown", handlePointerDown)
+
+    handleTextareaFocus = () => {
+      t.options.cursorBlink = true
+    }
+    handleTextareaBlur = () => {
+      t.options.cursorBlink = false
+    }
+
+    t.textarea?.addEventListener("focus", handleTextareaFocus)
+    t.textarea?.addEventListener("blur", handleTextareaBlur)
+
     focusTerminal()
 
     if (local.pty.buffer) {
       if (local.pty.rows && local.pty.cols) {
-        term.resize(local.pty.cols, local.pty.rows)
+        t.resize(local.pty.cols, local.pty.rows)
       }
-      term.reset()
-      term.write(local.pty.buffer)
-      if (local.pty.scrollY) {
-        term.scrollToLine(local.pty.scrollY)
-      }
-      fitAddon.fit()
+      t.write(local.pty.buffer, () => {
+        if (local.pty.scrollY) {
+          t.scrollToLine(local.pty.scrollY)
+        }
+        fitAddon.fit()
+      })
     }
 
     fitAddon.observeResize()
     handleResize = () => fitAddon.fit()
     window.addEventListener("resize", handleResize)
-    term.onResize(async (size) => {
-      if (ws && ws.readyState === WebSocket.OPEN) {
+    t.onResize(async (size) => {
+      if (socket.readyState === WebSocket.OPEN) {
         await sdk.client.pty
           .update({
             ptyID: local.pty.id,
@@ -169,39 +214,39 @@ export const Terminal = (props: TerminalProps) => {
           .catch(() => {})
       }
     })
-    term.onData((data) => {
-      if (ws && ws.readyState === WebSocket.OPEN) {
-        ws.send(data)
+    t.onData((data) => {
+      if (socket.readyState === WebSocket.OPEN) {
+        socket.send(data)
       }
     })
-    term.onKey((key) => {
+    t.onKey((key) => {
       if (key.key == "Enter") {
         props.onSubmit?.()
       }
     })
-    // term.onScroll((ydisp) => {
+    // t.onScroll((ydisp) => {
     // console.log("Scroll position:", ydisp)
     // })
-    ws.addEventListener("open", () => {
+    socket.addEventListener("open", () => {
       console.log("WebSocket connected")
       sdk.client.pty
         .update({
           ptyID: local.pty.id,
           size: {
-            cols: term.cols,
-            rows: term.rows,
+            cols: t.cols,
+            rows: t.rows,
           },
         })
         .catch(() => {})
     })
-    ws.addEventListener("message", (event) => {
-      term.write(event.data)
+    socket.addEventListener("message", (event) => {
+      t.write(event.data)
     })
-    ws.addEventListener("error", (error) => {
+    socket.addEventListener("error", (error) => {
       console.error("WebSocket error:", error)
       props.onConnectError?.(error)
     })
-    ws.addEventListener("close", () => {
+    socket.addEventListener("close", () => {
       console.log("WebSocket disconnected")
     })
   })
@@ -211,18 +256,23 @@ export const Terminal = (props: TerminalProps) => {
       window.removeEventListener("resize", handleResize)
     }
     container.removeEventListener("pointerdown", handlePointerDown)
-    if (serializeAddon && props.onCleanup) {
+    term?.textarea?.removeEventListener("focus", handleTextareaFocus)
+    term?.textarea?.removeEventListener("blur", handleTextareaBlur)
+
+    const t = term
+    if (serializeAddon && props.onCleanup && t) {
       const buffer = serializeAddon.serialize()
       props.onCleanup({
         ...local.pty,
         buffer,
-        rows: term.rows,
-        cols: term.cols,
-        scrollY: term.getViewportY(),
+        rows: t.rows,
+        cols: t.cols,
+        scrollY: t.getViewportY(),
       })
     }
+
     ws?.close()
-    term?.dispose()
+    t?.dispose()
   })
 
   return (

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

@@ -177,8 +177,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
     const dialog = useDialog()
 
     const options = createMemo(() => {
-      const all = registrations().flatMap((x) => x())
+      const seen = new Set<string>()
+      const all: CommandOption[] = []
+
+      for (const reg of registrations()) {
+        for (const opt of reg()) {
+          if (seen.has(opt.id)) continue
+          seen.add(opt.id)
+          all.push(opt)
+        }
+      }
+
       const suggested = all.filter((x) => x.suggested && !x.disabled)
+
       return [
         ...suggested.map((x) => ({
           ...x,

+ 149 - 38
packages/app/src/context/file.tsx

@@ -1,4 +1,4 @@
-import { createMemo, onCleanup } from "solid-js"
+import { createEffect, createMemo, createRoot, onCleanup } from "solid-js"
 import { createStore, produce } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import type { FileContent } from "@opencode-ai/sdk/v2"
@@ -7,7 +7,7 @@ import { useParams } from "@solidjs/router"
 import { getFilename } from "@opencode-ai/util/path"
 import { useSDK } from "./sdk"
 import { useSync } from "./sync"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
 
 export type FileSelection = {
   startLine: number
@@ -82,8 +82,106 @@ function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange {
   }
 }
 
+const WORKSPACE_KEY = "__workspace__"
+const MAX_FILE_VIEW_SESSIONS = 20
+const MAX_VIEW_FILES = 500
+
+type ViewSession = ReturnType<typeof createViewSession>
+
+type ViewCacheEntry = {
+  value: ViewSession
+  dispose: VoidFunction
+}
+
+function createViewSession(dir: string, id: string | undefined) {
+  const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1`
+
+  const [view, setView, _, ready] = persisted(
+    Persist.scoped(dir, id, "file-view", [legacyViewKey]),
+    createStore<{
+      file: Record<string, FileViewState>
+    }>({
+      file: {},
+    }),
+  )
+
+  const meta = { pruned: false }
+
+  const pruneView = (keep?: string) => {
+    const keys = Object.keys(view.file)
+    if (keys.length <= MAX_VIEW_FILES) return
+
+    const drop = keys.filter((key) => key !== keep).slice(0, keys.length - MAX_VIEW_FILES)
+    if (drop.length === 0) return
+
+    setView(
+      produce((draft) => {
+        for (const key of drop) {
+          delete draft.file[key]
+        }
+      }),
+    )
+  }
+
+  createEffect(() => {
+    if (!ready()) return
+    if (meta.pruned) return
+    meta.pruned = true
+    pruneView()
+  })
+
+  const scrollTop = (path: string) => view.file[path]?.scrollTop
+  const scrollLeft = (path: string) => view.file[path]?.scrollLeft
+  const selectedLines = (path: string) => view.file[path]?.selectedLines
+
+  const setScrollTop = (path: string, top: number) => {
+    setView("file", path, (current) => {
+      if (current?.scrollTop === top) return current
+      return {
+        ...(current ?? {}),
+        scrollTop: top,
+      }
+    })
+    pruneView(path)
+  }
+
+  const setScrollLeft = (path: string, left: number) => {
+    setView("file", path, (current) => {
+      if (current?.scrollLeft === left) return current
+      return {
+        ...(current ?? {}),
+        scrollLeft: left,
+      }
+    })
+    pruneView(path)
+  }
+
+  const setSelectedLines = (path: string, range: SelectedLineRange | null) => {
+    const next = range ? normalizeSelectedLines(range) : null
+    setView("file", path, (current) => {
+      if (current?.selectedLines === next) return current
+      return {
+        ...(current ?? {}),
+        selectedLines: next,
+      }
+    })
+    pruneView(path)
+  }
+
+  return {
+    ready,
+    scrollTop,
+    scrollLeft,
+    selectedLines,
+    setScrollTop,
+    setScrollLeft,
+    setSelectedLines,
+  }
+}
+
 export const { use: useFile, provider: FileProvider } = createSimpleContext({
   name: "File",
+  gate: false,
   init: () => {
     const sdk = useSDK()
     const sync = useSync()
@@ -134,16 +232,45 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
       file: {},
     })
 
-    const viewKey = createMemo(() => `${params.dir}/file${params.id ? "/" + params.id : ""}.v1`)
+    const viewCache = new Map<string, ViewCacheEntry>()
 
-    const [view, setView, _, ready] = persisted(
-      viewKey(),
-      createStore<{
-        file: Record<string, FileViewState>
-      }>({
-        file: {},
-      }),
-    )
+    const disposeViews = () => {
+      for (const entry of viewCache.values()) {
+        entry.dispose()
+      }
+      viewCache.clear()
+    }
+
+    const pruneViews = () => {
+      while (viewCache.size > MAX_FILE_VIEW_SESSIONS) {
+        const first = viewCache.keys().next().value
+        if (!first) return
+        const entry = viewCache.get(first)
+        entry?.dispose()
+        viewCache.delete(first)
+      }
+    }
+
+    const loadView = (dir: string, id: string | undefined) => {
+      const key = `${dir}:${id ?? WORKSPACE_KEY}`
+      const existing = viewCache.get(key)
+      if (existing) {
+        viewCache.delete(key)
+        viewCache.set(key, existing)
+        return existing.value
+      }
+
+      const entry = createRoot((dispose) => ({
+        value: createViewSession(dir, id),
+        dispose,
+      }))
+
+      viewCache.set(key, entry)
+      pruneViews()
+      return entry.value
+    }
+
+    const view = createMemo(() => loadView(params.dir!, params.id))
 
     function ensure(path: string) {
       if (!path) return
@@ -220,48 +347,32 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
 
     const get = (input: string) => store.file[normalize(input)]
 
-    const scrollTop = (input: string) => view.file[normalize(input)]?.scrollTop
-    const scrollLeft = (input: string) => view.file[normalize(input)]?.scrollLeft
-    const selectedLines = (input: string) => view.file[normalize(input)]?.selectedLines
+    const scrollTop = (input: string) => view().scrollTop(normalize(input))
+    const scrollLeft = (input: string) => view().scrollLeft(normalize(input))
+    const selectedLines = (input: string) => view().selectedLines(normalize(input))
 
     const setScrollTop = (input: string, top: number) => {
       const path = normalize(input)
-      setView("file", path, (current) => {
-        if (current?.scrollTop === top) return current
-        return {
-          ...(current ?? {}),
-          scrollTop: top,
-        }
-      })
+      view().setScrollTop(path, top)
     }
 
     const setScrollLeft = (input: string, left: number) => {
       const path = normalize(input)
-      setView("file", path, (current) => {
-        if (current?.scrollLeft === left) return current
-        return {
-          ...(current ?? {}),
-          scrollLeft: left,
-        }
-      })
+      view().setScrollLeft(path, left)
     }
 
     const setSelectedLines = (input: string, range: SelectedLineRange | null) => {
       const path = normalize(input)
-      const next = range ? normalizeSelectedLines(range) : null
-      setView("file", path, (current) => {
-        if (current?.selectedLines === next) return current
-        return {
-          ...(current ?? {}),
-          selectedLines: next,
-        }
-      })
+      view().setSelectedLines(path, range)
     }
 
-    onCleanup(() => stop())
+    onCleanup(() => {
+      stop()
+      disposeViews()
+    })
 
     return {
-      ready,
+      ready: () => view().ready(),
       normalize,
       tab,
       pathFromTab,

+ 69 - 4
packages/app/src/context/global-sdk.tsx

@@ -1,7 +1,7 @@
 import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createGlobalEmitter } from "@solid-primitives/event-bus"
-import { onCleanup } from "solid-js"
+import { batch, onCleanup } from "solid-js"
 import { usePlatform } from "./platform"
 import { useServer } from "./server"
 
@@ -19,14 +19,79 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
       [key: string]: Event
     }>()
 
+    type Queued = { directory: string; payload: Event }
+
+    let queue: Array<Queued | undefined> = []
+    const coalesced = new Map<string, number>()
+    let timer: ReturnType<typeof setTimeout> | undefined
+    let last = 0
+
+    const key = (directory: string, payload: Event) => {
+      if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
+      if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
+      if (payload.type === "message.part.updated") {
+        const part = payload.properties.part
+        return `message.part.updated:${directory}:${part.messageID}:${part.id}`
+      }
+    }
+
+    const flush = () => {
+      if (timer) clearTimeout(timer)
+      timer = undefined
+
+      const events = queue
+      queue = []
+      coalesced.clear()
+      if (events.length === 0) return
+
+      last = Date.now()
+      batch(() => {
+        for (const event of events) {
+          if (!event) continue
+          emitter.emit(event.directory, event.payload)
+        }
+      })
+    }
+
+    const schedule = () => {
+      if (timer) return
+      const elapsed = Date.now() - last
+      timer = setTimeout(flush, Math.max(0, 16 - elapsed))
+    }
+
+    const stop = () => {
+      flush()
+    }
+
     void (async () => {
       const events = await eventSdk.global.event()
+      let yielded = Date.now()
       for await (const event of events.stream) {
-        emitter.emit(event.directory ?? "global", event.payload)
+        const directory = event.directory ?? "global"
+        const payload = event.payload
+        const k = key(directory, payload)
+        if (k) {
+          const i = coalesced.get(k)
+          if (i !== undefined) {
+            queue[i] = undefined
+          }
+          coalesced.set(k, queue.length)
+        }
+        queue.push({ directory, payload })
+        schedule()
+
+        if (Date.now() - yielded < 8) continue
+        yielded = Date.now()
+        await new Promise<void>((resolve) => setTimeout(resolve, 0))
       }
-    })().catch(() => undefined)
+    })()
+      .finally(stop)
+      .catch(() => undefined)
 
-    onCleanup(() => abort.abort())
+    onCleanup(() => {
+      abort.abort()
+      stop()
+    })
 
     const platform = usePlatform()
     const sdk = createOpencodeClient({

+ 3 - 2
packages/app/src/context/global-sync.tsx

@@ -23,7 +23,7 @@ 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, onMount, type ParentProps, Switch, Match } from "solid-js"
+import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js"
 import { showToast } from "@opencode-ai/ui/toast"
 import { getFilename } from "@opencode-ai/util/path"
 
@@ -212,7 +212,7 @@ function createGlobalSync() {
       .catch((e) => setGlobalStore("error", e))
   }
 
-  globalSDK.event.listen((e) => {
+  const unsub = globalSDK.event.listen((e) => {
     const directory = e.name
     const event = e.details
 
@@ -404,6 +404,7 @@ function createGlobalSync() {
       }
     }
   })
+  onCleanup(unsub)
 
   async function bootstrap() {
     const health = await globalSDK.client.global

+ 73 - 0
packages/app/src/context/layout-scroll.test.ts

@@ -0,0 +1,73 @@
+import { describe, expect, test } from "bun:test"
+import { createRoot } from "solid-js"
+import { createStore } from "solid-js/store"
+import { makePersisted, type SyncStorage } from "@solid-primitives/storage"
+import { createScrollPersistence } from "./layout-scroll"
+
+describe("createScrollPersistence", () => {
+  test("debounces persisted scroll writes", async () => {
+    const key = "layout-scroll.test"
+    const data = new Map<string, string>()
+    const writes: string[] = []
+    const stats = { flushes: 0 }
+
+    const storage = {
+      getItem: (k: string) => data.get(k) ?? null,
+      setItem: (k: string, v: string) => {
+        data.set(k, v)
+        if (k === key) writes.push(v)
+      },
+      removeItem: (k: string) => {
+        data.delete(k)
+      },
+    } as SyncStorage
+
+    await new Promise<void>((resolve, reject) => {
+      createRoot((dispose) => {
+        const [raw, setRaw] = createStore({
+          sessionView: {} as Record<string, { scroll: Record<string, { x: number; y: number }> }>,
+        })
+
+        const [store, setStore] = makePersisted([raw, setRaw], { name: key, storage })
+
+        const scroll = createScrollPersistence({
+          debounceMs: 30,
+          getSnapshot: (sessionKey) => store.sessionView[sessionKey]?.scroll,
+          onFlush: (sessionKey, next) => {
+            stats.flushes += 1
+
+            const current = store.sessionView[sessionKey]
+            if (!current) {
+              setStore("sessionView", sessionKey, { scroll: next })
+              return
+            }
+            setStore("sessionView", sessionKey, "scroll", (prev) => ({ ...(prev ?? {}), ...next }))
+          },
+        })
+
+        const run = async () => {
+          await new Promise((r) => setTimeout(r, 0))
+          writes.length = 0
+
+          for (const i of Array.from({ length: 100 }, (_, n) => n)) {
+            scroll.setScroll("session", "review", { x: 0, y: i })
+          }
+
+          await new Promise((r) => setTimeout(r, 120))
+
+          expect(stats.flushes).toBeGreaterThanOrEqual(1)
+          expect(writes.length).toBeGreaterThanOrEqual(1)
+          expect(writes.length).toBeLessThanOrEqual(2)
+        }
+
+        void run()
+          .then(resolve)
+          .catch(reject)
+          .finally(() => {
+            scroll.dispose()
+            dispose()
+          })
+      })
+    })
+  })
+})

+ 118 - 0
packages/app/src/context/layout-scroll.ts

@@ -0,0 +1,118 @@
+import { createStore, produce } from "solid-js/store"
+
+export type SessionScroll = {
+  x: number
+  y: number
+}
+
+type ScrollMap = Record<string, SessionScroll>
+
+type Options = {
+  debounceMs?: number
+  getSnapshot: (sessionKey: string) => ScrollMap | undefined
+  onFlush: (sessionKey: string, scroll: ScrollMap) => void
+}
+
+export function createScrollPersistence(opts: Options) {
+  const wait = opts.debounceMs ?? 200
+  const [cache, setCache] = createStore<Record<string, ScrollMap>>({})
+  const dirty = new Set<string>()
+  const timers = new Map<string, ReturnType<typeof setTimeout>>()
+
+  function clone(input?: ScrollMap) {
+    const out: ScrollMap = {}
+    if (!input) return out
+
+    for (const key of Object.keys(input)) {
+      const pos = input[key]
+      if (!pos) continue
+      out[key] = { x: pos.x, y: pos.y }
+    }
+
+    return out
+  }
+
+  function seed(sessionKey: string) {
+    if (cache[sessionKey]) return
+    setCache(sessionKey, clone(opts.getSnapshot(sessionKey)))
+  }
+
+  function scroll(sessionKey: string, tab: string) {
+    seed(sessionKey)
+    return cache[sessionKey]?.[tab] ?? opts.getSnapshot(sessionKey)?.[tab]
+  }
+
+  function schedule(sessionKey: string) {
+    const prev = timers.get(sessionKey)
+    if (prev) clearTimeout(prev)
+    timers.set(
+      sessionKey,
+      setTimeout(() => flush(sessionKey), wait),
+    )
+  }
+
+  function setScroll(sessionKey: string, tab: string, pos: SessionScroll) {
+    seed(sessionKey)
+
+    const prev = cache[sessionKey]?.[tab]
+    if (prev?.x === pos.x && prev?.y === pos.y) return
+
+    setCache(sessionKey, tab, { x: pos.x, y: pos.y })
+    dirty.add(sessionKey)
+    schedule(sessionKey)
+  }
+
+  function flush(sessionKey: string) {
+    const timer = timers.get(sessionKey)
+    if (timer) clearTimeout(timer)
+    timers.delete(sessionKey)
+
+    if (!dirty.has(sessionKey)) return
+    dirty.delete(sessionKey)
+
+    opts.onFlush(sessionKey, clone(cache[sessionKey]))
+  }
+
+  function flushAll() {
+    const keys = Array.from(dirty)
+    if (keys.length === 0) return
+
+    for (const key of keys) {
+      flush(key)
+    }
+  }
+
+  function drop(keys: string[]) {
+    if (keys.length === 0) return
+
+    for (const key of keys) {
+      const timer = timers.get(key)
+      if (timer) clearTimeout(timer)
+      timers.delete(key)
+      dirty.delete(key)
+    }
+
+    setCache(
+      produce((draft) => {
+        for (const key of keys) {
+          delete draft[key]
+        }
+      }),
+    )
+  }
+
+  function dispose() {
+    drop(Array.from(timers.keys()))
+  }
+
+  return {
+    cache,
+    drop,
+    flush,
+    flushAll,
+    scroll,
+    seed,
+    setScroll,
+    dispose,
+  }
+}

+ 225 - 55
packages/app/src/context/layout.tsx

@@ -1,11 +1,13 @@
 import { createStore, produce } from "solid-js/store"
-import { batch, createMemo, onMount } from "solid-js"
+import { batch, createEffect, createMemo, onCleanup, onMount } from "solid-js"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useGlobalSync } from "./global-sync"
 import { useGlobalSDK } from "./global-sdk"
 import { useServer } from "./server"
 import { Project } from "@opencode-ai/sdk/v2"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted, removePersisted } from "@/utils/persist"
+import { same } from "@/utils/same"
+import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
 
 const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
 export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
@@ -23,26 +25,16 @@ export function getAvatarColors(key?: string) {
   }
 }
 
-function same<T>(a: readonly T[] | undefined, b: readonly T[] | undefined) {
-  if (a === b) return true
-  if (!a || !b) return false
-  if (a.length !== b.length) return false
-  return a.every((x, i) => x === b[i])
-}
-
 type SessionTabs = {
   active?: string
   all: string[]
 }
 
-type SessionScroll = {
-  x: number
-  y: number
-}
-
 type SessionView = {
   scroll: Record<string, SessionScroll>
   reviewOpen?: string[]
+  terminalOpened?: boolean
+  reviewPanelOpened?: boolean
 }
 
 export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
@@ -56,18 +48,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
     const globalSync = useGlobalSync()
     const server = useServer()
     const [store, setStore, _, ready] = persisted(
-      "layout.v6",
+      Persist.global("layout", ["layout.v6"]),
       createStore({
         sidebar: {
           opened: false,
           width: 280,
         },
         terminal: {
-          opened: false,
           height: 280,
         },
         review: {
-          opened: true,
           diffStyle: "split" as ReviewDiffStyle,
         },
         session: {
@@ -81,6 +71,121 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
       }),
     )
 
+    const MAX_SESSION_KEYS = 50
+    const meta = { active: undefined as string | undefined, pruned: false }
+    const used = new Map<string, number>()
+
+    const SESSION_STATE_KEYS = [
+      { key: "prompt", legacy: "prompt", version: "v2" },
+      { key: "terminal", legacy: "terminal", version: "v1" },
+      { key: "file-view", legacy: "file", version: "v1" },
+    ] as const
+
+    const dropSessionState = (keys: string[]) => {
+      for (const key of keys) {
+        const parts = key.split("/")
+        const dir = parts[0]
+        const session = parts[1]
+        if (!dir) continue
+
+        for (const entry of SESSION_STATE_KEYS) {
+          const target = session ? Persist.session(dir, session, entry.key) : Persist.workspace(dir, entry.key)
+          void removePersisted(target)
+
+          const legacyKey = `${dir}/${entry.legacy}${session ? "/" + session : ""}.${entry.version}`
+          void removePersisted({ key: legacyKey })
+        }
+      }
+    }
+
+    function prune(keep?: string) {
+      if (!keep) return
+
+      const keys = new Set<string>()
+      for (const key of Object.keys(store.sessionView)) keys.add(key)
+      for (const key of Object.keys(store.sessionTabs)) keys.add(key)
+      if (keys.size <= MAX_SESSION_KEYS) return
+
+      const score = (key: string) => {
+        if (key === keep) return Number.MAX_SAFE_INTEGER
+        return used.get(key) ?? 0
+      }
+
+      const ordered = Array.from(keys).sort((a, b) => score(b) - score(a))
+      const drop = ordered.slice(MAX_SESSION_KEYS)
+      if (drop.length === 0) return
+
+      setStore(
+        produce((draft) => {
+          for (const key of drop) {
+            delete draft.sessionView[key]
+            delete draft.sessionTabs[key]
+          }
+        }),
+      )
+
+      scroll.drop(drop)
+      dropSessionState(drop)
+
+      for (const key of drop) {
+        used.delete(key)
+      }
+    }
+
+    function touch(sessionKey: string) {
+      meta.active = sessionKey
+      used.set(sessionKey, Date.now())
+
+      if (!ready()) return
+      if (meta.pruned) return
+
+      meta.pruned = true
+      prune(sessionKey)
+    }
+
+    const scroll = createScrollPersistence({
+      debounceMs: 250,
+      getSnapshot: (sessionKey) => store.sessionView[sessionKey]?.scroll,
+      onFlush: (sessionKey, next) => {
+        const current = store.sessionView[sessionKey]
+        const keep = meta.active ?? sessionKey
+        if (!current) {
+          setStore("sessionView", sessionKey, { scroll: next, terminalOpened: false, reviewPanelOpened: true })
+          prune(keep)
+          return
+        }
+
+        setStore("sessionView", sessionKey, "scroll", (prev) => ({ ...(prev ?? {}), ...next }))
+        prune(keep)
+      },
+    })
+
+    createEffect(() => {
+      if (!ready()) return
+      if (meta.pruned) return
+      const active = meta.active
+      if (!active) return
+      meta.pruned = true
+      prune(active)
+    })
+
+    onMount(() => {
+      const flush = () => batch(() => scroll.flushAll())
+      const handleVisibility = () => {
+        if (document.visibilityState !== "hidden") return
+        flush()
+      }
+
+      window.addEventListener("pagehide", flush)
+      document.addEventListener("visibilitychange", handleVisibility)
+
+      onCleanup(() => {
+        window.removeEventListener("pagehide", flush)
+        document.removeEventListener("visibilitychange", handleVisibility)
+        scroll.dispose()
+      })
+    })
+
     const usedColors = new Set<AvatarColorKey>()
 
     function pickAvailableColor(): AvatarColorKey {
@@ -97,8 +202,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         : globalSync.data.project.find((x) => x.worktree === project.worktree)
       return [
         {
-          ...project,
           ...(metadata ?? {}),
+          ...project,
           icon: { url: metadata?.icon?.url, color: metadata?.icon?.color },
         },
       ]
@@ -115,6 +220,41 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
       return project
     }
 
+    const roots = createMemo(() => {
+      const map = new Map<string, string>()
+      for (const project of globalSync.data.project) {
+        const sandboxes = project.sandboxes ?? []
+        for (const sandbox of sandboxes) {
+          map.set(sandbox, project.worktree)
+        }
+      }
+      return map
+    })
+
+    createEffect(() => {
+      const map = roots()
+      if (map.size === 0) return
+
+      const projects = server.projects.list()
+      const seen = new Set(projects.map((project) => project.worktree))
+
+      batch(() => {
+        for (const project of projects) {
+          const root = map.get(project.worktree)
+          if (!root) continue
+
+          server.projects.close(project.worktree)
+
+          if (!seen.has(root)) {
+            server.projects.open(root)
+            seen.add(root)
+          }
+
+          if (project.expanded) server.projects.expand(root)
+        }
+      })
+    })
+
     const enriched = createMemo(() => server.projects.list().flatMap(enrich))
     const list = createMemo(() => enriched().flatMap(colorize))
 
@@ -131,11 +271,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
       projects: {
         list,
         open(directory: string) {
-          if (server.projects.list().find((x) => x.worktree === directory)) {
-            return
-          }
-          globalSync.project.loadSessions(directory)
-          server.projects.open(directory)
+          const root = roots().get(directory) ?? directory
+          if (server.projects.list().find((x) => x.worktree === root)) return
+          globalSync.project.loadSessions(root)
+          server.projects.open(root)
         },
         close(directory: string) {
           server.projects.close(directory)
@@ -167,40 +306,20 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         },
       },
       terminal: {
-        opened: createMemo(() => store.terminal.opened),
-        open() {
-          setStore("terminal", "opened", true)
-        },
-        close() {
-          setStore("terminal", "opened", false)
-        },
-        toggle() {
-          setStore("terminal", "opened", (x) => !x)
-        },
         height: createMemo(() => store.terminal.height),
         resize(height: number) {
           setStore("terminal", "height", height)
         },
       },
       review: {
-        opened: createMemo(() => store.review?.opened ?? true),
         diffStyle: createMemo(() => store.review?.diffStyle ?? "split"),
         setDiffStyle(diffStyle: ReviewDiffStyle) {
           if (!store.review) {
-            setStore("review", { opened: true, diffStyle })
+            setStore("review", { diffStyle })
             return
           }
           setStore("review", "diffStyle", diffStyle)
         },
-        open() {
-          setStore("review", "opened", true)
-        },
-        close() {
-          setStore("review", "opened", false)
-        },
-        toggle() {
-          setStore("review", "opened", (x) => !x)
-        },
       },
       session: {
         width: createMemo(() => store.session?.width ?? 600),
@@ -225,28 +344,78 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         },
       },
       view(sessionKey: string) {
+        touch(sessionKey)
+        scroll.seed(sessionKey)
         const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} })
+        const terminalOpened = createMemo(() => s().terminalOpened ?? false)
+        const reviewPanelOpened = createMemo(() => s().reviewPanelOpened ?? true)
+
+        function setTerminalOpened(next: boolean) {
+          const current = store.sessionView[sessionKey]
+          if (!current) {
+            setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: next, reviewPanelOpened: true })
+            return
+          }
+
+          const value = current.terminalOpened ?? false
+          if (value === next) return
+          setStore("sessionView", sessionKey, "terminalOpened", next)
+        }
+
+        function setReviewPanelOpened(next: boolean) {
+          const current = store.sessionView[sessionKey]
+          if (!current) {
+            setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: false, reviewPanelOpened: next })
+            return
+          }
+
+          const value = current.reviewPanelOpened ?? true
+          if (value === next) return
+          setStore("sessionView", sessionKey, "reviewPanelOpened", next)
+        }
+
         return {
           scroll(tab: string) {
-            return s().scroll?.[tab]
+            return scroll.scroll(sessionKey, tab)
           },
           setScroll(tab: string, pos: SessionScroll) {
-            const current = store.sessionView[sessionKey]
-            if (!current) {
-              setStore("sessionView", sessionKey, { scroll: { [tab]: pos } })
-              return
-            }
-
-            const prev = current.scroll?.[tab]
-            if (prev?.x === pos.x && prev?.y === pos.y) return
-            setStore("sessionView", sessionKey, "scroll", tab, pos)
+            scroll.setScroll(sessionKey, tab, pos)
+          },
+          terminal: {
+            opened: terminalOpened,
+            open() {
+              setTerminalOpened(true)
+            },
+            close() {
+              setTerminalOpened(false)
+            },
+            toggle() {
+              setTerminalOpened(!terminalOpened())
+            },
+          },
+          reviewPanel: {
+            opened: reviewPanelOpened,
+            open() {
+              setReviewPanelOpened(true)
+            },
+            close() {
+              setReviewPanelOpened(false)
+            },
+            toggle() {
+              setReviewPanelOpened(!reviewPanelOpened())
+            },
           },
           review: {
             open: createMemo(() => s().reviewOpen),
             setOpen(open: string[]) {
               const current = store.sessionView[sessionKey]
               if (!current) {
-                setStore("sessionView", sessionKey, { scroll: {}, reviewOpen: open })
+                setStore("sessionView", sessionKey, {
+                  scroll: {},
+                  terminalOpened: false,
+                  reviewPanelOpened: true,
+                  reviewOpen: open,
+                })
                 return
               }
 
@@ -257,6 +426,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         }
       },
       tabs(sessionKey: string) {
+        touch(sessionKey)
         const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
         return {
           tabs,

+ 12 - 5
packages/app/src/context/local.tsx

@@ -1,5 +1,5 @@
 import { createStore, produce, reconcile } from "solid-js/store"
-import { batch, createMemo } from "solid-js"
+import { batch, createMemo, onCleanup } from "solid-js"
 import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
 import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
 import { createSimpleContext } from "@opencode-ai/ui/context"
@@ -8,7 +8,7 @@ import { useSync } from "./sync"
 import { base64Encode } from "@opencode-ai/util/encode"
 import { useProviders } from "@/hooks/use-providers"
 import { DateTime } from "luxon"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
 import { showToast } from "@opencode-ai/ui/toast"
 
 export type LocalFile = FileNode &
@@ -111,7 +111,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
 
     const model = (() => {
       const [store, setStore, _, modelReady] = persisted(
-        "model.v1",
+        Persist.global("model", ["model.v1"]),
         createStore<{
           user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
           recent: ModelKey[]
@@ -276,7 +276,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
         visible(model: ModelKey) {
           const key = `${model.providerID}:${model.modelID}`
           const visibility = userVisibilityMap().get(key)
-          return visibility !== "hide" && (latestSet().has(key) || visibility === "show")
+          if (visibility === "hide") return false
+          if (visibility === "show") return true
+          if (latestSet().has(key)) return true
+          // For models without valid release_date (e.g. custom models), show by default
+          const m = find(model)
+          if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true
+          return false
         },
         setVisibility(model: ModelKey, visible: boolean) {
           updateVisibility(model, visible ? "show" : "hide")
@@ -465,7 +471,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       const searchFilesAndDirectories = (query: string) =>
         sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!)
 
-      sdk.event.listen((e) => {
+      const unsub = sdk.event.listen((e) => {
         const event = e.details
         switch (event.type) {
           case "file.watcher.updated":
@@ -475,6 +481,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
             break
         }
       })
+      onCleanup(unsub)
 
       return {
         node: async (path: string) => {

+ 30 - 5
packages/app/src/context/notification.tsx

@@ -1,4 +1,5 @@
 import { createStore } from "solid-js/store"
+import { createEffect, onCleanup } from "solid-js"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useGlobalSDK } from "./global-sdk"
 import { useGlobalSync } from "./global-sync"
@@ -9,7 +10,7 @@ import { EventSessionError } from "@opencode-ai/sdk/v2"
 import { makeAudioPlayer } from "@solid-primitives/audio"
 import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
 import errorSound from "@opencode-ai/ui/audio/nope-03.aac"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
 
 type NotificationBase = {
   directory?: string
@@ -30,6 +31,16 @@ type ErrorNotification = NotificationBase & {
 
 export type Notification = TurnCompleteNotification | ErrorNotification
 
+const MAX_NOTIFICATIONS = 500
+const NOTIFICATION_TTL_MS = 1000 * 60 * 60 * 24 * 30
+
+function pruneNotifications(list: Notification[]) {
+  const cutoff = Date.now() - NOTIFICATION_TTL_MS
+  const pruned = list.filter((n) => n.time >= cutoff)
+  if (pruned.length <= MAX_NOTIFICATIONS) return pruned
+  return pruned.slice(pruned.length - MAX_NOTIFICATIONS)
+}
+
 export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
   name: "Notification",
   init: () => {
@@ -48,13 +59,26 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
     const platform = usePlatform()
 
     const [store, setStore, _, ready] = persisted(
-      "notification.v1",
+      Persist.global("notification", ["notification.v1"]),
       createStore({
         list: [] as Notification[],
       }),
     )
 
-    globalSDK.event.listen((e) => {
+    const meta = { pruned: false }
+
+    createEffect(() => {
+      if (!ready()) return
+      if (meta.pruned) return
+      meta.pruned = true
+      setStore("list", pruneNotifications(store.list))
+    })
+
+    const append = (notification: Notification) => {
+      setStore("list", (list) => pruneNotifications([...list, notification]))
+    }
+
+    const unsub = globalSDK.event.listen((e) => {
       const directory = e.name
       const event = e.details
       const base = {
@@ -72,7 +96,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
           try {
             idlePlayer?.play()
           } catch {}
-          setStore("list", store.list.length, {
+          append({
             ...base,
             type: "turn-complete",
             session: sessionID,
@@ -91,7 +115,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
             errorPlayer?.play()
           } catch {}
           const error = "error" in event.properties ? event.properties.error : undefined
-          setStore("list", store.list.length, {
+          append({
             ...base,
             type: "error",
             session: sessionID ?? "global",
@@ -104,6 +128,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
         }
       }
     })
+    onCleanup(unsub)
 
     return {
       ready,

+ 65 - 20
packages/app/src/context/permission.tsx

@@ -1,12 +1,12 @@
 import { createMemo, onCleanup } from "solid-js"
-import { createStore } from "solid-js/store"
+import { createStore, produce } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { useGlobalSync } from "./global-sync"
 import { useParams } from "@solidjs/router"
-import { base64Decode } from "@opencode-ai/util/encode"
+import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
 
 type PermissionRespondFn = (input: {
   sessionID: string
@@ -19,6 +19,32 @@ function shouldAutoAccept(perm: PermissionRequest) {
   return perm.permission === "edit"
 }
 
+function isNonAllowRule(rule: unknown) {
+  if (!rule) return false
+  if (typeof rule === "string") return rule !== "allow"
+  if (typeof rule !== "object") return false
+  if (Array.isArray(rule)) return false
+
+  for (const action of Object.values(rule)) {
+    if (action !== "allow") return true
+  }
+
+  return false
+}
+
+function hasAutoAcceptPermissionConfig(permission: unknown) {
+  if (!permission) return false
+  if (typeof permission === "string") return permission !== "allow"
+  if (typeof permission !== "object") return false
+  if (Array.isArray(permission)) return false
+
+  const config = permission as Record<string, unknown>
+  if (isNonAllowRule(config.edit)) return true
+  if (isNonAllowRule(config.write)) return true
+
+  return false
+}
+
 export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
   name: "Permission",
   init: () => {
@@ -27,13 +53,14 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
     const globalSync = useGlobalSync()
 
     const permissionsEnabled = createMemo(() => {
-      if (!params.dir || !base64Decode(params.dir)) return false
-      const [store] = globalSync.child(base64Decode(params.dir))
-      return store.config.permission !== undefined
+      const directory = params.dir ? base64Decode(params.dir) : undefined
+      if (!directory) return false
+      const [store] = globalSync.child(directory)
+      return hasAutoAcceptPermissionConfig(store.config.permission)
     })
 
     const [store, setStore, _, ready] = persisted(
-      "permission.v3",
+      Persist.global("permission", ["permission.v3"]),
       createStore({
         autoAcceptEdits: {} as Record<string, boolean>,
       }),
@@ -58,8 +85,14 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
       })
     }
 
-    function isAutoAccepting(sessionID: string) {
-      return store.autoAcceptEdits[sessionID] ?? false
+    function acceptKey(sessionID: string, directory?: string) {
+      if (!directory) return sessionID
+      return `${base64Encode(directory)}/${sessionID}`
+    }
+
+    function isAutoAccepting(sessionID: string, directory?: string) {
+      const key = acceptKey(sessionID, directory)
+      return store.autoAcceptEdits[key] ?? store.autoAcceptEdits[sessionID] ?? false
     }
 
     const unsubscribe = globalSDK.event.listen((e) => {
@@ -67,7 +100,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
       if (event?.type !== "permission.asked") return
 
       const perm = event.properties
-      if (!isAutoAccepting(perm.sessionID)) return
+      if (!isAutoAccepting(perm.sessionID, e.name)) return
       if (!shouldAutoAccept(perm)) return
 
       respondOnce(perm, e.name)
@@ -75,7 +108,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
     onCleanup(unsubscribe)
 
     function enable(sessionID: string, directory: string) {
-      setStore("autoAcceptEdits", sessionID, true)
+      const key = acceptKey(sessionID, directory)
+      setStore(
+        produce((draft) => {
+          draft.autoAcceptEdits[key] = true
+          delete draft.autoAcceptEdits[sessionID]
+        }),
+      )
 
       globalSDK.client.permission
         .list({ directory })
@@ -90,31 +129,37 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
         .catch(() => undefined)
     }
 
-    function disable(sessionID: string) {
-      setStore("autoAcceptEdits", sessionID, false)
+    function disable(sessionID: string, directory?: string) {
+      const key = directory ? acceptKey(sessionID, directory) : undefined
+      setStore(
+        produce((draft) => {
+          if (key) delete draft.autoAcceptEdits[key]
+          delete draft.autoAcceptEdits[sessionID]
+        }),
+      )
     }
 
     return {
       ready,
       respond,
-      autoResponds(permission: PermissionRequest) {
-        return isAutoAccepting(permission.sessionID) && shouldAutoAccept(permission)
+      autoResponds(permission: PermissionRequest, directory?: string) {
+        return isAutoAccepting(permission.sessionID, directory) && shouldAutoAccept(permission)
       },
       isAutoAccepting,
       toggleAutoAccept(sessionID: string, directory: string) {
-        if (isAutoAccepting(sessionID)) {
-          disable(sessionID)
+        if (isAutoAccepting(sessionID, directory)) {
+          disable(sessionID, directory)
           return
         }
 
         enable(sessionID, directory)
       },
       enableAutoAccept(sessionID: string, directory: string) {
-        if (isAutoAccepting(sessionID)) return
+        if (isAutoAccepting(sessionID, directory)) return
         enable(sessionID, directory)
       },
-      disableAutoAccept(sessionID: string) {
-        disable(sessionID)
+      disableAutoAccept(sessionID: string, directory?: string) {
+        disable(sessionID, directory)
       },
       permissionsEnabled,
     }

+ 133 - 61
packages/app/src/context/prompt.tsx

@@ -1,9 +1,9 @@
 import { createStore } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
-import { batch, createMemo } from "solid-js"
+import { batch, createMemo, createRoot, onCleanup } from "solid-js"
 import { useParams } from "@solidjs/router"
 import type { FileSelection } from "@/context/file"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
 
 interface PartBase {
   content: string
@@ -99,74 +99,146 @@ function clonePrompt(prompt: Prompt): Prompt {
   return prompt.map(clonePart)
 }
 
+const WORKSPACE_KEY = "__workspace__"
+const MAX_PROMPT_SESSIONS = 20
+
+type PromptSession = ReturnType<typeof createPromptSession>
+
+type PromptCacheEntry = {
+  value: PromptSession
+  dispose: VoidFunction
+}
+
+function createPromptSession(dir: string, id: string | undefined) {
+  const legacy = `${dir}/prompt${id ? "/" + id : ""}.v2`
+
+  const [store, setStore, _, ready] = persisted(
+    Persist.scoped(dir, id, "prompt", [legacy]),
+    createStore<{
+      prompt: Prompt
+      cursor?: number
+      context: {
+        activeTab: boolean
+        items: (ContextItem & { key: string })[]
+      }
+    }>({
+      prompt: clonePrompt(DEFAULT_PROMPT),
+      cursor: undefined,
+      context: {
+        activeTab: true,
+        items: [],
+      },
+    }),
+  )
+
+  function keyForItem(item: ContextItem) {
+    if (item.type !== "file") return item.type
+    const start = item.selection?.startLine
+    const end = item.selection?.endLine
+    return `${item.type}:${item.path}:${start}:${end}`
+  }
+
+  return {
+    ready,
+    current: createMemo(() => store.prompt),
+    cursor: createMemo(() => store.cursor),
+    dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
+    context: {
+      activeTab: createMemo(() => store.context.activeTab),
+      items: createMemo(() => store.context.items),
+      addActive() {
+        setStore("context", "activeTab", true)
+      },
+      removeActive() {
+        setStore("context", "activeTab", false)
+      },
+      add(item: ContextItem) {
+        const key = keyForItem(item)
+        if (store.context.items.find((x) => x.key === key)) return
+        setStore("context", "items", (items) => [...items, { key, ...item }])
+      },
+      remove(key: string) {
+        setStore("context", "items", (items) => items.filter((x) => x.key !== key))
+      },
+    },
+    set(prompt: Prompt, cursorPosition?: number) {
+      const next = clonePrompt(prompt)
+      batch(() => {
+        setStore("prompt", next)
+        if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
+      })
+    },
+    reset() {
+      batch(() => {
+        setStore("prompt", clonePrompt(DEFAULT_PROMPT))
+        setStore("cursor", 0)
+      })
+    },
+  }
+}
+
 export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
   name: "Prompt",
+  gate: false,
   init: () => {
     const params = useParams()
-    const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v2`)
-
-    const [store, setStore, _, ready] = persisted(
-      name(),
-      createStore<{
-        prompt: Prompt
-        cursor?: number
-        context: {
-          activeTab: boolean
-          items: (ContextItem & { key: string })[]
-        }
-      }>({
-        prompt: clonePrompt(DEFAULT_PROMPT),
-        cursor: undefined,
-        context: {
-          activeTab: true,
-          items: [],
-        },
-      }),
-    )
-
-    function keyForItem(item: ContextItem) {
-      if (item.type !== "file") return item.type
-      const start = item.selection?.startLine
-      const end = item.selection?.endLine
-      return `${item.type}:${item.path}:${start}:${end}`
+    const cache = new Map<string, PromptCacheEntry>()
+
+    const disposeAll = () => {
+      for (const entry of cache.values()) {
+        entry.dispose()
+      }
+      cache.clear()
+    }
+
+    onCleanup(disposeAll)
+
+    const prune = () => {
+      while (cache.size > MAX_PROMPT_SESSIONS) {
+        const first = cache.keys().next().value
+        if (!first) return
+        const entry = cache.get(first)
+        entry?.dispose()
+        cache.delete(first)
+      }
+    }
+
+    const load = (dir: string, id: string | undefined) => {
+      const key = `${dir}:${id ?? WORKSPACE_KEY}`
+      const existing = cache.get(key)
+      if (existing) {
+        cache.delete(key)
+        cache.set(key, existing)
+        return existing.value
+      }
+
+      const entry = createRoot((dispose) => ({
+        value: createPromptSession(dir, id),
+        dispose,
+      }))
+
+      cache.set(key, entry)
+      prune()
+      return entry.value
     }
 
+    const session = createMemo(() => load(params.dir!, params.id))
+
     return {
-      ready,
-      current: createMemo(() => store.prompt),
-      cursor: createMemo(() => store.cursor),
-      dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
+      ready: () => session().ready(),
+      current: () => session().current(),
+      cursor: () => session().cursor(),
+      dirty: () => session().dirty(),
       context: {
-        activeTab: createMemo(() => store.context.activeTab),
-        items: createMemo(() => store.context.items),
-        addActive() {
-          setStore("context", "activeTab", true)
-        },
-        removeActive() {
-          setStore("context", "activeTab", false)
-        },
-        add(item: ContextItem) {
-          const key = keyForItem(item)
-          if (store.context.items.find((x) => x.key === key)) return
-          setStore("context", "items", (items) => [...items, { key, ...item }])
-        },
-        remove(key: string) {
-          setStore("context", "items", (items) => items.filter((x) => x.key !== key))
-        },
-      },
-      set(prompt: Prompt, cursorPosition?: number) {
-        const next = clonePrompt(prompt)
-        batch(() => {
-          setStore("prompt", next)
-          if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
-        })
-      },
-      reset() {
-        batch(() => {
-          setStore("prompt", clonePrompt(DEFAULT_PROMPT))
-          setStore("cursor", 0)
-        })
+        activeTab: () => session().context.activeTab(),
+        items: () => session().context.items(),
+        addActive: () => session().context.addActive(),
+        removeActive: () => session().context.removeActive(),
+        add: (item: ContextItem) => session().context.add(item),
+        remove: (key: string) => session().context.remove(key),
       },
+      set: (prompt: Prompt, cursorPosition?: number) => session().set(prompt, cursorPosition),
+      reset: () => session().reset(),
     }
   },
 })

+ 3 - 1
packages/app/src/context/sdk.tsx

@@ -1,6 +1,7 @@
 import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createGlobalEmitter } from "@solid-primitives/event-bus"
+import { onCleanup } from "solid-js"
 import { useGlobalSDK } from "./global-sdk"
 import { usePlatform } from "./platform"
 
@@ -20,9 +21,10 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
       [key in Event["type"]]: Extract<Event, { type: key }>
     }>()
 
-    globalSDK.event.on(props.directory, async (event) => {
+    const unsub = globalSDK.event.on(props.directory, (event) => {
       emitter.emit(event.type, event)
     })
+    onCleanup(unsub)
 
     return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url }
   },

+ 44 - 22
packages/app/src/context/server.tsx

@@ -1,9 +1,9 @@
 import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
 import { createSimpleContext } from "@opencode-ai/ui/context"
-import { batch, createEffect, createMemo, createResource, createSignal, onCleanup } from "solid-js"
+import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
 import { createStore } from "solid-js/store"
 import { usePlatform } from "@/context/platform"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
 
 type StoredProject = { worktree: string; expanded: boolean }
 
@@ -35,7 +35,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
     const platform = usePlatform()
 
     const [store, setStore, _, ready] = persisted(
-      "server.v3",
+      Persist.global("server", ["server.v3"]),
       createStore({
         list: [] as string[],
         projects: {} as Record<string, StoredProject[]>,
@@ -91,27 +91,49 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
 
     const isReady = createMemo(() => ready() && !!active())
 
-    const [healthy, { refetch }] = createResource(
-      () => active() || undefined,
-      async (url) => {
-        if (!url) return
-
-        const sdk = createOpencodeClient({
-          baseUrl: url,
-          fetch: platform.fetch,
-          signal: AbortSignal.timeout(3000),
-        })
-        return sdk.global
-          .health()
-          .then((x) => x.data?.healthy === true)
-          .catch(() => false)
-      },
-    )
+    const [healthy, setHealthy] = createSignal<boolean | undefined>(undefined)
+
+    const check = (url: string) => {
+      const sdk = createOpencodeClient({
+        baseUrl: url,
+        fetch: platform.fetch,
+        signal: AbortSignal.timeout(3000),
+      })
+      return sdk.global
+        .health()
+        .then((x) => x.data?.healthy === true)
+        .catch(() => false)
+    }
 
     createEffect(() => {
-      if (!active()) return
-      const interval = setInterval(() => refetch(), 10_000)
-      onCleanup(() => clearInterval(interval))
+      const url = active()
+      if (!url) return
+
+      setHealthy(undefined)
+
+      let alive = true
+      let busy = false
+
+      const run = () => {
+        if (busy) return
+        busy = true
+        void check(url)
+          .then((next) => {
+            if (!alive) return
+            setHealthy(next)
+          })
+          .finally(() => {
+            busy = false
+          })
+      }
+
+      run()
+      const interval = setInterval(run, 10_000)
+
+      onCleanup(() => {
+        alive = false
+        clearInterval(interval)
+      })
     })
 
     const origin = createMemo(() => projectsKey(active()))

+ 162 - 56
packages/app/src/context/sync.tsx

@@ -1,5 +1,5 @@
 import { batch, createMemo } from "solid-js"
-import { produce, reconcile } from "solid-js/store"
+import { createStore, produce, reconcile } from "solid-js/store"
 import { Binary } from "@opencode-ai/util/binary"
 import { retry } from "@opencode-ai/util/retry"
 import { createSimpleContext } from "@opencode-ai/ui/context"
@@ -14,6 +14,76 @@ 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 inflight = new Map<string, Promise<void>>()
+    const inflightDiff = new Map<string, Promise<void>>()
+    const inflightTodo = new Map<string, Promise<void>>()
+    const [meta, setMeta] = createStore({
+      limit: {} as Record<string, number>,
+      complete: {} as Record<string, boolean>,
+      loading: {} as Record<string, boolean>,
+    })
+
+    const getSession = (sessionID: string) => {
+      const match = Binary.search(store.session, sessionID, (s) => s.id)
+      if (match.found) return store.session[match.index]
+      return undefined
+    }
+
+    const limitFor = (count: number) => {
+      if (count <= chunk) return chunk
+      return Math.ceil(count / chunk) * chunk
+    }
+
+    const hydrateMessages = (sessionID: string) => {
+      if (meta.limit[sessionID] !== undefined) return
+
+      const messages = store.message[sessionID]
+      if (!messages) return
+
+      const limit = limitFor(messages.length)
+      setMeta("limit", sessionID, limit)
+      setMeta("complete", sessionID, messages.length < limit)
+    }
+
+    const loadMessages = async (sessionID: string, limit: number) => {
+      if (meta.loading[sessionID]) return
+
+      setMeta("loading", sessionID, true)
+      await retry(() => sdk.client.session.messages({ sessionID, limit }))
+        .then((messages) => {
+          const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
+          const next = items
+            .map((x) => x.info)
+            .filter((m) => !!m?.id)
+            .slice()
+            .sort((a, b) => a.id.localeCompare(b.id))
+
+          batch(() => {
+            setStore("message", sessionID, reconcile(next, { key: "id" }))
+
+            for (const message of items) {
+              setStore(
+                "part",
+                message.info.id,
+                reconcile(
+                  message.parts
+                    .filter((p) => !!p?.id)
+                    .slice()
+                    .sort((a, b) => a.id.localeCompare(b.id)),
+                  { key: "id" },
+                ),
+              )
+            }
+
+            setMeta("limit", sessionID, limit)
+            setMeta("complete", sessionID, next.length < limit)
+          })
+        })
+        .finally(() => {
+          setMeta("loading", sessionID, false)
+        })
+    }
 
     return {
       data: store,
@@ -30,11 +100,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
         return undefined
       },
       session: {
-        get(sessionID: string) {
-          const match = Binary.search(store.session, sessionID, (s) => s.id)
-          if (match.found) return store.session[match.index]
-          return undefined
-        },
+        get: getSession,
         addOptimisticMessage(input: {
           sessionID: string
           messageID: string
@@ -66,58 +132,98 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
             }),
           )
         },
-        async sync(sessionID: string, _isRetry = false) {
-          const [session, messages, todo, diff] = await Promise.all([
-            retry(() => sdk.client.session.get({ sessionID })),
-            retry(() => sdk.client.session.messages({ sessionID, limit: 1000 })),
-            retry(() => sdk.client.session.todo({ sessionID })),
-            retry(() => sdk.client.session.diff({ sessionID })),
-          ])
+        async sync(sessionID: string) {
+          const hasSession = getSession(sessionID) !== undefined
+          hydrateMessages(sessionID)
 
-          batch(() => {
-            setStore(
-              "session",
-              produce((draft) => {
-                const match = Binary.search(draft, sessionID, (s) => s.id)
-                if (match.found) {
-                  draft[match.index] = session.data!
-                  return
-                }
-                draft.splice(match.index, 0, session.data!)
-              }),
-            )
-
-            setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
-            setStore(
-              "message",
-              sessionID,
-              reconcile(
-                (messages.data ?? [])
-                  .map((x) => x.info)
-                  .filter((m) => !!m?.id)
-                  .slice()
-                  .sort((a, b) => a.id.localeCompare(b.id)),
-                { key: "id" },
-              ),
-            )
-
-            for (const message of messages.data ?? []) {
-              if (!message?.info?.id) continue
-              setStore(
-                "part",
-                message.info.id,
-                reconcile(
-                  message.parts
-                    .filter((p) => !!p?.id)
-                    .slice()
-                    .sort((a, b) => a.id.localeCompare(b.id)),
-                  { key: "id" },
-                ),
-              )
-            }
+          const hasMessages = store.message[sessionID] !== undefined
+          if (hasSession && hasMessages) return
 
-            setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
-          })
+          const pending = inflight.get(sessionID)
+          if (pending) return pending
+
+          const limit = meta.limit[sessionID] ?? chunk
+
+          const sessionReq = hasSession
+            ? Promise.resolve()
+            : retry(() => sdk.client.session.get({ sessionID })).then((session) => {
+                const data = session.data
+                if (!data) return
+                setStore(
+                  "session",
+                  produce((draft) => {
+                    const match = Binary.search(draft, sessionID, (s) => s.id)
+                    if (match.found) {
+                      draft[match.index] = data
+                      return
+                    }
+                    draft.splice(match.index, 0, data)
+                  }),
+                )
+              })
+
+          const messagesReq = hasMessages ? Promise.resolve() : loadMessages(sessionID, limit)
+
+          const promise = Promise.all([sessionReq, messagesReq])
+            .then(() => {})
+            .finally(() => {
+              inflight.delete(sessionID)
+            })
+
+          inflight.set(sessionID, promise)
+          return promise
+        },
+        async diff(sessionID: string) {
+          if (store.session_diff[sessionID] !== undefined) return
+
+          const pending = inflightDiff.get(sessionID)
+          if (pending) return pending
+
+          const promise = retry(() => sdk.client.session.diff({ sessionID }))
+            .then((diff) => {
+              setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
+            })
+            .finally(() => {
+              inflightDiff.delete(sessionID)
+            })
+
+          inflightDiff.set(sessionID, promise)
+          return promise
+        },
+        async todo(sessionID: string) {
+          if (store.todo[sessionID] !== undefined) return
+
+          const pending = inflightTodo.get(sessionID)
+          if (pending) return pending
+
+          const promise = retry(() => sdk.client.session.todo({ sessionID }))
+            .then((todo) => {
+              setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
+            })
+            .finally(() => {
+              inflightTodo.delete(sessionID)
+            })
+
+          inflightTodo.set(sessionID, promise)
+          return promise
+        },
+        history: {
+          more(sessionID: string) {
+            if (store.message[sessionID] === undefined) return false
+            if (meta.limit[sessionID] === undefined) return false
+            if (meta.complete[sessionID]) return false
+            return true
+          },
+          loading(sessionID: string) {
+            return meta.loading[sessionID] ?? false
+          },
+          async loadMore(sessionID: string, count = chunk) {
+            if (meta.loading[sessionID]) return
+            if (meta.complete[sessionID]) return
+
+            const current = meta.limit[sessionID] ?? chunk
+            await loadMessages(sessionID, current + count)
+          },
         },
         fetch: async (count = 10) => {
           setStore("limit", (x) => x + count)

+ 164 - 97
packages/app/src/context/terminal.tsx

@@ -1,9 +1,9 @@
 import { createStore, produce } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
-import { batch, createMemo } from "solid-js"
+import { batch, createMemo, createRoot, onCleanup } from "solid-js"
 import { useParams } from "@solidjs/router"
 import { useSDK } from "./sdk"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
 
 export type LocalPTY = {
   id: string
@@ -14,108 +14,175 @@ export type LocalPTY = {
   scrollY?: number
 }
 
-export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({
-  name: "Terminal",
-  init: () => {
-    const sdk = useSDK()
-    const params = useParams()
-    const name = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`)
-
-    const [store, setStore, _, ready] = persisted(
-      name(),
-      createStore<{
-        active?: string
-        all: LocalPTY[]
-      }>({
-        all: [],
-      }),
-    )
+const WORKSPACE_KEY = "__workspace__"
+const MAX_TERMINAL_SESSIONS = 20
 
-    return {
-      ready,
-      all: createMemo(() => Object.values(store.all)),
-      active: createMemo(() => store.active),
-      new() {
-        sdk.client.pty
-          .create({ title: `Terminal ${store.all.length + 1}` })
-          .then((pty) => {
-            const id = pty.data?.id
-            if (!id) return
-            setStore("all", [
-              ...store.all,
-              {
-                id,
-                title: pty.data?.title ?? "Terminal",
-              },
-            ])
-            setStore("active", id)
-          })
-          .catch((e) => {
-            console.error("Failed to create terminal", e)
-          })
-      },
-      update(pty: Partial<LocalPTY> & { id: string }) {
-        setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
-        sdk.client.pty
-          .update({
-            ptyID: pty.id,
-            title: pty.title,
-            size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
-          })
-          .catch((e) => {
-            console.error("Failed to update terminal", e)
-          })
-      },
-      async clone(id: string) {
-        const index = store.all.findIndex((x) => x.id === id)
-        const pty = store.all[index]
-        if (!pty) return
-        const clone = await sdk.client.pty
-          .create({
-            title: pty.title,
-          })
-          .catch((e) => {
-            console.error("Failed to clone terminal", e)
-            return undefined
-          })
-        if (!clone?.data) return
-        setStore("all", index, {
-          ...pty,
-          ...clone.data,
+type TerminalSession = ReturnType<typeof createTerminalSession>
+
+type TerminalCacheEntry = {
+  value: TerminalSession
+  dispose: VoidFunction
+}
+
+function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id: string | undefined) {
+  const legacy = `${dir}/terminal${id ? "/" + id : ""}.v1`
+
+  const [store, setStore, _, ready] = persisted(
+    Persist.scoped(dir, id, "terminal", [legacy]),
+    createStore<{
+      active?: string
+      all: LocalPTY[]
+    }>({
+      all: [],
+    }),
+  )
+
+  return {
+    ready,
+    all: createMemo(() => Object.values(store.all)),
+    active: createMemo(() => store.active),
+    new() {
+      sdk.client.pty
+        .create({ title: `Terminal ${store.all.length + 1}` })
+        .then((pty) => {
+          const id = pty.data?.id
+          if (!id) return
+          setStore("all", [
+            ...store.all,
+            {
+              id,
+              title: pty.data?.title ?? "Terminal",
+            },
+          ])
+          setStore("active", id)
         })
-        if (store.active === pty.id) {
-          setStore("active", clone.data.id)
-        }
-      },
-      open(id: string) {
-        setStore("active", id)
-      },
-      async close(id: string) {
-        batch(() => {
-          setStore(
-            "all",
-            store.all.filter((x) => x.id !== id),
-          )
-          if (store.active === id) {
-            const index = store.all.findIndex((f) => f.id === id)
-            const previous = store.all[Math.max(0, index - 1)]
-            setStore("active", previous?.id)
-          }
+        .catch((e) => {
+          console.error("Failed to create terminal", e)
+        })
+    },
+    update(pty: Partial<LocalPTY> & { id: string }) {
+      setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
+      sdk.client.pty
+        .update({
+          ptyID: pty.id,
+          title: pty.title,
+          size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
+        })
+        .catch((e) => {
+          console.error("Failed to update terminal", e)
         })
-        await sdk.client.pty.remove({ ptyID: id }).catch((e) => {
-          console.error("Failed to close terminal", e)
+    },
+    async clone(id: string) {
+      const index = store.all.findIndex((x) => x.id === id)
+      const pty = store.all[index]
+      if (!pty) return
+      const clone = await sdk.client.pty
+        .create({
+          title: pty.title,
         })
-      },
-      move(id: string, to: number) {
-        const index = store.all.findIndex((f) => f.id === id)
-        if (index === -1) return
+        .catch((e) => {
+          console.error("Failed to clone terminal", e)
+          return undefined
+        })
+      if (!clone?.data) return
+      setStore("all", index, {
+        ...pty,
+        ...clone.data,
+      })
+      if (store.active === pty.id) {
+        setStore("active", clone.data.id)
+      }
+    },
+    open(id: string) {
+      setStore("active", id)
+    },
+    async close(id: string) {
+      batch(() => {
         setStore(
           "all",
-          produce((all) => {
-            all.splice(to, 0, all.splice(index, 1)[0])
-          }),
+          store.all.filter((x) => x.id !== id),
         )
-      },
+        if (store.active === id) {
+          const index = store.all.findIndex((f) => f.id === id)
+          const previous = store.all[Math.max(0, index - 1)]
+          setStore("active", previous?.id)
+        }
+      })
+      await sdk.client.pty.remove({ ptyID: id }).catch((e) => {
+        console.error("Failed to close terminal", e)
+      })
+    },
+    move(id: string, to: number) {
+      const index = store.all.findIndex((f) => f.id === id)
+      if (index === -1) return
+      setStore(
+        "all",
+        produce((all) => {
+          all.splice(to, 0, all.splice(index, 1)[0])
+        }),
+      )
+    },
+  }
+}
+
+export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({
+  name: "Terminal",
+  gate: false,
+  init: () => {
+    const sdk = useSDK()
+    const params = useParams()
+    const cache = new Map<string, TerminalCacheEntry>()
+
+    const disposeAll = () => {
+      for (const entry of cache.values()) {
+        entry.dispose()
+      }
+      cache.clear()
+    }
+
+    onCleanup(disposeAll)
+
+    const prune = () => {
+      while (cache.size > MAX_TERMINAL_SESSIONS) {
+        const first = cache.keys().next().value
+        if (!first) return
+        const entry = cache.get(first)
+        entry?.dispose()
+        cache.delete(first)
+      }
+    }
+
+    const load = (dir: string, id: string | undefined) => {
+      const key = `${dir}:${id ?? WORKSPACE_KEY}`
+      const existing = cache.get(key)
+      if (existing) {
+        cache.delete(key)
+        cache.set(key, existing)
+        return existing.value
+      }
+
+      const entry = createRoot((dispose) => ({
+        value: createTerminalSession(sdk, dir, id),
+        dispose,
+      }))
+
+      cache.set(key, entry)
+      prune()
+      return entry.value
+    }
+
+    const session = createMemo(() => load(params.dir!, params.id))
+
+    return {
+      ready: () => session().ready(),
+      all: () => session().all(),
+      active: () => session().active(),
+      new: () => session().new(),
+      update: (pty: Partial<LocalPTY> & { id: string }) => session().update(pty),
+      clone: (id: string) => session().clone(id),
+      open: (id: string) => session().open(id),
+      close: (id: string) => session().close(id),
+      move: (id: string, to: number) => session().move(id, to),
     }
   },
 })

+ 4 - 2
packages/app/src/entry.tsx

@@ -1,6 +1,6 @@
 // @refresh reload
 import { render } from "solid-js/web"
-import { App } from "@/app"
+import { AppBaseProviders, AppInterface } from "@/app"
 import { Platform, PlatformProvider } from "@/context/platform"
 import pkg from "../package.json"
 
@@ -55,7 +55,9 @@ const platform: Platform = {
 render(
   () => (
     <PlatformProvider value={platform}>
-      <App />
+      <AppBaseProviders>
+        <AppInterface />
+      </AppBaseProviders>
     </PlatformProvider>
   ),
   root!,

+ 1 - 1
packages/app/src/index.ts

@@ -1,2 +1,2 @@
 export { PlatformProvider, type Platform } from "./context/platform"
-export { App } from "./app"
+export { AppBaseProviders, AppInterface } from "./app"

+ 12 - 2
packages/app/src/pages/directory-layout.tsx

@@ -1,5 +1,5 @@
 import { createMemo, Show, type ParentProps } from "solid-js"
-import { useParams } from "@solidjs/router"
+import { useNavigate, useParams } from "@solidjs/router"
 import { SDKProvider, useSDK } from "@/context/sdk"
 import { SyncProvider, useSync } from "@/context/sync"
 import { LocalProvider } from "@/context/local"
@@ -10,6 +10,7 @@ import { iife } from "@opencode-ai/util/iife"
 
 export default function Layout(props: ParentProps) {
   const params = useParams()
+  const navigate = useNavigate()
   const directory = createMemo(() => {
     return base64Decode(params.dir!)
   })
@@ -26,8 +27,17 @@ export default function Layout(props: ParentProps) {
               response: "once" | "always" | "reject"
             }) => sdk.client.permission.respond(input)
 
+            const navigateToSession = (sessionID: string) => {
+              navigate(`/${params.dir}/session/${sessionID}`)
+            }
+
             return (
-              <DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
+              <DataProvider
+                data={sync.data}
+                directory={directory()}
+                onPermissionRespond={respond}
+                onNavigateToSession={navigateToSession}
+              >
                 <LocalProvider>{props.children}</LocalProvider>
               </DataProvider>
             )

+ 224 - 24
packages/app/src/pages/layout.tsx

@@ -1,4 +1,5 @@
 import {
+  batch,
   createEffect,
   createMemo,
   createSignal,
@@ -31,7 +32,7 @@ import { getFilename } from "@opencode-ai/util/path"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
 import { Session } from "@opencode-ai/sdk/v2/client"
 import { usePlatform } from "@/context/platform"
-import { createStore, produce } from "solid-js/store"
+import { createStore, produce, reconcile } from "solid-js/store"
 import {
   DragDropProvider,
   DragDropSensors,
@@ -47,6 +48,7 @@ import { useGlobalSDK } from "@/context/global-sdk"
 import { useNotification } from "@/context/notification"
 import { usePermission } from "@/context/permission"
 import { Binary } from "@opencode-ai/util/binary"
+import { retry } from "@opencode-ai/util/retry"
 
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
@@ -55,6 +57,7 @@ import { DialogEditProject } from "@/components/dialog-edit-project"
 import { DialogSelectServer } from "@/components/dialog-select-server"
 import { useCommand, type CommandOption } from "@/context/command"
 import { ConstrainDragXAxis } from "@/utils/solid-dnd"
+import { navStart } from "@/utils/perf"
 import { DialogSelectDirectory } from "@/components/dialog-select-directory"
 import { useServer } from "@/context/server"
 
@@ -170,7 +173,7 @@ export default function Layout(props: ParentProps) {
       if (e.details?.type !== "permission.asked") return
       const directory = e.name
       const perm = e.details.properties
-      if (permission.autoResponds(perm)) return
+      if (permission.autoResponds(perm, directory)) return
 
       const [store] = globalSync.child(directory)
       const session = store.session.find((s) => s.id === perm.sessionID)
@@ -266,24 +269,170 @@ export default function Layout(props: ParentProps) {
     }
   }
 
-  function projectSessions(directory: string) {
-    if (!directory) return []
-    const sessions = globalSync.child(directory)[0].session.toSorted(sortSessions)
-    return (sessions ?? []).filter((s) => !s.parentID)
+  const currentProject = createMemo(() => {
+    const directory = params.dir ? base64Decode(params.dir) : undefined
+    if (!directory) return
+    return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
+  })
+
+  function projectSessions(project: LocalProject | undefined) {
+    if (!project) return []
+    const dirs = [project.worktree, ...(project.sandboxes ?? [])]
+    const stores = dirs.map((dir) => globalSync.child(dir)[0])
+    const sessions = stores
+      .flatMap((store) => store.session.filter((session) => session.directory === store.path.directory))
+      .toSorted(sortSessions)
+    return sessions.filter((s) => !s.parentID)
   }
 
-  const currentSessions = createMemo(() => {
-    if (!params.dir) return []
-    const directory = base64Decode(params.dir)
-    return projectSessions(directory)
+  const currentSessions = createMemo(() => projectSessions(currentProject()))
+
+  type PrefetchQueue = {
+    inflight: Set<string>
+    pending: string[]
+    pendingSet: Set<string>
+    running: number
+  }
+
+  const prefetchChunk = 200
+  const prefetchConcurrency = 1
+  const prefetchPendingLimit = 6
+  const prefetchToken = { value: 0 }
+  const prefetchQueues = new Map<string, PrefetchQueue>()
+
+  createEffect(() => {
+    params.dir
+    globalSDK.url
+
+    prefetchToken.value += 1
+    for (const q of prefetchQueues.values()) {
+      q.pending.length = 0
+      q.pendingSet.clear()
+    }
+  })
+
+  const queueFor = (directory: string) => {
+    const existing = prefetchQueues.get(directory)
+    if (existing) return existing
+
+    const created: PrefetchQueue = {
+      inflight: new Set(),
+      pending: [],
+      pendingSet: new Set(),
+      running: 0,
+    }
+    prefetchQueues.set(directory, created)
+    return created
+  }
+
+  const prefetchMessages = (directory: string, sessionID: string, token: number) => {
+    const [, setStore] = globalSync.child(directory)
+
+    return retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
+      .then((messages) => {
+        if (prefetchToken.value !== token) return
+
+        const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
+        const next = items
+          .map((x) => x.info)
+          .filter((m) => !!m?.id)
+          .slice()
+          .sort((a, b) => a.id.localeCompare(b.id))
+
+        batch(() => {
+          setStore("message", sessionID, reconcile(next, { key: "id" }))
+
+          for (const message of items) {
+            setStore(
+              "part",
+              message.info.id,
+              reconcile(
+                message.parts
+                  .filter((p) => !!p?.id)
+                  .slice()
+                  .sort((a, b) => a.id.localeCompare(b.id)),
+                { key: "id" },
+              ),
+            )
+          }
+        })
+      })
+      .catch(() => undefined)
+  }
+
+  const pumpPrefetch = (directory: string) => {
+    const q = queueFor(directory)
+    if (q.running >= prefetchConcurrency) return
+
+    const sessionID = q.pending.shift()
+    if (!sessionID) return
+
+    q.pendingSet.delete(sessionID)
+    q.inflight.add(sessionID)
+    q.running += 1
+
+    const token = prefetchToken.value
+
+    void prefetchMessages(directory, sessionID, token).finally(() => {
+      q.running -= 1
+      q.inflight.delete(sessionID)
+      pumpPrefetch(directory)
+    })
+  }
+
+  const prefetchSession = (session: Session, priority: "high" | "low" = "low") => {
+    const directory = session.directory
+    if (!directory) return
+
+    const [store] = globalSync.child(directory)
+    if (store.message[session.id] !== undefined) return
+
+    const q = queueFor(directory)
+    if (q.inflight.has(session.id)) return
+    if (q.pendingSet.has(session.id)) return
+
+    if (priority === "high") q.pending.unshift(session.id)
+    if (priority !== "high") q.pending.push(session.id)
+    q.pendingSet.add(session.id)
+
+    while (q.pending.length > prefetchPendingLimit) {
+      const dropped = q.pending.pop()
+      if (!dropped) continue
+      q.pendingSet.delete(dropped)
+    }
+
+    pumpPrefetch(directory)
+  }
+
+  createEffect(() => {
+    const sessions = currentSessions()
+    const id = params.id
+
+    if (!id) {
+      const first = sessions[0]
+      if (first) prefetchSession(first)
+
+      const second = sessions[1]
+      if (second) prefetchSession(second)
+      return
+    }
+
+    const index = sessions.findIndex((s) => s.id === id)
+    if (index === -1) return
+
+    const next = sessions[index + 1]
+    if (next) prefetchSession(next)
+
+    const prev = sessions[index - 1]
+    if (prev) prefetchSession(prev)
   })
 
   function navigateSessionByOffset(offset: number) {
     const projects = layout.projects.list()
     if (projects.length === 0) return
 
-    const currentDirectory = params.dir ? base64Decode(params.dir) : undefined
-    const projectIndex = currentDirectory ? projects.findIndex((p) => p.worktree === currentDirectory) : -1
+    const project = currentProject()
+    const projectIndex = project ? projects.findIndex((p) => p.worktree === project.worktree) : -1
 
     if (projectIndex === -1) {
       const targetProject = offset > 0 ? projects[0] : projects[projects.length - 1]
@@ -303,6 +452,27 @@ export default function Layout(props: ParentProps) {
 
     if (targetIndex >= 0 && targetIndex < sessions.length) {
       const session = sessions[targetIndex]
+      const next = sessions[targetIndex + 1]
+      const prev = sessions[targetIndex - 1]
+
+      if (offset > 0) {
+        if (next) prefetchSession(next, "high")
+        if (prev) prefetchSession(prev)
+      }
+
+      if (offset < 0) {
+        if (prev) prefetchSession(prev, "high")
+        if (next) prefetchSession(next)
+      }
+
+      if (import.meta.env.DEV) {
+        navStart({
+          dir: base64Encode(session.directory),
+          from: params.id,
+          to: session.id,
+          trigger: offset > 0 ? "alt+arrowdown" : "alt+arrowup",
+        })
+      }
       navigateToSession(session)
       queueMicrotask(() => scrollToSession(session.id))
       return
@@ -312,14 +482,34 @@ export default function Layout(props: ParentProps) {
     const nextProject = projects[nextProjectIndex]
     if (!nextProject) return
 
-    const nextProjectSessions = projectSessions(nextProject.worktree)
+    const nextProjectSessions = projectSessions(nextProject)
     if (nextProjectSessions.length === 0) {
       navigateToProject(nextProject.worktree)
       return
     }
 
-    const targetSession = offset > 0 ? nextProjectSessions[0] : nextProjectSessions[nextProjectSessions.length - 1]
-    navigate(`/${base64Encode(nextProject.worktree)}/session/${targetSession.id}`)
+    const index = offset > 0 ? 0 : nextProjectSessions.length - 1
+    const targetSession = nextProjectSessions[index]
+    const nextSession = nextProjectSessions[index + 1]
+    const prevSession = nextProjectSessions[index - 1]
+
+    if (offset > 0) {
+      if (nextSession) prefetchSession(nextSession, "high")
+    }
+
+    if (offset < 0) {
+      if (prevSession) prefetchSession(prevSession, "high")
+    }
+
+    if (import.meta.env.DEV) {
+      navStart({
+        dir: base64Encode(targetSession.directory),
+        from: params.id,
+        to: targetSession.id,
+        trigger: offset > 0 ? "alt+arrowdown" : "alt+arrowup",
+      })
+    }
+    navigateToSession(targetSession)
     queueMicrotask(() => scrollToSession(targetSession.id))
   }
 
@@ -465,7 +655,7 @@ export default function Layout(props: ParentProps) {
 
   function navigateToSession(session: Session | undefined) {
     if (!session) return
-    navigate(`/${params.dir}/session/${session?.id}`)
+    navigate(`/${base64Encode(session.directory)}/session/${session.id}`)
     layout.mobileSidebar.hide()
   }
 
@@ -514,7 +704,8 @@ export default function Layout(props: ParentProps) {
     const id = params.id
     setStore("lastSession", directory, id)
     notification.session.markViewed(id)
-    untrack(() => layout.projects.expand(directory))
+    const project = currentProject()
+    untrack(() => layout.projects.expand(project?.worktree ?? directory))
     requestAnimationFrame(() => scrollToSession(id))
   })
 
@@ -644,13 +835,13 @@ export default function Layout(props: ParentProps) {
     const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
     const notifications = createMemo(() => notification.session.unseen(props.session.id))
     const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
+    const [sessionStore] = globalSync.child(props.session.directory)
     const hasPermissions = createMemo(() => {
-      const store = globalSync.child(props.project.worktree)[0]
-      const permissions = store.permission?.[props.session.id] ?? []
+      const permissions = sessionStore.permission?.[props.session.id] ?? []
       if (permissions.length > 0) return true
-      const childSessions = store.session.filter((s) => s.parentID === props.session.id)
+      const childSessions = sessionStore.session.filter((s) => s.parentID === props.session.id)
       for (const child of childSessions) {
-        const childPermissions = store.permission?.[child.id] ?? []
+        const childPermissions = sessionStore.permission?.[child.id] ?? []
         if (childPermissions.length > 0) return true
       }
       return false
@@ -658,7 +849,7 @@ export default function Layout(props: ParentProps) {
     const isWorking = createMemo(() => {
       if (props.session.id === params.id) return false
       if (hasPermissions()) return false
-      const status = globalSync.child(props.project.worktree)[0].session_status[props.session.id]
+      const status = sessionStore.session_status[props.session.id]
       return status?.type === "busy" || status?.type === "retry"
     })
     return (
@@ -672,6 +863,8 @@ export default function Layout(props: ParentProps) {
             <A
               href={`${props.slug}/session/${props.session.id}`}
               class="flex flex-col min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1"
+              onMouseEnter={() => prefetchSession(props.session, "high")}
+              onFocus={() => prefetchSession(props.session, "high")}
             >
               <div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
                 <span
@@ -759,6 +952,10 @@ export default function Layout(props: ParentProps) {
     const isExpanded = createMemo(() =>
       props.mobile ? mobileProjects.expanded(props.project.worktree) : props.project.expanded,
     )
+    const isActive = createMemo(() => {
+      const current = params.dir ? base64Decode(params.dir) : ""
+      return props.project.worktree === current || props.project.sandboxes?.includes(current)
+    })
     const handleOpenChange = (open: boolean) => {
       if (props.mobile) {
         if (open) mobileProjects.expand(props.project.worktree)
@@ -777,7 +974,10 @@ export default function Layout(props: ParentProps) {
               <Button
                 as={"div"}
                 variant="ghost"
-                class="group/session flex items-center justify-between gap-3 w-full px-1.5 self-stretch h-auto border-none rounded-lg"
+                classList={{
+                  "group/session flex items-center justify-between gap-3 w-full px-1.5 self-stretch h-auto border-none rounded-lg": true,
+                  "bg-surface-raised-base-hover": isActive() && !isExpanded(),
+                }}
               >
                 <Collapsible.Trigger class="group/trigger flex items-center gap-3 p-0 text-left min-w-0 grow border-none">
                   <ProjectAvatar
@@ -861,7 +1061,7 @@ export default function Layout(props: ParentProps) {
             </Collapsible>
           </Match>
           <Match when={true}>
-            <Tooltip placement="right" value={props.project.worktree}>
+            <Tooltip placement="right" value={getFilename(props.project.worktree)}>
               <ProjectVisual project={props.project} />
             </Tooltip>
           </Match>

+ 593 - 209
packages/app/src/pages/session.tsx

@@ -1,5 +1,6 @@
-import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, on, batch } from "solid-js"
+import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js"
 import { createMediaQuery } from "@solid-primitives/media"
+import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { Dynamic } from "solid-js/web"
 import { useLocal } from "@/context/local"
 import { selectionFromLines, useFile, type SelectedLineRange } from "@/context/file"
@@ -7,6 +8,7 @@ import { createStore } from "solid-js/store"
 import { PromptInput } from "@/components/prompt-input"
 import { SessionContextUsage } from "@/components/session-context-usage"
 import { IconButton } from "@opencode-ai/ui/icon-button"
+import { Button } from "@opencode-ai/ui/button"
 import { Icon } from "@opencode-ai/ui/icon"
 import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
 import { DiffChanges } from "@opencode-ai/ui/diff-changes"
@@ -24,7 +26,7 @@ import { useSync } from "@/context/sync"
 import { useTerminal, type LocalPTY } from "@/context/terminal"
 import { useLayout } from "@/context/layout"
 import { Terminal } from "@/components/terminal"
-import { checksum, base64Decode } from "@opencode-ai/util/encode"
+import { checksum, base64Encode, base64Decode } from "@opencode-ai/util/encode"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { DialogSelectFile } from "@/components/dialog-select-file"
 import { DialogSelectModel } from "@/components/dialog-select-model"
@@ -48,20 +50,23 @@ import {
   NewSessionView,
 } from "@/components/session"
 import { usePlatform } from "@/context/platform"
-
-function same<T>(a: readonly T[], b: readonly T[]) {
-  if (a === b) return true
-  if (a.length !== b.length) return false
-  return a.every((x, i) => x === b[i])
-}
+import { navMark, navParams } from "@/utils/perf"
+import { same } from "@/utils/same"
 
 type DiffStyle = "unified" | "split"
 
+const handoff = {
+  prompt: "",
+  terminals: [] as string[],
+  files: {} as Record<string, SelectedLineRange | null>,
+}
+
 interface SessionReviewTabProps {
   diffs: () => FileDiff[]
   view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
   diffStyle: DiffStyle
   onDiffStyleChange?: (style: DiffStyle) => void
+  onViewFile?: (file: string) => void
   classes?: {
     root?: string
     header?: string
@@ -141,6 +146,7 @@ function SessionReviewTab(props: SessionReviewTabProps) {
       diffs={props.diffs()}
       diffStyle={props.diffStyle}
       onDiffStyleChange={props.onDiffStyleChange}
+      onViewFile={props.onViewFile}
     />
   )
 }
@@ -164,6 +170,46 @@ export default function Page() {
   const tabs = createMemo(() => layout.tabs(sessionKey()))
   const view = createMemo(() => layout.view(sessionKey()))
 
+  if (import.meta.env.DEV) {
+    createEffect(
+      on(
+        () => [params.dir, params.id] as const,
+        ([dir, id], prev) => {
+          if (!id) return
+          navParams({ dir, from: prev?.[1], to: id })
+        },
+      ),
+    )
+
+    createEffect(() => {
+      const id = params.id
+      if (!id) return
+      if (!prompt.ready()) return
+      navMark({ dir: params.dir, to: id, name: "storage:prompt-ready" })
+    })
+
+    createEffect(() => {
+      const id = params.id
+      if (!id) return
+      if (!terminal.ready()) return
+      navMark({ dir: params.dir, to: id, name: "storage:terminal-ready" })
+    })
+
+    createEffect(() => {
+      const id = params.id
+      if (!id) return
+      if (!file.ready()) return
+      navMark({ dir: params.dir, to: id, name: "storage:file-view-ready" })
+    })
+
+    createEffect(() => {
+      const id = params.id
+      if (!id) return
+      if (sync.data.message[id] === undefined) return
+      navMark({ dir: params.dir, to: id, name: "session:data-ready" })
+    })
+  }
+
   const isDesktop = createMediaQuery("(min-width: 768px)")
 
   function normalizeTab(tab: string) {
@@ -218,6 +264,8 @@ export default function Page() {
   })
 
   const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
+  const reviewCount = createMemo(() => info()?.summary?.files ?? 0)
+  const hasReview = createMemo(() => reviewCount() > 0)
   const revertMessageID = createMemo(() => info()?.revert?.messageID)
   const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
   const messagesReady = createMemo(() => {
@@ -225,6 +273,16 @@ export default function Page() {
     if (!id) return true
     return sync.data.message[id] !== undefined
   })
+  const historyMore = createMemo(() => {
+    const id = params.id
+    if (!id) return false
+    return sync.session.history.more(id)
+  })
+  const historyLoading = createMemo(() => {
+    const id = params.id
+    if (!id) return false
+    return sync.session.history.loading(id)
+  })
   const emptyUserMessages: UserMessage[] = []
   const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[], emptyUserMessages)
   const visibleUserMessages = createMemo(() => {
@@ -251,8 +309,25 @@ export default function Page() {
     activeTerminalDraggable: undefined as string | undefined,
     expanded: {} as Record<string, boolean>,
     messageId: undefined as string | undefined,
+    turnStart: 0,
     mobileTab: "session" as "session" | "review",
     newSessionWorktree: "main",
+    promptHeight: 0,
+  })
+
+  const renderedUserMessages = createMemo(() => {
+    const msgs = visibleUserMessages()
+    const start = store.turnStart
+    if (start <= 0) return msgs
+    if (start >= msgs.length) return emptyUserMessages
+    return msgs.slice(start)
+  }, emptyUserMessages)
+
+  const newSessionWorktree = createMemo(() => {
+    if (store.newSessionWorktree === "create") return "create"
+    const project = sync.project
+    if (project && sync.data.path.directory !== project.worktree) return sync.data.path.directory
+    return "main"
   })
 
   const activeMessage = createMemo(() => {
@@ -284,9 +359,17 @@ export default function Page() {
   }
 
   const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
+  const diffsReady = createMemo(() => {
+    const id = params.id
+    if (!id) return true
+    if (!hasReview()) return true
+    return sync.data.session_diff[id] !== undefined
+  })
 
   const idle = { type: "idle" as const }
   let inputRef!: HTMLDivElement
+  let promptDock: HTMLDivElement | undefined
+  let scroller: HTMLDivElement | undefined
 
   createEffect(() => {
     if (!params.id) return
@@ -294,11 +377,10 @@ export default function Page() {
   })
 
   createEffect(() => {
-    if (layout.terminal.opened()) {
-      if (terminal.all().length === 0) {
-        terminal.new()
-      }
-    }
+    if (!view().terminal.opened()) return
+    if (!terminal.ready()) return
+    if (terminal.all().length !== 0) return
+    terminal.new()
   })
 
   createEffect(
@@ -358,7 +440,7 @@ export default function Page() {
       category: "View",
       keybind: "ctrl+`",
       slash: "terminal",
-      onSelect: () => layout.terminal.toggle(),
+      onSelect: () => view().terminal.toggle(),
     },
     {
       id: "review.toggle",
@@ -366,7 +448,7 @@ export default function Page() {
       description: "Show or hide the review panel",
       category: "View",
       keybind: "mod+shift+r",
-      onSelect: () => layout.review.toggle(),
+      onSelect: () => view().reviewPanel.toggle(),
     },
     {
       id: "terminal.new",
@@ -459,7 +541,10 @@ export default function Page() {
     },
     {
       id: "permissions.autoaccept",
-      title: params.id && permission.isAutoAccepting(params.id) ? "Stop auto-accepting edits" : "Auto-accept edits",
+      title:
+        params.id && permission.isAutoAccepting(params.id, sdk.directory)
+          ? "Stop auto-accepting edits"
+          : "Auto-accept edits",
       category: "Permissions",
       keybind: "mod+shift+a",
       disabled: !params.id || !permission.permissionsEnabled(),
@@ -468,8 +553,10 @@ export default function Page() {
         if (!sessionID) return
         permission.toggleAutoAccept(sessionID, sdk.directory)
         showToast({
-          title: permission.isAutoAccepting(sessionID) ? "Auto-accepting edits" : "Stopped auto-accepting edits",
-          description: permission.isAutoAccepting(sessionID)
+          title: permission.isAutoAccepting(sessionID, sdk.directory)
+            ? "Auto-accepting edits"
+            : "Stopped auto-accepting edits",
+          description: permission.isAutoAccepting(sessionID, sdk.directory)
             ? "Edit and write permissions will be automatically approved"
             : "Edit and write permissions will require approval",
         })
@@ -496,7 +583,7 @@ export default function Page() {
         // Restore the prompt from the reverted message
         const parts = sync.data.part[message.id]
         if (parts) {
-          const restored = extractPromptFromParts(parts)
+          const restored = extractPromptFromParts(parts, { directory: sdk.directory })
           prompt.set(restored)
         }
         // Navigate to the message before the reverted one (which will be the new last visible message)
@@ -630,11 +717,11 @@ export default function Page() {
       .filter((tab) => tab !== "context"),
   )
 
-  const reviewTab = createMemo(() => diffs().length > 0 || tabs().active() === "review")
-  const mobileReview = createMemo(() => !isDesktop() && diffs().length > 0 && store.mobileTab === "review")
+  const reviewTab = createMemo(() => hasReview() || tabs().active() === "review")
+  const mobileReview = createMemo(() => !isDesktop() && hasReview() && store.mobileTab === "review")
 
   const showTabs = createMemo(
-    () => layout.review.opened() && (diffs().length > 0 || tabs().all().length > 0 || contextOpen()),
+    () => view().reviewPanel.opened() && (hasReview() || tabs().all().length > 0 || contextOpen()),
   )
 
   const activeTab = createMemo(() => {
@@ -651,10 +738,22 @@ export default function Page() {
   createEffect(() => {
     if (!layout.ready()) return
     if (tabs().active()) return
-    if (diffs().length === 0 && openedTabs().length === 0 && !contextOpen()) return
+    if (!hasReview() && openedTabs().length === 0 && !contextOpen()) return
     tabs().setActive(activeTab())
   })
 
+  createEffect(() => {
+    const id = params.id
+    if (!id) return
+    if (!hasReview()) return
+
+    const wants = isDesktop() ? view().reviewPanel.opened() && activeTab() === "review" : store.mobileTab === "review"
+    if (!wants) return
+    if (diffsReady()) return
+
+    sync.session.diff(id)
+  })
+
   const isWorking = createMemo(() => status().type !== "idle")
   const autoScroll = createAutoScroll({
     working: isWorking,
@@ -666,9 +765,112 @@ export default function Page() {
   const anchor = (id: string) => `message-${id}`
 
   const setScrollRef = (el: HTMLDivElement | undefined) => {
+    scroller = el
     autoScroll.scrollRef(el)
   }
 
+  const turnInit = 20
+  const turnBatch = 20
+  let turnHandle: number | undefined
+  let turnIdle = false
+
+  function cancelTurnBackfill() {
+    const handle = turnHandle
+    if (handle === undefined) return
+    turnHandle = undefined
+
+    if (turnIdle && window.cancelIdleCallback) {
+      window.cancelIdleCallback(handle)
+      return
+    }
+
+    clearTimeout(handle)
+  }
+
+  function scheduleTurnBackfill() {
+    if (turnHandle !== undefined) return
+    if (store.turnStart <= 0) return
+
+    if (window.requestIdleCallback) {
+      turnIdle = true
+      turnHandle = window.requestIdleCallback(() => {
+        turnHandle = undefined
+        backfillTurns()
+      })
+      return
+    }
+
+    turnIdle = false
+    turnHandle = window.setTimeout(() => {
+      turnHandle = undefined
+      backfillTurns()
+    }, 0)
+  }
+
+  function backfillTurns() {
+    const start = store.turnStart
+    if (start <= 0) return
+
+    const next = start - turnBatch
+    const nextStart = next > 0 ? next : 0
+
+    const el = scroller
+    if (!el) {
+      setStore("turnStart", nextStart)
+      scheduleTurnBackfill()
+      return
+    }
+
+    const beforeTop = el.scrollTop
+    const beforeHeight = el.scrollHeight
+
+    setStore("turnStart", nextStart)
+
+    requestAnimationFrame(() => {
+      const delta = el.scrollHeight - beforeHeight
+      if (delta) el.scrollTop = beforeTop + delta
+    })
+
+    scheduleTurnBackfill()
+  }
+
+  createEffect(
+    on(
+      () => [params.id, messagesReady()] as const,
+      ([id, ready]) => {
+        cancelTurnBackfill()
+        setStore("turnStart", 0)
+        if (!id || !ready) return
+
+        const len = visibleUserMessages().length
+        const start = len > turnInit ? len - turnInit : 0
+        setStore("turnStart", start)
+        scheduleTurnBackfill()
+      },
+      { defer: true },
+    ),
+  )
+
+  createResizeObserver(
+    () => promptDock,
+    ({ height }) => {
+      const next = Math.ceil(height)
+
+      if (next === store.promptHeight) return
+
+      const el = scroller
+      const stick = el ? el.scrollHeight - el.clientHeight - el.scrollTop < 10 : false
+
+      setStore("promptHeight", next)
+
+      if (stick && el) {
+        requestAnimationFrame(() => {
+          el.scrollTo({ top: el.scrollHeight, behavior: "auto" })
+        })
+      }
+    },
+  )
+
   const updateHash = (id: string) => {
     window.history.replaceState(null, "", `#${anchor(id)}`)
   }
@@ -676,6 +878,21 @@ export default function Page() {
   const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
     setActiveMessage(message)
 
+    const msgs = visibleUserMessages()
+    const index = msgs.findIndex((m) => m.id === message.id)
+    if (index !== -1 && index < store.turnStart) {
+      setStore("turnStart", index)
+      scheduleTurnBackfill()
+
+      requestAnimationFrame(() => {
+        const el = document.getElementById(anchor(message.id))
+        if (el) el.scrollIntoView({ behavior, block: "start" })
+      })
+
+      updateHash(message.id)
+      return
+    }
+
     const el = document.getElementById(anchor(message.id))
     if (el) el.scrollIntoView({ behavior, block: "start" })
     updateHash(message.id)
@@ -721,12 +938,27 @@ export default function Page() {
     if (!sessionID || !ready) return
 
     requestAnimationFrame(() => {
-      const id = window.location.hash.slice(1)
-      const hashTarget = id ? document.getElementById(id) : undefined
+      const hash = window.location.hash.slice(1)
+      if (!hash) {
+        autoScroll.forceScrollToBottom()
+        return
+      }
+
+      const hashTarget = document.getElementById(hash)
       if (hashTarget) {
         hashTarget.scrollIntoView({ behavior: "auto", block: "start" })
         return
       }
+
+      const match = hash.match(/^message-(.+)$/)
+      if (match) {
+        const msg = visibleUserMessages().find((m) => m.id === match[1])
+        if (msg) {
+          scrollToMessage(msg, "auto")
+          return
+        }
+      }
+
       autoScroll.forceScrollToBottom()
     })
   })
@@ -735,7 +967,43 @@ export default function Page() {
     document.addEventListener("keydown", handleKeyDown)
   })
 
+  const previewPrompt = () =>
+    prompt
+      .current()
+      .map((part) => {
+        if (part.type === "file") return `[file:${part.path}]`
+        if (part.type === "agent") return `@${part.name}`
+        if (part.type === "image") return `[image:${part.filename}]`
+        return part.content
+      })
+      .join("")
+      .trim()
+
+  createEffect(() => {
+    if (!prompt.ready()) return
+    handoff.prompt = previewPrompt()
+  })
+
+  createEffect(() => {
+    if (!terminal.ready()) return
+    handoff.terminals = terminal.all().map((t) => t.title)
+  })
+
+  createEffect(() => {
+    if (!file.ready()) return
+    handoff.files = Object.fromEntries(
+      tabs()
+        .all()
+        .flatMap((tab) => {
+          const path = file.pathFromTab(tab)
+          if (!path) return []
+          return [[path, file.selectedLines(path) ?? null] as const]
+        }),
+    )
+  })
+
   onCleanup(() => {
+    cancelTurnBackfill()
     document.removeEventListener("keydown", handleKeyDown)
     if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)
   })
@@ -745,7 +1013,7 @@ export default function Page() {
       <SessionHeader />
       <div class="flex-1 min-h-0 flex flex-col md:flex-row">
         {/* Mobile tab bar - only shown on mobile when there are diffs */}
-        <Show when={!isDesktop() && diffs().length > 0}>
+        <Show when={!isDesktop() && hasReview()}>
           <Tabs class="h-auto">
             <Tabs.List>
               <Tabs.Trigger
@@ -762,7 +1030,7 @@ export default function Page() {
                 classes={{ button: "w-full" }}
                 onClick={() => setStore("mobileTab", "review")}
               >
-                {diffs().length} Files Changed
+                {reviewCount()} Files Changed
               </Tabs.Trigger>
             </Tabs.List>
           </Tabs>
@@ -774,7 +1042,10 @@ export default function Page() {
             "@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true,
             "flex-1 md:flex-none py-6 md:py-3": true,
           }}
-          style={{ width: isDesktop() && showTabs() ? `${layout.session.width()}px` : "100%" }}
+          style={{
+            width: isDesktop() && showTabs() ? `${layout.session.width()}px` : "100%",
+            "--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined,
+          }}
         >
           <div class="flex-1 min-h-0 overflow-hidden">
             <Switch>
@@ -784,16 +1055,26 @@ export default function Page() {
                     when={!mobileReview()}
                     fallback={
                       <div class="relative h-full overflow-hidden">
-                        <SessionReviewTab
-                          diffs={diffs}
-                          view={view}
-                          diffStyle="unified"
-                          classes={{
-                            root: "pb-32",
-                            header: "px-4",
-                            container: "px-4",
-                          }}
-                        />
+                        <Show
+                          when={diffsReady()}
+                          fallback={<div class="px-4 py-4 text-text-weak">Loading changes...</div>}
+                        >
+                          <SessionReviewTab
+                            diffs={diffs}
+                            view={view}
+                            diffStyle="unified"
+                            onViewFile={(path) => {
+                              const value = file.tab(path)
+                              tabs().open(value)
+                              file.load(path)
+                            }}
+                            classes={{
+                              root: "pb-[calc(var(--prompt-height,8rem)+32px)]",
+                              header: "px-4",
+                              container: "px-4",
+                            }}
+                          />
+                        </Show>
                       </div>
                     }
                   >
@@ -820,48 +1101,88 @@ export default function Page() {
                       >
                         <div
                           ref={autoScroll.contentRef}
-                          class="flex flex-col gap-32 items-start justify-start pb-32 md:pb-40 transition-[margin]"
+                          class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
                           classList={{
                             "mt-0.5": !showTabs(),
                             "mt-0": showTabs(),
                           }}
                         >
-                          <For each={visibleUserMessages()}>
-                            {(message) => (
-                              <div
-                                id={anchor(message.id)}
-                                data-message-id={message.id}
-                                classList={{
-                                  "min-w-0 w-full max-w-full": true,
-                                  "last:min-h-[calc(100vh-13.5rem)] md:last:min-h-[calc(100vh-14.5rem)]":
-                                    platform.platform !== "desktop",
-                                  "last:min-h-[calc(100vh-15rem)] md:last:min-h-[calc(100vh-16rem)]":
-                                    platform.platform === "desktop",
+                          <Show when={store.turnStart > 0}>
+                            <div class="w-full flex justify-center">
+                              <Button
+                                variant="ghost"
+                                size="large"
+                                class="text-12-medium opacity-50"
+                                onClick={() => setStore("turnStart", 0)}
+                              >
+                                Render earlier messages
+                              </Button>
+                            </div>
+                          </Show>
+                          <Show when={historyMore()}>
+                            <div class="w-full flex justify-center">
+                              <Button
+                                variant="ghost"
+                                size="large"
+                                class="text-12-medium opacity-50"
+                                disabled={historyLoading()}
+                                onClick={() => {
+                                  const id = params.id
+                                  if (!id) return
+                                  setStore("turnStart", 0)
+                                  sync.session.history.loadMore(id)
                                 }}
                               >
-                                <SessionTurn
-                                  sessionID={params.id!}
-                                  messageID={message.id}
-                                  lastUserMessageID={lastUserMessage()?.id}
-                                  stepsExpanded={store.expanded[message.id] ?? false}
-                                  onStepsExpandedToggle={() =>
-                                    setStore("expanded", message.id, (open: boolean | undefined) => !open)
-                                  }
-                                  classes={{
-                                    root: "min-w-0 w-full relative",
-                                    content:
-                                      "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
-                                    container:
-                                      "px-4 md:px-6 " +
-                                      (!showTabs()
-                                        ? "md:max-w-200 md:mx-auto"
-                                        : visibleUserMessages().length > 1
-                                          ? "md:pr-6 md:pl-18"
-                                          : ""),
+                                {historyLoading() ? "Loading earlier messages..." : "Load earlier messages"}
+                              </Button>
+                            </div>
+                          </Show>
+                          <For each={renderedUserMessages()}>
+                            {(message) => {
+                              if (import.meta.env.DEV) {
+                                onMount(() => {
+                                  const id = params.id
+                                  if (!id) return
+                                  navMark({ dir: params.dir, to: id, name: "session:first-turn-mounted" })
+                                })
+                              }
+
+                              return (
+                                <div
+                                  id={anchor(message.id)}
+                                  data-message-id={message.id}
+                                  classList={{
+                                    "min-w-0 w-full max-w-full": true,
+                                    "last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]":
+                                      platform.platform !== "desktop",
+                                    "last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-64px)]":
+                                      platform.platform === "desktop",
                                   }}
-                                />
-                              </div>
-                            )}
+                                >
+                                  <SessionTurn
+                                    sessionID={params.id!}
+                                    messageID={message.id}
+                                    lastUserMessageID={lastUserMessage()?.id}
+                                    stepsExpanded={store.expanded[message.id] ?? false}
+                                    onStepsExpandedToggle={() =>
+                                      setStore("expanded", message.id, (open: boolean | undefined) => !open)
+                                    }
+                                    classes={{
+                                      root: "min-w-0 w-full relative",
+                                      content:
+                                        "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
+                                      container:
+                                        "px-4 md:px-6 " +
+                                        (!showTabs()
+                                          ? "md:max-w-200 md:mx-auto"
+                                          : visibleUserMessages().length > 1
+                                            ? "md:pr-6 md:pl-18"
+                                            : ""),
+                                    }}
+                                  />
+                                </div>
+                              )
+                            }}
                           </For>
                         </div>
                       </div>
@@ -871,28 +1192,53 @@ export default function Page() {
               </Match>
               <Match when={true}>
                 <NewSessionView
-                  worktree={store.newSessionWorktree}
-                  onWorktreeChange={(value) => setStore("newSessionWorktree", value)}
+                  worktree={newSessionWorktree()}
+                  onWorktreeChange={(value) => {
+                    if (value === "create") {
+                      setStore("newSessionWorktree", value)
+                      return
+                    }
+
+                    setStore("newSessionWorktree", "main")
+
+                    const target = value === "main" ? sync.project?.worktree : value
+                    if (!target) return
+                    if (target === sync.data.path.directory) return
+                    layout.projects.open(target)
+                    navigate(`/${base64Encode(target)}/session`)
+                  }}
                 />
               </Match>
             </Switch>
           </div>
 
           {/* Prompt input */}
-          <div class="absolute inset-x-0 bottom-0 pt-12 pb-4 md:pb-8 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none">
+          <div
+            ref={(el) => (promptDock = el)}
+            class="absolute inset-x-0 bottom-0 pt-12 pb-4 md:pb-8 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none"
+          >
             <div
               classList={{
                 "w-full md:px-6 pointer-events-auto": true,
                 "md:max-w-200": !showTabs(),
               }}
             >
-              <PromptInput
-                ref={(el) => {
-                  inputRef = el
-                }}
-                newSessionWorktree={store.newSessionWorktree}
-                onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
-              />
+              <Show
+                when={prompt.ready()}
+                fallback={
+                  <div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
+                    {handoff.prompt || "Loading prompt..."}
+                  </div>
+                }
+              >
+                <PromptInput
+                  ref={(el) => {
+                    inputRef = el
+                  }}
+                  newSessionWorktree={newSessionWorktree()}
+                  onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
+                />
+              </Show>
             </div>
           </div>
 
@@ -947,6 +1293,7 @@ export default function Page() {
                           </Tooltip>
                         }
                         hideCloseButton
+                        onMiddleClick={() => tabs().close("context")}
                       >
                         <div class="flex items-center gap-2">
                           <SessionContextUsage variant="indicator" />
@@ -975,26 +1322,40 @@ export default function Page() {
                 </div>
                 <Show when={reviewTab()}>
                   <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
-                    <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
-                      <SessionReviewTab
-                        diffs={diffs}
-                        view={view}
-                        diffStyle={layout.review.diffStyle()}
-                        onDiffStyleChange={layout.review.setDiffStyle}
-                      />
-                    </div>
+                    <Show when={activeTab() === "review"}>
+                      <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
+                        <Show
+                          when={diffsReady()}
+                          fallback={<div class="px-6 py-4 text-text-weak">Loading changes...</div>}
+                        >
+                          <SessionReviewTab
+                            diffs={diffs}
+                            view={view}
+                            diffStyle={layout.review.diffStyle()}
+                            onDiffStyleChange={layout.review.setDiffStyle}
+                            onViewFile={(path) => {
+                              const value = file.tab(path)
+                              tabs().open(value)
+                              file.load(path)
+                            }}
+                          />
+                        </Show>
+                      </div>
+                    </Show>
                   </Tabs.Content>
                 </Show>
                 <Show when={contextOpen()}>
                   <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
-                    <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
-                      <SessionContextTab
-                        messages={messages}
-                        visibleUserMessages={visibleUserMessages}
-                        view={view}
-                        info={info}
-                      />
-                    </div>
+                    <Show when={activeTab() === "context"}>
+                      <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
+                        <SessionContextTab
+                          messages={messages}
+                          visibleUserMessages={visibleUserMessages}
+                          view={view}
+                          info={info}
+                        />
+                      </div>
+                    </Show>
                   </Tabs.Content>
                 </Show>
                 <For each={openedTabs()}>
@@ -1043,7 +1404,8 @@ export default function Page() {
                     const selectedLines = createMemo(() => {
                       const p = path()
                       if (!p) return null
-                      return file.selectedLines(p) ?? null
+                      if (file.ready()) return file.selectedLines(p) ?? null
+                      return handoff.files[p] ?? null
                     })
                     const selection = createMemo(() => {
                       const range = selectedLines()
@@ -1140,37 +1502,63 @@ export default function Page() {
                         }}
                         onScroll={handleScroll}
                       >
-                        <Show when={selection()}>
-                          {(sel) => (
-                            <div class="hidden sticky top-0 z-10 px-6 py-2 _flex justify-end bg-background-base border-b border-border-weak-base">
-                              <button
-                                type="button"
-                                class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base text-12-regular text-text-strong hover:bg-surface-raised-base-hover"
-                                onClick={() => {
-                                  const p = path()
-                                  if (!p) return
-                                  prompt.context.add({ type: "file", path: p, selection: sel() })
-                                }}
-                              >
-                                <Icon name="plus-small" size="small" />
-                                <span>Add {selectionLabel()} to context</span>
-                              </button>
-                            </div>
-                          )}
-                        </Show>
-                        <Switch>
-                          <Match when={state()?.loaded && isImage()}>
-                            <div class="px-6 py-4 pb-40">
-                              <img src={imageDataUrl()} alt={path()} class="max-w-full" />
-                            </div>
-                          </Match>
-                          <Match when={state()?.loaded && isSvg()}>
-                            <div class="flex flex-col gap-4 px-6 py-4">
+                        <Show when={activeTab() === tab}>
+                          <Show when={selection()}>
+                            {(sel) => (
+                              <div class="hidden sticky top-0 z-10 px-6 py-2 _flex justify-end bg-background-base border-b border-border-weak-base">
+                                <button
+                                  type="button"
+                                  class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base text-12-regular text-text-strong hover:bg-surface-raised-base-hover"
+                                  onClick={() => {
+                                    const p = path()
+                                    if (!p) return
+                                    prompt.context.add({ type: "file", path: p, selection: sel() })
+                                  }}
+                                >
+                                  <Icon name="plus-small" size="small" />
+                                  <span>Add {selectionLabel()} to context</span>
+                                </button>
+                              </div>
+                            )}
+                          </Show>
+                          <Switch>
+                            <Match when={state()?.loaded && isImage()}>
+                              <div class="px-6 py-4 pb-40">
+                                <img src={imageDataUrl()} alt={path()} class="max-w-full" />
+                              </div>
+                            </Match>
+                            <Match when={state()?.loaded && isSvg()}>
+                              <div class="flex flex-col gap-4 px-6 py-4">
+                                <Dynamic
+                                  component={codeComponent}
+                                  file={{
+                                    name: path() ?? "",
+                                    contents: svgContent() ?? "",
+                                    cacheKey: cacheKey(),
+                                  }}
+                                  enableLineSelection
+                                  selectedLines={selectedLines()}
+                                  onLineSelected={(range: SelectedLineRange | null) => {
+                                    const p = path()
+                                    if (!p) return
+                                    file.setSelectedLines(p, range)
+                                  }}
+                                  overflow="scroll"
+                                  class="select-text"
+                                />
+                                <Show when={svgPreviewUrl()}>
+                                  <div class="flex justify-center pb-40">
+                                    <img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
+                                  </div>
+                                </Show>
+                              </div>
+                            </Match>
+                            <Match when={state()?.loaded}>
                               <Dynamic
                                 component={codeComponent}
                                 file={{
                                   name: path() ?? "",
-                                  contents: svgContent() ?? "",
+                                  contents: contents(),
                                   cacheKey: cacheKey(),
                                 }}
                                 enableLineSelection
@@ -1181,41 +1569,17 @@ export default function Page() {
                                   file.setSelectedLines(p, range)
                                 }}
                                 overflow="scroll"
-                                class="select-text"
+                                class="select-text pb-40"
                               />
-                              <Show when={svgPreviewUrl()}>
-                                <div class="flex justify-center pb-40">
-                                  <img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
-                                </div>
-                              </Show>
-                            </div>
-                          </Match>
-                          <Match when={state()?.loaded}>
-                            <Dynamic
-                              component={codeComponent}
-                              file={{
-                                name: path() ?? "",
-                                contents: contents(),
-                                cacheKey: cacheKey(),
-                              }}
-                              enableLineSelection
-                              selectedLines={selectedLines()}
-                              onLineSelected={(range: SelectedLineRange | null) => {
-                                const p = path()
-                                if (!p) return
-                                file.setSelectedLines(p, range)
-                              }}
-                              overflow="scroll"
-                              class="select-text pb-40"
-                            />
-                          </Match>
-                          <Match when={state()?.loading}>
-                            <div class="px-6 py-4 text-text-weak">Loading...</div>
-                          </Match>
-                          <Match when={state()?.error}>
-                            {(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}
-                          </Match>
-                        </Switch>
+                            </Match>
+                            <Match when={state()?.loading}>
+                              <div class="px-6 py-4 text-text-weak">Loading...</div>
+                            </Match>
+                            <Match when={state()?.error}>
+                              {(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}
+                            </Match>
+                          </Switch>
+                        </Show>
                       </Tabs.Content>
                     )
                   }}
@@ -1238,7 +1602,7 @@ export default function Page() {
         </Show>
       </div>
 
-      <Show when={isDesktop() && layout.terminal.opened()}>
+      <Show when={isDesktop() && view().terminal.opened()}>
         <div
           class="relative w-full flex-col shrink-0 border-t border-border-weak-base"
           style={{ height: `${layout.terminal.height()}px` }}
@@ -1250,56 +1614,76 @@ export default function Page() {
             max={window.innerHeight * 0.6}
             collapseThreshold={50}
             onResize={layout.terminal.resize}
-            onCollapse={layout.terminal.close}
+            onCollapse={view().terminal.close}
           />
-          <DragDropProvider
-            onDragStart={handleTerminalDragStart}
-            onDragEnd={handleTerminalDragEnd}
-            onDragOver={handleTerminalDragOver}
-            collisionDetector={closestCenter}
-          >
-            <DragDropSensors />
-            <ConstrainDragYAxis />
-            <Tabs variant="alt" value={terminal.active()} onChange={terminal.open}>
-              <Tabs.List class="h-10">
-                <SortableProvider ids={terminal.all().map((t: LocalPTY) => t.id)}>
-                  <For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
-                </SortableProvider>
-                <div class="h-full flex items-center justify-center">
-                  <TooltipKeybind
-                    title="New terminal"
-                    keybind={command.keybind("terminal.new")}
-                    class="flex items-center"
-                  >
-                    <IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} />
-                  </TooltipKeybind>
+          <Show
+            when={terminal.ready()}
+            fallback={
+              <div class="flex flex-col h-full pointer-events-none">
+                <div class="h-10 flex items-center gap-2 px-2 border-b border-border-weak-base bg-background-stronger overflow-hidden">
+                  <For each={handoff.terminals}>
+                    {(title) => (
+                      <div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40">
+                        {title}
+                      </div>
+                    )}
+                  </For>
+                  <div class="flex-1" />
+                  <div class="text-text-weak pr-2">Loading...</div>
                 </div>
-              </Tabs.List>
-              <For each={terminal.all()}>
-                {(pty) => (
-                  <Tabs.Content value={pty.id}>
-                    <Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} />
-                  </Tabs.Content>
-                )}
-              </For>
-            </Tabs>
-            <DragOverlay>
-              <Show when={store.activeTerminalDraggable}>
-                {(draggedId) => {
-                  const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId()))
-                  return (
-                    <Show when={pty()}>
-                      {(t) => (
-                        <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
-                          {t().title}
-                        </div>
-                      )}
-                    </Show>
-                  )
-                }}
-              </Show>
-            </DragOverlay>
-          </DragDropProvider>
+                <div class="flex-1 flex items-center justify-center text-text-weak">Loading terminal...</div>
+              </div>
+            }
+          >
+            <DragDropProvider
+              onDragStart={handleTerminalDragStart}
+              onDragEnd={handleTerminalDragEnd}
+              onDragOver={handleTerminalDragOver}
+              collisionDetector={closestCenter}
+            >
+              <DragDropSensors />
+              <ConstrainDragYAxis />
+              <Tabs variant="alt" value={terminal.active()} onChange={terminal.open}>
+                <Tabs.List class="h-10">
+                  <SortableProvider ids={terminal.all().map((t: LocalPTY) => t.id)}>
+                    <For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
+                  </SortableProvider>
+                  <div class="h-full flex items-center justify-center">
+                    <TooltipKeybind
+                      title="New terminal"
+                      keybind={command.keybind("terminal.new")}
+                      class="flex items-center"
+                    >
+                      <IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} />
+                    </TooltipKeybind>
+                  </div>
+                </Tabs.List>
+                <For each={terminal.all()}>
+                  {(pty) => (
+                    <Tabs.Content value={pty.id}>
+                      <Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} />
+                    </Tabs.Content>
+                  )}
+                </For>
+              </Tabs>
+              <DragOverlay>
+                <Show when={store.activeTerminalDraggable}>
+                  {(draggedId) => {
+                    const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId()))
+                    return (
+                      <Show when={pty()}>
+                        {(t) => (
+                          <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
+                            {t().title}
+                          </div>
+                        )}
+                      </Show>
+                    )
+                  }}
+                </Show>
+              </DragOverlay>
+            </DragDropProvider>
+          </Show>
         </div>
       </Show>
     </div>

+ 135 - 0
packages/app/src/utils/perf.ts

@@ -0,0 +1,135 @@
+type Nav = {
+  id: string
+  dir?: string
+  from?: string
+  to: string
+  trigger?: string
+  start: number
+  marks: Record<string, number>
+  logged: boolean
+  timer?: ReturnType<typeof setTimeout>
+}
+
+const dev = import.meta.env.DEV
+
+const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}`
+
+const now = () => performance.now()
+
+const uid = () => crypto.randomUUID?.() ?? Math.random().toString(16).slice(2)
+
+const navs = new Map<string, Nav>()
+const pending = new Map<string, string>()
+const active = new Map<string, string>()
+
+const required = [
+  "session:params",
+  "session:data-ready",
+  "session:first-turn-mounted",
+  "storage:prompt-ready",
+  "storage:terminal-ready",
+  "storage:file-view-ready",
+]
+
+function flush(id: string, reason: "complete" | "timeout") {
+  if (!dev) return
+  const nav = navs.get(id)
+  if (!nav) return
+  if (nav.logged) return
+
+  nav.logged = true
+  if (nav.timer) clearTimeout(nav.timer)
+
+  const baseName = nav.marks["navigate:start"] !== undefined ? "navigate:start" : "session:params"
+  const base = nav.marks[baseName] ?? nav.start
+
+  const ms = Object.fromEntries(
+    Object.entries(nav.marks)
+      .slice()
+      .sort(([a], [b]) => a.localeCompare(b))
+      .map(([name, t]) => [name, Math.round((t - base) * 100) / 100]),
+  )
+
+  console.log(
+    "perf.session-nav " +
+      JSON.stringify({
+        type: "perf.session-nav.v0",
+        id: nav.id,
+        dir: nav.dir,
+        from: nav.from,
+        to: nav.to,
+        trigger: nav.trigger,
+        base: baseName,
+        reason,
+        ms,
+      }),
+  )
+
+  navs.delete(id)
+}
+
+function maybeFlush(id: string) {
+  if (!dev) return
+  const nav = navs.get(id)
+  if (!nav) return
+  if (nav.logged) return
+  if (!required.every((name) => nav.marks[name] !== undefined)) return
+  flush(id, "complete")
+}
+
+function ensure(id: string, data: Omit<Nav, "marks" | "logged" | "timer">) {
+  const existing = navs.get(id)
+  if (existing) return existing
+
+  const nav: Nav = {
+    ...data,
+    marks: {},
+    logged: false,
+  }
+  nav.timer = setTimeout(() => flush(id, "timeout"), 5000)
+  navs.set(id, nav)
+  return nav
+}
+
+export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) {
+  if (!dev) return
+
+  const id = uid()
+  const start = now()
+  const nav = ensure(id, { ...input, id, start })
+  nav.marks["navigate:start"] = start
+
+  pending.set(key(input.dir, input.to), id)
+  return id
+}
+
+export function navParams(input: { dir?: string; from?: string; to: string }) {
+  if (!dev) return
+
+  const k = key(input.dir, input.to)
+  const pendingId = pending.get(k)
+  if (pendingId) pending.delete(k)
+  const id = pendingId ?? uid()
+
+  const start = now()
+  const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" })
+  nav.marks["session:params"] = start
+
+  active.set(k, id)
+  maybeFlush(id)
+  return id
+}
+
+export function navMark(input: { dir?: string; to: string; name: string }) {
+  if (!dev) return
+
+  const id = active.get(key(input.dir, input.to))
+  if (!id) return
+
+  const nav = navs.get(id)
+  if (!nav) return
+  if (nav.marks[input.name] !== undefined) return
+
+  nav.marks[input.name] = now()
+  maybeFlush(id)
+}

+ 223 - 5
packages/app/src/utils/persist.ts

@@ -1,17 +1,235 @@
 import { usePlatform } from "@/context/platform"
-import { makePersisted } from "@solid-primitives/storage"
+import { makePersisted, type AsyncStorage, type SyncStorage } from "@solid-primitives/storage"
+import { checksum } from "@opencode-ai/util/encode"
 import { createResource, type Accessor } from "solid-js"
 import type { SetStoreFunction, Store } from "solid-js/store"
 
 type InitType = Promise<string> | string | null
 type PersistedWithReady<T> = [Store<T>, SetStoreFunction<T>, InitType, Accessor<boolean>]
 
-export function persisted<T>(key: string, store: [Store<T>, SetStoreFunction<T>]): PersistedWithReady<T> {
+type PersistTarget = {
+  storage?: string
+  key: string
+  legacy?: string[]
+  migrate?: (value: unknown) => unknown
+}
+
+const LEGACY_STORAGE = "default.dat"
+const GLOBAL_STORAGE = "opencode.global.dat"
+
+function snapshot(value: unknown) {
+  return JSON.parse(JSON.stringify(value)) as unknown
+}
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+  return typeof value === "object" && value !== null && !Array.isArray(value)
+}
+
+function merge(defaults: unknown, value: unknown): unknown {
+  if (value === undefined) return defaults
+  if (value === null) return value
+
+  if (Array.isArray(defaults)) {
+    if (Array.isArray(value)) return value
+    return defaults
+  }
+
+  if (isRecord(defaults)) {
+    if (!isRecord(value)) return defaults
+
+    const result: Record<string, unknown> = { ...defaults }
+    for (const key of Object.keys(value)) {
+      if (key in defaults) {
+        result[key] = merge((defaults as Record<string, unknown>)[key], (value as Record<string, unknown>)[key])
+      } else {
+        result[key] = (value as Record<string, unknown>)[key]
+      }
+    }
+    return result
+  }
+
+  return value
+}
+
+function parse(value: string) {
+  try {
+    return JSON.parse(value) as unknown
+  } catch {
+    return undefined
+  }
+}
+
+function workspaceStorage(dir: string) {
+  const head = dir.slice(0, 12) || "workspace"
+  const sum = checksum(dir) ?? "0"
+  return `opencode.workspace.${head}.${sum}.dat`
+}
+
+function localStorageWithPrefix(prefix: string): SyncStorage {
+  const base = `${prefix}:`
+  return {
+    getItem: (key) => localStorage.getItem(base + key),
+    setItem: (key, value) => localStorage.setItem(base + key, value),
+    removeItem: (key) => localStorage.removeItem(base + key),
+  }
+}
+
+export const Persist = {
+  global(key: string, legacy?: string[]): PersistTarget {
+    return { storage: GLOBAL_STORAGE, key, legacy }
+  },
+  workspace(dir: string, key: string, legacy?: string[]): PersistTarget {
+    return { storage: workspaceStorage(dir), key: `workspace:${key}`, legacy }
+  },
+  session(dir: string, session: string, key: string, legacy?: string[]): PersistTarget {
+    return { storage: workspaceStorage(dir), key: `session:${session}:${key}`, legacy }
+  },
+  scoped(dir: string, session: string | undefined, key: string, legacy?: string[]): PersistTarget {
+    if (session) return Persist.session(dir, session, key, legacy)
+    return Persist.workspace(dir, key, legacy)
+  },
+}
+
+export function removePersisted(target: { storage?: string; key: string }) {
   const platform = usePlatform()
-  const [state, setState, init] = makePersisted(store, { name: key, storage: platform.storage?.() ?? localStorage })
+  const isDesktop = platform.platform === "desktop" && !!platform.storage
+
+  if (isDesktop) {
+    return platform.storage?.(target.storage)?.removeItem(target.key)
+  }
+
+  if (!target.storage) {
+    localStorage.removeItem(target.key)
+    return
+  }
+
+  localStorageWithPrefix(target.storage).removeItem(target.key)
+}
+
+export function persisted<T>(
+  target: string | PersistTarget,
+  store: [Store<T>, SetStoreFunction<T>],
+): PersistedWithReady<T> {
+  const platform = usePlatform()
+  const config: PersistTarget = typeof target === "string" ? { key: target } : target
+
+  const defaults = snapshot(store[0])
+  const legacy = config.legacy ?? []
+
+  const isDesktop = platform.platform === "desktop" && !!platform.storage
+
+  const currentStorage = (() => {
+    if (isDesktop) return platform.storage?.(config.storage)
+    if (!config.storage) return localStorage
+    return localStorageWithPrefix(config.storage)
+  })()
+
+  const legacyStorage = (() => {
+    if (!isDesktop) return localStorage
+    if (!config.storage) return platform.storage?.()
+    return platform.storage?.(LEGACY_STORAGE)
+  })()
+
+  const storage = (() => {
+    if (!isDesktop) {
+      const current = currentStorage as SyncStorage
+      const legacyStore = legacyStorage as SyncStorage
+
+      const api: SyncStorage = {
+        getItem: (key) => {
+          const raw = current.getItem(key)
+          if (raw !== null) {
+            const parsed = parse(raw)
+            if (parsed === undefined) return raw
+
+            const migrated = config.migrate ? config.migrate(parsed) : parsed
+            const merged = merge(defaults, migrated)
+            const next = JSON.stringify(merged)
+            if (raw !== next) current.setItem(key, next)
+            return next
+          }
+
+          for (const legacyKey of legacy) {
+            const legacyRaw = legacyStore.getItem(legacyKey)
+            if (legacyRaw === null) continue
+
+            current.setItem(key, legacyRaw)
+            legacyStore.removeItem(legacyKey)
+
+            const parsed = parse(legacyRaw)
+            if (parsed === undefined) return legacyRaw
+
+            const migrated = config.migrate ? config.migrate(parsed) : parsed
+            const merged = merge(defaults, migrated)
+            const next = JSON.stringify(merged)
+            if (legacyRaw !== next) current.setItem(key, next)
+            return next
+          }
+
+          return null
+        },
+        setItem: (key, value) => {
+          current.setItem(key, value)
+        },
+        removeItem: (key) => {
+          current.removeItem(key)
+        },
+      }
+
+      return api
+    }
+
+    const current = currentStorage as AsyncStorage
+    const legacyStore = legacyStorage as AsyncStorage | undefined
+
+    const api: AsyncStorage = {
+      getItem: async (key) => {
+        const raw = await current.getItem(key)
+        if (raw !== null) {
+          const parsed = parse(raw)
+          if (parsed === undefined) return raw
+
+          const migrated = config.migrate ? config.migrate(parsed) : parsed
+          const merged = merge(defaults, migrated)
+          const next = JSON.stringify(merged)
+          if (raw !== next) await current.setItem(key, next)
+          return next
+        }
+
+        if (!legacyStore) return null
+
+        for (const legacyKey of legacy) {
+          const legacyRaw = await legacyStore.getItem(legacyKey)
+          if (legacyRaw === null) continue
+
+          await current.setItem(key, legacyRaw)
+          await legacyStore.removeItem(legacyKey)
+
+          const parsed = parse(legacyRaw)
+          if (parsed === undefined) return legacyRaw
+
+          const migrated = config.migrate ? config.migrate(parsed) : parsed
+          const merged = merge(defaults, migrated)
+          const next = JSON.stringify(merged)
+          if (legacyRaw !== next) await current.setItem(key, next)
+          return next
+        }
+
+        return null
+      },
+      setItem: async (key, value) => {
+        await current.setItem(key, value)
+      },
+      removeItem: async (key) => {
+        await current.removeItem(key)
+      },
+    }
+
+    return api
+  })()
+
+  const [state, setState, init] = makePersisted(store, { name: config.key, storage })
 
-  // Create a resource that resolves when the store is initialized
-  // This integrates with Suspense and provides a ready signal
   const isAsync = init instanceof Promise
   const [ready] = createResource(
     () => init,

+ 184 - 29
packages/app/src/utils/prompt.ts

@@ -1,47 +1,202 @@
-import type { Part, TextPart, FilePart } from "@opencode-ai/sdk/v2"
-import type { Prompt, FileAttachmentPart } from "@/context/prompt"
+import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@opencode-ai/sdk/v2"
+import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
+
+type Inline =
+  | {
+      type: "file"
+      start: number
+      end: number
+      value: string
+      path: string
+      selection?: {
+        startLine: number
+        endLine: number
+        startChar: number
+        endChar: number
+      }
+    }
+  | {
+      type: "agent"
+      start: number
+      end: number
+      value: string
+      name: string
+    }
+
+function selectionFromFileUrl(url: string): Extract<Inline, { type: "file" }>["selection"] {
+  const queryIndex = url.indexOf("?")
+  if (queryIndex === -1) return undefined
+  const params = new URLSearchParams(url.slice(queryIndex + 1))
+  const startLine = Number(params.get("start"))
+  const endLine = Number(params.get("end"))
+  if (!Number.isFinite(startLine) || !Number.isFinite(endLine)) return undefined
+  return {
+    startLine,
+    endLine,
+    startChar: 0,
+    endChar: 0,
+  }
+}
+
+function textPartValue(parts: Part[]) {
+  const candidates = parts
+    .filter((part): part is TextPart => part.type === "text")
+    .filter((part) => !part.synthetic && !part.ignored)
+  return candidates.reduce((best: TextPart | undefined, part) => {
+    if (!best) return part
+    if (part.text.length > best.text.length) return part
+    return best
+  }, undefined)
+}
 
 /**
  * Extract prompt content from message parts for restoring into the prompt input.
  * This is used by undo to restore the original user prompt.
  */
-export function extractPromptFromParts(parts: Part[]): Prompt {
-  const result: Prompt = []
-  let position = 0
+export function extractPromptFromParts(parts: Part[], opts?: { directory?: string }): Prompt {
+  const textPart = textPartValue(parts)
+  const text = textPart?.text ?? ""
+  const directory = opts?.directory
+
+  const toRelative = (path: string) => {
+    if (!directory) return path
+
+    const prefix = directory.endsWith("/") ? directory : directory + "/"
+    if (path.startsWith(prefix)) return path.slice(prefix.length)
+
+    if (path.startsWith(directory)) {
+      const next = path.slice(directory.length)
+      if (next.startsWith("/")) return next.slice(1)
+      return next
+    }
+
+    return path
+  }
+
+  const inline: Inline[] = []
+  const images: ImageAttachmentPart[] = []
 
   for (const part of parts) {
-    if (part.type === "text") {
-      const textPart = part as TextPart
-      if (!textPart.synthetic && textPart.text) {
-        result.push({
-          type: "text",
-          content: textPart.text,
-          start: position,
-          end: position + textPart.text.length,
-        })
-        position += textPart.text.length
-      }
-    } else if (part.type === "file") {
+    if (part.type === "file") {
       const filePart = part as FilePart
-      if (filePart.source?.type === "file") {
-        const path = filePart.source.path
-        const content = "@" + path
-        const attachment: FileAttachmentPart = {
-          type: "file",
-          path,
-          content,
-          start: position,
-          end: position + content.length,
+      const sourceText = filePart.source?.text
+      if (sourceText) {
+        const value = sourceText.value
+        const start = sourceText.start
+        const end = sourceText.end
+        let path = value
+        if (value.startsWith("@")) path = value.slice(1)
+        if (!value.startsWith("@") && filePart.source && "path" in filePart.source) {
+          path = filePart.source.path
         }
-        result.push(attachment)
-        position += content.length
+        inline.push({
+          type: "file",
+          start,
+          end,
+          value,
+          path: toRelative(path),
+          selection: selectionFromFileUrl(filePart.url),
+        })
+        continue
+      }
+
+      if (filePart.url.startsWith("data:")) {
+        images.push({
+          type: "image",
+          id: filePart.id,
+          filename: filePart.filename ?? "attachment",
+          mime: filePart.mime,
+          dataUrl: filePart.url,
+        })
       }
     }
+
+    if (part.type === "agent") {
+      const agentPart = part as MessageAgentPart
+      const source = agentPart.source
+      if (!source) continue
+      inline.push({
+        type: "agent",
+        start: source.start,
+        end: source.end,
+        value: source.value,
+        name: agentPart.name,
+      })
+    }
+  }
+
+  inline.sort((a, b) => {
+    if (a.start !== b.start) return a.start - b.start
+    return a.end - b.end
+  })
+
+  const result: Prompt = []
+  let position = 0
+  let cursor = 0
+
+  const pushText = (content: string) => {
+    if (!content) return
+    result.push({
+      type: "text",
+      content,
+      start: position,
+      end: position + content.length,
+    })
+    position += content.length
+  }
+
+  const pushFile = (item: Extract<Inline, { type: "file" }>) => {
+    const content = item.value
+    const attachment: FileAttachmentPart = {
+      type: "file",
+      path: item.path,
+      content,
+      start: position,
+      end: position + content.length,
+      selection: item.selection,
+    }
+    result.push(attachment)
+    position += content.length
+  }
+
+  const pushAgent = (item: Extract<Inline, { type: "agent" }>) => {
+    const content = item.value
+    const mention: AgentPart = {
+      type: "agent",
+      name: item.name,
+      content,
+      start: position,
+      end: position + content.length,
+    }
+    result.push(mention)
+    position += content.length
+  }
+
+  for (const item of inline) {
+    if (item.start < 0 || item.end < item.start) continue
+
+    const expected = item.value
+    if (!expected) continue
+
+    const mismatch = item.end > text.length || item.start < cursor || text.slice(item.start, item.end) !== expected
+    const start = mismatch ? text.indexOf(expected, cursor) : item.start
+    if (start === -1) continue
+    const end = mismatch ? start + expected.length : item.end
+
+    pushText(text.slice(cursor, start))
+
+    if (item.type === "file") pushFile(item)
+    if (item.type === "agent") pushAgent(item)
+
+    cursor = end
   }
 
+  pushText(text.slice(cursor))
+
   if (result.length === 0) {
     result.push({ type: "text", content: "", start: 0, end: 0 })
   }
 
-  return result
+  if (images.length === 0) return result
+  return [...result, ...images]
 }

+ 6 - 0
packages/app/src/utils/same.ts

@@ -0,0 +1,6 @@
+export function same<T>(a: readonly T[] | undefined, b: readonly T[] | undefined) {
+  if (a === b) return true
+  if (!a || !b) return false
+  if (a.length !== b.length) return false
+  return a.every((x, i) => x === b[i])
+}

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

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/console-app",
-  "version": "1.1.2",
+  "version": "1.1.11",
   "type": "module",
   "license": "MIT",
   "scripts": {
@@ -26,6 +26,7 @@
     "chart.js": "4.5.1",
     "nitro": "3.0.1-alpha.1",
     "solid-js": "catalog:",
+    "solid-list": "0.3.0",
     "vite": "catalog:",
     "zod": "catalog:"
   },

BIN
packages/console/app/src/asset/black/hero.png


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

@@ -9,8 +9,8 @@ export const config = {
   github: {
     repoUrl: "https://github.com/anomalyco/opencode",
     starsFormatted: {
-      compact: "45K",
-      full: "45,000",
+      compact: "50K",
+      full: "50,000",
     },
   },
 

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

@@ -0,0 +1,221 @@
+[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;
+      }
+    }
+  }
+}

Разница между файлами не показана из-за своего большого размера
+ 104 - 0
packages/console/app/src/routes/black/index.tsx


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

@@ -0,0 +1,214 @@
+[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="select-workspace"] {
+      display: flex;
+      margin-top: -24px;
+      width: 100%;
+      max-width: 480px;
+      height: 305px;
+      padding: 32px 20px 0 20px;
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 24px;
+
+      border: 1px solid #303030;
+      background: #0a0a0a;
+      box-shadow:
+        0 100px 80px 0 rgba(0, 0, 0, 0.04),
+        0 41.778px 33.422px 0 rgba(0, 0, 0, 0.05),
+        0 22.336px 17.869px 0 rgba(0, 0, 0, 0.06),
+        0 12.522px 10.017px 0 rgba(0, 0, 0, 0.08),
+        0 6.65px 5.32px 0 rgba(0, 0, 0, 0.09),
+        0 2.767px 2.214px 0 rgba(0, 0, 0, 0.13);
+
+      [data-slot="select-workspace-title"] {
+        flex-shrink: 0;
+        align-self: stretch;
+        color: rgba(255, 255, 255, 0.59);
+        text-align: center;
+        font-size: 16px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: 160%; /* 25.6px */
+      }
+
+      [data-slot="workspaces"] {
+        width: 100%;
+        padding: 0;
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 8px;
+        align-self: stretch;
+        outline: none;
+        overflow-y: auto;
+        flex: 1;
+        min-height: 0;
+
+        scrollbar-width: none;
+        &::-webkit-scrollbar {
+          display: none;
+        }
+
+        [data-slot="workspace"] {
+          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";
+            font-size: 16px;
+            font-style: normal;
+            font-weight: 400;
+            line-height: 160%; /* 25.6px */
+          }
+
+          a {
+            color: rgba(255, 255, 255, 0.92);
+            font-size: 16px;
+            font-style: normal;
+            font-weight: 400;
+            line-height: 160%; /* 25.6px */
+            text-decoration: none;
+          }
+        }
+
+        [data-slot="workspace"]:hover,
+        [data-slot="workspace"][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;
+      }
+    }
+  }
+}

Разница между файлами не показана из-за своего большого размера
+ 134 - 0
packages/console/app/src/routes/black/workspace.tsx


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

@@ -129,9 +129,9 @@ export default function Download() {
                 </code>
                 <CopyStatus />
               </button>
-              <button data-component="cli-row" onClick={handleCopyClick("brew install opencode")}>
+              <button data-component="cli-row" onClick={handleCopyClick("brew install anomalyco/tap/opencode")}>
                 <code>
-                  brew install <strong>opencode</strong>
+                  brew install <strong>anomalyco/tap/opencode</strong>
                 </code>
                 <CopyStatus />
               </button>

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

@@ -140,7 +140,7 @@ export default function Home() {
                     <button data-copy data-slot="command" onClick={handleCopyClick}>
                       <span>
                         <span data-slot="protocol">brew install </span>
-                        <span data-slot="highlight">opencode</span>
+                        <span data-slot="highlight">anomalyco/tap/opencode</span>
                       </span>
                       <CopyStatus />
                     </button>
@@ -195,6 +195,12 @@ export default function Home() {
                   <strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account
                 </div>
               </li>
+              <li>
+                <span>[*]</span>
+                <div>
+                  <strong>ChatGPT Plus/Pro</strong> Log in with OpenAI to use your ChatGPT Plus or Pro account
+                </div>
+              </li>
               <li>
                 <span>[*]</span>
                 <div>

+ 370 - 3
packages/console/app/src/routes/stripe/webhook.ts

@@ -1,11 +1,13 @@
 import { Billing } from "@opencode-ai/console-core/billing.js"
 import type { APIEvent } from "@solidjs/start/server"
-import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
-import { BillingTable, PaymentTable } from "@opencode-ai/console-core/schema/billing.sql.js"
+import { and, Database, eq, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
+import { BillingTable, PaymentTable, SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
 import { Identifier } from "@opencode-ai/console-core/identifier.js"
 import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
 import { Actor } from "@opencode-ai/console-core/actor.js"
 import { Resource } from "@opencode-ai/console-resource"
+import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
+import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
 
 export async function POST(input: APIEvent) {
   const body = await Billing.stripe().webhooks.constructEventAsync(
@@ -39,7 +41,7 @@ export async function POST(input: APIEvent) {
           .where(eq(BillingTable.customerID, customerID))
       })
     }
-    if (body.type === "checkout.session.completed") {
+    if (body.type === "checkout.session.completed" && body.data.object.mode === "payment") {
       const workspaceID = body.data.object.metadata?.workspaceID
       const amountInCents = body.data.object.metadata?.amount && parseInt(body.data.object.metadata?.amount)
       const customerID = body.data.object.customer as string
@@ -102,6 +104,371 @@ export async function POST(input: APIEvent) {
         })
       })
     }
+    if (body.type === "checkout.session.completed" && body.data.object.mode === "subscription") {
+      const workspaceID = body.data.object.custom_fields.find((f) => f.key === "workspaceid")?.text?.value
+      const amountInCents = body.data.object.amount_total as number
+      const customerID = body.data.object.customer as string
+      const customerEmail = body.data.object.customer_details?.email as string
+      const invoiceID = body.data.object.invoice as string
+      const subscriptionID = body.data.object.subscription as string
+      const promoCode = body.data.object.discounts?.[0]?.promotion_code as string
+
+      if (!workspaceID) throw new Error("Workspace ID not found")
+      if (!customerID) throw new Error("Customer ID not found")
+      if (!amountInCents) throw new Error("Amount not found")
+      if (!invoiceID) throw new Error("Invoice ID not found")
+      if (!subscriptionID) throw new Error("Subscription ID not found")
+
+      // get payment id from invoice
+      const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
+        expand: ["payments"],
+      })
+      const paymentID = invoice.payments?.data[0].payment.payment_intent as string
+      if (!paymentID) throw new Error("Payment ID not found")
+
+      // get payment method for the payment intent
+      const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
+        expand: ["payment_method"],
+      })
+      const paymentMethod = paymentIntent.payment_method
+      if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
+
+      // get coupon id from promotion code
+      const couponID = await (async () => {
+        if (!promoCode) return
+        const coupon = await Billing.stripe().promotionCodes.retrieve(promoCode)
+        const couponID = coupon.coupon.id
+        if (!couponID) throw new Error("Coupon not found for promotion code")
+        return couponID
+      })()
+
+      // get user
+
+      await Actor.provide("system", { workspaceID }, async () => {
+        // look up current billing
+        const billing = await Billing.get()
+        if (!billing) throw new Error(`Workspace with ID ${workspaceID} not found`)
+
+        // Temporarily skip this check because during Black drop, user can checkout
+        // as a new customer
+        //if (billing.customerID !== customerID) throw new Error("Customer ID mismatch")
+
+        // Temporarily check the user to apply to. After Black drop, we will allow
+        // look up the user to apply to
+        const users = await Database.use((tx) =>
+          tx
+            .select({ id: UserTable.id, email: AuthTable.subject })
+            .from(UserTable)
+            .innerJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email")))
+            .where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
+        )
+        const user = users.find((u) => u.email === customerEmail) ?? users[0]
+        if (!user) {
+          console.error(`Error: User with email ${customerEmail} not found in workspace ${workspaceID}`)
+          process.exit(1)
+        }
+
+        // set customer metadata
+        if (!billing?.customerID) {
+          await Billing.stripe().customers.update(customerID, {
+            metadata: {
+              workspaceID,
+            },
+          })
+        }
+
+        await Database.transaction(async (tx) => {
+          await tx
+            .update(BillingTable)
+            .set({
+              customerID,
+              subscriptionID,
+              subscriptionCouponID: couponID,
+              paymentMethodID: paymentMethod.id,
+              paymentMethodLast4: paymentMethod.card?.last4 ?? null,
+              paymentMethodType: paymentMethod.type,
+            })
+            .where(eq(BillingTable.workspaceID, workspaceID))
+
+          await tx.insert(SubscriptionTable).values({
+            workspaceID,
+            id: Identifier.create("subscription"),
+            userID: user.id,
+          })
+
+          await tx.insert(PaymentTable).values({
+            workspaceID,
+            id: Identifier.create("payment"),
+            amount: centsToMicroCents(amountInCents),
+            paymentID,
+            invoiceID,
+            customerID,
+            enrichment: {
+              type: "subscription",
+              couponID,
+            },
+          })
+        })
+      })
+    }
+    if (body.type === "customer.subscription.created") {
+      const data = {
+        id: "evt_1Smq802SrMQ2Fneksse5FMNV",
+        object: "event",
+        api_version: "2025-07-30.basil",
+        created: 1767766916,
+        data: {
+          object: {
+            id: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
+            object: "subscription",
+            application: null,
+            application_fee_percent: null,
+            automatic_tax: {
+              disabled_reason: null,
+              enabled: false,
+              liability: null,
+            },
+            billing_cycle_anchor: 1770445200,
+            billing_cycle_anchor_config: null,
+            billing_mode: {
+              flexible: {
+                proration_discounts: "included",
+              },
+              type: "flexible",
+              updated_at: 1770445200,
+            },
+            billing_thresholds: null,
+            cancel_at: null,
+            cancel_at_period_end: false,
+            canceled_at: null,
+            cancellation_details: {
+              comment: null,
+              feedback: null,
+              reason: null,
+            },
+            collection_method: "charge_automatically",
+            created: 1770445200,
+            currency: "usd",
+            customer: "cus_TkKmZZvysJ2wej",
+            customer_account: null,
+            days_until_due: null,
+            default_payment_method: null,
+            default_source: "card_1Smq7u2SrMQ2FneknjyOa7sq",
+            default_tax_rates: [],
+            description: null,
+            discounts: [],
+            ended_at: null,
+            invoice_settings: {
+              account_tax_ids: null,
+              issuer: {
+                type: "self",
+              },
+            },
+            items: {
+              object: "list",
+              data: [
+                {
+                  id: "si_TkKnBKXFX76t0O",
+                  object: "subscription_item",
+                  billing_thresholds: null,
+                  created: 1770445200,
+                  current_period_end: 1772864400,
+                  current_period_start: 1770445200,
+                  discounts: [],
+                  metadata: {},
+                  plan: {
+                    id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
+                    object: "plan",
+                    active: true,
+                    amount: 20000,
+                    amount_decimal: "20000",
+                    billing_scheme: "per_unit",
+                    created: 1767725082,
+                    currency: "usd",
+                    interval: "month",
+                    interval_count: 1,
+                    livemode: false,
+                    metadata: {},
+                    meter: null,
+                    nickname: null,
+                    product: "prod_Tk9LjWT1n0DgYm",
+                    tiers_mode: null,
+                    transform_usage: null,
+                    trial_period_days: null,
+                    usage_type: "licensed",
+                  },
+                  price: {
+                    id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
+                    object: "price",
+                    active: true,
+                    billing_scheme: "per_unit",
+                    created: 1767725082,
+                    currency: "usd",
+                    custom_unit_amount: null,
+                    livemode: false,
+                    lookup_key: null,
+                    metadata: {},
+                    nickname: null,
+                    product: "prod_Tk9LjWT1n0DgYm",
+                    recurring: {
+                      interval: "month",
+                      interval_count: 1,
+                      meter: null,
+                      trial_period_days: null,
+                      usage_type: "licensed",
+                    },
+                    tax_behavior: "unspecified",
+                    tiers_mode: null,
+                    transform_quantity: null,
+                    type: "recurring",
+                    unit_amount: 20000,
+                    unit_amount_decimal: "20000",
+                  },
+                  quantity: 1,
+                  subscription: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
+                  tax_rates: [],
+                },
+              ],
+              has_more: false,
+              total_count: 1,
+              url: "/v1/subscription_items?subscription=sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
+            },
+            latest_invoice: "in_1Smq7x2SrMQ2FnekSJesfPwE",
+            livemode: false,
+            metadata: {},
+            next_pending_invoice_item_invoice: null,
+            on_behalf_of: null,
+            pause_collection: null,
+            payment_settings: {
+              payment_method_options: null,
+              payment_method_types: null,
+              save_default_payment_method: "off",
+            },
+            pending_invoice_item_interval: null,
+            pending_setup_intent: null,
+            pending_update: null,
+            plan: {
+              id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
+              object: "plan",
+              active: true,
+              amount: 20000,
+              amount_decimal: "20000",
+              billing_scheme: "per_unit",
+              created: 1767725082,
+              currency: "usd",
+              interval: "month",
+              interval_count: 1,
+              livemode: false,
+              metadata: {},
+              meter: null,
+              nickname: null,
+              product: "prod_Tk9LjWT1n0DgYm",
+              tiers_mode: null,
+              transform_usage: null,
+              trial_period_days: null,
+              usage_type: "licensed",
+            },
+            quantity: 1,
+            schedule: null,
+            start_date: 1770445200,
+            status: "active",
+            test_clock: "clock_1Smq6n2SrMQ2FnekQw4yt2PZ",
+            transfer_data: null,
+            trial_end: null,
+            trial_settings: {
+              end_behavior: {
+                missing_payment_method: "create_invoice",
+              },
+            },
+            trial_start: null,
+          },
+        },
+        livemode: false,
+        pending_webhooks: 0,
+        request: {
+          id: "req_6YO9stvB155WJD",
+          idempotency_key: "581ba059-6f86-49b2-9c49-0d8450255322",
+        },
+        type: "customer.subscription.created",
+      }
+    }
+    if (body.type === "customer.subscription.deleted") {
+      const subscriptionID = body.data.object.id
+      if (!subscriptionID) throw new Error("Subscription ID not found")
+
+      const workspaceID = await Database.use((tx) =>
+        tx
+          .select({ workspaceID: BillingTable.workspaceID })
+          .from(BillingTable)
+          .where(eq(BillingTable.subscriptionID, subscriptionID))
+          .then((rows) => rows[0]?.workspaceID),
+      )
+      if (!workspaceID) throw new Error("Workspace ID not found for subscription")
+
+      await Database.transaction(async (tx) => {
+        await tx
+          .update(BillingTable)
+          .set({ subscriptionID: null, subscriptionCouponID: null })
+          .where(eq(BillingTable.workspaceID, workspaceID))
+
+        await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
+      })
+    }
+    if (body.type === "invoice.payment_succeeded") {
+      if (body.data.object.billing_reason === "subscription_cycle") {
+        const invoiceID = body.data.object.id as string
+        const amountInCents = body.data.object.amount_paid
+        const customerID = body.data.object.customer as string
+        const subscriptionID = body.data.object.parent?.subscription_details?.subscription as string
+
+        if (!customerID) throw new Error("Customer ID not found")
+        if (!invoiceID) throw new Error("Invoice ID not found")
+        if (!subscriptionID) throw new Error("Subscription ID not found")
+
+        // get coupon id from subscription
+        const subscriptionData = await Billing.stripe().subscriptions.retrieve(subscriptionID, {
+          expand: ["discounts"],
+        })
+        const couponID =
+          typeof subscriptionData.discounts[0] === "string"
+            ? subscriptionData.discounts[0]
+            : subscriptionData.discounts[0]?.coupon?.id
+
+        // get payment id from invoice
+        const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
+          expand: ["payments"],
+        })
+        const paymentID = invoice.payments?.data[0].payment.payment_intent as string
+        if (!paymentID) {
+          // payment id can be undefined when using coupon
+          if (!couponID) throw new Error("Payment ID not found")
+        }
+
+        const workspaceID = await Database.use((tx) =>
+          tx
+            .select({ workspaceID: BillingTable.workspaceID })
+            .from(BillingTable)
+            .where(eq(BillingTable.customerID, customerID))
+            .then((rows) => rows[0]?.workspaceID),
+        )
+        if (!workspaceID) throw new Error("Workspace ID not found for customer")
+
+        await Database.use((tx) =>
+          tx.insert(PaymentTable).values({
+            workspaceID,
+            id: Identifier.create("payment"),
+            amount: centsToMicroCents(amountInCents),
+            paymentID,
+            invoiceID,
+            customerID,
+            enrichment: {
+              type: "subscription",
+              couponID,
+            },
+          }),
+        )
+      }
+    }
     if (body.type === "charge.refunded") {
       const customerID = body.data.object.customer as string
       const paymentIntentID = body.data.object.payment_intent as string

+ 3 - 0
packages/console/app/src/routes/temp.tsx

@@ -91,6 +91,9 @@ export default function Home() {
             <li>
               <strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account
             </li>
+            <li>
+              <strong>ChatGPT Plus/Pro</strong> Log in with OpenAI to use your ChatGPT Plus or Pro account
+            </li>
             <li>
               <strong>Use any model</strong> Supports 75+ LLM providers through{" "}
               <a href="https://models.dev">Models.dev</a>, including local models

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

@@ -0,0 +1,8 @@
+.root {
+  [data-slot="title-row"] {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    gap: var(--space-4);
+  }
+}

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

@@ -0,0 +1,58 @@
+import { action, useParams, useAction, useSubmission, json } from "@solidjs/router"
+import { createStore } from "solid-js/store"
+import { Billing } from "@opencode-ai/console-core/billing.js"
+import { withActor } from "~/context/auth.withActor"
+import { queryBillingInfo } from "../../common"
+import styles from "./black-section.module.css"
+
+const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
+  "use server"
+  return json(
+    await withActor(
+      () =>
+        Billing.generateSessionUrl({ returnUrl })
+          .then((data) => ({ error: undefined, data }))
+          .catch((e) => ({
+            error: e.message as string,
+            data: undefined,
+          })),
+      workspaceID,
+    ),
+    { revalidate: queryBillingInfo.key },
+  )
+}, "sessionUrl")
+
+export function BlackSection() {
+  const params = useParams()
+  const sessionAction = useAction(createSessionUrl)
+  const sessionSubmission = useSubmission(createSessionUrl)
+  const [store, setStore] = createStore({
+    sessionRedirecting: false,
+  })
+
+  async function onClickSession() {
+    const result = await sessionAction(params.id!, window.location.href)
+    if (result.data) {
+      setStore("sessionRedirecting", true)
+      window.location.href = result.data
+    }
+  }
+
+  return (
+    <section class={styles.root}>
+      <div data-slot="section-title">
+        <h2>Subscription</h2>
+        <div data-slot="title-row">
+          <p>You are subscribed to OpenCode Black for $200 per month.</p>
+          <button
+            data-color="primary"
+            disabled={sessionSubmission.pending || store.sessionRedirecting}
+            onClick={onClickSession}
+          >
+            {sessionSubmission.pending || store.sessionRedirecting ? "Loading..." : "Manage Subscription"}
+          </button>
+        </div>
+      </div>
+    </section>
+  )
+}

+ 6 - 2
packages/console/app/src/routes/workspace/[id]/billing/index.tsx

@@ -2,19 +2,23 @@ import { MonthlyLimitSection } from "./monthly-limit-section"
 import { BillingSection } from "./billing-section"
 import { ReloadSection } from "./reload-section"
 import { PaymentSection } from "./payment-section"
+import { BlackSection } from "./black-section"
 import { Show } from "solid-js"
 import { createAsync, useParams } from "@solidjs/router"
 import { queryBillingInfo, querySessionInfo } from "../../common"
 
 export default function () {
   const params = useParams()
-  const userInfo = createAsync(() => querySessionInfo(params.id!))
+  const sessionInfo = createAsync(() => querySessionInfo(params.id!))
   const billingInfo = createAsync(() => queryBillingInfo(params.id!))
 
   return (
     <div data-page="workspace-[id]">
       <div data-slot="sections">
-        <Show when={userInfo()?.isAdmin}>
+        <Show when={sessionInfo()?.isAdmin}>
+          <Show when={billingInfo()?.subscriptionID}>
+            <BlackSection />
+          </Show>
           <BillingSection />
           <Show when={billingInfo()?.customerID}>
             <ReloadSection />

+ 19 - 2
packages/console/app/src/routes/workspace/[id]/billing/payment-section.module.css

@@ -45,6 +45,19 @@
           text-decoration: line-through;
         }
       }
+
+      &[data-slot="payment-receipt"] {
+        span {
+          display: inline-block;
+          padding: var(--space-3) var(--space-4);
+          font-size: var(--font-size-sm);
+          line-height: 1.5;
+        }
+
+        button {
+          font-size: var(--font-size-sm);
+        }
+      }
     }
 
     tbody tr {
@@ -61,13 +74,17 @@
       }
 
       th {
-        &:nth-child(2) /* Payment ID */ {
+        &:nth-child(2)
+
+        /* Payment ID */ {
           display: none;
         }
       }
 
       td {
-        &:nth-child(2) /* Payment ID */ {
+        &:nth-child(2)
+
+        /* Payment ID */ {
           display: none;
         }
       }

+ 23 - 13
packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx

@@ -1,6 +1,6 @@
 import { Billing } from "@opencode-ai/console-core/billing.js"
 import { query, action, useParams, createAsync, useAction } from "@solidjs/router"
-import { For, Show } from "solid-js"
+import { For, Match, Show, Switch } from "solid-js"
 import { withActor } from "~/context/auth.withActor"
 import { formatDateUTC, formatDateForTable } from "../../common"
 import styles from "./payment-section.module.css"
@@ -77,6 +77,8 @@ export function PaymentSection() {
               <For each={payments()!}>
                 {(payment) => {
                   const date = new Date(payment.timeCreated)
+                  const amount =
+                    payment.enrichment?.type === "subscription" && payment.enrichment.couponID ? 0 : payment.amount
                   return (
                     <tr>
                       <td data-slot="payment-date" title={formatDateUTC(date)}>
@@ -84,20 +86,28 @@ export function PaymentSection() {
                       </td>
                       <td data-slot="payment-id">{payment.id}</td>
                       <td data-slot="payment-amount" data-refunded={!!payment.timeRefunded}>
-                        ${((payment.amount ?? 0) / 100000000).toFixed(2)}
+                        ${((amount ?? 0) / 100000000).toFixed(2)}
+                        <Switch>
+                          <Match when={payment.enrichment?.type === "credit"}> (credit)</Match>
+                          <Match when={payment.enrichment?.type === "subscription"}> (subscription)</Match>
+                        </Switch>
                       </td>
                       <td data-slot="payment-receipt">
-                        <button
-                          onClick={async () => {
-                            const receiptUrl = await downloadReceiptAction(params.id!, payment.paymentID!)
-                            if (receiptUrl) {
-                              window.open(receiptUrl, "_blank")
-                            }
-                          }}
-                          data-slot="receipt-button"
-                        >
-                          View
-                        </button>
+                        {payment.paymentID ? (
+                          <button
+                            onClick={async () => {
+                              const receiptUrl = await downloadReceiptAction(params.id!, payment.paymentID!)
+                              if (receiptUrl) {
+                                window.open(receiptUrl, "_blank")
+                              }
+                            }}
+                            data-slot="receipt-button"
+                          >
+                            View
+                          </button>
+                        ) : (
+                          <span>-</span>
+                        )}
                       </td>
                     </tr>
                   )

+ 1 - 0
packages/console/app/src/routes/workspace/[id]/graph-section.tsx

@@ -44,6 +44,7 @@ async function getCosts(workspaceID: string, year: number, month: number) {
             eq(UsageTable.workspaceID, workspaceID),
             gte(UsageTable.timeCreated, startDate),
             lte(UsageTable.timeCreated, endDate),
+            or(isNull(UsageTable.enrichment), sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan') != 'sub'`),
           ),
         )
         .groupBy(sql`DATE(${UsageTable.timeCreated})`, UsageTable.model, UsageTable.keyID)

+ 3 - 1
packages/console/app/src/routes/workspace/[id]/usage-section.tsx

@@ -169,7 +169,9 @@ export function UsageSection() {
                           </Show>
                         </div>
                       </td>
-                      <td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
+                      <td data-slot="usage-cost">
+                        ${usage.enrichment?.plan === "sub" ? "0.0000" : ((usage.cost ?? 0) / 100000000).toFixed(4)}
+                      </td>
                     </tr>
                   )
                 }}

+ 7 - 0
packages/console/app/src/routes/zen/util/error.ts

@@ -1,6 +1,13 @@
 export class AuthError extends Error {}
 export class CreditsError extends Error {}
 export class MonthlyLimitError extends Error {}
+export class SubscriptionError extends Error {
+  retryAfter?: number
+  constructor(message: string, retryAfter?: number) {
+    super(message)
+    this.retryAfter = retryAfter
+  }
+}
 export class UserLimitError extends Error {}
 export class ModelError extends Error {}
 export class RateLimitError extends Error {}

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

@@ -1,18 +1,28 @@
 import type { APIEvent } from "@solidjs/start/server"
 import { and, Database, eq, isNull, lt, or, sql } from "@opencode-ai/console-core/drizzle/index.js"
 import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
-import { BillingTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
+import { BillingTable, SubscriptionTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
 import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
+import { getWeekBounds } from "@opencode-ai/console-core/util/date.js"
 import { Identifier } from "@opencode-ai/console-core/identifier.js"
 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 { 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"
 import { logger } from "./logger"
-import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
+import {
+  AuthError,
+  CreditsError,
+  MonthlyLimitError,
+  SubscriptionError,
+  UserLimitError,
+  ModelError,
+  RateLimitError,
+} from "./error"
 import { createBodyConverter, createStreamPartConverter, createResponseConverter, UsageInfo } from "./provider/provider"
 import { anthropicHelper } from "./provider/anthropic"
 import { googleHelper } from "./provider/google"
@@ -69,13 +79,13 @@ export async function handler(
     const dataDumper = createDataDumper(sessionId, requestId, projectId)
     const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
     const isTrial = await trialLimiter?.isTrial()
-    const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
+    const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
     await rateLimiter?.check()
     const stickyTracker = createStickyTracker(modelInfo.stickyProvider ?? false, sessionId)
     const stickyProvider = await stickyTracker?.get()
+    const authInfo = await authenticate(modelInfo)
 
     const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
-      const authInfo = await authenticate(modelInfo)
       const providerInfo = selectProvider(
         zenData,
         authInfo,
@@ -135,10 +145,10 @@ export async function handler(
         })
       }
 
-      return { providerInfo, authInfo, reqBody, res, startTimestamp }
+      return { providerInfo, reqBody, res, startTimestamp }
     }
 
-    const { providerInfo, authInfo, reqBody, res, startTimestamp } = await retriableRequest()
+    const { providerInfo, reqBody, res, startTimestamp } = await retriableRequest()
 
     // Store model request
     dataDumper?.provideModel(providerInfo.storeModel)
@@ -279,14 +289,19 @@ export async function handler(
         { status: 401 },
       )
 
-    if (error instanceof RateLimitError)
+    if (error instanceof RateLimitError || error instanceof SubscriptionError) {
+      const headers = new Headers()
+      if (error instanceof SubscriptionError && error.retryAfter) {
+        headers.set("retry-after", String(error.retryAfter))
+      }
       return new Response(
         JSON.stringify({
           type: "error",
           error: { type: error.constructor.name, message: error.message },
         }),
-        { status: 429 },
+        { status: 429, headers },
       )
+    }
 
     return new Response(
       JSON.stringify({
@@ -400,6 +415,13 @@ export async function handler(
             monthlyUsage: UserTable.monthlyUsage,
             timeMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated,
           },
+          subscription: {
+            id: SubscriptionTable.id,
+            rollingUsage: SubscriptionTable.rollingUsage,
+            fixedUsage: SubscriptionTable.fixedUsage,
+            timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
+            timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
+          },
           provider: {
             credentials: ProviderTable.credentials,
           },
@@ -419,6 +441,14 @@ export async function handler(
               )
             : sql`false`,
         )
+        .leftJoin(
+          SubscriptionTable,
+          and(
+            eq(SubscriptionTable.workspaceID, KeyTable.workspaceID),
+            eq(SubscriptionTable.userID, KeyTable.userID),
+            isNull(SubscriptionTable.timeDeleted),
+          ),
+        )
         .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
         .then((rows) => rows[0]),
     )
@@ -427,6 +457,7 @@ export async function handler(
     logger.metric({
       api_key: data.apiKey,
       workspace: data.workspaceID,
+      isSubscription: data.subscription ? true : false,
     })
 
     return {
@@ -434,6 +465,7 @@ export async function handler(
       workspaceID: data.workspaceID,
       billing: data.billing,
       user: data.user,
+      subscription: data.subscription,
       provider: data.provider,
       isFree: FREE_WORKSPACES.includes(data.workspaceID),
       isDisabled: !!data.timeDisabled,
@@ -446,6 +478,50 @@ export async function handler(
     if (authInfo.isFree) return
     if (modelInfo.allowAnonymous) return
 
+    // Validate subscription billing
+    if (authInfo.subscription) {
+      const black = BlackData.get()
+      const sub = authInfo.subscription
+      const now = new Date()
+
+      const formatRetryTime = (seconds: number) => {
+        const days = Math.floor(seconds / 86400)
+        if (days >= 1) return `${days} day${days > 1 ? "s" : ""}`
+        const hours = Math.floor(seconds / 3600)
+        const minutes = Math.ceil((seconds % 3600) / 60)
+        if (hours >= 1) return `${hours}hr ${minutes}min`
+        return `${minutes}min`
+      }
+
+      // 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)
+          throw new SubscriptionError(
+            `Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
+            retryAfter,
+          )
+        }
+      }
+
+      // 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)
+          throw new SubscriptionError(
+            `Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
+            retryAfter,
+          )
+        }
+      }
+
+      return
+    }
+
+    // Validate pay as you go billing
     const billing = authInfo.billing
     if (!billing.paymentMethodID)
       throw new CreditsError(
@@ -463,29 +539,25 @@ export async function handler(
       billing.monthlyLimit &&
       billing.monthlyUsage &&
       billing.timeMonthlyUsageUpdated &&
-      billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100)
-    ) {
-      const dateYear = billing.timeMonthlyUsageUpdated.getUTCFullYear()
-      const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth()
-      if (currentYear === dateYear && currentMonth === dateMonth)
-        throw new MonthlyLimitError(
-          `Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
-        )
-    }
+      billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100) &&
+      currentYear === billing.timeMonthlyUsageUpdated.getUTCFullYear() &&
+      currentMonth === billing.timeMonthlyUsageUpdated.getUTCMonth()
+    )
+      throw new MonthlyLimitError(
+        `Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
+      )
 
     if (
       authInfo.user.monthlyLimit &&
       authInfo.user.monthlyUsage &&
       authInfo.user.timeMonthlyUsageUpdated &&
-      authInfo.user.monthlyUsage >= centsToMicroCents(authInfo.user.monthlyLimit * 100)
-    ) {
-      const dateYear = authInfo.user.timeMonthlyUsageUpdated.getUTCFullYear()
-      const dateMonth = authInfo.user.timeMonthlyUsageUpdated.getUTCMonth()
-      if (currentYear === dateYear && currentMonth === dateMonth)
-        throw new UserLimitError(
-          `You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/members`,
-        )
-    }
+      authInfo.user.monthlyUsage >= centsToMicroCents(authInfo.user.monthlyLimit * 100) &&
+      currentYear === authInfo.user.timeMonthlyUsageUpdated.getUTCFullYear() &&
+      currentMonth === authInfo.user.timeMonthlyUsageUpdated.getUTCMonth()
+    )
+      throw new UserLimitError(
+        `You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/members`,
+      )
   }
 
   function validateModelSettings(authInfo: AuthInfo) {
@@ -560,54 +632,95 @@ export async function handler(
 
     if (!authInfo) return
 
-    const cost = authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
-    await Database.transaction(async (tx) => {
-      await tx.insert(UsageTable).values({
-        workspaceID: authInfo.workspaceID,
-        id: Identifier.create("usage"),
-        model: modelInfo.id,
-        provider: providerInfo.id,
-        inputTokens,
-        outputTokens,
-        reasoningTokens,
-        cacheReadTokens,
-        cacheWrite5mTokens,
-        cacheWrite1hTokens,
-        cost,
-        keyID: authInfo.apiKeyId,
-      })
-      await tx
-        .update(BillingTable)
-        .set({
-          balance: sql`${BillingTable.balance} - ${cost}`,
-          monthlyUsage: sql`
+    const cost = authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
+    await Database.use((db) =>
+      Promise.all([
+        db.insert(UsageTable).values({
+          workspaceID: authInfo.workspaceID,
+          id: Identifier.create("usage"),
+          model: modelInfo.id,
+          provider: providerInfo.id,
+          inputTokens,
+          outputTokens,
+          reasoningTokens,
+          cacheReadTokens,
+          cacheWrite5mTokens,
+          cacheWrite1hTokens,
+          cost,
+          keyID: authInfo.apiKeyId,
+          enrichment: authInfo.subscription ? { plan: "sub" } : undefined,
+        }),
+        db
+          .update(KeyTable)
+          .set({ timeUsed: sql`now()` })
+          .where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
+        ...(authInfo.subscription
+          ? (() => {
+              const black = BlackData.get()
+              const week = getWeekBounds(new Date())
+              const rollingWindowSeconds = black.rollingWindow * 3600
+              return [
+                db
+                  .update(SubscriptionTable)
+                  .set({
+                    fixedUsage: sql`
+              CASE
+                WHEN ${SubscriptionTable.timeFixedUpdated} >= ${week.start} THEN ${SubscriptionTable.fixedUsage} + ${cost}
+                ELSE ${cost}
+              END
+            `,
+                    timeFixedUpdated: sql`now()`,
+                    rollingUsage: sql`
+              CASE
+                WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.rollingUsage} + ${cost}
+                ELSE ${cost}
+              END
+            `,
+                    timeRollingUpdated: sql`
+              CASE
+                WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.timeRollingUpdated}
+                ELSE now()
+              END
+            `,
+                  })
+                  .where(
+                    and(
+                      eq(SubscriptionTable.workspaceID, authInfo.workspaceID),
+                      eq(SubscriptionTable.userID, authInfo.user.id),
+                    ),
+                  ),
+              ]
+            })()
+          : [
+              db
+                .update(BillingTable)
+                .set({
+                  balance: authInfo.isFree
+                    ? sql`${BillingTable.balance} - ${0}`
+                    : sql`${BillingTable.balance} - ${cost}`,
+                  monthlyUsage: sql`
               CASE
                 WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost}
                 ELSE ${cost}
               END
             `,
-          timeMonthlyUsageUpdated: sql`now()`,
-        })
-        .where(eq(BillingTable.workspaceID, authInfo.workspaceID))
-      await tx
-        .update(UserTable)
-        .set({
-          monthlyUsage: sql`
+                  timeMonthlyUsageUpdated: sql`now()`,
+                })
+                .where(eq(BillingTable.workspaceID, authInfo.workspaceID)),
+              db
+                .update(UserTable)
+                .set({
+                  monthlyUsage: sql`
               CASE
                 WHEN MONTH(${UserTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${UserTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${UserTable.monthlyUsage} + ${cost}
                 ELSE ${cost}
               END
             `,
-          timeMonthlyUsageUpdated: sql`now()`,
-        })
-        .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id)))
-    })
-
-    await Database.use((tx) =>
-      tx
-        .update(KeyTable)
-        .set({ timeUsed: sql`now()` })
-        .where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
+                  timeMonthlyUsageUpdated: sql`now()`,
+                })
+                .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id))),
+            ]),
+      ]),
     )
 
     return { costInMicroCents: cost }
@@ -617,6 +730,7 @@ export async function handler(
     if (!authInfo) return
     if (authInfo.isFree) return
     if (authInfo.provider?.credentials) return
+    if (authInfo.subscription) return
 
     if (!costInfo) return
 

+ 20 - 14
packages/console/app/src/routes/zen/util/rateLimiter.ts

@@ -1,28 +1,34 @@
-import { Resource } from "@opencode-ai/console-resource"
+import { Database, eq, and, sql, inArray } from "@opencode-ai/console-core/drizzle/index.js"
+import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
 import { RateLimitError } from "./error"
 import { logger } from "./logger"
 
-export function createRateLimiter(model: string, limit: number | undefined, ip: string) {
+export function createRateLimiter(limit: number | undefined, rawIp: string) {
   if (!limit) return
 
+  const ip = !rawIp.length ? "unknown" : rawIp
   const now = Date.now()
-  const currKey = `usage:${ip}:${model}:${buildYYYYMMDDHH(now)}`
-  const prevKey = `usage:${ip}:${model}:${buildYYYYMMDDHH(now - 3_600_000)}`
-  let currRate: number
-  let prevRate: number
+  const intervals = [buildYYYYMMDDHH(now), buildYYYYMMDDHH(now - 3_600_000), buildYYYYMMDDHH(now - 7_200_000)]
 
   return {
     track: async () => {
-      await Resource.GatewayKv.put(currKey, currRate + 1, { expirationTtl: 3600 })
+      await Database.use((tx) =>
+        tx
+          .insert(IpRateLimitTable)
+          .values({ ip, interval: intervals[0], count: 1 })
+          .onDuplicateKeyUpdate({ set: { count: sql`${IpRateLimitTable.count} + 1` } }),
+      )
     },
     check: async () => {
-      const values = await Resource.GatewayKv.get([currKey, prevKey])
-      const prevValue = values?.get(prevKey)
-      const currValue = values?.get(currKey)
-      prevRate = prevValue ? parseInt(prevValue) : 0
-      currRate = currValue ? parseInt(currValue) : 0
-      logger.debug(`rate limit ${model} prev/curr: ${prevRate}/${currRate}`)
-      if (prevRate + currRate >= limit) throw new RateLimitError(`Rate limit exceeded. Please try again later.`)
+      const rows = await Database.use((tx) =>
+        tx
+          .select({ count: IpRateLimitTable.count })
+          .from(IpRateLimitTable)
+          .where(and(eq(IpRateLimitTable.ip, ip), inArray(IpRateLimitTable.interval, intervals))),
+      )
+      const total = rows.reduce((sum, r) => sum + r.count, 0)
+      logger.debug(`rate limit total: ${total}`)
+      if (total >= limit) throw new RateLimitError(`Rate limit exceeded. Please try again later.`)
     },
   }
 }

+ 6 - 0
packages/console/core/migrations/0041_odd_misty_knight.sql

@@ -0,0 +1,6 @@
+CREATE TABLE `ip_rate_limit` (
+	`ip` varchar(45) NOT NULL,
+	`interval` varchar(10) NOT NULL,
+	`count` int NOT NULL,
+	CONSTRAINT `ip_rate_limit_ip_interval_pk` PRIMARY KEY(`ip`,`interval`)
+);

+ 7 - 0
packages/console/core/migrations/0042_flat_nightmare.sql

@@ -0,0 +1,7 @@
+ALTER TABLE `billing` ADD `subscription_id` varchar(28);--> statement-breakpoint
+ALTER TABLE `usage` ADD `data` json;--> statement-breakpoint
+ALTER TABLE `user` ADD `time_subscribed` timestamp(3);--> statement-breakpoint
+ALTER TABLE `user` ADD `sub_recent_usage` bigint;--> statement-breakpoint
+ALTER TABLE `user` ADD `sub_monthly_usage` bigint;--> statement-breakpoint
+ALTER TABLE `user` ADD `sub_time_recent_usage_updated` timestamp(3);--> statement-breakpoint
+ALTER TABLE `user` ADD `sub_time_monthly_usage_updated` timestamp(3);

+ 2 - 0
packages/console/core/migrations/0043_lame_calypso.sql

@@ -0,0 +1,2 @@
+ALTER TABLE `user` RENAME COLUMN `sub_recent_usage` TO `sub_interval_usage`;--> statement-breakpoint
+ALTER TABLE `user` RENAME COLUMN `sub_time_recent_usage_updated` TO `sub_time_interval_usage_updated`;

+ 1 - 0
packages/console/core/migrations/0044_tiny_captain_midlands.sql

@@ -0,0 +1 @@
+ALTER TABLE `usage` RENAME COLUMN `data` TO `enrichment`;

+ 1 - 0
packages/console/core/migrations/0045_cuddly_diamondback.sql

@@ -0,0 +1 @@
+ALTER TABLE `billing` ADD CONSTRAINT `global_subscription_id` UNIQUE(`subscription_id`);

+ 13 - 0
packages/console/core/migrations/0046_charming_black_bolt.sql

@@ -0,0 +1,13 @@
+CREATE TABLE `subscription` (
+	`id` varchar(30) NOT NULL,
+	`workspace_id` varchar(30) NOT NULL,
+	`time_created` timestamp(3) NOT NULL DEFAULT (now()),
+	`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
+	`time_deleted` timestamp(3),
+	`user_id` varchar(30) NOT NULL,
+	`rolling_usage` bigint,
+	`fixed_usage` bigint,
+	`time_rolling_updated` timestamp(3),
+	`time_fixed_updated` timestamp(3),
+	CONSTRAINT `subscription_workspace_id_id_pk` PRIMARY KEY(`workspace_id`,`id`)
+);

+ 6 - 0
packages/console/core/migrations/0047_huge_omega_red.sql

@@ -0,0 +1,6 @@
+CREATE INDEX `workspace_user_id` ON `subscription` (`workspace_id`,`user_id`);--> statement-breakpoint
+ALTER TABLE `user` DROP COLUMN `time_subscribed`;--> statement-breakpoint
+ALTER TABLE `user` DROP COLUMN `sub_interval_usage`;--> statement-breakpoint
+ALTER TABLE `user` DROP COLUMN `sub_monthly_usage`;--> statement-breakpoint
+ALTER TABLE `user` DROP COLUMN `sub_time_interval_usage_updated`;--> statement-breakpoint
+ALTER TABLE `user` DROP COLUMN `sub_time_monthly_usage_updated`;

+ 2 - 0
packages/console/core/migrations/0048_mean_frank_castle.sql

@@ -0,0 +1,2 @@
+DROP INDEX `workspace_user_id` ON `subscription`;--> statement-breakpoint
+ALTER TABLE `subscription` ADD CONSTRAINT `workspace_user_id` UNIQUE(`workspace_id`,`user_id`);

+ 1 - 0
packages/console/core/migrations/0049_noisy_domino.sql

@@ -0,0 +1 @@
+ALTER TABLE `billing` ADD `subscription_coupon_id` varchar(28);

+ 1 - 0
packages/console/core/migrations/0050_bumpy_mephistopheles.sql

@@ -0,0 +1 @@
+ALTER TABLE `payment` ADD `enrichment` json;

+ 1095 - 0
packages/console/core/migrations/meta/0041_snapshot.json

@@ -0,0 +1,1095 @@
+{
+  "version": "5",
+  "dialect": "mysql",
+  "id": "9cf10c24-6029-4cb4-866e-ff9b501eaf7e",
+  "prevId": "bf19cd74-71f9-4bdf-b50e-67c2436f3408",
+  "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
+        }
+      },
+      "indexes": {
+        "global_customer_id": {
+          "name": "global_customer_id",
+          "columns": ["customer_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
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "payment_workspace_id_id_pk": {
+          "name": "payment_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
+        }
+      },
+      "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": {}
+  }
+}

+ 1144 - 0
packages/console/core/migrations/meta/0042_snapshot.json

@@ -0,0 +1,1144 @@
+{
+  "version": "5",
+  "dialect": "mysql",
+  "id": "4775571c-ad9c-4104-a202-2374b1963cfe",
+  "prevId": "9cf10c24-6029-4cb4-866e-ff9b501eaf7e",
+  "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
+        }
+      },
+      "indexes": {
+        "global_customer_id": {
+          "name": "global_customer_id",
+          "columns": ["customer_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
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "payment_workspace_id_id_pk": {
+          "name": "payment_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
+        },
+        "data": {
+          "name": "data",
+          "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
+        },
+        "time_subscribed": {
+          "name": "time_subscribed",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_recent_usage": {
+          "name": "sub_recent_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_monthly_usage": {
+          "name": "sub_monthly_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_time_recent_usage_updated": {
+          "name": "sub_time_recent_usage_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_time_monthly_usage_updated": {
+          "name": "sub_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": {}
+  }
+}

+ 1147 - 0
packages/console/core/migrations/meta/0043_snapshot.json

@@ -0,0 +1,1147 @@
+{
+  "version": "5",
+  "dialect": "mysql",
+  "id": "3ff862f3-eeb6-4b10-8c78-254de3778ab3",
+  "prevId": "4775571c-ad9c-4104-a202-2374b1963cfe",
+  "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
+        }
+      },
+      "indexes": {
+        "global_customer_id": {
+          "name": "global_customer_id",
+          "columns": ["customer_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
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "payment_workspace_id_id_pk": {
+          "name": "payment_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
+        },
+        "data": {
+          "name": "data",
+          "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
+        },
+        "time_subscribed": {
+          "name": "time_subscribed",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_interval_usage": {
+          "name": "sub_interval_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_monthly_usage": {
+          "name": "sub_monthly_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_time_interval_usage_updated": {
+          "name": "sub_time_interval_usage_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_time_monthly_usage_updated": {
+          "name": "sub_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": {
+      "\"user\".\"sub_recent_usage\"": "\"user\".\"sub_interval_usage\"",
+      "\"user\".\"sub_time_recent_usage_updated\"": "\"user\".\"sub_time_interval_usage_updated\""
+    }
+  },
+  "internal": {
+    "tables": {},
+    "indexes": {}
+  }
+}

+ 1146 - 0
packages/console/core/migrations/meta/0044_snapshot.json

@@ -0,0 +1,1146 @@
+{
+  "version": "5",
+  "dialect": "mysql",
+  "id": "70394850-2c28-4012-a3d5-69357e3348b6",
+  "prevId": "3ff862f3-eeb6-4b10-8c78-254de3778ab3",
+  "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
+        }
+      },
+      "indexes": {
+        "global_customer_id": {
+          "name": "global_customer_id",
+          "columns": ["customer_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
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "payment_workspace_id_id_pk": {
+          "name": "payment_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
+        },
+        "time_subscribed": {
+          "name": "time_subscribed",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_interval_usage": {
+          "name": "sub_interval_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_monthly_usage": {
+          "name": "sub_monthly_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_time_interval_usage_updated": {
+          "name": "sub_time_interval_usage_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_time_monthly_usage_updated": {
+          "name": "sub_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": {
+      "\"usage\".\"data\"": "\"usage\".\"enrichment\""
+    }
+  },
+  "internal": {
+    "tables": {},
+    "indexes": {}
+  }
+}

+ 1149 - 0
packages/console/core/migrations/meta/0045_snapshot.json

@@ -0,0 +1,1149 @@
+{
+  "version": "5",
+  "dialect": "mysql",
+  "id": "27c1a3eb-b125-46d4-b436-abe5764fe4b7",
+  "prevId": "70394850-2c28-4012-a3d5-69357e3348b6",
+  "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
+        }
+      },
+      "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
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "payment_workspace_id_id_pk": {
+          "name": "payment_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
+        },
+        "time_subscribed": {
+          "name": "time_subscribed",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_interval_usage": {
+          "name": "sub_interval_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_monthly_usage": {
+          "name": "sub_monthly_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_time_interval_usage_updated": {
+          "name": "sub_time_interval_usage_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_time_monthly_usage_updated": {
+          "name": "sub_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": {}
+  }
+}

+ 1236 - 0
packages/console/core/migrations/meta/0046_snapshot.json

@@ -0,0 +1,1236 @@
+{
+  "version": "5",
+  "dialect": "mysql",
+  "id": "f3725f6d-5f33-4497-b4ba-cf05c46fb873",
+  "prevId": "27c1a3eb-b125-46d4-b436-abe5764fe4b7",
+  "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
+        }
+      },
+      "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
+        }
+      },
+      "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": {},
+      "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
+        },
+        "time_subscribed": {
+          "name": "time_subscribed",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_interval_usage": {
+          "name": "sub_interval_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_monthly_usage": {
+          "name": "sub_monthly_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_time_interval_usage_updated": {
+          "name": "sub_time_interval_usage_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "sub_time_monthly_usage_updated": {
+          "name": "sub_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": {}
+  }
+}

+ 1207 - 0
packages/console/core/migrations/meta/0047_snapshot.json

@@ -0,0 +1,1207 @@
+{
+  "version": "5",
+  "dialect": "mysql",
+  "id": "fec4cb15-6f13-465d-a902-b76b026872f4",
+  "prevId": "f3725f6d-5f33-4497-b4ba-cf05c46fb873",
+  "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
+        }
+      },
+      "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
+        }
+      },
+      "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": false
+        }
+      },
+      "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": {}
+  }
+}

+ 1207 - 0
packages/console/core/migrations/meta/0048_snapshot.json

@@ -0,0 +1,1207 @@
+{
+  "version": "5",
+  "dialect": "mysql",
+  "id": "bb90bb3e-fd08-439a-b92f-5f433807480e",
+  "prevId": "fec4cb15-6f13-465d-a902-b76b026872f4",
+  "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
+        }
+      },
+      "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
+        }
+      },
+      "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": {}
+  }
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов