mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
Compare commits
179 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 542b9fa342 | |||
| 9159afb54b | |||
| 536934548a | |||
| 1c59530115 | |||
| ab8471a7ff | |||
| 4c674b075b | |||
| ba8a4c5e9f | |||
| 790fe72f39 | |||
| 2d2d4641cb | |||
| d3caa55c10 | |||
| ca534a36e5 | |||
| 278ffb9a4e | |||
| b2ff4be4c6 | |||
| 2267ce2511 | |||
| e29d1d339c | |||
| 92bc78a2d3 | |||
| 1ba5535460 | |||
| 7fa9a73bf0 | |||
| b3fcc9a81d | |||
| e8751d976e | |||
| 43c9702aa7 | |||
| ae609be710 | |||
| 86ee36f562 | |||
| 0657f09139 | |||
| 182949dee4 | |||
| 83655a3b09 | |||
| 62e5f4b154 | |||
| ea926f0e1a | |||
| 6191232d5f | |||
| 95f4ce86d6 | |||
| 5999aefde3 | |||
| babe3a0f40 | |||
| 29b95dee53 | |||
| ef9a1e911e | |||
| 7eddaa806d | |||
| d07e79e6ad | |||
| f17a7cde8d | |||
| 6d446c2a03 | |||
| 61f6091de1 | |||
| 289783f627 | |||
| 4c464cf4c0 | |||
| 83be5b0171 | |||
| 0c022ef39d | |||
| 717b544633 | |||
| c1a420717a | |||
| 42c2ffd842 | |||
| 5192c51843 | |||
| 96d7ccea48 | |||
| 49e859cfd6 | |||
| 6c57a69af4 | |||
| 4d019430e2 | |||
| 37e6c8342f | |||
| c04e892991 | |||
| bb82d43094 | |||
| 2893b6e3a5 | |||
| 54c3361be7 | |||
| c50cf21f18 | |||
| cb73e2d9e1 | |||
| 48057c2c21 | |||
| 1923ddab6e | |||
| b8249cde4b | |||
| 19b3f3d7ce | |||
| e5e05d390d | |||
| 38ad6707cf | |||
| 7ef246f98f | |||
| b91582d68a | |||
| 682d30bd12 | |||
| 4d68ee5d2c | |||
| dbe9fd00b7 | |||
| cd13a8524e | |||
| 59765e0157 | |||
| d0519be0d0 | |||
| 066e4f064d | |||
| f81c469f17 | |||
| a398013ecb | |||
| 53d9717d90 | |||
| 5885b691b9 | |||
| fd70b9b057 | |||
| de13ccb757 | |||
| 7e1abb7bbf | |||
| 83afcb9c42 | |||
| afb406c5ff | |||
| 36cf9b9922 | |||
| 0d21164255 | |||
| 3ad6f84adb | |||
| 24a5b16af8 | |||
| b4171aa8e8 | |||
| d7a79733ea | |||
| 34e5b9bdb0 | |||
| d32ec9bd52 | |||
| 89fcfcc50b | |||
| 9a6fd6a5ee | |||
| f144a0384d | |||
| a67920a25e | |||
| 67f894e5d0 | |||
| fc1eda5c77 | |||
| 371fddc820 | |||
| 8e89c38480 | |||
| b732b4caeb | |||
| 1940d1cf87 | |||
| 1f0ed24402 | |||
| 133da0f448 | |||
| f93e1e5c92 | |||
| ae4af54c7d | |||
| 9d30bc692c | |||
| 44b63dc259 | |||
| de2b4f6538 | |||
| b6b82aa847 | |||
| 2d35b78333 | |||
| c7dfbbeed0 | |||
| b946fd21b1 | |||
| daa0ca40f2 | |||
| 5b27130d60 | |||
| ee1eb35269 | |||
| 4dda7cc6a4 | |||
| cc590364e9 | |||
| f14cd4a3db | |||
| 07645e0705 | |||
| f053862018 | |||
| 69127aeaa0 | |||
| 847455383d | |||
| 9da95cb805 | |||
| 48008f91ac | |||
| d8b3aa9382 | |||
| ea9b5b8d76 | |||
| 4227b89ebc | |||
| ee846235f2 | |||
| 9463ce8006 | |||
| 756fb61691 | |||
| 94d0a3d888 | |||
| d83af721a6 | |||
| 0bc00bef32 | |||
| 98c13a965b | |||
| 310065bd0a | |||
| 34ec6cc978 | |||
| 5a90e5f9e2 | |||
| 5ee3063aab | |||
| 920373d252 | |||
| c9155c117a | |||
| 28d617d867 | |||
| 593d0737b5 | |||
| 64409182ec | |||
| 8d4607ebd5 | |||
| 250393978b | |||
| fec70ae9c9 | |||
| ad7b4b1fcd | |||
| 03d5089436 | |||
| 9b52d33889 | |||
| bc0e00cbb7 | |||
| 096710a8cc | |||
| 50bb201187 | |||
| f211fc45a3 | |||
| d91781c639 | |||
| f3b71007d2 | |||
| 60dd987efd | |||
| 0a96d254e8 | |||
| 51e9979457 | |||
| dfc7ac4cf0 | |||
| c2950d26f0 | |||
| 47dfebf277 | |||
| f3b5021936 | |||
| 7be9a84b72 | |||
| 78321a95e8 | |||
| 225adc46ba | |||
| eb4b5721cd | |||
| 979c9ea569 | |||
| c0bd29155d | |||
| c5b5795636 | |||
| 3ed4f1078f | |||
| 5b1fd7e539 | |||
| d18b6673e6 | |||
| c93c0d402d | |||
| b168bfe40d | |||
| 1d621260ff | |||
| a63fa64dec | |||
| 3c282c3c37 | |||
| 2046f2e8e7 | |||
| af684c80d4 | |||
| 99b72eb1ea |
@@ -0,0 +1,20 @@
|
||||
name: "Setup Bun"
|
||||
description: "Setup Bun with caching and install dependencies"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Cache ~/.bun
|
||||
id: cache-bun
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb', 'bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
shell: bash
|
||||
@@ -15,11 +15,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.2.21
|
||||
|
||||
- run: bun install
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||
env:
|
||||
|
||||
@@ -20,13 +20,10 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.2.21
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: run
|
||||
run: |
|
||||
bun install
|
||||
./script/format.ts
|
||||
env:
|
||||
CI: true
|
||||
|
||||
@@ -19,16 +19,13 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.21
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- run: git fetch --force --tags
|
||||
- run: bun install -g @vscode/vsce
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
bun install
|
||||
./script/publish
|
||||
working-directory: ./sdks/vscode
|
||||
env:
|
||||
|
||||
@@ -35,18 +35,7 @@ jobs:
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.21
|
||||
|
||||
- name: Cache ~/.bun
|
||||
id: cache-bun
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.bun
|
||||
key: ${{ runner.os }}-bun-1-2-21-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-1-2-21-
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install makepkg
|
||||
run: |
|
||||
@@ -60,8 +49,6 @@ jobs:
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Install OpenCode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
@@ -71,6 +58,7 @@ jobs:
|
||||
./script/publish.ts
|
||||
env:
|
||||
OPENCODE_BUMP: ${{ inputs.bump }}
|
||||
OPENCODE_CHANNEL: latest
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -24,27 +24,11 @@ jobs:
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.21
|
||||
|
||||
- name: Cache ~/.bun
|
||||
id: cache-bun
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.bun
|
||||
key: ${{ runner.os }}-bun-1-2-21-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-1-2-21-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
./packages/opencode/script/publish.ts
|
||||
./script/publish.ts
|
||||
env:
|
||||
OPENCODE_SNAPSHOT: true
|
||||
OPENCODE_TAG: ${{ github.ref_name }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -16,9 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run stats script
|
||||
run: bun script/stats.ts
|
||||
|
||||
@@ -18,15 +18,13 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.2.21
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: run
|
||||
run: |
|
||||
git config --global user.email "bot@opencode.ai"
|
||||
git config --global user.name "opencode"
|
||||
bun install
|
||||
bun turbo typecheck
|
||||
bun turbo test
|
||||
env:
|
||||
CI: true
|
||||
|
||||
@@ -13,12 +13,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.2.21
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run typecheck
|
||||
run: bun typecheck
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: Git commit and push
|
||||
---
|
||||
|
||||
commit and push
|
||||
|
||||
make sure it includes a prefix like
|
||||
@@ -8,5 +12,9 @@ ci:
|
||||
ignore:
|
||||
wip:
|
||||
|
||||
For anything in the packages/web use the docs: prefix.
|
||||
|
||||
For anything in the packages/app use the ignore: prefix.
|
||||
|
||||
prefer to explain WHY something was done from an end user perspective instead of
|
||||
WHAT was done.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
description: Spellcheck all markdown file changes
|
||||
---
|
||||
|
||||
Look at all the unstaged changes to markdown (.md, .mdx) files, pull out the lines that have changed, and check for spelling and grammar errors.
|
||||
@@ -76,7 +76,7 @@ Take a look at the git history to see what kind of PRs we end up merging.
|
||||
|
||||
To run OpenCode locally you need.
|
||||
|
||||
- Bun
|
||||
- Bun 1.3 or higher
|
||||
- Golang 1.24.x
|
||||
|
||||
And run.
|
||||
@@ -98,6 +98,7 @@ It's very similar to Claude Code in terms of capability. Here are the key differ
|
||||
|
||||
- 100% open source
|
||||
- Not coupled to any provider. Although Anthropic is recommended, OpenCode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
||||
- Out of the box LSP support
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -1,104 +1,112 @@
|
||||
# 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) |
|
||||
| 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) |
|
||||
|
||||
+1
-1
@@ -14,6 +14,6 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@octokit/graphql": "9.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@opencode-ai/sdk": "0.5.4"
|
||||
"@opencode-ai/sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-5
@@ -3,7 +3,7 @@
|
||||
"name": "opencode",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.2.21",
|
||||
"packageManager": "bun@1.3.0",
|
||||
"scripts": {
|
||||
"dev": "bun run packages/opencode/src/index.ts",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
@@ -13,30 +13,45 @@
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"packages/console/*",
|
||||
"packages/sdk/js"
|
||||
"packages/sdk/js",
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@types/bun": "1.2.21",
|
||||
"@types/bun": "1.3.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/node": "22.13.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"diff": "8.0.2",
|
||||
"ai": "5.0.8",
|
||||
"hono": "4.7.10",
|
||||
"fuzzysort": "3.1.0",
|
||||
"luxon": "3.6.1",
|
||||
"typescript": "5.8.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251014.1",
|
||||
"zod": "4.1.8",
|
||||
"remeda": "2.26.0",
|
||||
"solid-js": "1.9.9"
|
||||
"solid-js": "1.9.9",
|
||||
"tailwindcss": "4.1.11",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"vite": "7.1.4",
|
||||
"vite-plugin-solid": "2.11.8"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
"sst": "3.17.13",
|
||||
"sst": "3.17.19",
|
||||
"turbo": "2.5.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sst/opencode"
|
||||
@@ -56,5 +71,9 @@
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"@solidjs/start@1.1.7": "patches/@solidjs%2Fstart@1.1.7.patch"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"name": "@opencode-ai/console-app",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"dev": "vinxi dev --host 0.0.0.0",
|
||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
||||
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||
"start": "vinxi start",
|
||||
"version": "0.14.6"
|
||||
"version": "0.15.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -22,6 +22,10 @@
|
||||
"vinxi": "^0.5.7",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
[data-component="dropdown"] {
|
||||
position: relative;
|
||||
|
||||
[data-slot="trigger"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-sans);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="chevron"] {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
[data-slot="dropdown"] {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
z-index: 1000;
|
||||
margin-top: var(--space-1);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
min-width: 160px;
|
||||
|
||||
&[data-align="left"] {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&[data-align="right"] {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="item"] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--space-2-5) var(--space-3);
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-sans);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-surface);
|
||||
}
|
||||
|
||||
&[data-selected="true"] {
|
||||
background-color: var(--color-accent-alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { JSX, Show, createEffect, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { IconChevron } from "./icon"
|
||||
import "./dropdown.css"
|
||||
|
||||
interface DropdownProps {
|
||||
trigger: JSX.Element | string
|
||||
children: JSX.Element
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
align?: "left" | "right"
|
||||
class?: string
|
||||
}
|
||||
|
||||
export function Dropdown(props: DropdownProps) {
|
||||
const [store, setStore] = createStore({
|
||||
isOpen: props.open ?? false,
|
||||
})
|
||||
let dropdownRef: HTMLDivElement | undefined
|
||||
|
||||
createEffect(() => {
|
||||
if (props.open !== undefined) {
|
||||
setStore("isOpen", props.open)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
|
||||
setStore("isOpen", false)
|
||||
props.onOpenChange?.(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", handleClickOutside)
|
||||
onCleanup(() => document.removeEventListener("click", handleClickOutside))
|
||||
})
|
||||
|
||||
const toggle = () => {
|
||||
const newValue = !store.isOpen
|
||||
setStore("isOpen", newValue)
|
||||
props.onOpenChange?.(newValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-component="dropdown" class={props.class} ref={dropdownRef}>
|
||||
<button data-slot="trigger" type="button" onClick={toggle}>
|
||||
{typeof props.trigger === "string" ? <span>{props.trigger}</span> : props.trigger}
|
||||
<IconChevron data-slot="chevron" />
|
||||
</button>
|
||||
|
||||
<Show when={store.isOpen}>
|
||||
<div data-slot="dropdown" data-align={props.align ?? "left"}>
|
||||
{props.children}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface DropdownItemProps {
|
||||
children: JSX.Element
|
||||
selected?: boolean
|
||||
onClick?: () => void
|
||||
type?: "button" | "submit" | "reset"
|
||||
}
|
||||
|
||||
export function DropdownItem(props: DropdownItemProps) {
|
||||
return (
|
||||
<button
|
||||
data-slot="item"
|
||||
data-selected={props.selected ?? false}
|
||||
type={props.type ?? "button"}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -4,9 +4,11 @@ import { A, createAsync } from "@solidjs/router"
|
||||
import { createMemo, Match, Show, Switch } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { github } from "~/lib/github"
|
||||
import { queryIsLoggedIn } from "~/routes/workspace/common"
|
||||
|
||||
export function Header(props: { zen?: boolean }) {
|
||||
const githubData = createAsync(() => github())
|
||||
const isLoggedIn = createAsync(() => queryIsLoggedIn())
|
||||
const starCount = createMemo(() =>
|
||||
githubData()?.stars
|
||||
? new Intl.NumberFormat("en-US", {
|
||||
@@ -39,7 +41,7 @@ export function Header(props: { zen?: boolean }) {
|
||||
<li>
|
||||
<Switch>
|
||||
<Match when={props.zen}>
|
||||
<a href="/auth">Login</a>
|
||||
<a href="/auth">{isLoggedIn() ? "Workspace" : "Login"}</a>
|
||||
</Match>
|
||||
<Match when={!props.zen}>
|
||||
<A href="/zen">Zen</A>
|
||||
@@ -110,7 +112,7 @@ export function Header(props: { zen?: boolean }) {
|
||||
<li>
|
||||
<Switch>
|
||||
<Match when={props.zen}>
|
||||
<a href="/auth">Login</a>
|
||||
<a href="/auth">{isLoggedIn() ? "Workspace" : "Login"}</a>
|
||||
</Match>
|
||||
<Match when={!props.zen}>
|
||||
<A href="/zen">Zen</A>
|
||||
|
||||
@@ -2,26 +2,70 @@ import { JSX } from "solid-js"
|
||||
|
||||
export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} width="234" height="42" viewBox="0 0 234 42" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M54 36H36V42H30V6H54V36ZM36 30H48V12H36V30Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M24 36H0V6H24V36ZM6 30H18V12H6V30Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="currentColor"/>
|
||||
<path d="M108 12H96V36H90V6H108V12Z" fill="currentColor"/>
|
||||
<path d="M114 36H108V12H114V36Z" fill="currentColor"/>
|
||||
<path d="M144 12H126V30H144V36H120V6H144V12Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M174 36H150V6H174V36ZM156 30H168V12H156V30Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M204 36H180V6H198V0H204V36ZM186 30H198V12H186V30Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M234 24H216V30H234V36H210V6H234V24ZM216 18H228V12H216V18Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
)
|
||||
<svg width="64" height="32" viewBox="0 0 64 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 9.14333V4.5719H4.57143V9.14333H0Z" fill="currentColor" />
|
||||
<path d="M4.57178 9.14333V4.5719H9.14321V9.14333H4.57178Z" fill="currentColor" />
|
||||
<path d="M9.1438 9.14333V4.5719H13.7152V9.14333H9.1438Z" fill="currentColor" />
|
||||
<path d="M13.7124 9.14333V4.5719H18.2838V9.14333H13.7124Z" fill="currentColor" />
|
||||
<path d="M13.7124 13.7136V9.14221H18.2838V13.7136H13.7124Z" fill="currentColor" />
|
||||
<path d="M0 18.2857V13.7142H4.57143V18.2857H0Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<rect width="4.57143" height="4.57143" transform="translate(4.57178 13.7141)" fill="currentColor" />
|
||||
<path d="M4.57178 18.2855V13.7141H9.14321V18.2855H4.57178Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M9.1438 18.2855V13.7141H13.7152V18.2855H9.1438Z" fill="currentColor" />
|
||||
<path d="M13.7156 18.2855V13.7141H18.287V18.2855H13.7156Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<rect width="4.57143" height="4.57143" transform="translate(0 18.2859)" fill="currentColor" />
|
||||
<path d="M0 22.8572V18.2858H4.57143V22.8572H0Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<rect
|
||||
width="4.57143"
|
||||
height="4.57143"
|
||||
transform="translate(4.57178 18.2859)"
|
||||
fill="currentColor"
|
||||
fill-opacity="0.2"
|
||||
/>
|
||||
<path d="M4.57178 22.8573V18.2859H9.14321V22.8573H4.57178Z" fill="currentColor" />
|
||||
<path d="M9.1438 22.8573V18.2859H13.7152V22.8573H9.1438Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M13.7156 22.8573V18.2859H18.287V22.8573H13.7156Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M0 27.4292V22.8578H4.57143V27.4292H0Z" fill="currentColor" />
|
||||
<path d="M4.57178 27.4292V22.8578H9.14321V27.4292H4.57178Z" fill="currentColor" />
|
||||
<path d="M9.1438 27.4276V22.8562H13.7152V27.4276H9.1438Z" fill="currentColor" />
|
||||
<path d="M13.7124 27.4292V22.8578H18.2838V27.4292H13.7124Z" fill="currentColor" />
|
||||
<path d="M22.8572 9.14333V4.5719H27.4286V9.14333H22.8572Z" fill="currentColor" />
|
||||
<path d="M27.426 9.14333V4.5719H31.9975V9.14333H27.426Z" fill="currentColor" />
|
||||
<path d="M32.001 9.14333V4.5719H36.5724V9.14333H32.001Z" fill="currentColor" />
|
||||
<path d="M36.5698 9.14333V4.5719H41.1413V9.14333H36.5698Z" fill="currentColor" />
|
||||
<path d="M22.8572 13.7152V9.1438H27.4286V13.7152H22.8572Z" fill="currentColor" />
|
||||
<path d="M36.5698 13.7152V9.1438H41.1413V13.7152H36.5698Z" fill="currentColor" />
|
||||
<path d="M22.8572 18.2855V13.7141H27.4286V18.2855H22.8572Z" fill="currentColor" />
|
||||
<path d="M27.4292 18.2855V13.7141H32.0006V18.2855H27.4292Z" fill="currentColor" />
|
||||
<path d="M32.001 18.2855V13.7141H36.5724V18.2855H32.001Z" fill="currentColor" />
|
||||
<path d="M36.5698 18.2855V13.7141H41.1413V18.2855H36.5698Z" fill="currentColor" />
|
||||
<path d="M22.8572 22.8573V18.2859H27.4286V22.8573H22.8572Z" fill="currentColor" />
|
||||
<path d="M27.4292 22.8573V18.2859H32.0006V22.8573H27.4292Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M32.001 22.8573V18.2859H36.5724V22.8573H32.001Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M36.5698 22.8573V18.2859H41.1413V22.8573H36.5698Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M22.8572 27.4292V22.8578H27.4286V27.4292H22.8572Z" fill="currentColor" />
|
||||
<path d="M27.4292 27.4276V22.8562H32.0006V27.4276H27.4292Z" fill="currentColor" />
|
||||
<path d="M32.001 27.4276V22.8562H36.5724V27.4276H32.001Z" fill="currentColor" />
|
||||
<path d="M36.5698 27.4292V22.8578H41.1413V27.4292H36.5698Z" fill="currentColor" />
|
||||
<path d="M45.7144 9.14333V4.5719H50.2858V9.14333H45.7144Z" fill="currentColor" />
|
||||
<path d="M50.2861 9.14333V4.5719H54.8576V9.14333H50.2861Z" fill="currentColor" />
|
||||
<path d="M54.855 9.14333V4.5719H59.4264V9.14333H54.855Z" fill="currentColor" />
|
||||
<path d="M45.7144 13.7136V9.14221H50.2858V13.7136H45.7144Z" fill="currentColor" />
|
||||
<path d="M59.4299 13.7152V9.1438H64.0014V13.7152H59.4299Z" fill="currentColor" />
|
||||
<path d="M45.7144 18.2855V13.7141H50.2858V18.2855H45.7144Z" fill="currentColor" />
|
||||
<path d="M50.2861 18.2857V13.7142H54.8576V18.2857H50.2861Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M54.8579 18.2855V13.7141H59.4293V18.2855H54.8579Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M59.4299 18.2855V13.7141H64.0014V18.2855H59.4299Z" fill="currentColor" />
|
||||
<path d="M45.7144 22.8573V18.2859H50.2858V22.8573H45.7144Z" fill="currentColor" />
|
||||
<path d="M50.2861 22.8572V18.2858H54.8576V22.8572H50.2861Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M54.8579 22.8573V18.2859H59.4293V22.8573H54.8579Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M59.4299 22.8573V18.2859H64.0014V22.8573H59.4299Z" fill="currentColor" />
|
||||
<path d="M45.7144 27.4292V22.8578H50.2858V27.4292H45.7144Z" fill="currentColor" />
|
||||
<path d="M50.2861 27.4286V22.8572H54.8576V27.4286H50.2861Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M54.8579 27.4285V22.8571H59.4293V27.4285H54.8579Z" fill="currentColor" fill-opacity="0.2" />
|
||||
<path d="M59.4299 27.4292V22.8578H64.0014V27.4292H59.4299Z" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
@@ -29,7 +73,7 @@ export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
<svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.75 8.75V2.75H21.25V15.25H15.25M15.25 8.75H2.75V21.25H15.25V8.75Z"
|
||||
stroke="#8E8B8B"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
@@ -55,3 +99,105 @@ export function IconCreditCard(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4.00024 5.04041L7.37401 1.66663L6.66691 0.959525L4.00024 3.62619L1.33357 0.959525L0.626465 1.66663L4.00024 5.04041Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6H6V24H18V6ZM24 30H0V0H24V30Z" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none">
|
||||
<path
|
||||
d="M8.43799 8.06943V6.09387C8.43799 5.92749 8.50347 5.80267 8.65601 5.71959L12.8206 3.43211C13.3875 3.1202 14.0635 2.9747 14.7611 2.9747C17.3775 2.9747 19.0347 4.9087 19.0347 6.96734C19.0347 7.11288 19.0347 7.27926 19.0128 7.44564L14.6956 5.03335C14.434 4.88785 14.1723 4.88785 13.9107 5.03335L8.43799 8.06943ZM18.1624 15.7637V11.0431C18.1624 10.7519 18.0315 10.544 17.7699 10.3984L12.2972 7.36234L14.0851 6.3849C14.2377 6.30182 14.3686 6.30182 14.5212 6.3849L18.6858 8.67238C19.8851 9.3379 20.6917 10.7519 20.6917 12.1243C20.6917 13.7047 19.7106 15.1604 18.1624 15.7636V15.7637ZM7.15158 11.6047L5.36369 10.6066C5.21114 10.5235 5.14566 10.3986 5.14566 10.2323V5.65735C5.14566 3.43233 6.93355 1.7478 9.35381 1.7478C10.2697 1.7478 11.1199 2.039 11.8396 2.55886L7.54424 4.92959C7.28268 5.07508 7.15181 5.28303 7.15181 5.57427V11.6049L7.15158 11.6047ZM11 13.7258L8.43799 12.3533V9.44209L11 8.06965L13.5618 9.44209V12.3533L11 13.7258ZM12.6461 20.0476C11.7303 20.0476 10.8801 19.7564 10.1604 19.2366L14.4557 16.8658C14.7173 16.7203 14.8482 16.5124 14.8482 16.2211V10.1905L16.658 11.1886C16.8105 11.2717 16.876 11.3965 16.876 11.563V16.1379C16.876 18.3629 15.0662 20.0474 12.6461 20.0474V20.0476ZM7.47863 15.4103L3.314 13.1229C2.11471 12.4573 1.30808 11.0433 1.30808 9.67088C1.30808 8.06965 2.31106 6.6348 3.85903 6.03168V10.773C3.85903 11.0642 3.98995 11.2721 4.25151 11.4177L9.70253 14.4328L7.91464 15.4103C7.76209 15.4934 7.63117 15.4934 7.47863 15.4103ZM7.23892 18.8207C4.77508 18.8207 2.96533 17.0531 2.96533 14.8696C2.96533 14.7032 2.98719 14.5368 3.00886 14.3704L7.30418 16.7412C7.56574 16.8867 7.82752 16.8867 8.08909 16.7412L13.5618 13.726V15.7015C13.5618 15.8679 13.4964 15.9927 13.3438 16.0758L9.17918 18.3633C8.61225 18.6752 7.93631 18.8207 7.23869 18.8207H7.23892ZM12.6461 21.2952C15.2844 21.2952 17.4865 19.5069 17.9882 17.1362C20.4301 16.5331 22 14.3495 22 12.1245C22 10.6688 21.346 9.25482 20.1685 8.23581C20.2775 7.79908 20.343 7.36234 20.343 6.92582C20.343 3.95215 17.8137 1.72691 14.892 1.72691C14.3034 1.72691 13.7365 1.80999 13.1695 1.99726C12.1882 1.08223 10.8364 0.5 9.35381 0.5C6.71557 0.5 4.51352 2.28829 4.01185 4.65902C1.56987 5.26214 0 7.44564 0 9.67067C0 11.1264 0.654039 12.5404 1.83147 13.5594C1.72246 13.9961 1.65702 14.4328 1.65702 14.8694C1.65702 17.8431 4.1863 20.0683 7.108 20.0683C7.69661 20.0683 8.26354 19.9852 8.83046 19.7979C9.81155 20.713 11.1634 21.2952 12.6461 21.2952Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M13.7891 3.93188L20.2223 20.068H23.7502L17.317 3.93188H13.7891Z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M6.32538 13.6827L8.52662 8.01201L10.7279 13.6827H6.32538ZM6.68225 3.93188L0.25 20.068H3.84652L5.16202 16.6794H11.8914L13.2067 20.068H16.8033L10.371 3.93188H6.68225Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconXai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M9.16861 16.0529L17.2018 9.85156C17.5957 9.54755 18.1586 9.66612 18.3463 10.1384C19.3339 12.6288 18.8926 15.6217 16.9276 17.6766C14.9626 19.7314 12.2285 20.1821 9.72948 19.1557L6.9995 20.4775C10.9151 23.2763 15.6699 22.5841 18.6411 19.4749C20.9979 17.0103 21.7278 13.6508 21.0453 10.6214L21.0515 10.6278C20.0617 6.17736 21.2948 4.39847 23.8207 0.760904C23.8804 0.674655 23.9402 0.588405 24 0.5L20.6762 3.97585V3.96506L9.16658 16.0551"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.37742 16.7017C4.67579 14.0395 5.14158 9.91963 7.44676 7.54383C9.15135 5.78544 11.9442 5.06779 14.3821 6.12281L17.0005 4.87559C16.5288 4.52392 15.9242 4.14566 15.2305 3.87986C12.0948 2.54882 8.34069 3.21127 5.79171 5.8386C3.33985 8.36779 2.56881 12.2567 3.89286 15.5751C4.88192 18.0552 3.26056 19.8094 1.62731 21.5801C1.04853 22.2078 0.467774 22.8355 0 23.5L7.3754 16.7037"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconAlibaba(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="currentColor"
|
||||
d="M11.6043 0.340162C11.9973 1.03016 12.3883 1.72215 12.7783 2.41514C12.7941 2.44286 12.8169 2.46589 12.8445 2.48187C12.8721 2.49786 12.9034 2.50624 12.9353 2.50614H18.4873C18.6612 2.50614 18.8092 2.61614 18.9332 2.83314L20.3872 5.40311C20.5772 5.74011 20.6272 5.88111 20.4112 6.24011C20.1512 6.6701 19.8982 7.1041 19.6512 7.54009L19.2842 8.19809C19.1782 8.39409 19.0612 8.47809 19.2442 8.71008L21.8962 13.347C22.0682 13.648 22.0072 13.841 21.8532 14.117C21.4162 14.902 20.9712 15.681 20.5182 16.457C20.3592 16.729 20.1662 16.832 19.8382 16.827C19.0612 16.811 18.2863 16.817 17.5113 16.843C17.4946 16.8439 17.4785 16.8489 17.4644 16.8576C17.4502 16.8664 17.4385 16.8785 17.4303 16.893C16.5361 18.4773 15.6344 20.0573 14.7253 21.633C14.5563 21.926 14.3453 21.996 14.0003 21.997C13.0033 22 11.9983 22.001 10.9833 21.999C10.8889 21.9987 10.7961 21.9735 10.7145 21.9259C10.6328 21.8783 10.5652 21.8101 10.5184 21.728L9.18337 19.405C9.1756 19.3898 9.16368 19.3771 9.14898 19.3684C9.13429 19.3598 9.11743 19.3554 9.10037 19.356H3.98244C3.69744 19.386 3.42944 19.355 3.17745 19.264L1.57447 16.494C1.52706 16.412 1.50193 16.319 1.50158 16.2243C1.50123 16.1296 1.52567 16.0364 1.57247 15.954L2.77945 13.834C2.79665 13.8041 2.80569 13.7701 2.80569 13.7355C2.80569 13.701 2.79665 13.667 2.77945 13.637C2.15073 12.5485 1.52573 11.4579 0.904476 10.3651L0.114486 8.97008C-0.0455115 8.66008 -0.0585113 8.47409 0.209485 8.00509C0.674479 7.1921 1.13647 6.38011 1.59647 5.56911C1.72847 5.33512 1.90046 5.23512 2.18046 5.23412C3.04344 5.23048 3.90644 5.23015 4.76943 5.23312C4.79123 5.23295 4.81259 5.22704 4.83138 5.21597C4.85016 5.20491 4.8657 5.1891 4.87643 5.17012L7.68239 0.275163C7.72491 0.200697 7.78631 0.138751 7.86039 0.0955646C7.93448 0.0523783 8.01863 0.0294762 8.10439 0.0291651C8.62838 0.0281651 9.15737 0.029165 9.68736 0.0231651L10.7044 0.000165317C11.0453 -0.00283466 11.4283 0.032165 11.6043 0.340162ZM8.17238 0.743158C8.16185 0.743152 8.15149 0.745921 8.14236 0.751187C8.13323 0.756454 8.12565 0.764031 8.12038 0.773158L5.25442 5.78811C5.24066 5.81174 5.22097 5.83137 5.19729 5.84505C5.17361 5.85873 5.14677 5.86599 5.11942 5.86611H2.25346C2.19746 5.86611 2.18346 5.89111 2.21246 5.94011L8.02239 16.096C8.04739 16.138 8.03539 16.158 7.98839 16.159L5.19342 16.174C5.15256 16.1727 5.11214 16.1828 5.07678 16.2033C5.04141 16.2238 5.01253 16.2539 4.99342 16.29L3.67344 18.6C3.62944 18.678 3.65244 18.718 3.74144 18.718L9.45737 18.726C9.50337 18.726 9.53737 18.746 9.56137 18.787L10.9643 21.241C11.0103 21.322 11.0563 21.323 11.1033 21.241L16.1093 12.481L16.8923 11.0991C16.897 11.0905 16.904 11.0834 16.9125 11.0785C16.9209 11.0735 16.9305 11.0709 16.9403 11.0709C16.9501 11.0709 16.9597 11.0735 16.9681 11.0785C16.9765 11.0834 16.9835 11.0905 16.9883 11.0991L18.4123 13.629C18.4229 13.648 18.4385 13.6637 18.4573 13.6746C18.4761 13.6855 18.4975 13.6912 18.5193 13.691L21.2822 13.671C21.2893 13.6711 21.2963 13.6693 21.3024 13.6658C21.3086 13.6623 21.3137 13.6572 21.3172 13.651C21.3206 13.6449 21.3224 13.638 21.3224 13.631C21.3224 13.624 21.3206 13.6172 21.3172 13.611L18.4173 8.52508C18.4068 8.50809 18.4013 8.48853 18.4013 8.46859C18.4013 8.44864 18.4068 8.42908 18.4173 8.41209L18.7102 7.90509L19.8302 5.92811C19.8542 5.88711 19.8422 5.86611 19.7952 5.86611H8.20038C8.14138 5.86611 8.12738 5.84011 8.15738 5.78911L9.59137 3.28413C9.60211 3.26706 9.60781 3.24731 9.60781 3.22714C9.60781 3.20697 9.60211 3.18721 9.59137 3.17014L8.22538 0.774158C8.22016 0.764697 8.21248 0.756822 8.20315 0.751365C8.19382 0.745909 8.18319 0.743073 8.17238 0.743158ZM14.4623 8.76308C14.5083 8.76308 14.5203 8.78308 14.4963 8.82308L13.6643 10.2881L11.0513 14.873C11.0464 14.8819 11.0392 14.8894 11.0304 14.8945C11.0216 14.8996 11.0115 14.9022 11.0013 14.902C10.9912 14.902 10.9813 14.8993 10.9725 14.8942C10.9637 14.8891 10.9564 14.8818 10.9513 14.873L7.49839 8.84108C7.47839 8.80708 7.48839 8.78908 7.52639 8.78708L7.74239 8.77508L14.4643 8.76308H14.4623Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMoonshotAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M20.052 6.0364C18.7527 4.28846 16.8758 2.94985 14.6066 2.34296C12.3374 1.73606 10.0411 1.95816 8.04092 2.82331L20.052 6.0364ZM3.75774 6.34071C4.64576 5.04902 5.81872 3.99836 7.16325 3.25459L16.0115 5.62191C15.4308 5.95635 14.8088 6.53268 14.3273 7.16626L21.6025 9.11263C21.809 9.79398 21.9435 10.5003 22 11.2213L3.75774 6.34071ZM21.6866 14.5876C21.584 14.9707 21.4603 15.3425 21.3172 15.7019L2.10543 10.5623C2.16147 10.1792 2.24079 9.7957 2.34339 9.41263C2.58479 8.51262 2.94172 7.67459 3.39435 6.91016L12.7957 9.42554C12.4194 9.96271 12.0766 10.5464 11.7749 11.1709L21.8517 13.8671C21.8056 14.1077 21.7504 14.3478 21.6862 14.5884L21.6866 14.5876ZM2.58134 15.3529C2.11535 14.05 1.91662 12.6408 2.03215 11.2088L10.8985 13.5808C10.8347 13.7823 10.7748 13.9863 10.7192 14.1933C10.6062 14.6147 10.514 15.0352 10.4424 15.4527L20.0166 18.0142C19.5709 18.6051 19.0631 19.1406 18.5057 19.6132L2.58134 15.3529ZM9.42338 21.6568C6.40111 20.8481 4.07415 18.7424 2.88266 16.1001L10.0976 18.0305C10.0859 18.7024 10.1278 19.3571 10.2196 19.9851L15.4619 21.3874C13.5906 22.0743 11.496 22.2112 9.42338 21.6568Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
|
||||
<path
|
||||
d="M24 15.5L18.1816 14.2871L18.1328 14.3115C16.9036 11.7879 15.6135 9.29301 14.2607 6.82812C14.0172 6.38435 13.771 5.94188 13.5234 5.5C13.6911 5.97998 13.8606 6.45942 14.0322 6.9375C14.9902 9.60529 16.012 12.2429 17.0947 14.8516L12 17.5L6.9043 14.8516C7.98712 12.2428 9.00977 9.6054 9.96777 6.9375C10.1394 6.45942 10.3089 5.97998 10.4766 5.5C10.229 5.94188 9.98281 6.38435 9.73926 6.82812C8.38629 9.29339 7.09557 11.7884 5.86621 14.3125L5.81738 14.2871L0 15.5L12 0.5L24 15.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="modal"][data-slot="overlay"] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
animation: fadeIn 0.2s ease;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
[data-slot="content"] {
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--space-6);
|
||||
min-width: 400px;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
animation: slideUp 0.2s ease;
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
min-width: 300px;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="title"] {
|
||||
margin: 0 0 var(--space-4) 0;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { JSX, Show } from "solid-js"
|
||||
import "./modal.css"
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
title?: string
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
export function Modal(props: ModalProps) {
|
||||
return (
|
||||
<Show when={props.open}>
|
||||
<div data-component="modal" data-slot="overlay" onClick={props.onClose}>
|
||||
<div data-slot="content" onClick={(e) => e.stopPropagation()}>
|
||||
<Show when={props.title}>
|
||||
<h2 data-slot="title">{props.title}</h2>
|
||||
</Show>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -74,6 +74,7 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
||||
userID: user.id,
|
||||
workspaceID: user.workspaceID,
|
||||
accountID: user.accountID,
|
||||
role: user.role,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { query } from "@solidjs/router"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
|
||||
export const beta = query(async (workspaceID?: string) => {
|
||||
"use server"
|
||||
return Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true
|
||||
}, "beta")
|
||||
@@ -43,7 +43,7 @@ export default function Home() {
|
||||
<main data-page="opencode">
|
||||
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
|
||||
<Title>OpenCode | The AI coding agent built for the terminal</Title>
|
||||
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
||||
<Link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<Meta property="og:image" content="/social-share.png" />
|
||||
<Meta name="twitter:image" content="/social-share.png" />
|
||||
<div data-component="container">
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[data-component="user-menu"] {
|
||||
[data-component="dropdown"] {
|
||||
[data-slot="trigger"] span {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
[data-slot="dropdown"] {
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="item"] {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { action, redirect } from "@solidjs/router"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { useAuthSession } from "~/context/auth.session"
|
||||
import { Dropdown } from "~/component/dropdown"
|
||||
import "./user-menu.css"
|
||||
|
||||
const logout = action(async () => {
|
||||
"use server"
|
||||
const auth = await useAuthSession()
|
||||
const event = getRequestEvent()
|
||||
const current = auth.data.current
|
||||
if (current)
|
||||
await auth.update((val) => {
|
||||
delete val.account?.[current]
|
||||
const first = Object.keys(val.account ?? {})[0]
|
||||
val.current = first
|
||||
event!.locals.actor = undefined
|
||||
return val
|
||||
})
|
||||
throw redirect("/zen")
|
||||
})
|
||||
|
||||
export function UserMenu(props: { email: string | null | undefined }) {
|
||||
return (
|
||||
<div data-component="user-menu">
|
||||
<Dropdown trigger={props.email ?? ""} align="right">
|
||||
<form action={logout} method="post">
|
||||
<button type="submit" formaction={logout} data-slot="item">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,71 +1,24 @@
|
||||
[data-component="workspace-picker"] {
|
||||
position: relative;
|
||||
/* Override blue accent colors with neutral colors */
|
||||
--color-accent: var(--color-border);
|
||||
--color-accent-hover: var(--color-border);
|
||||
--color-accent-active: var(--color-border);
|
||||
--color-primary: var(--color-border);
|
||||
--color-primary-hover: var(--color-border);
|
||||
--color-primary-active: var(--color-border);
|
||||
--color-primary-alpha-20: transparent;
|
||||
[data-component="dropdown"] {
|
||||
[data-slot="trigger"] {
|
||||
/* Override blue accent colors with neutral colors for dropdown trigger */
|
||||
--color-accent: var(--color-border);
|
||||
--color-accent-hover: var(--color-border);
|
||||
--color-accent-active: var(--color-border);
|
||||
--color-primary: var(--color-border);
|
||||
--color-primary-hover: var(--color-border);
|
||||
--color-primary-active: var(--color-border);
|
||||
--color-primary-alpha-20: transparent;
|
||||
}
|
||||
|
||||
[data-slot="trigger"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-sans);
|
||||
cursor: pointer;
|
||||
min-width: 200px;
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
[data-slot="dropdown"] {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="chevron"] {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
[data-slot="dropdown"] button {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* Ensure text inside buttons has no underline */
|
||||
[data-slot="dropdown"] button * {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
[data-slot="dropdown"] {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
margin-top: var(--space-1);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="option"],
|
||||
[data-slot="create-option"] {
|
||||
[data-slot="create-item"] {
|
||||
width: 100%;
|
||||
padding: var(--space-2-5) var(--space-3);
|
||||
border: none;
|
||||
@@ -75,59 +28,27 @@
|
||||
font-family: var(--font-sans);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-surface);
|
||||
text-decoration: none;
|
||||
background-color: var(--color-bg-surface);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: var(--border-radius-sm);
|
||||
border-top-right-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: var(--border-radius-sm);
|
||||
border-bottom-right-radius: var(--border-radius-sm);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="option"][data-selected="true"] {
|
||||
background-color: transparent;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
[data-slot="create-option"] {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="create-form"] {
|
||||
margin-top: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-surface);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="create-input-group"] {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
[data-slot="button-group"] {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
[data-slot="create-input"] {
|
||||
@@ -150,35 +71,4 @@
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
button[type="submit"],
|
||||
button[type="button"] {
|
||||
padding: var(--space-2-5) var(--space-4);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&[data-color="primary"] {
|
||||
background-color: var(--color-text-secondary);
|
||||
border-color: var(--color-text-secondary);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { query, useParams, action, createAsync, redirect } from "@solidjs/router"
|
||||
import { For, Show, createEffect, onCleanup } from "solid-js"
|
||||
import { query, useParams, action, createAsync, redirect, useSubmission } from "@solidjs/router"
|
||||
import { For, Show, createEffect } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
@@ -7,6 +7,8 @@ import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/ind
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { Workspace } from "@opencode-ai/console-core/workspace.js"
|
||||
import { Dropdown, DropdownItem } from "~/component/dropdown"
|
||||
import { Modal } from "~/component/modal"
|
||||
import "./workspace-picker.css"
|
||||
|
||||
const getWorkspaces = query(async () => {
|
||||
@@ -40,11 +42,11 @@ const createWorkspace = action(async (form: FormData) => {
|
||||
export function WorkspacePicker() {
|
||||
const params = useParams()
|
||||
const workspaces = createAsync(() => getWorkspaces())
|
||||
const submission = useSubmission(createWorkspace)
|
||||
const [store, setStore] = createStore({
|
||||
showForm: false,
|
||||
showDropdown: false,
|
||||
})
|
||||
let dropdownRef: HTMLDivElement | undefined
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
|
||||
const currentWorkspace = () => {
|
||||
const ws = workspaces()?.find((w) => w.id === params.id)
|
||||
@@ -52,15 +54,17 @@ export function WorkspacePicker() {
|
||||
}
|
||||
|
||||
const handleWorkspaceNew = () => {
|
||||
setStore({ showForm: true, showDropdown: false })
|
||||
setStore("showForm", true)
|
||||
}
|
||||
|
||||
const handleSelectWorkspace = (workspaceID: string) => {
|
||||
if (workspaceID === params.id) {
|
||||
setStore("showDropdown", false)
|
||||
return
|
||||
createEffect(() => {
|
||||
if (store.showForm && inputRef) {
|
||||
setTimeout(() => inputRef?.focus(), 0)
|
||||
}
|
||||
})
|
||||
|
||||
const handleSelectWorkspace = (workspaceID: string) => {
|
||||
if (workspaceID === params.id) return
|
||||
window.location.href = `/workspace/${workspaceID}`
|
||||
}
|
||||
|
||||
@@ -68,77 +72,45 @@ export function WorkspacePicker() {
|
||||
createEffect(() => {
|
||||
params.id
|
||||
setStore("showForm", false)
|
||||
setStore("showDropdown", false)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
|
||||
setStore("showDropdown", false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", handleClickOutside)
|
||||
onCleanup(() => document.removeEventListener("click", handleClickOutside))
|
||||
})
|
||||
|
||||
return (
|
||||
<div data-component="workspace-picker">
|
||||
<div ref={dropdownRef}>
|
||||
<div data-slot="trigger" onClick={() => setStore("showDropdown", !store.showDropdown)}>
|
||||
<span>{currentWorkspace()}</span>
|
||||
<svg data-slot="chevron" width="12" height="8" viewBox="0 0 12 8" fill="none">
|
||||
<path
|
||||
d="M1 1L6 6L11 1"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<Dropdown trigger={currentWorkspace()} align="left">
|
||||
<For each={workspaces()}>
|
||||
{(workspace) => (
|
||||
<DropdownItem selected={workspace.id === params.id} onClick={() => handleSelectWorkspace(workspace.id)}>
|
||||
{workspace.name || workspace.slug}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</For>
|
||||
<button data-slot="create-item" type="button" onClick={() => handleWorkspaceNew()}>
|
||||
+ Create New Workspace
|
||||
</button>
|
||||
</Dropdown>
|
||||
|
||||
<Show when={store.showDropdown}>
|
||||
<div data-slot="dropdown">
|
||||
<For each={workspaces()}>
|
||||
{(workspace) => (
|
||||
<button
|
||||
data-slot="option"
|
||||
data-selected={workspace.id === params.id}
|
||||
type="button"
|
||||
onClick={() => handleSelectWorkspace(workspace.id)}
|
||||
>
|
||||
{workspace.name || workspace.slug}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<button data-slot="create-option" type="button" onClick={() => handleWorkspaceNew()}>
|
||||
+ Create New Workspace
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={store.showForm}>
|
||||
<Modal open={store.showForm} onClose={() => setStore("showForm", false)} title="Create New Workspace">
|
||||
<form data-slot="create-form" action={createWorkspace} method="post">
|
||||
<div data-slot="create-input-group">
|
||||
<input
|
||||
ref={inputRef}
|
||||
data-slot="create-input"
|
||||
type="text"
|
||||
name="workspaceName"
|
||||
placeholder="Enter workspace name"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
<button type="submit" data-color="primary">
|
||||
Create
|
||||
</button>
|
||||
<button type="button" onClick={() => setStore("showForm", false)}>
|
||||
Cancel
|
||||
</button>
|
||||
<div data-slot="button-group">
|
||||
<button type="button" data-color="ghost" onClick={() => setStore("showForm", false)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||
{submission.pending ? "Creating..." : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Show>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
@@ -55,9 +54,6 @@
|
||||
|
||||
a {
|
||||
color: var(--color-text);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: var(--space-0-75);
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
|
||||
/* Workspace Header */
|
||||
@@ -80,16 +76,14 @@
|
||||
[data-slot="header-brand"] {
|
||||
flex: 0 0 auto;
|
||||
padding-top: 4px;
|
||||
|
||||
svg {
|
||||
width: 138px;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
|
||||
[data-component="site-title"] {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
}
|
||||
@@ -109,19 +103,5 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
color: var(--color-text);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: var(--space-0-75);
|
||||
text-decoration-thickness: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,37 @@
|
||||
import { Show } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { query, action, redirect, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
|
||||
import { query, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
|
||||
import "./workspace.css"
|
||||
import { useAuthSession } from "~/context/auth.session"
|
||||
import { IconLogo } from "../component/icon"
|
||||
import { IconWorkspaceLogo } from "../component/icon"
|
||||
import { WorkspacePicker } from "./workspace-picker"
|
||||
import { UserMenu } from "./user-menu"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { beta } from "~/lib/beta"
|
||||
import { Link } from "@solidjs/meta"
|
||||
|
||||
const getUserInfo = query(async (workspaceID: string) => {
|
||||
const getUserEmail = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
const actor = Actor.assert("user")
|
||||
const email = await User.getAccountEmail(actor.properties.userID)
|
||||
return { email }
|
||||
return email
|
||||
}, workspaceID)
|
||||
}, "userInfo")
|
||||
|
||||
const logout = action(async () => {
|
||||
"use server"
|
||||
const auth = await useAuthSession()
|
||||
const event = getRequestEvent()
|
||||
const current = auth.data.current
|
||||
if (current)
|
||||
await auth.update((val) => {
|
||||
delete val.account?.[current]
|
||||
const first = Object.keys(val.account ?? {})[0]
|
||||
val.current = first
|
||||
event!.locals.actor = undefined
|
||||
return val
|
||||
})
|
||||
throw redirect("/zen")
|
||||
})
|
||||
}, "userEmail")
|
||||
|
||||
export default function WorkspaceLayout(props: RouteSectionProps) {
|
||||
const params = useParams()
|
||||
const userInfo = createAsync(() => getUserInfo(params.id))
|
||||
const isBeta = createAsync(() => beta(params.id))
|
||||
const userEmail = createAsync(() => getUserEmail(params.id))
|
||||
return (
|
||||
<main data-page="workspace">
|
||||
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
||||
<header data-component="workspace-header">
|
||||
<div data-slot="header-brand">
|
||||
<A href="/" data-component="site-title">
|
||||
<IconLogo />
|
||||
<IconWorkspaceLogo />
|
||||
</A>
|
||||
<WorkspacePicker />
|
||||
</div>
|
||||
<div data-slot="header-actions">
|
||||
<Show when={isBeta()}>
|
||||
<WorkspacePicker />
|
||||
</Show>
|
||||
<span data-slot="user">{userInfo()?.email}</span>
|
||||
<form action={logout} method="post">
|
||||
<button type="submit" formaction={logout}>
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
<UserMenu email={userEmail()} />
|
||||
</div>
|
||||
</header>
|
||||
<div>{props.children}</div>
|
||||
|
||||
@@ -1,7 +1,72 @@
|
||||
[data-page="workspace"] {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Workspace Layout */
|
||||
[data-component="workspace-container"] {
|
||||
display: flex;
|
||||
height: calc(100vh - 73px);
|
||||
}
|
||||
|
||||
[data-component="workspace-nav"] {
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
padding: var(--space-6) var(--space-4);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
[data-component="workspace-nav-items"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
|
||||
[data-nav-button] {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-text);
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: calc(-1 * var(--space-0-5));
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background-color: var(--color-text);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="workspace-content"] {
|
||||
flex: 1;
|
||||
padding: var(--space-6) var(--space-8);
|
||||
overflow-y: auto;
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
padding: var(--space-6) var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
[data-page="workspace-[id]"] {
|
||||
max-width: 64rem;
|
||||
padding: var(--space-10) var(--space-4);
|
||||
margin: 0 auto;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -32,7 +97,6 @@
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
/* Section titles */
|
||||
[data-slot="section-title"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -44,8 +108,7 @@
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.03125rem;
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text);
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
font-size: var(--font-size-md);
|
||||
@@ -66,7 +129,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="section-content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-8);
|
||||
}
|
||||
}
|
||||
|
||||
section:not(:last-child) {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: var(--space-16);
|
||||
@@ -78,7 +149,7 @@
|
||||
}
|
||||
|
||||
/* Title section */
|
||||
[data-component="title-section"] {
|
||||
[data-component="header-section"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
@@ -105,11 +176,50 @@
|
||||
p {
|
||||
line-height: 1.5;
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
[data-slot="billing-info"] {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
[data-slot="balance"] {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
|
||||
b {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
[data-component="workspace-container"] {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-component="workspace-nav"] {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,37 @@
|
||||
import "./[id].css"
|
||||
import { MonthlyLimitSection } from "./monthly-limit-section"
|
||||
import { NewUserSection } from "./new-user-section"
|
||||
import { BillingSection } from "./billing-section"
|
||||
import { PaymentSection } from "./payment-section"
|
||||
import { UsageSection } from "./usage-section"
|
||||
import { KeySection } from "./key-section"
|
||||
import { MemberSection } from "./member-section"
|
||||
import { SettingsSection } from "./settings-section"
|
||||
import { Show } from "solid-js"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
import { beta } from "~/lib/beta"
|
||||
import { createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
|
||||
import { querySessionInfo } from "./common"
|
||||
import "./[id].css"
|
||||
|
||||
const getUserInfo = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
const actor = Actor.assert("user")
|
||||
const user = await User.fromID(actor.properties.userID)
|
||||
return {
|
||||
isAdmin: user?.role === "admin",
|
||||
}
|
||||
}, workspaceID)
|
||||
}, "user.get")
|
||||
|
||||
export default function () {
|
||||
export default function WorkspaceLayout(props: RouteSectionProps) {
|
||||
const params = useParams()
|
||||
const userInfo = createAsync(() => getUserInfo(params.id))
|
||||
const isBeta = createAsync(() => beta(params.id))
|
||||
|
||||
const userInfo = createAsync(() => querySessionInfo(params.id))
|
||||
return (
|
||||
<div data-page="workspace-[id]">
|
||||
<section data-component="title-section">
|
||||
<h1>Zen</h1>
|
||||
<p>
|
||||
Curated list of models provided by opencode.{" "}
|
||||
<a target="_blank" href="/docs/zen">
|
||||
Learn more
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div data-slot="sections">
|
||||
<NewUserSection />
|
||||
<KeySection />
|
||||
<Show when={userInfo()?.isAdmin}>
|
||||
<Show when={isBeta()}>
|
||||
<SettingsSection />
|
||||
<MemberSection />
|
||||
</Show>
|
||||
<BillingSection />
|
||||
<MonthlyLimitSection />
|
||||
</Show>
|
||||
<UsageSection />
|
||||
<Show when={userInfo()?.isAdmin}>
|
||||
<PaymentSection />
|
||||
</Show>
|
||||
<main data-page="workspace">
|
||||
<div data-component="workspace-container">
|
||||
<nav data-component="workspace-nav">
|
||||
<div data-component="workspace-nav-items">
|
||||
<A href={`/workspace/${params.id}`} end activeClass="active" data-nav-button>
|
||||
Zen
|
||||
</A>
|
||||
<A href={`/workspace/${params.id}/keys`} activeClass="active" data-nav-button>
|
||||
API Keys
|
||||
</A>
|
||||
<A href={`/workspace/${params.id}/members`} activeClass="active" data-nav-button>
|
||||
Members
|
||||
</A>
|
||||
<Show when={userInfo()?.isAdmin}>
|
||||
<A href={`/workspace/${params.id}/billing`} activeClass="active" data-nav-button>
|
||||
Billing
|
||||
</A>
|
||||
<A href={`/workspace/${params.id}/settings`} activeClass="active" data-nav-button>
|
||||
Settings
|
||||
</A>
|
||||
</Show>
|
||||
</div>
|
||||
</nav>
|
||||
<div data-component="workspace-content">{props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
+6
-9
@@ -1,10 +1,4 @@
|
||||
.root {
|
||||
[data-slot="section-content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
[data-slot="reload-error"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -29,6 +23,7 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="payment"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -86,7 +81,7 @@
|
||||
@media (max-width: 30rem) {
|
||||
flex-direction: column;
|
||||
|
||||
> button {
|
||||
>button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -96,19 +91,21 @@
|
||||
}
|
||||
|
||||
/* Make Enable Billing button full width when it's the only button */
|
||||
> button {
|
||||
>button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="usage"] {
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
b {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-5
@@ -6,11 +6,7 @@ import { IconCreditCard } from "~/component/icon"
|
||||
import styles from "./billing-section.module.css"
|
||||
import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||
|
||||
const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => {
|
||||
"use server"
|
||||
return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
|
||||
}, "checkoutUrl")
|
||||
import { createCheckoutUrl } from "../../common"
|
||||
|
||||
const reload = action(async (form: FormData) => {
|
||||
"use server"
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MonthlyLimitSection } from "./monthly-limit-section"
|
||||
import { BillingSection } from "./billing-section"
|
||||
import { PaymentSection } from "./payment-section"
|
||||
import { Show } from "solid-js"
|
||||
import { createAsync, useParams } from "@solidjs/router"
|
||||
import { querySessionInfo } from "../../common"
|
||||
|
||||
export default function () {
|
||||
const params = useParams()
|
||||
const userInfo = createAsync(() => querySessionInfo(params.id))
|
||||
|
||||
return (
|
||||
<div data-page="workspace-[id]">
|
||||
<div data-slot="sections">
|
||||
<Show when={userInfo()?.isAdmin}>
|
||||
<BillingSection />
|
||||
<MonthlyLimitSection />
|
||||
<PaymentSection />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1
-7
@@ -1,10 +1,4 @@
|
||||
.root {
|
||||
[data-slot="section-content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
[data-slot="balance"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -99,4 +93,4 @@
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-7
@@ -1,8 +1,8 @@
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import { query, action, useParams, createAsync, useAction } from "@solidjs/router"
|
||||
import { For } from "solid-js"
|
||||
import { For, Show } from "solid-js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { formatDateUTC, formatDateForTable } from "./common"
|
||||
import { formatDateUTC, formatDateForTable } from "../../common"
|
||||
import styles from "./payment-section.module.css"
|
||||
|
||||
const getPaymentsInfo = query(async (workspaceID: string) => {
|
||||
@@ -19,7 +19,6 @@ const downloadReceipt = action(async (workspaceID: string, paymentID: string) =>
|
||||
|
||||
export function PaymentSection() {
|
||||
const params = useParams()
|
||||
// ORIGINAL CODE - COMMENTED OUT FOR TESTING
|
||||
const payments = createAsync(() => getPaymentsInfo(params.id))
|
||||
const downloadReceiptAction = useAction(downloadReceipt)
|
||||
|
||||
@@ -58,8 +57,7 @@ export function PaymentSection() {
|
||||
// ]
|
||||
|
||||
return (
|
||||
payments() &&
|
||||
payments()!.length > 0 && (
|
||||
<Show when={payments() && payments()!.length > 0}>
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>Payments History</h2>
|
||||
@@ -98,7 +96,7 @@ export function PaymentSection() {
|
||||
}}
|
||||
data-slot="receipt-button"
|
||||
>
|
||||
view
|
||||
View
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -109,6 +107,6 @@ export function PaymentSection() {
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { NewUserSection } from "./new-user-section"
|
||||
import { UsageSection } from "./usage-section"
|
||||
import { ModelSection } from "./model-section"
|
||||
import { ProviderSection } from "./provider-section"
|
||||
import { IconLogo } from "~/component/icon"
|
||||
import { createAsync, useParams, useAction, useSubmission } from "@solidjs/router"
|
||||
import { querySessionInfo, queryBillingInfo, createCheckoutUrl } from "../common"
|
||||
import { Show, createMemo } from "solid-js"
|
||||
|
||||
export default function () {
|
||||
const params = useParams()
|
||||
const userInfo = createAsync(() => querySessionInfo(params.id))
|
||||
const billingInfo = createAsync(() => queryBillingInfo(params.id))
|
||||
const createCheckoutUrlAction = useAction(createCheckoutUrl)
|
||||
const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
|
||||
|
||||
const balanceAmount = createMemo(() => {
|
||||
return ((billingInfo()?.balance ?? 0) / 100000000).toFixed(2)
|
||||
})
|
||||
|
||||
return (
|
||||
<div data-page="workspace-[id]">
|
||||
<section data-component="header-section">
|
||||
<IconLogo />
|
||||
<p>
|
||||
<span>
|
||||
Reliable optimized models for coding agents.{" "}
|
||||
<a target="_blank" href="/docs/zen">
|
||||
Learn more
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
<Show when={userInfo()?.isAdmin}>
|
||||
<span data-slot="billing-info">
|
||||
<Show
|
||||
when={billingInfo()?.reload}
|
||||
fallback={
|
||||
<button
|
||||
data-color="primary"
|
||||
data-size="sm"
|
||||
disabled={createCheckoutUrlSubmission.pending}
|
||||
onClick={async () => {
|
||||
const baseUrl = window.location.href
|
||||
const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
|
||||
if (checkoutUrl) {
|
||||
window.location.href = checkoutUrl
|
||||
}
|
||||
}}
|
||||
>
|
||||
{createCheckoutUrlSubmission.pending ? "Loading..." : "Enable billing"}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<span data-slot="balance">
|
||||
Current balance <b>${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
|
||||
</span>
|
||||
</Show>
|
||||
</span>
|
||||
</Show>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div data-slot="sections">
|
||||
<NewUserSection />
|
||||
<ModelSection />
|
||||
<Show when={userInfo()?.isAdmin}>
|
||||
<ProviderSection />
|
||||
</Show>
|
||||
<UsageSection />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { KeySection } from "./key-section"
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<div data-page="workspace-[id]">
|
||||
<div data-slot="sections">
|
||||
<KeySection />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+31
-3
@@ -1,4 +1,11 @@
|
||||
.root {
|
||||
[data-slot="title-row"] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
[data-component="empty-state"] {
|
||||
padding: var(--space-20) var(--space-6);
|
||||
text-align: center;
|
||||
@@ -107,6 +114,7 @@
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
margin-left: calc(-1 * var(--space-3));
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 400;
|
||||
border: none;
|
||||
@@ -140,16 +148,30 @@
|
||||
|
||||
&[data-slot="key-actions"] {
|
||||
font-family: var(--font-sans);
|
||||
|
||||
button {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
&:hover {
|
||||
[data-slot="key-actions"] button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
@@ -157,16 +179,22 @@
|
||||
}
|
||||
|
||||
th {
|
||||
&:nth-child(3) /* Date */ {
|
||||
&:nth-child(3)
|
||||
|
||||
/* Date */
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
&:nth-child(3) /* Date */ {
|
||||
&:nth-child(3)
|
||||
|
||||
/* Date */
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
-68
@@ -4,7 +4,7 @@ import { IconCopy, IconCheck } from "~/component/icon"
|
||||
import { Key } from "@opencode-ai/console-core/key.js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { formatDateUTC, formatDateForTable } from "./common"
|
||||
import { formatDateUTC, formatDateForTable } from "../../common"
|
||||
import styles from "./key-section.module.css"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
|
||||
@@ -43,8 +43,9 @@ const listKeys = query(async (workspaceID: string) => {
|
||||
return withActor(() => Key.list(), workspaceID)
|
||||
}, "key.list")
|
||||
|
||||
export function KeyCreateForm() {
|
||||
export function KeySection() {
|
||||
const params = useParams()
|
||||
const keys = createAsync(() => listKeys(params.id))
|
||||
const submission = useSubmission(createKey)
|
||||
const [store, setStore] = createStore({ show: false })
|
||||
|
||||
@@ -52,74 +53,59 @@ export function KeyCreateForm() {
|
||||
|
||||
createEffect(() => {
|
||||
if (!submission.pending && submission.result && !submission.result.error) {
|
||||
hide()
|
||||
setStore("show", false)
|
||||
}
|
||||
})
|
||||
|
||||
function show() {
|
||||
// submission.clear() does not clear the result in some cases, ie.
|
||||
// 1. Create key with empty name => error shows
|
||||
// 2. Put in a key name and creates the key => form hides
|
||||
// 3. Click add key button again => form shows with the same error if
|
||||
// submission.clear() is called only once
|
||||
while (true) {
|
||||
submission.clear()
|
||||
if (!submission.result) break
|
||||
}
|
||||
setStore("show", true)
|
||||
input.focus()
|
||||
setTimeout(() => input?.focus(), 0)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
setStore("show", false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={store.show}
|
||||
fallback={
|
||||
<button data-color="primary" onClick={() => show()}>
|
||||
Create API Key
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<form action={createKey} method="post" data-slot="create-form">
|
||||
<div data-slot="input-container">
|
||||
<input ref={(r) => (input = r)} data-component="input" name="name" type="text" placeholder="Enter key name" />
|
||||
<Show when={submission.result && submission.result.error}>
|
||||
{(err) => <div data-slot="form-error">{err()}</div>}
|
||||
</Show>
|
||||
</div>
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
<div data-slot="form-actions">
|
||||
<button type="reset" data-color="ghost" onClick={() => hide()}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||
{submission.pending ? "Creating..." : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
export function KeySection() {
|
||||
const params = useParams()
|
||||
const keys = createAsync(() => listKeys(params.id))
|
||||
|
||||
function formatKey(key: string) {
|
||||
if (key.length <= 11) return key
|
||||
return `${key.slice(0, 7)}...${key.slice(-4)}`
|
||||
}
|
||||
|
||||
return (
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>API Keys</h2>
|
||||
<p>Manage your API keys for accessing opencode services.</p>
|
||||
<div data-slot="title-row">
|
||||
<p>Manage your API keys for accessing opencode services.</p>
|
||||
<button data-color="primary" onClick={() => show()}>
|
||||
Create API Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<KeyCreateForm />
|
||||
<Show when={store.show}>
|
||||
<form action={createKey} method="post" data-slot="create-form">
|
||||
<div data-slot="input-container">
|
||||
<input
|
||||
ref={(r) => (input = r)}
|
||||
data-component="input"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Enter key name"
|
||||
/>
|
||||
<Show when={submission.result && submission.result.error}>
|
||||
{(err) => <div data-slot="form-error">{err()}</div>}
|
||||
</Show>
|
||||
</div>
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
<div data-slot="form-actions">
|
||||
<button type="reset" data-color="ghost" onClick={() => hide()}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||
{submission.pending ? "Creating..." : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Show>
|
||||
<div data-slot="api-keys-table">
|
||||
<Show
|
||||
when={keys()?.length}
|
||||
@@ -134,7 +120,8 @@ export function KeySection() {
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Key</th>
|
||||
<th>Created</th>
|
||||
<th>Created By</th>
|
||||
<th>Last Used</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -147,24 +134,27 @@ export function KeySection() {
|
||||
<tr>
|
||||
<td data-slot="key-name">{key.name}</td>
|
||||
<td data-slot="key-value">
|
||||
<button
|
||||
data-color="ghost"
|
||||
disabled={copied()}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(key.key)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1000)
|
||||
}}
|
||||
title="Copy API key"
|
||||
>
|
||||
<span>{formatKey(key.key)}</span>
|
||||
<Show when={copied()} fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}>
|
||||
<IconCheck style={{ width: "14px", height: "14px" }} />
|
||||
</Show>
|
||||
</button>
|
||||
<Show when={key.key} fallback={<span>{key.keyDisplay}</span>}>
|
||||
<button
|
||||
data-color="ghost"
|
||||
disabled={copied()}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(key.key!)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1000)
|
||||
}}
|
||||
title="Copy API key"
|
||||
>
|
||||
<span>{key.keyDisplay}</span>
|
||||
<Show when={copied()} fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}>
|
||||
<IconCheck style={{ width: "14px", height: "14px" }} />
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
</td>
|
||||
<td data-slot="key-date" title={formatDateUTC(key.timeCreated)}>
|
||||
{formatDateForTable(key.timeCreated)}
|
||||
<td data-slot="key-user-email">{key.email}</td>
|
||||
<td data-slot="key-last-used" title={key.timeUsed ? formatDateUTC(key.timeUsed) : undefined}>
|
||||
{key.timeUsed ? formatDateForTable(key.timeUsed) : "-"}
|
||||
</td>
|
||||
<td data-slot="key-actions">
|
||||
<form action={removeKey} method="post">
|
||||
@@ -0,0 +1,11 @@
|
||||
import { MemberSection } from "./member-section"
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<div data-page="workspace-[id]">
|
||||
<div data-slot="sections">
|
||||
<MemberSection />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+101
-44
@@ -1,4 +1,11 @@
|
||||
.root {
|
||||
[data-slot="title-row"] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
[data-component="empty-state"] {
|
||||
padding: var(--space-20) var(--space-6);
|
||||
text-align: center;
|
||||
@@ -23,51 +30,74 @@
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
|
||||
[data-slot="input-container"] {
|
||||
[data-slot="input-row"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--space-3);
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="input-field"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-mono);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
p {
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
input {
|
||||
flex: 1;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
min-width: 0;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 3px var(--color-accent-alpha);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="form-actions"] {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
|
||||
>button[type="reset"] {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="form-error"] {
|
||||
color: var(--color-danger);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-top: var(--space-1);
|
||||
line-height: 1.4;
|
||||
margin-top: calc(var(--space-1) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="members-table"] {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 200px;
|
||||
margin-bottom: -200px;
|
||||
}
|
||||
|
||||
[data-slot="members-table-element"] {
|
||||
@@ -85,6 +115,14 @@
|
||||
font-weight: normal;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
@@ -101,35 +139,29 @@
|
||||
|
||||
&[data-slot="member-role"] {
|
||||
font-family: var(--font-mono);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
&[data-slot="member-usage"] {
|
||||
input {
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 400;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-mono);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-transform: none;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
font-family: var(--font-mono);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-bg-surface);
|
||||
color: var(--color-text);
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 3px var(--color-accent-alpha);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: inherit;
|
||||
&::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,10 +172,35 @@
|
||||
|
||||
&[data-slot="member-actions"] {
|
||||
font-family: var(--font-sans);
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
|
||||
[data-slot="inline-edit-form"] {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
|
||||
button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
form:not([data-slot="inline-edit-form"]) button {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
&:hover {
|
||||
[data-slot="member-actions"] form:not([data-slot="inline-edit-form"]) button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
|
||||
import { createEffect, For, Show } from "solid-js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { createStore } from "solid-js/store"
|
||||
import styles from "./member-section.module.css"
|
||||
import { UserRole } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
import { RoleDropdown } from "./role-dropdown"
|
||||
|
||||
const listMembers = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
return {
|
||||
members: await User.list(),
|
||||
actorID: Actor.userID(),
|
||||
actorRole: Actor.userRole(),
|
||||
}
|
||||
}, workspaceID)
|
||||
}, "member.list")
|
||||
|
||||
const inviteMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const email = form.get("email")?.toString().trim()
|
||||
if (!email) return { error: "Email is required" }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
||||
if (!role) return { error: "Role is required" }
|
||||
const limit = form.get("limit")?.toString()
|
||||
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
||||
if (monthlyLimit !== null && monthlyLimit < 0) return { error: "Set a valid monthly limit" }
|
||||
return json(
|
||||
await withActor(
|
||||
() =>
|
||||
User.invite({ email, role, monthlyLimit })
|
||||
.then((data) => ({ error: undefined, data }))
|
||||
.catch((e) => ({ error: e.message as string })),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: listMembers.key },
|
||||
)
|
||||
}, "member.create")
|
||||
|
||||
const removeMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const id = form.get("id")?.toString()
|
||||
if (!id) return { error: "ID is required" }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
return json(
|
||||
await withActor(
|
||||
() =>
|
||||
User.remove(id)
|
||||
.then((data) => ({ error: undefined, data }))
|
||||
.catch((e) => ({ error: e.message as string })),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: listMembers.key },
|
||||
)
|
||||
}, "member.remove")
|
||||
|
||||
const updateMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
|
||||
const id = form.get("id")?.toString()
|
||||
if (!id) return { error: "ID is required" }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
||||
if (!role) return { error: "Role is required" }
|
||||
const limit = form.get("limit")?.toString()
|
||||
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
||||
if (monthlyLimit !== null && monthlyLimit < 0) return { error: "Set a valid monthly limit" }
|
||||
|
||||
return json(
|
||||
await withActor(
|
||||
() =>
|
||||
User.update({ id, role, monthlyLimit })
|
||||
.then((data) => ({ error: undefined, data }))
|
||||
.catch((e) => ({ error: e.message as string })),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: listMembers.key },
|
||||
)
|
||||
}, "member.update")
|
||||
|
||||
function MemberRow(props: { member: any; workspaceID: string; actorID: string; actorRole: string }) {
|
||||
const submission = useSubmission(updateMember)
|
||||
const isCurrentUser = () => props.actorID === props.member.id
|
||||
const isAdmin = () => props.actorRole === "admin"
|
||||
const [store, setStore] = createStore({
|
||||
editing: false,
|
||||
selectedRole: props.member.role as (typeof UserRole)[number],
|
||||
limit: "",
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!submission.pending && submission.result && !submission.result.error) {
|
||||
setStore("editing", false)
|
||||
}
|
||||
})
|
||||
|
||||
function show() {
|
||||
while (true) {
|
||||
submission.clear()
|
||||
if (!submission.result) break
|
||||
}
|
||||
setStore("editing", true)
|
||||
setStore("selectedRole", props.member.role)
|
||||
setStore("limit", props.member.monthlyLimit?.toString() ?? "")
|
||||
}
|
||||
|
||||
function hide() {
|
||||
setStore("editing", false)
|
||||
}
|
||||
|
||||
function getUsageDisplay() {
|
||||
const currentUsage = (() => {
|
||||
const dateLastUsed = props.member.timeMonthlyUsageUpdated
|
||||
if (!dateLastUsed) return 0
|
||||
|
||||
const current = new Date().toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
const lastUsed = dateLastUsed.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
return current === lastUsed ? (props.member.monthlyUsage ?? 0) : 0
|
||||
})()
|
||||
|
||||
const limit = props.member.monthlyLimit ? `$${props.member.monthlyLimit}` : "no limit"
|
||||
return `$${(currentUsage / 100000000).toFixed(2)} / ${limit}`
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td data-slot="member-email">{props.member.accountEmail ?? props.member.email}</td>
|
||||
<td data-slot="member-role">
|
||||
<Show when={store.editing && !isCurrentUser()} fallback={<span>{props.member.role}</span>}>
|
||||
<RoleDropdown
|
||||
value={store.selectedRole}
|
||||
options={roleOptions}
|
||||
onChange={(value) => setStore("selectedRole", value as (typeof UserRole)[number])}
|
||||
/>
|
||||
</Show>
|
||||
</td>
|
||||
<td data-slot="member-usage">
|
||||
<Show when={store.editing} fallback={<span>{getUsageDisplay()}</span>}>
|
||||
<input
|
||||
data-component="input"
|
||||
type="number"
|
||||
value={store.limit}
|
||||
onInput={(e) => setStore("limit", e.currentTarget.value)}
|
||||
placeholder="No limit"
|
||||
min="0"
|
||||
/>
|
||||
</Show>
|
||||
</td>
|
||||
<td data-slot="member-joined">{props.member.timeSeen ? "" : "invited"}</td>
|
||||
<Show when={isAdmin()}>
|
||||
<td data-slot="member-actions">
|
||||
<Show
|
||||
when={store.editing}
|
||||
fallback={
|
||||
<>
|
||||
<button data-color="ghost" onClick={() => show()}>
|
||||
Edit
|
||||
</button>
|
||||
<Show when={!isCurrentUser()}>
|
||||
<form action={removeMember} method="post">
|
||||
<input type="hidden" name="id" value={props.member.id} />
|
||||
<input type="hidden" name="workspaceID" value={props.workspaceID} />
|
||||
<button data-color="ghost">Delete</button>
|
||||
</form>
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form action={updateMember} method="post" data-slot="inline-edit-form">
|
||||
<input type="hidden" name="id" value={props.member.id} />
|
||||
<input type="hidden" name="workspaceID" value={props.workspaceID} />
|
||||
<input type="hidden" name="role" value={store.selectedRole} />
|
||||
<input type="hidden" name="limit" value={store.limit} />
|
||||
<button type="submit" data-color="ghost" disabled={submission.pending}>
|
||||
{submission.pending ? "Saving..." : "Save"}
|
||||
</button>
|
||||
<Show when={!submission.pending}>
|
||||
<button type="button" data-color="ghost" onClick={() => hide()}>
|
||||
Cancel
|
||||
</button>
|
||||
</Show>
|
||||
</form>
|
||||
</Show>
|
||||
</td>
|
||||
</Show>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const roleOptions = [
|
||||
{ value: "admin", description: "Can manage models, members, and billing" },
|
||||
{ value: "member", description: "Can only generate API keys for themselves" },
|
||||
]
|
||||
|
||||
export function MemberSection() {
|
||||
const params = useParams()
|
||||
const data = createAsync(() => listMembers(params.id))
|
||||
const submission = useSubmission(inviteMember)
|
||||
const [store, setStore] = createStore({
|
||||
show: false,
|
||||
selectedRole: "member" as (typeof UserRole)[number],
|
||||
limit: "",
|
||||
})
|
||||
|
||||
let input: HTMLInputElement
|
||||
|
||||
createEffect(() => {
|
||||
if (!submission.pending && submission.result && !submission.result.error) {
|
||||
setStore("show", false)
|
||||
}
|
||||
})
|
||||
|
||||
function show() {
|
||||
while (true) {
|
||||
submission.clear()
|
||||
if (!submission.result) break
|
||||
}
|
||||
setStore("show", true)
|
||||
setStore("selectedRole", "member")
|
||||
setStore("limit", "")
|
||||
setTimeout(() => input?.focus(), 0)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
setStore("show", false)
|
||||
}
|
||||
|
||||
return (
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>Members</h2>
|
||||
<div data-slot="title-row">
|
||||
<p>Manage workspace members and their permissions.</p>
|
||||
<Show when={data()?.actorRole === "admin"}>
|
||||
<button data-color="primary" onClick={() => show()}>
|
||||
Invite Member
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={store.show}>
|
||||
<form action={inviteMember} method="post" data-slot="create-form">
|
||||
<div data-slot="input-row">
|
||||
<div data-slot="input-field">
|
||||
<p>Invitee</p>
|
||||
<input
|
||||
ref={(r) => (input = r)}
|
||||
data-component="input"
|
||||
name="email"
|
||||
type="text"
|
||||
placeholder="Enter email"
|
||||
/>
|
||||
</div>
|
||||
<div data-slot="input-field">
|
||||
<p>Role</p>
|
||||
<RoleDropdown
|
||||
value={store.selectedRole}
|
||||
options={roleOptions}
|
||||
onChange={(value) => setStore("selectedRole", value as (typeof UserRole)[number])}
|
||||
/>
|
||||
</div>
|
||||
<div data-slot="input-field">
|
||||
<p>Monthly spending limit</p>
|
||||
<input
|
||||
data-component="input"
|
||||
name="limit"
|
||||
type="number"
|
||||
placeholder="No limit"
|
||||
value={store.limit}
|
||||
onInput={(e) => setStore("limit", e.currentTarget.value)}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={submission.result && submission.result.error}>
|
||||
{(err) => <div data-slot="form-error">{err()}</div>}
|
||||
</Show>
|
||||
<input type="hidden" name="role" value={store.selectedRole} />
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
<div data-slot="form-actions">
|
||||
<button type="reset" data-color="ghost" onClick={() => hide()}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||
{submission.pending ? "Inviting..." : "Invite"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Show>
|
||||
<div data-slot="members-table">
|
||||
<table data-slot="members-table-element">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Month limit</th>
|
||||
<th></th>
|
||||
<Show when={data()?.actorRole === "admin"}>
|
||||
<th></th>
|
||||
</Show>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={data() && data()!.members.length > 0}>
|
||||
<For each={data()!.members}>
|
||||
{(member) => (
|
||||
<MemberRow
|
||||
member={member}
|
||||
workspaceID={params.id}
|
||||
actorID={data()!.actorID}
|
||||
actorRole={data()!.actorRole}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
.role-dropdown {
|
||||
[data-slot="trigger"] {
|
||||
border: 1px solid var(--color-border);
|
||||
background-color: var(--color-bg);
|
||||
width: 100%;
|
||||
text-transform: capitalize;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
min-width: 0;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-accent);
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 3px var(--color-accent-alpha);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="chevron"] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
[data-slot="dropdown"] {
|
||||
padding: var(--space-1);
|
||||
min-width: 280px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
[data-slot="role-item"] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius-sm);
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-surface);
|
||||
}
|
||||
|
||||
&[data-selected="true"] {
|
||||
background-color: var(--color-accent-alpha);
|
||||
}
|
||||
|
||||
div {
|
||||
strong {
|
||||
display: block;
|
||||
color: var(--color-text);
|
||||
margin-bottom: var(--space-1);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createSignal } from "solid-js"
|
||||
import { Dropdown } from "~/component/dropdown"
|
||||
import "./role-dropdown.css"
|
||||
|
||||
interface RoleOption {
|
||||
value: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface RoleDropdownProps {
|
||||
value: string
|
||||
options: RoleOption[]
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function RoleDropdown(props: RoleDropdownProps) {
|
||||
const [open, setOpen] = createSignal(false)
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
props.onChange(value)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown trigger={props.value} open={open()} onOpenChange={setOpen} class="role-dropdown">
|
||||
<>
|
||||
{props.options.map((option) => (
|
||||
<button
|
||||
data-slot="role-item"
|
||||
data-selected={props.value === option.value}
|
||||
type="button"
|
||||
onClick={() => handleSelect(option.value)}
|
||||
>
|
||||
<div>
|
||||
<strong>{option.value}</strong>
|
||||
<p>{option.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
[data-slot="models-list"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-slot="models-table"] {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
[data-slot="models-table-element"] {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
thead {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-mono);
|
||||
|
||||
&[data-slot="model-name"] {
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-slot="training-data"] {
|
||||
text-align: center;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&[data-slot="model-toggle"] {
|
||||
text-align: left;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
[data-slot="model-toggle-label"] {
|
||||
/* Toggle container */
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 2.5rem;
|
||||
height: 1.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
/* Hidden checkbox input */
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Toggle track (background) */
|
||||
span {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: #ccc;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
/* Toggle handle (slider) */
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0.125rem;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* Checked state - track */
|
||||
input:checked + span {
|
||||
background-color: #21ad0e;
|
||||
border-color: #148605;
|
||||
|
||||
/* Checked state - handle */
|
||||
&::before {
|
||||
transform: translateX(1rem) translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hover states */
|
||||
&:hover span {
|
||||
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
|
||||
input:checked:hover + span {
|
||||
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
&:has(input:disabled) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input:disabled + span {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input:disabled:checked + span {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
input:disabled ~ span:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&[data-disabled="true"] {
|
||||
td[data-slot="model-name"] {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
[data-slot="models-table-element"] {
|
||||
th,
|
||||
td {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
th {
|
||||
&:nth-child(2)
|
||||
|
||||
/* Training Data */ {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
&:nth-child(2)
|
||||
|
||||
/* Training Data */ {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Model } from "@opencode-ai/console-core/model.js"
|
||||
import { query, action, useParams, createAsync, json } from "@solidjs/router"
|
||||
import { createMemo, For, Show } from "solid-js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { ZenModel } from "@opencode-ai/console-core/model.js"
|
||||
import styles from "./model-section.module.css"
|
||||
import { querySessionInfo } from "../common"
|
||||
import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon"
|
||||
|
||||
const getModelLab = (modelId: string) => {
|
||||
if (modelId.startsWith("claude")) return "Anthropic"
|
||||
if (modelId.startsWith("gpt")) return "OpenAI"
|
||||
if (modelId.startsWith("kimi")) return "Moonshot AI"
|
||||
if (modelId.startsWith("glm")) return "Z.ai"
|
||||
if (modelId.startsWith("qwen")) return "Alibaba"
|
||||
if (modelId.startsWith("grok")) return "xAI"
|
||||
return "Stealth"
|
||||
}
|
||||
|
||||
const getModelsInfo = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
return {
|
||||
all: Object.entries(ZenModel.list())
|
||||
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
|
||||
.filter(([id, _model]) => !id.startsWith("an-"))
|
||||
.sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name))
|
||||
.map(([id, model]) => ({ id, name: model.name })),
|
||||
disabled: await Model.listDisabled(),
|
||||
}
|
||||
}, workspaceID)
|
||||
}, "model.info")
|
||||
|
||||
const updateModel = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const model = form.get("model")?.toString()
|
||||
if (!model) return { error: "Model is required" }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
const enabled = form.get("enabled")?.toString() === "true"
|
||||
return json(
|
||||
withActor(async () => {
|
||||
if (enabled) {
|
||||
await Model.disable({ model })
|
||||
} else {
|
||||
await Model.enable({ model })
|
||||
}
|
||||
}, workspaceID),
|
||||
{ revalidate: getModelsInfo.key },
|
||||
)
|
||||
}, "model.toggle")
|
||||
|
||||
export function ModelSection() {
|
||||
const params = useParams()
|
||||
const modelsInfo = createAsync(() => getModelsInfo(params.id))
|
||||
const userInfo = createAsync(() => querySessionInfo(params.id))
|
||||
|
||||
const modelsWithLab = createMemo(() => {
|
||||
const info = modelsInfo()
|
||||
if (!info) return []
|
||||
return info.all.map((model) => ({
|
||||
...model,
|
||||
lab: getModelLab(model.id),
|
||||
}))
|
||||
})
|
||||
return (
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>Models</h2>
|
||||
<p>
|
||||
Manage which models workspace members can access. <a href="/docs/zen#pricing ">Learn more</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div data-slot="models-list">
|
||||
<Show when={modelsInfo()}>
|
||||
<div data-slot="models-table">
|
||||
<table data-slot="models-table-element">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Model</th>
|
||||
<th></th>
|
||||
<th>Enabled</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={modelsWithLab()}>
|
||||
{({ id, name, lab }) => {
|
||||
const isEnabled = createMemo(() => !modelsInfo()!.disabled.includes(id))
|
||||
return (
|
||||
<tr data-slot="model-row" data-disabled={!isEnabled()}>
|
||||
<td data-slot="model-name">
|
||||
<div>
|
||||
{(() => {
|
||||
switch (lab) {
|
||||
case "OpenAI":
|
||||
return <IconOpenAI width={16} height={16} />
|
||||
case "Anthropic":
|
||||
return <IconAnthropic width={16} height={16} />
|
||||
case "Moonshot AI":
|
||||
return <IconMoonshotAI width={16} height={16} />
|
||||
case "Z.ai":
|
||||
return <IconZai width={16} height={16} />
|
||||
case "Alibaba":
|
||||
return <IconAlibaba width={16} height={16} />
|
||||
case "xAI":
|
||||
return <IconXai width={16} height={16} />
|
||||
default:
|
||||
return <IconStealth width={16} height={16} />
|
||||
}
|
||||
})()}
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td data-slot="model-lab">{lab}</td>
|
||||
<td data-slot="model-toggle">
|
||||
<form action={updateModel} method="post">
|
||||
<input type="hidden" name="model" value={id} />
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
<input type="hidden" name="enabled" value={isEnabled().toString()} />
|
||||
<label data-slot="model-toggle-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isEnabled()}
|
||||
disabled={!userInfo()?.isAdmin}
|
||||
onChange={(e) => {
|
||||
const form = e.currentTarget.closest("form")
|
||||
if (form) form.requestSubmit()
|
||||
}}
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
+1
-21
@@ -53,26 +53,6 @@
|
||||
flex-direction: column;
|
||||
gap: var(--space-6);
|
||||
|
||||
[data-slot="section-title"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.03125rem;
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="key-display"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -160,4 +140,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-3
@@ -28,7 +28,14 @@ export function NewUserSection() {
|
||||
const usageList = usage()
|
||||
return keysList?.length === 1 && (!usageList || usageList.length === 0)
|
||||
})
|
||||
const defaultKey = createMemo(() => keys()?.at(-1)?.key)
|
||||
const defaultKey = createMemo(() => {
|
||||
const key = keys()?.at(-1)?.key
|
||||
if (!key) return undefined
|
||||
return {
|
||||
actual: key,
|
||||
masked: key.slice(0, 8) + "*".repeat(key.length - 12) + key.slice(-4),
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={isNew()}>
|
||||
@@ -52,12 +59,12 @@ export function NewUserSection() {
|
||||
<Show when={defaultKey()}>
|
||||
<div data-slot="key-display">
|
||||
<div data-slot="key-container">
|
||||
<code data-slot="key-value">{defaultKey()}</code>
|
||||
<code data-slot="key-value">{defaultKey()?.masked}</code>
|
||||
<button
|
||||
data-color="primary"
|
||||
disabled={copiedKey()}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(defaultKey() ?? "")
|
||||
await navigator.clipboard.writeText(defaultKey()?.actual ?? "")
|
||||
setCopiedKey(true)
|
||||
setTimeout(() => setCopiedKey(false), 2000)
|
||||
}}
|
||||
@@ -0,0 +1,139 @@
|
||||
.root {
|
||||
[data-slot="providers-table"] {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
[data-slot="providers-table-element"] {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
thead {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-mono);
|
||||
|
||||
&[data-slot="provider-name"] {
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&[data-slot="provider-key"] {
|
||||
text-align: left;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
[data-slot="edit-form"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
max-width: 100%;
|
||||
|
||||
[data-slot="input-wrapper"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
max-width: 100%;
|
||||
|
||||
input {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-mono);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="form-error"] {
|
||||
color: var(--color-danger);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-slot="provider-action"] {
|
||||
text-align: left;
|
||||
font-family: var(--font-sans);
|
||||
white-space: nowrap;
|
||||
|
||||
[data-slot="configured-actions"] {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
|
||||
[data-slot="delete-form"] {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover [data-slot="delete-form"] {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="form-actions"] {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
&:hover {
|
||||
[data-slot="provider-action"] [data-slot="delete-form"] {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
|
||||
import { createEffect, For, Show } from "solid-js"
|
||||
import { Provider } from "@opencode-ai/console-core/provider.js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { createStore } from "solid-js/store"
|
||||
import styles from "./provider-section.module.css"
|
||||
|
||||
const PROVIDERS = [
|
||||
{ name: "OpenAI", key: "openai", prefix: "sk-" },
|
||||
{ name: "Anthropic", key: "anthropic", prefix: "sk-ant-" },
|
||||
] as const
|
||||
|
||||
type Provider = (typeof PROVIDERS)[number]
|
||||
|
||||
function maskCredentials(credentials: string) {
|
||||
return `${credentials.slice(0, 8)}...${credentials.slice(-8)}`
|
||||
}
|
||||
|
||||
const removeProvider = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const provider = form.get("provider")?.toString()
|
||||
if (!provider) return { error: "Provider is required" }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
return json(await withActor(() => Provider.remove({ provider }), workspaceID), { revalidate: listProviders.key })
|
||||
}, "provider.remove")
|
||||
|
||||
const saveProvider = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const provider = form.get("provider")?.toString()
|
||||
const credentials = form.get("credentials")?.toString()
|
||||
if (!provider) return { error: "Provider is required" }
|
||||
if (!credentials) return { error: "API key is required" }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
return json(
|
||||
await withActor(
|
||||
() =>
|
||||
Provider.create({ provider, credentials })
|
||||
.then(() => ({ error: undefined }))
|
||||
.catch((e) => ({ error: e.message as string })),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: listProviders.key },
|
||||
)
|
||||
}, "provider.save")
|
||||
|
||||
const listProviders = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(() => Provider.list(), workspaceID)
|
||||
}, "provider.list")
|
||||
|
||||
function ProviderRow(props: { provider: Provider }) {
|
||||
const params = useParams()
|
||||
const providers = createAsync(() => listProviders(params.id))
|
||||
const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key)
|
||||
const removeSubmission = useSubmission(
|
||||
removeProvider,
|
||||
([fd]) => fd.get("provider")?.toString() === props.provider.key,
|
||||
)
|
||||
const [store, setStore] = createStore({ editing: false })
|
||||
|
||||
let input: HTMLInputElement
|
||||
|
||||
const providerData = () => providers()?.find((p) => p.provider === props.provider.key)
|
||||
|
||||
createEffect(() => {
|
||||
if (!saveSubmission.pending && saveSubmission.result && !saveSubmission.result.error) {
|
||||
hide()
|
||||
}
|
||||
})
|
||||
|
||||
function show() {
|
||||
while (true) {
|
||||
saveSubmission.clear()
|
||||
if (!saveSubmission.result) break
|
||||
}
|
||||
setStore("editing", true)
|
||||
setTimeout(() => input?.focus(), 0)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
setStore("editing", false)
|
||||
}
|
||||
|
||||
return (
|
||||
<tr data-slot="provider-row">
|
||||
<td data-slot="provider-name">{props.provider.name}</td>
|
||||
<td data-slot="provider-key">
|
||||
<Show
|
||||
when={store.editing}
|
||||
fallback={<span>{providerData() ? maskCredentials(providerData()!.credentials) : "-"}</span>}
|
||||
>
|
||||
<form id={`provider-form-${props.provider.key}`} action={saveProvider} method="post" data-slot="edit-form">
|
||||
<div data-slot="input-wrapper">
|
||||
<input
|
||||
ref={(r) => (input = r)}
|
||||
name="credentials"
|
||||
type="text"
|
||||
placeholder={`Enter ${props.provider.name} API key (${props.provider.prefix}...)`}
|
||||
autocomplete="off"
|
||||
data-form-type="other"
|
||||
data-lpignore="true"
|
||||
/>
|
||||
<Show when={saveSubmission.result && saveSubmission.result.error}>
|
||||
{(err) => <div data-slot="form-error">{err()}</div>}
|
||||
</Show>
|
||||
</div>
|
||||
<input type="hidden" name="provider" value={props.provider.key} />
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
</form>
|
||||
</Show>
|
||||
</td>
|
||||
<td data-slot="provider-action">
|
||||
<Show
|
||||
when={store.editing}
|
||||
fallback={
|
||||
<Show
|
||||
when={!!providerData()}
|
||||
fallback={
|
||||
<button data-color="ghost" onClick={() => show()}>
|
||||
Configure
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div data-slot="configured-actions">
|
||||
<button data-color="ghost" onClick={() => show()}>
|
||||
Edit
|
||||
</button>
|
||||
<form action={removeProvider} method="post" data-slot="delete-form">
|
||||
<input type="hidden" name="provider" value={props.provider.key} />
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
<button data-color="ghost" type="submit" disabled={removeSubmission.pending}>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<div data-slot="form-actions">
|
||||
<button
|
||||
type="submit"
|
||||
data-color="ghost"
|
||||
disabled={saveSubmission.pending}
|
||||
form={`provider-form-${props.provider.key}`}
|
||||
>
|
||||
{saveSubmission.pending ? "Saving..." : "Save"}
|
||||
</button>
|
||||
<Show when={!saveSubmission.pending}>
|
||||
<button type="reset" data-color="ghost" onClick={() => hide()}>
|
||||
Cancel
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProviderSection() {
|
||||
return (
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>Bring Your Own Key</h2>
|
||||
<p>Configure your own API keys from AI providers.</p>
|
||||
</div>
|
||||
<div data-slot="providers-table">
|
||||
<table data-slot="providers-table-element">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Provider</th>
|
||||
<th>API Key</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={PROVIDERS}>{(provider) => <ProviderRow provider={provider} />}</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { SettingsSection } from "./settings-section"
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<div data-page="workspace-[id]">
|
||||
<div data-slot="sections">
|
||||
<SettingsSection />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+43
-44
@@ -1,63 +1,61 @@
|
||||
.root {
|
||||
[data-slot="section-content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
max-width: 40rem;
|
||||
|
||||
[data-slot="setting"] {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="setting-info"] {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
gap: var(--space-3);
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 500;
|
||||
p {
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
[data-slot="value-with-action"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="current-value"] {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text);
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
>button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="create-form"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
min-width: 15rem;
|
||||
width: fit-content;
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
gap: var(--space-2);
|
||||
|
||||
[data-slot="input-container"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
button {
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
@@ -68,11 +66,13 @@
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-mono);
|
||||
line-height: 1.5;
|
||||
min-width: 0;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 3px var(--color-accent-alpha);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
@@ -80,16 +80,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="form-actions"] {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
justify-content: flex-end;
|
||||
>button[type="reset"] {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
[data-slot="form-error"] {
|
||||
color: var(--color-danger);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.4;
|
||||
margin-top: calc(var(--space-1) * -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-16
@@ -79,10 +79,7 @@ export function SettingsSection() {
|
||||
</div>
|
||||
<div data-slot="section-content">
|
||||
<div data-slot="setting">
|
||||
<div data-slot="setting-info">
|
||||
<h3>Workspace Name</h3>
|
||||
<p data-slot="current-value">{workspaceInfo()?.name}</p>
|
||||
</div>
|
||||
<p>Workspace name</p>
|
||||
<Show
|
||||
when={!store.show}
|
||||
fallback={
|
||||
@@ -97,25 +94,26 @@ export function SettingsSection() {
|
||||
placeholder="Workspace name"
|
||||
value={workspaceInfo()?.name ?? "Default"}
|
||||
/>
|
||||
<Show when={submission.result && submission.result.error}>
|
||||
{(err) => <div data-slot="form-error">{err()}</div>}
|
||||
</Show>
|
||||
</div>
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
<div data-slot="form-actions">
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||
{submission.pending ? "Updating..." : "Save"}
|
||||
</button>
|
||||
<button type="reset" data-color="ghost" onClick={() => hide()}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||
{submission.pending ? "Updating..." : "Update"}
|
||||
</button>
|
||||
</div>
|
||||
<Show when={submission.result && submission.result.error}>
|
||||
{(err) => <div data-slot="form-error">{err()}</div>}
|
||||
</Show>
|
||||
</form>
|
||||
}
|
||||
>
|
||||
<button data-color="primary" onClick={() => show()}>
|
||||
Edit Name
|
||||
</button>
|
||||
<div data-slot="value-with-action">
|
||||
<p data-slot="current-value">{workspaceInfo()?.name}</p>
|
||||
<button data-color="primary" onClick={() => show()}>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import { query, useParams, createAsync } from "@solidjs/router"
|
||||
import { createMemo, For, Show } from "solid-js"
|
||||
import { formatDateUTC, formatDateForTable } from "./common"
|
||||
import { formatDateUTC, formatDateForTable } from "../common"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import styles from "./usage-section.module.css"
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { action, query } from "@solidjs/router"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
|
||||
export function formatDateForTable(date: Date) {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
day: "numeric",
|
||||
@@ -23,3 +29,35 @@ export function formatDateUTC(date: Date) {
|
||||
}
|
||||
return date.toLocaleDateString("en-US", options)
|
||||
}
|
||||
|
||||
export const queryIsLoggedIn = query(async () => {
|
||||
"use server"
|
||||
return withActor(() => {
|
||||
try {
|
||||
Actor.assert("account")
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}, "isLoggedIn.get")
|
||||
|
||||
export const querySessionInfo = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(() => {
|
||||
return {
|
||||
isAdmin: Actor.userRole() === "admin",
|
||||
isBeta: Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true,
|
||||
}
|
||||
}, workspaceID)
|
||||
}, "session.get")
|
||||
|
||||
export const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => {
|
||||
"use server"
|
||||
return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
|
||||
}, "checkoutUrl")
|
||||
|
||||
export const queryBillingInfo = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(() => Billing.get(), workspaceID)
|
||||
}, "billing.get")
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
|
||||
import { createEffect, createSignal, For, Show } from "solid-js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { createStore } from "solid-js/store"
|
||||
import styles from "./member-section.module.css"
|
||||
import { UserRole } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
|
||||
const listMembers = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
const actor = Actor.assert("user")
|
||||
return {
|
||||
members: await User.list(),
|
||||
currentUserID: actor.properties.userID,
|
||||
}
|
||||
}, workspaceID)
|
||||
}, "member.list")
|
||||
|
||||
const inviteMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const email = form.get("email")?.toString().trim()
|
||||
if (!email) return { error: "Email is required" }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
||||
if (!role) return { error: "Role is required" }
|
||||
return json(
|
||||
await withActor(
|
||||
() =>
|
||||
User.invite({ email, role })
|
||||
.then((data) => ({ error: undefined, data }))
|
||||
.catch((e) => ({ error: e.message as string })),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: listMembers.key },
|
||||
)
|
||||
}, "member.create")
|
||||
|
||||
const removeMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const id = form.get("id")?.toString()
|
||||
if (!id) return { error: "ID is required" }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
return json(
|
||||
await withActor(
|
||||
() =>
|
||||
User.remove(id)
|
||||
.then((data) => ({ error: undefined, data }))
|
||||
.catch((e) => ({ error: e.message as string })),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: listMembers.key },
|
||||
)
|
||||
}, "member.remove")
|
||||
|
||||
const updateMember = action(async (form: FormData) => {
|
||||
"use server"
|
||||
|
||||
const id = form.get("id")?.toString()
|
||||
if (!id) return { error: "ID is required" }
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
||||
if (!role) return { error: "Role is required" }
|
||||
const limit = form.get("limit")?.toString()
|
||||
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
||||
if (monthlyLimit !== null && monthlyLimit < 0) return { error: "Set a valid monthly limit" }
|
||||
|
||||
return json(
|
||||
await withActor(
|
||||
() =>
|
||||
User.update({ id, role, monthlyLimit })
|
||||
.then((data) => ({ error: undefined, data }))
|
||||
.catch((e) => ({ error: e.message as string })),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: listMembers.key },
|
||||
)
|
||||
}, "member.update")
|
||||
|
||||
export function MemberCreateForm() {
|
||||
const params = useParams()
|
||||
const submission = useSubmission(inviteMember)
|
||||
const [store, setStore] = createStore({ show: false })
|
||||
|
||||
let input: HTMLInputElement
|
||||
|
||||
createEffect(() => {
|
||||
if (!submission.pending && submission.result && !submission.result.error) {
|
||||
hide()
|
||||
}
|
||||
})
|
||||
|
||||
function show() {
|
||||
// submission.clear() does not clear the result in some cases, ie.
|
||||
// 1. Create key with empty name => error shows
|
||||
// 2. Put in a key name and creates the key => form hides
|
||||
// 3. Click add key button again => form shows with the same error if
|
||||
// submission.clear() is called only once
|
||||
while (true) {
|
||||
submission.clear()
|
||||
if (!submission.result) break
|
||||
}
|
||||
setStore("show", true)
|
||||
input.focus()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
setStore("show", false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={store.show}
|
||||
fallback={
|
||||
<button data-color="primary" onClick={() => show()}>
|
||||
Invite Member
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<form action={inviteMember} method="post" data-slot="create-form">
|
||||
<div data-slot="input-container">
|
||||
<input ref={(r) => (input = r)} data-component="input" name="email" type="text" placeholder="Enter email" />
|
||||
<div data-slot="role-selector">
|
||||
<label>
|
||||
<input type="radio" name="role" value="admin" checked />
|
||||
<div>
|
||||
<strong>Admin</strong>
|
||||
<p>Can manage models, members, and billing</p>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="role" value="member" />
|
||||
<div>
|
||||
<strong>Member</strong>
|
||||
<p>Can only generate API keys for themselves</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<Show when={submission.result && submission.result.error}>
|
||||
{(err) => <div data-slot="form-error">{err()}</div>}
|
||||
</Show>
|
||||
</div>
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
<div data-slot="form-actions">
|
||||
<button type="reset" data-color="ghost" onClick={() => hide()}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||
{submission.pending ? "Inviting..." : "Invite"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function MemberRow(props: { member: any; workspaceID: string; currentUserID: string | null }) {
|
||||
const [editing, setEditing] = createSignal(false)
|
||||
const submission = useSubmission(updateMember)
|
||||
const isCurrentUser = () => props.currentUserID === props.member.id
|
||||
|
||||
createEffect(() => {
|
||||
if (!submission.pending && submission.result && !submission.result.error) {
|
||||
setEditing(false)
|
||||
}
|
||||
})
|
||||
|
||||
function getUsageDisplay() {
|
||||
const currentUsage = (() => {
|
||||
const dateLastUsed = props.member.timeMonthlyUsageUpdated
|
||||
if (!dateLastUsed) return 0
|
||||
|
||||
const current = new Date().toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
const lastUsed = dateLastUsed.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
if (current !== lastUsed) return 0
|
||||
return ((props.member.monthlyUsage ?? 0) / 100000000).toFixed(2)
|
||||
})()
|
||||
|
||||
const limit = props.member.monthlyLimit ? `$${props.member.monthlyLimit}` : "no limit"
|
||||
return `$${currentUsage} / ${limit}`
|
||||
}
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={editing()}
|
||||
fallback={
|
||||
<tr>
|
||||
<td data-slot="member-email">{props.member.accountEmail ?? props.member.email}</td>
|
||||
<td data-slot="member-role">{props.member.role}</td>
|
||||
<td data-slot="member-usage">{getUsageDisplay()}</td>
|
||||
<Show when={!props.member.timeSeen} fallback={<td data-slot="member-joined"></td>}>
|
||||
<td data-slot="member-joined">invited</td>
|
||||
</Show>
|
||||
<td data-slot="member-actions">
|
||||
<button data-color="ghost" onClick={() => setEditing(true)}>
|
||||
Edit
|
||||
</button>
|
||||
<Show when={!isCurrentUser()}>
|
||||
<form action={removeMember} method="post">
|
||||
<input type="hidden" name="id" value={props.member.id} />
|
||||
<input type="hidden" name="workspaceID" value={props.workspaceID} />
|
||||
<button data-color="ghost">Delete</button>
|
||||
</form>
|
||||
</Show>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
>
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<form action={updateMember} method="post">
|
||||
<div data-slot="edit-member-email">{props.member.accountEmail ?? props.member.email}</div>
|
||||
<input type="hidden" name="id" value={props.member.id} />
|
||||
<input type="hidden" name="workspaceID" value={props.workspaceID} />
|
||||
|
||||
<Show
|
||||
when={!isCurrentUser()}
|
||||
fallback={
|
||||
<>
|
||||
<div data-slot="current-user-role">Role: {props.member.role}</div>
|
||||
<input type="hidden" name="role" value={props.member.role} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div data-slot="role-selector">
|
||||
<label>
|
||||
<input type="radio" name="role" value="admin" checked={props.member.role === "admin"} />
|
||||
<div>
|
||||
<strong>Admin</strong>
|
||||
<p>Can manage models, members, and billing</p>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="role" value="member" checked={props.member.role === "member"} />
|
||||
<div>
|
||||
<strong>Member</strong>
|
||||
<p>Can only generate API keys for themselves</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div data-slot="limit-selector">
|
||||
<label>
|
||||
<strong>Monthly Limit</strong>
|
||||
<input
|
||||
type="number"
|
||||
name="limit"
|
||||
value={props.member.monthlyLimit ?? ""}
|
||||
placeholder="No limit"
|
||||
min="0"
|
||||
/>
|
||||
<p>Set a monthly spending limit for this user</p>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Show when={submission.result && submission.result.error}>
|
||||
{(err) => <div data-slot="form-error">{err()}</div>}
|
||||
</Show>
|
||||
|
||||
<div data-slot="form-actions">
|
||||
<button type="button" data-color="ghost" onClick={() => setEditing(false)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" data-color="primary" disabled={submission.pending}>
|
||||
{submission.pending ? "Saving..." : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
export function MemberSection() {
|
||||
const params = useParams()
|
||||
const data = createAsync(() => listMembers(params.id))
|
||||
|
||||
return (
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>Members</h2>
|
||||
<p>Manage your members for accessing opencode services.</p>
|
||||
</div>
|
||||
<MemberCreateForm />
|
||||
<div data-slot="members-table">
|
||||
<Show
|
||||
when={data()?.members.length}
|
||||
fallback={
|
||||
<div data-component="empty-state">
|
||||
<p>Invite a member to your workspace</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<table data-slot="members-table-element">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Usage</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={data()!.members}>
|
||||
{(member) => (
|
||||
<MemberRow member={member} workspaceID={params.id} currentUserID={data()!.currentUserID} />
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { ZenModel } from "@opencode-ai/console-core/model.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"
|
||||
|
||||
export async function handler(
|
||||
input: APIEvent,
|
||||
@@ -41,6 +43,7 @@ export async function handler(
|
||||
|
||||
const FREE_WORKSPACES = [
|
||||
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
|
||||
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
|
||||
]
|
||||
|
||||
const logger = {
|
||||
@@ -65,8 +68,10 @@ export async function handler(
|
||||
})
|
||||
const modelInfo = validateModel(body.model)
|
||||
const providerInfo = selectProvider(modelInfo)
|
||||
const authInfo = await authenticate(modelInfo)
|
||||
const authInfo = await authenticate(modelInfo, providerInfo)
|
||||
validateBilling(modelInfo, authInfo)
|
||||
validateModelSettings(authInfo)
|
||||
updateProviderKey(authInfo, providerInfo)
|
||||
logger.metric({ provider: providerInfo.id })
|
||||
|
||||
// Request to model provider
|
||||
@@ -222,14 +227,17 @@ export async function handler(
|
||||
return { id: modelId, ...modelData }
|
||||
}
|
||||
|
||||
function selectProvider(model: Model) {
|
||||
function selectProvider(model: Awaited<ReturnType<typeof validateModel>>) {
|
||||
const providers = model.providers
|
||||
.filter((provider) => !provider.disabled)
|
||||
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
|
||||
return providers[Math.floor(Math.random() * providers.length)]
|
||||
}
|
||||
|
||||
async function authenticate(model: Model) {
|
||||
async function authenticate(
|
||||
model: Awaited<ReturnType<typeof validateModel>>,
|
||||
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
|
||||
) {
|
||||
const apiKey = opts.parseApiKey(input.request.headers)
|
||||
if (!apiKey) {
|
||||
if (model.allowAnonymous) return
|
||||
@@ -241,20 +249,33 @@ export async function handler(
|
||||
.select({
|
||||
apiKey: KeyTable.id,
|
||||
workspaceID: KeyTable.workspaceID,
|
||||
balance: BillingTable.balance,
|
||||
paymentMethodID: BillingTable.paymentMethodID,
|
||||
monthlyLimit: BillingTable.monthlyLimit,
|
||||
monthlyUsage: BillingTable.monthlyUsage,
|
||||
timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
|
||||
userID: UserTable.id,
|
||||
userMonthlyLimit: UserTable.monthlyLimit,
|
||||
userMonthlyUsage: UserTable.monthlyUsage,
|
||||
timeUserMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated,
|
||||
billing: {
|
||||
balance: BillingTable.balance,
|
||||
paymentMethodID: BillingTable.paymentMethodID,
|
||||
monthlyLimit: BillingTable.monthlyLimit,
|
||||
monthlyUsage: BillingTable.monthlyUsage,
|
||||
timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
|
||||
},
|
||||
user: {
|
||||
id: UserTable.id,
|
||||
monthlyLimit: UserTable.monthlyLimit,
|
||||
monthlyUsage: UserTable.monthlyUsage,
|
||||
timeMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated,
|
||||
},
|
||||
provider: {
|
||||
credentials: ProviderTable.credentials,
|
||||
},
|
||||
timeDisabled: ModelTable.timeCreated,
|
||||
})
|
||||
.from(KeyTable)
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
|
||||
.innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID))
|
||||
.innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)))
|
||||
.leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, model.id)))
|
||||
.leftJoin(
|
||||
ProviderTable,
|
||||
and(eq(ProviderTable.workspaceID, KeyTable.workspaceID), eq(ProviderTable.provider, providerInfo.id)),
|
||||
)
|
||||
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
@@ -265,25 +286,14 @@ export async function handler(
|
||||
workspace: data.workspaceID,
|
||||
})
|
||||
|
||||
const isFree = FREE_WORKSPACES.includes(data.workspaceID)
|
||||
|
||||
return {
|
||||
apiKeyId: data.apiKey,
|
||||
workspaceID: data.workspaceID,
|
||||
billing: {
|
||||
paymentMethodID: data.paymentMethodID,
|
||||
balance: data.balance,
|
||||
monthlyLimit: data.monthlyLimit,
|
||||
monthlyUsage: data.monthlyUsage,
|
||||
timeMonthlyUsageUpdated: data.timeMonthlyUsageUpdated,
|
||||
},
|
||||
user: {
|
||||
id: data.userID,
|
||||
monthlyLimit: data.userMonthlyLimit,
|
||||
monthlyUsage: data.userMonthlyUsage,
|
||||
timeMonthlyUsageUpdated: data.timeUserMonthlyUsageUpdated,
|
||||
},
|
||||
isFree,
|
||||
billing: data.billing,
|
||||
user: data.user,
|
||||
provider: data.provider,
|
||||
isFree: FREE_WORKSPACES.includes(data.workspaceID),
|
||||
isDisabled: !!data.timeDisabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +335,20 @@ export async function handler(
|
||||
}
|
||||
}
|
||||
|
||||
function validateModelSettings(authInfo: Awaited<ReturnType<typeof authenticate>>) {
|
||||
if (!authInfo) return
|
||||
if (authInfo.isDisabled) throw new ModelError("Model is disabled")
|
||||
}
|
||||
|
||||
function updateProviderKey(
|
||||
authInfo: Awaited<ReturnType<typeof authenticate>>,
|
||||
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
|
||||
) {
|
||||
if (!authInfo) return
|
||||
if (!authInfo.provider?.credentials) return
|
||||
providerInfo.apiKey = authInfo.provider.credentials
|
||||
}
|
||||
|
||||
async function trackUsage(
|
||||
authInfo: Awaited<ReturnType<typeof authenticate>>,
|
||||
modelInfo: ReturnType<typeof validateModel>,
|
||||
@@ -387,7 +411,7 @@ export async function handler(
|
||||
|
||||
if (!authInfo) return
|
||||
|
||||
const cost = authInfo.isFree ? 0 : centsToMicroCents(totalCostInCent)
|
||||
const cost = authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
|
||||
await Database.transaction(async (tx) => {
|
||||
await tx.insert(UsageTable).values({
|
||||
workspaceID: authInfo.workspaceID,
|
||||
@@ -401,6 +425,7 @@ export async function handler(
|
||||
cacheWrite5mTokens,
|
||||
cacheWrite1hTokens,
|
||||
cost,
|
||||
keyID: authInfo.apiKeyId,
|
||||
})
|
||||
await tx
|
||||
.update(BillingTable)
|
||||
@@ -439,6 +464,8 @@ export async function handler(
|
||||
|
||||
async function reload(authInfo: Awaited<ReturnType<typeof authenticate>>) {
|
||||
if (!authInfo) return
|
||||
if (authInfo.isFree) return
|
||||
if (authInfo.provider?.credentials) return
|
||||
|
||||
const lock = await Database.use((tx) =>
|
||||
tx
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "./index.css"
|
||||
import { createAsync } from "@solidjs/router"
|
||||
import { Title, Meta, Link } from "@solidjs/meta"
|
||||
import { HttpHeader } from "@solidjs/start"
|
||||
import zenLogoLight from "../../asset/zen-ornate-light.svg"
|
||||
@@ -15,8 +16,10 @@ import { Faq } from "~/component/faq"
|
||||
import { Legal } from "~/component/legal"
|
||||
import { Footer } from "~/component/footer"
|
||||
import { Header } from "~/component/header"
|
||||
import { queryIsLoggedIn } from "~/routes/workspace/common"
|
||||
|
||||
export default function Home() {
|
||||
const isLoggedIn = createAsync(() => queryIsLoggedIn())
|
||||
return (
|
||||
<main data-page="zen">
|
||||
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
|
||||
@@ -102,7 +105,7 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
<a href="/auth">
|
||||
<span>Get started with Zen </span>
|
||||
<span>{isLoggedIn() ? "Go to workspace " : "Get started with Zen "}</span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE `model` (
|
||||
`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),
|
||||
`model` varchar(64) NOT NULL,
|
||||
CONSTRAINT `model_workspace_id_id_pk` PRIMARY KEY(`workspace_id`,`id`),
|
||||
CONSTRAINT `model_workspace_model` UNIQUE(`workspace_id`,`model`)
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE `provider` (
|
||||
`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),
|
||||
`provider` varchar(64) NOT NULL,
|
||||
`credentials` text NOT NULL,
|
||||
CONSTRAINT `provider_workspace_id_id_pk` PRIMARY KEY(`workspace_id`,`id`),
|
||||
CONSTRAINT `workspace_provider` UNIQUE(`workspace_id`,`provider`)
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `usage` ADD `key_id` varchar(30);
|
||||
@@ -0,0 +1,801 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "eae45fcf-dc0f-4756-bc5d-30791f2965a2",
|
||||
"prevId": "33551b4c-fc2e-4753-8d9d-0971f333e65d",
|
||||
"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
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"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_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_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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,879 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "9dceb591-8e08-4991-a49c-1f1741ec1e57",
|
||||
"prevId": "eae45fcf-dc0f-4756-bc5d-30791f2965a2",
|
||||
"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
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"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_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_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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,886 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "b2406421-f22d-4153-a2a4-6deafe70ee54",
|
||||
"prevId": "9dceb591-8e08-4991-a49c-1f1741ec1e57",
|
||||
"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
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"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_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_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": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +211,27 @@
|
||||
"when": 1759811835558,
|
||||
"tag": "0029_panoramic_harrier",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 30,
|
||||
"version": "5",
|
||||
"when": 1759878278492,
|
||||
"tag": "0030_ordinary_ultragirl",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 31,
|
||||
"version": "5",
|
||||
"when": 1759940238478,
|
||||
"tag": "0031_outgoing_outlaw_kid",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 32,
|
||||
"version": "5",
|
||||
"when": 1759976329502,
|
||||
"tag": "0032_white_doctor_doom",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "0.14.6",
|
||||
"version": "0.15.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@@ -14,7 +14,8 @@
|
||||
"drizzle-orm": "0.41.0",
|
||||
"postgres": "3.4.7",
|
||||
"stripe": "18.0.0",
|
||||
"ulid": "3.0.0"
|
||||
"ulid": "3.0.0",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"exports": {
|
||||
"./*": "./src/*"
|
||||
@@ -26,10 +27,16 @@
|
||||
"update-models": "script/update-models.ts",
|
||||
"promote-models-to-dev": "script/promote-models.ts dev",
|
||||
"promote-models-to-prod": "script/promote-models.ts production",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.0",
|
||||
"@types/node": "catalog:",
|
||||
"drizzle-kit": "0.30.5",
|
||||
"mysql2": "3.14.4"
|
||||
"mysql2": "3.14.4",
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Context } from "./context"
|
||||
import { UserRole } from "./schema/user.sql"
|
||||
import { Log } from "./util/log"
|
||||
|
||||
export namespace Actor {
|
||||
@@ -21,6 +22,7 @@ export namespace Actor {
|
||||
userID: string
|
||||
workspaceID: string
|
||||
accountID: string
|
||||
role: (typeof UserRole)[number]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +67,11 @@ export namespace Actor {
|
||||
return actor as Extract<Info, { type: T }>
|
||||
}
|
||||
|
||||
export const assertAdmin = () => {
|
||||
if (userRole() === "admin") return
|
||||
throw new Error(`Action not allowed. Ask your workspace admin to perform this action.`)
|
||||
}
|
||||
|
||||
export function workspace() {
|
||||
const actor = use()
|
||||
if ("workspaceID" in actor.properties) {
|
||||
@@ -80,4 +87,12 @@ export namespace Actor {
|
||||
}
|
||||
throw new Error(`actor of type "${actor.type}" is not associated with an account`)
|
||||
}
|
||||
|
||||
export function userID() {
|
||||
return Actor.assert("user").properties.userID
|
||||
}
|
||||
|
||||
export function userRole() {
|
||||
return Actor.assert("user").properties.role
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export namespace Billing {
|
||||
export const stripe = () =>
|
||||
new Stripe(Resource.STRIPE_SECRET_KEY.value, {
|
||||
apiVersion: "2025-03-31.basil",
|
||||
httpClient: Stripe.createFetchHttpClient(),
|
||||
})
|
||||
|
||||
export const get = async () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ export namespace Context {
|
||||
return result
|
||||
},
|
||||
provide<R>(value: T, fn: () => R) {
|
||||
return storage.run<R>(value, fn)
|
||||
return storage.run(value, fn)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ export namespace Identifier {
|
||||
account: "acc",
|
||||
billing: "bil",
|
||||
key: "key",
|
||||
model: "mod",
|
||||
payment: "pay",
|
||||
provider: "prv",
|
||||
usage: "usg",
|
||||
user: "usr",
|
||||
workspace: "wrk",
|
||||
|
||||
@@ -4,19 +4,43 @@ import { Actor } from "./actor"
|
||||
import { and, Database, eq, isNull, sql } from "./drizzle"
|
||||
import { Identifier } from "./identifier"
|
||||
import { KeyTable } from "./schema/key.sql"
|
||||
import { AccountTable } from "./schema/account.sql"
|
||||
import { UserTable } from "./schema/user.sql"
|
||||
import { User } from "./user"
|
||||
|
||||
export namespace Key {
|
||||
export const list = async () => {
|
||||
const workspace = Actor.workspace()
|
||||
export const list = fn(z.void(), async () => {
|
||||
const keys = await Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.select({
|
||||
id: KeyTable.id,
|
||||
name: KeyTable.name,
|
||||
key: KeyTable.key,
|
||||
timeUsed: KeyTable.timeUsed,
|
||||
userID: KeyTable.userID,
|
||||
email: AccountTable.email,
|
||||
})
|
||||
.from(KeyTable)
|
||||
.where(and(eq(KeyTable.workspaceID, workspace), isNull(KeyTable.timeDeleted)))
|
||||
.orderBy(sql`${KeyTable.timeCreated} DESC`),
|
||||
.innerJoin(UserTable, and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID)))
|
||||
.innerJoin(AccountTable, eq(UserTable.accountID, AccountTable.id))
|
||||
.where(
|
||||
and(
|
||||
...[
|
||||
eq(KeyTable.workspaceID, Actor.workspace()),
|
||||
isNull(KeyTable.timeDeleted),
|
||||
...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]),
|
||||
],
|
||||
),
|
||||
)
|
||||
.orderBy(sql`${KeyTable.name} DESC`),
|
||||
)
|
||||
return keys
|
||||
}
|
||||
// only return value for user's keys
|
||||
return keys.map((key) => ({
|
||||
...key,
|
||||
key: key.userID === Actor.userID() ? key.key : undefined,
|
||||
keyDisplay: `${key.key.slice(0, 7)}...${key.key.slice(-4)}`,
|
||||
}))
|
||||
})
|
||||
|
||||
export const create = fn(
|
||||
z.object({
|
||||
@@ -52,14 +76,22 @@ export namespace Key {
|
||||
)
|
||||
|
||||
export const remove = fn(z.object({ id: z.string() }), async (input) => {
|
||||
const workspace = Actor.workspace()
|
||||
await Database.transaction((tx) =>
|
||||
// only admin can remove other user's keys
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(KeyTable)
|
||||
.set({
|
||||
timeDeleted: sql`now()`,
|
||||
})
|
||||
.where(and(eq(KeyTable.id, input.id), eq(KeyTable.workspaceID, workspace))),
|
||||
.where(
|
||||
and(
|
||||
...[
|
||||
eq(KeyTable.id, input.id),
|
||||
eq(KeyTable.workspaceID, Actor.workspace()),
|
||||
...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { z } from "zod"
|
||||
import { eq, and } from "drizzle-orm"
|
||||
import { Database } from "./drizzle"
|
||||
import { ModelTable } from "./schema/model.sql"
|
||||
import { Identifier } from "./identifier"
|
||||
import { fn } from "./util/fn"
|
||||
import { Actor } from "./actor"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
|
||||
export namespace ZenModel {
|
||||
const ModelCostSchema = z.object({
|
||||
@@ -10,6 +17,7 @@ export namespace ZenModel {
|
||||
})
|
||||
|
||||
export const ModelSchema = z.object({
|
||||
name: z.string(),
|
||||
cost: ModelCostSchema,
|
||||
cost200K: ModelCostSchema.optional(),
|
||||
allowAnonymous: z.boolean().optional(),
|
||||
@@ -27,4 +35,60 @@ export namespace ZenModel {
|
||||
})
|
||||
|
||||
export const ModelsSchema = z.record(z.string(), ModelSchema)
|
||||
|
||||
export const list = fn(z.void(), () => ModelsSchema.parse(JSON.parse(Resource.ZEN_MODELS.value)))
|
||||
}
|
||||
|
||||
export namespace Model {
|
||||
export const enable = fn(z.object({ model: z.string() }), ({ model }) => {
|
||||
Actor.assertAdmin()
|
||||
return Database.use((db) =>
|
||||
db.delete(ModelTable).where(and(eq(ModelTable.workspaceID, Actor.workspace()), eq(ModelTable.model, model))),
|
||||
)
|
||||
})
|
||||
|
||||
export const disable = fn(z.object({ model: z.string() }), ({ model }) => {
|
||||
Actor.assertAdmin()
|
||||
return Database.use((db) =>
|
||||
db
|
||||
.insert(ModelTable)
|
||||
.values({
|
||||
id: Identifier.create("model"),
|
||||
workspaceID: Actor.workspace(),
|
||||
model: model,
|
||||
})
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
timeDeleted: null,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
export const listDisabled = fn(z.void(), () => {
|
||||
return Database.use((db) =>
|
||||
db
|
||||
.select({ model: ModelTable.model })
|
||||
.from(ModelTable)
|
||||
.where(eq(ModelTable.workspaceID, Actor.workspace()))
|
||||
.then((rows) => rows.map((row) => row.model)),
|
||||
)
|
||||
})
|
||||
|
||||
export const isDisabled = fn(
|
||||
z.object({
|
||||
model: z.string(),
|
||||
}),
|
||||
({ model }) => {
|
||||
return Database.use(async (db) => {
|
||||
const result = await db
|
||||
.select()
|
||||
.from(ModelTable)
|
||||
.where(and(eq(ModelTable.workspaceID, Actor.workspace()), eq(ModelTable.model, model)))
|
||||
.limit(1)
|
||||
|
||||
return result.length > 0
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { z } from "zod"
|
||||
import { fn } from "./util/fn"
|
||||
import { Actor } from "./actor"
|
||||
import { and, Database, eq, isNull } from "./drizzle"
|
||||
import { Identifier } from "./identifier"
|
||||
import { ProviderTable } from "./schema/provider.sql"
|
||||
|
||||
export namespace Provider {
|
||||
export const list = fn(z.void(), () =>
|
||||
Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(ProviderTable)
|
||||
.where(and(eq(ProviderTable.workspaceID, Actor.workspace()), isNull(ProviderTable.timeDeleted))),
|
||||
),
|
||||
)
|
||||
|
||||
export const create = fn(
|
||||
z.object({
|
||||
provider: z.string().min(1).max(64),
|
||||
credentials: z.string(),
|
||||
}),
|
||||
async ({ provider, credentials }) => {
|
||||
Actor.assertAdmin()
|
||||
return Database.use((tx) =>
|
||||
tx
|
||||
.insert(ProviderTable)
|
||||
.values({
|
||||
id: Identifier.create("provider"),
|
||||
workspaceID: Actor.workspace(),
|
||||
provider,
|
||||
credentials,
|
||||
})
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
credentials,
|
||||
timeDeleted: null,
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const remove = fn(
|
||||
z.object({
|
||||
provider: z.string(),
|
||||
}),
|
||||
async ({ provider }) => {
|
||||
Actor.assertAdmin()
|
||||
return Database.transaction((tx) =>
|
||||
tx
|
||||
.delete(ProviderTable)
|
||||
.where(and(eq(ProviderTable.provider, provider), eq(ProviderTable.workspaceID, Actor.workspace()))),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { bigint, boolean, int, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
export const BillingTable = mysqlTable(
|
||||
@@ -50,6 +50,7 @@ export const UsageTable = mysqlTable(
|
||||
cacheWrite5mTokens: int("cache_write_5m_tokens"),
|
||||
cacheWrite1hTokens: int("cache_write_1h_tokens"),
|
||||
cost: bigint("cost", { mode: "number" }).notNull(),
|
||||
keyID: ulid("key_id"),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table)],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { mysqlTable, varchar, uniqueIndex } from "drizzle-orm/mysql-core"
|
||||
import { timestamps, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
export const ModelTable = mysqlTable(
|
||||
"model",
|
||||
{
|
||||
...workspaceColumns,
|
||||
...timestamps,
|
||||
model: varchar("model", { length: 64 }).notNull(),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table), uniqueIndex("model_workspace_model").on(table.workspaceID, table.model)],
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
import { mysqlTable, text, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { timestamps, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
export const ProviderTable = mysqlTable(
|
||||
"provider",
|
||||
{
|
||||
...workspaceColumns,
|
||||
...timestamps,
|
||||
provider: varchar("provider", { length: 64 }).notNull(),
|
||||
credentials: text("credentials").notNull(),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table), uniqueIndex("workspace_provider").on(table.workspaceID, table.provider)],
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod"
|
||||
import { and, eq, getTableColumns, inArray, isNull, or, sql } from "drizzle-orm"
|
||||
import { and, eq, getTableColumns, isNull, sql } from "drizzle-orm"
|
||||
import { fn } from "./util/fn"
|
||||
import { Database } from "./drizzle"
|
||||
import { UserRole, UserTable } from "./schema/user.sql"
|
||||
@@ -11,21 +11,12 @@ import { Account } from "./account"
|
||||
import { AccountTable } from "./schema/account.sql"
|
||||
import { Key } from "./key"
|
||||
import { KeyTable } from "./schema/key.sql"
|
||||
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||
|
||||
export namespace User {
|
||||
const assertAdmin = async () => {
|
||||
const actor = Actor.assert("user")
|
||||
const user = await User.fromID(actor.properties.userID)
|
||||
if (user?.role !== "admin") {
|
||||
throw new Error(`Expected admin user, got ${user?.role}`)
|
||||
}
|
||||
}
|
||||
|
||||
const assertNotSelf = (id: string) => {
|
||||
const actor = Actor.assert("user")
|
||||
if (actor.properties.userID === id) {
|
||||
throw new Error(`Expected not self actor, got self actor`)
|
||||
}
|
||||
if (Actor.userID() !== id) return
|
||||
throw new Error(`Expected not self actor, got self actor`)
|
||||
}
|
||||
|
||||
export const list = fn(z.void(), () =>
|
||||
@@ -68,9 +59,10 @@ export namespace User {
|
||||
z.object({
|
||||
email: z.string(),
|
||||
role: z.enum(UserRole),
|
||||
monthlyLimit: z.number().nullable().optional(),
|
||||
}),
|
||||
async ({ email, role }) => {
|
||||
await assertAdmin()
|
||||
async ({ email, role, monthlyLimit }) => {
|
||||
Actor.assertAdmin()
|
||||
const workspaceID = Actor.workspace()
|
||||
|
||||
// create user
|
||||
@@ -90,10 +82,12 @@ export namespace User {
|
||||
}),
|
||||
workspaceID,
|
||||
role,
|
||||
monthlyLimit,
|
||||
})
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
role,
|
||||
monthlyLimit,
|
||||
timeDeleted: null,
|
||||
},
|
||||
}),
|
||||
@@ -122,15 +116,32 @@ export namespace User {
|
||||
|
||||
// send email, ignore errors
|
||||
try {
|
||||
const emailInfo = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
email: AccountTable.email,
|
||||
workspaceName: WorkspaceTable.name,
|
||||
})
|
||||
.from(UserTable)
|
||||
.innerJoin(AccountTable, eq(UserTable.accountID, AccountTable.id))
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, workspaceID))
|
||||
.where(
|
||||
and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, Actor.assert("user").properties.userID)),
|
||||
)
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
const { InviteEmail } = await import("@opencode-ai/console-mail/InviteEmail.jsx")
|
||||
await AWS.sendEmail({
|
||||
to: email,
|
||||
subject: `You've been invited to join the ${workspaceID} workspace on OpenCode Zen`,
|
||||
subject: `You've been invited to join the ${emailInfo.workspaceName} workspace on OpenCode`,
|
||||
body: render(
|
||||
// @ts-ignore
|
||||
InviteEmail({
|
||||
inviter: emailInfo.email,
|
||||
assetsUrl: `https://opencode.ai/email`,
|
||||
workspace: workspaceID,
|
||||
workspaceID: workspaceID,
|
||||
workspaceName: emailInfo.workspaceName,
|
||||
}),
|
||||
),
|
||||
})
|
||||
@@ -181,7 +192,7 @@ export namespace User {
|
||||
monthlyLimit: z.number().nullable(),
|
||||
}),
|
||||
async ({ id, role, monthlyLimit }) => {
|
||||
await assertAdmin()
|
||||
Actor.assertAdmin()
|
||||
if (role === "member") assertNotSelf(id)
|
||||
return await Database.use((tx) =>
|
||||
tx
|
||||
@@ -193,7 +204,7 @@ export namespace User {
|
||||
)
|
||||
|
||||
export const remove = fn(z.string(), async (id) => {
|
||||
await assertAdmin()
|
||||
Actor.assertAdmin()
|
||||
assertNotSelf(id)
|
||||
|
||||
return await Database.use((tx) =>
|
||||
|
||||
@@ -52,6 +52,7 @@ export namespace Workspace {
|
||||
name: z.string().min(1).max(255),
|
||||
}),
|
||||
async ({ name }) => {
|
||||
Actor.assertAdmin()
|
||||
const workspaceID = Actor.workspace()
|
||||
return await Database.use((tx) =>
|
||||
tx
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "0.14.6",
|
||||
"version": "0.15.4",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "4.20250522.0",
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/node": "catalog:",
|
||||
"openai": "5.11.0",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
"@ai-sdk/openai-compatible": "1.0.1",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
"@opencode-ai/console-resource": "workspace:*",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"ai": "catalog:",
|
||||
"hono": "catalog:",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { KVNamespace } from "@cloudflare/workers-types"
|
||||
import { z } from "zod"
|
||||
import { issuer } from "@openauthjs/openauth"
|
||||
import type { Theme } from "@openauthjs/openauth/ui/theme"
|
||||
@@ -94,6 +95,7 @@ export default {
|
||||
// }),
|
||||
},
|
||||
storage: CloudflareStorage({
|
||||
// @ts-ignore
|
||||
namespace: env.AuthStorage,
|
||||
}),
|
||||
subjects,
|
||||
@@ -118,6 +120,10 @@ export default {
|
||||
|
||||
if (!email) throw new Error("No email found")
|
||||
|
||||
if (Resource.App.stage !== "production" && !email.endsWith("@anoma.ly")) {
|
||||
throw new Error("Invalid email")
|
||||
}
|
||||
|
||||
let accountID = await Account.fromEmail(email).then((x) => x?.id)
|
||||
if (!accountID) {
|
||||
console.log("creating account for", email)
|
||||
|
||||
@@ -1,26 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import React from "react"
|
||||
import { Font, Hr as JEHr, Text as JEText, type HrProps, type TextProps } from "@jsx-email/all"
|
||||
import { DIVIDER_COLOR, SURFACE_DIVIDER_COLOR, textColor } from "./styles"
|
||||
import { Font, Text as JEText, type TextProps } from "@jsx-email/all"
|
||||
import { baseText } from "./styles"
|
||||
|
||||
export function Text(props: TextProps) {
|
||||
return <JEText {...props} style={{ ...textColor, ...props.style }} />
|
||||
}
|
||||
|
||||
export function Hr(props: HrProps) {
|
||||
return <JEHr {...props} style={{ borderTop: `1px solid ${DIVIDER_COLOR}`, ...props.style }} />
|
||||
}
|
||||
|
||||
export function SurfaceHr(props: HrProps) {
|
||||
return (
|
||||
<JEHr
|
||||
{...props}
|
||||
style={{
|
||||
borderTop: `1px solid ${SURFACE_DIVIDER_COLOR}`,
|
||||
...props.style,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
return <JEText {...props} style={{ ...baseText, ...props.style }} />
|
||||
}
|
||||
|
||||
export function Title({ children }: TitleProps) {
|
||||
@@ -43,45 +27,25 @@ export function Fonts({ assetsUrl }: { assetsUrl: string }) {
|
||||
return (
|
||||
<>
|
||||
<Font
|
||||
fontFamily="IBM Plex Mono"
|
||||
fontFamily="JetBrains Mono"
|
||||
fallbackFontFamily="monospace"
|
||||
webFont={{
|
||||
url: `${assetsUrl}/ibm-plex-mono-latin-400.woff2`,
|
||||
url: `${assetsUrl}/JetBrainsMono-Regular.woff2`,
|
||||
format: "woff2",
|
||||
}}
|
||||
fontWeight="400"
|
||||
fontStyle="normal"
|
||||
/>
|
||||
<Font
|
||||
fontFamily="IBM Plex Mono"
|
||||
fontFamily="JetBrains Mono"
|
||||
fallbackFontFamily="monospace"
|
||||
webFont={{
|
||||
url: `${assetsUrl}/ibm-plex-mono-latin-500.woff2`,
|
||||
url: `${assetsUrl}/JetBrainsMono-Medium.woff2`,
|
||||
format: "woff2",
|
||||
}}
|
||||
fontWeight="500"
|
||||
fontStyle="normal"
|
||||
/>
|
||||
<Font
|
||||
fontFamily="IBM Plex Mono"
|
||||
fallbackFontFamily="monospace"
|
||||
webFont={{
|
||||
url: `${assetsUrl}/ibm-plex-mono-latin-600.woff2`,
|
||||
format: "woff2",
|
||||
}}
|
||||
fontWeight="600"
|
||||
fontStyle="normal"
|
||||
/>
|
||||
<Font
|
||||
fontFamily="IBM Plex Mono"
|
||||
fallbackFontFamily="monospace"
|
||||
webFont={{
|
||||
url: `${assetsUrl}/ibm-plex-mono-latin-700.woff2`,
|
||||
format: "woff2",
|
||||
}}
|
||||
fontWeight="700"
|
||||
fontStyle="normal"
|
||||
/>
|
||||
<Font
|
||||
fontFamily="Rubik"
|
||||
fallbackFontFamily={["Helvetica", "Arial", "sans-serif"]}
|
||||
|
||||
@@ -1,110 +1,91 @@
|
||||
export const unit = 16;
|
||||
|
||||
export const GREY_COLOR = [
|
||||
"#1A1A2E", //0
|
||||
"#2F2F41", //1
|
||||
"#444454", //2
|
||||
"#585867", //3
|
||||
"#6D6D7A", //4
|
||||
"#82828D", //5
|
||||
"#9797A0", //6
|
||||
"#ACACB3", //7
|
||||
"#C1C1C6", //8
|
||||
"#D5D5D9", //9
|
||||
"#EAEAEC", //10
|
||||
"#FFFFFF", //11
|
||||
];
|
||||
|
||||
export const BLUE_COLOR = "#395C6B";
|
||||
export const DANGER_COLOR = "#ED322C";
|
||||
export const TEXT_COLOR = GREY_COLOR[0];
|
||||
export const SECONDARY_COLOR = GREY_COLOR[5];
|
||||
export const DIMMED_COLOR = GREY_COLOR[7];
|
||||
export const DIVIDER_COLOR = GREY_COLOR[10];
|
||||
export const BACKGROUND_COLOR = "#F0F0F1";
|
||||
export const SURFACE_COLOR = DIVIDER_COLOR;
|
||||
export const SURFACE_DIVIDER_COLOR = GREY_COLOR[9];
|
||||
// @ts-nocheck
|
||||
export const unit = 12
|
||||
export const PRIMARY_COLOR = "#211E1E"
|
||||
export const TEXT_COLOR = "#656363"
|
||||
export const LINK_COLOR = "#007AFF"
|
||||
export const LINK_BACKGROUND_COLOR = "#F9F8F8"
|
||||
export const BACKGROUND_COLOR = "#F0F0F1"
|
||||
export const SURFACE_DIVIDER_COLOR = "#D5D5D9"
|
||||
|
||||
export const body = {
|
||||
background: BACKGROUND_COLOR,
|
||||
};
|
||||
}
|
||||
|
||||
export const container = {
|
||||
minWidth: "600px",
|
||||
};
|
||||
|
||||
export const medium = {
|
||||
fontWeight: 500,
|
||||
};
|
||||
|
||||
export const danger = {
|
||||
color: DANGER_COLOR,
|
||||
};
|
||||
padding: "64px 0px",
|
||||
}
|
||||
|
||||
export const frame = {
|
||||
padding: `${unit * 1.5}px`,
|
||||
padding: `${unit * 2}px`,
|
||||
border: `1px solid ${SURFACE_DIVIDER_COLOR}`,
|
||||
background: "#FFF",
|
||||
borderRadius: "6px",
|
||||
boxShadow: `0 1px 2px rgba(0,0,0,0.03),
|
||||
0 2px 4px rgba(0,0,0,0.03),
|
||||
0 2px 6px rgba(0,0,0,0.03)`,
|
||||
};
|
||||
}
|
||||
|
||||
export const textColor = {
|
||||
export const baseText = {
|
||||
fontFamily: "JetBrains Mono, monospace",
|
||||
}
|
||||
|
||||
export const headingText = {
|
||||
color: PRIMARY_COLOR,
|
||||
fontSize: "16px",
|
||||
fontStyle: "normal",
|
||||
fontWeight: 500,
|
||||
lineHeight: "normal",
|
||||
}
|
||||
|
||||
export const contentText = {
|
||||
color: TEXT_COLOR,
|
||||
};
|
||||
fontSize: "14px",
|
||||
fontStyle: "normal",
|
||||
fontWeight: 400,
|
||||
lineHeight: "180%",
|
||||
}
|
||||
|
||||
export const code = {
|
||||
fontFamily: "IBM Plex Mono, monospace",
|
||||
};
|
||||
export const buttonText = {
|
||||
color: "#FDFCFC",
|
||||
fontSize: "16px",
|
||||
fontWeight: 500,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
}
|
||||
|
||||
export const headingHr = {
|
||||
margin: `${unit}px 0`,
|
||||
};
|
||||
|
||||
export const buttonPrimary = {
|
||||
...code,
|
||||
padding: "12px 18px",
|
||||
color: "#FFF",
|
||||
export const linkText = {
|
||||
color: LINK_COLOR,
|
||||
fontSize: "14px",
|
||||
fontStyle: "normal",
|
||||
fontWeight: 400,
|
||||
lineHeight: "150%",
|
||||
textDecorationLine: "underline",
|
||||
textDecorationStyle: "solid" as const,
|
||||
textDecorationSkipInk: "auto" as const,
|
||||
textDecorationThickness: "auto",
|
||||
textUnderlineOffset: "auto",
|
||||
textUnderlinePosition: "from-font",
|
||||
borderRadius: "4px",
|
||||
background: BLUE_COLOR,
|
||||
fontSize: "12px",
|
||||
fontWeight: 500,
|
||||
};
|
||||
background: LINK_BACKGROUND_COLOR,
|
||||
padding: "8px 12px",
|
||||
textAlign: "center" as const,
|
||||
}
|
||||
|
||||
export const compactText = {
|
||||
margin: "0 0 2px",
|
||||
};
|
||||
export const contentHighlightText = {
|
||||
color: PRIMARY_COLOR,
|
||||
}
|
||||
|
||||
export const breadcrumb = {
|
||||
fontSize: "14px",
|
||||
color: SECONDARY_COLOR,
|
||||
};
|
||||
|
||||
export const breadcrumbColonSeparator = {
|
||||
padding: " 0 4px",
|
||||
color: DIMMED_COLOR,
|
||||
};
|
||||
|
||||
export const breadcrumbSeparator = {
|
||||
color: DIVIDER_COLOR,
|
||||
};
|
||||
|
||||
export const heading = {
|
||||
fontSize: "22px",
|
||||
fontWeight: 500,
|
||||
};
|
||||
|
||||
export const sectionLabel = {
|
||||
...code,
|
||||
...compactText,
|
||||
letterSpacing: "0.5px",
|
||||
fontSize: "13px",
|
||||
fontWeight: 500,
|
||||
color: DIMMED_COLOR,
|
||||
};
|
||||
|
||||
export const footerLink = {
|
||||
fontSize: "14px",
|
||||
};
|
||||
export const button = {
|
||||
display: "inline-grid",
|
||||
padding: "8px 12px 8px 20px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
flexShrink: "0",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: PRIMARY_COLOR,
|
||||
}
|
||||
|
||||
@@ -1,39 +1,40 @@
|
||||
// @ts-nocheck
|
||||
import React from "react"
|
||||
import { Img, Row, Html, Link, Body, Head, Button, Column, Preview, Section, Container } from "@jsx-email/all"
|
||||
import { Hr, Text, Fonts, SplitString, Title, A, Span } from "../components"
|
||||
import { Text, Fonts, Title, A, Span } from "../components"
|
||||
import {
|
||||
unit,
|
||||
body,
|
||||
code,
|
||||
frame,
|
||||
medium,
|
||||
heading,
|
||||
headingText,
|
||||
container,
|
||||
headingHr,
|
||||
footerLink,
|
||||
breadcrumb,
|
||||
compactText,
|
||||
buttonPrimary,
|
||||
breadcrumbColonSeparator,
|
||||
contentText,
|
||||
button,
|
||||
contentHighlightText,
|
||||
linkText,
|
||||
buttonText,
|
||||
} from "../styles"
|
||||
|
||||
const LOCAL_ASSETS_URL = "/static"
|
||||
const CONSOLE_URL = "https://opencode.ai/"
|
||||
const DOC_URL = "https://opencode.ai/docs/zen"
|
||||
|
||||
interface InviteEmailProps {
|
||||
workspace: string
|
||||
inviter: string
|
||||
workspaceID: string
|
||||
workspaceName: string
|
||||
assetsUrl: string
|
||||
}
|
||||
export const InviteEmail = ({ workspace, assetsUrl = LOCAL_ASSETS_URL }: InviteEmailProps) => {
|
||||
const subject = `Join the ${workspace} workspace`
|
||||
const messagePlain = `You've been invited to join the ${workspace} workspace in the OpenCode Zen Console.`
|
||||
const url = `${CONSOLE_URL}workspace/${workspace}`
|
||||
export const InviteEmail = ({
|
||||
inviter = "test@anoma.ly",
|
||||
workspaceID = "wrk_01K6XFY7V53T8XN0A7X8G9BTN3",
|
||||
workspaceName = "anomaly",
|
||||
assetsUrl = `${CONSOLE_URL}email`,
|
||||
}: InviteEmailProps) => {
|
||||
const messagePlain = `${inviter} invited you to join the ${workspaceName} workspace.`
|
||||
const url = `${CONSOLE_URL}workspace/${workspaceID}`
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<Title>{`OpenCode Zen — ${messagePlain}`}</Title>
|
||||
<Title>{`OpenCode — ${messagePlain}`}</Title>
|
||||
</Head>
|
||||
<Fonts assetsUrl={assetsUrl} />
|
||||
<Preview>{messagePlain}</Preview>
|
||||
@@ -42,67 +43,35 @@ export const InviteEmail = ({ workspace, assetsUrl = LOCAL_ASSETS_URL }: InviteE
|
||||
<Section style={frame}>
|
||||
<Row>
|
||||
<Column>
|
||||
<A href={CONSOLE_URL}>
|
||||
<Img height="32" alt="OpenCode Zen Logo" src={`${assetsUrl}/zen-logo.png`} />
|
||||
<A href={`${CONSOLE_URL}zen`}>
|
||||
<Img height="32" alt="OpenCode Logo" src={`${assetsUrl}/logo.png`} />
|
||||
</A>
|
||||
</Column>
|
||||
<Column align="right">
|
||||
<Button style={buttonPrimary} href={url}>
|
||||
<Span style={code}>Join Workspace</Span>
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row style={headingHr}>
|
||||
<Column>
|
||||
<Hr />
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Section>
|
||||
<Text style={{ ...compactText, ...breadcrumb }}>
|
||||
<Span>OpenCode Zen</Span>
|
||||
<Span style={{ ...code, ...breadcrumbColonSeparator }}>:</Span>
|
||||
<Span>{workspace}</Span>
|
||||
</Text>
|
||||
<Text style={{ ...heading, ...compactText }}>
|
||||
<Link href={url}>
|
||||
<SplitString text={subject} split={40} />
|
||||
</Link>
|
||||
<Section style={{ padding: `${unit * 2}px 0 0 0` }}>
|
||||
<Text style={headingText}>Join your team's OpenCode workspace</Text>
|
||||
<Text style={contentText}>
|
||||
You have been invited by <Span style={contentHighlightText}>{inviter}</Span> to join the{" "}
|
||||
<Span style={contentHighlightText}>{workspaceName}</Span> workspace on OpenCode.
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Section style={{ padding: `${unit}px 0 0 0` }}>
|
||||
<Text style={{ ...compactText }}>
|
||||
You've been invited to join the{" "}
|
||||
<Link style={medium} href={url}>
|
||||
{workspace}
|
||||
</Link>{" "}
|
||||
workspace in the{" "}
|
||||
<Link style={medium} href={CONSOLE_URL}>
|
||||
OpenCode Zen Console
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
<Button style={button} href={url}>
|
||||
<Text style={buttonText}>
|
||||
Join workspace
|
||||
<Img width="24" height="24" src={`${assetsUrl}/right-arrow.png`} alt="Arrow right" />
|
||||
</Text>
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<Row style={headingHr}>
|
||||
<Column>
|
||||
<Hr />
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Column>
|
||||
<Link href={CONSOLE_URL} style={footerLink}>
|
||||
Console
|
||||
</Link>
|
||||
</Column>
|
||||
<Column align="right">
|
||||
<Link style={footerLink} href={DOC_URL}>
|
||||
About
|
||||
</Link>
|
||||
</Column>
|
||||
</Row>
|
||||
<Section style={{ padding: `${unit}px 0 0 0` }}>
|
||||
<Text style={contentText}>Button not working? Copy the following link...</Text>
|
||||
<Link href={url}>
|
||||
<Text style={linkText}>{url}</Text>
|
||||
</Link>
|
||||
</Section>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 308 B |
@@ -1,19 +1,21 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "0.14.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.15.4",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@types/react": "18.0.25",
|
||||
"react": "18.2.0"
|
||||
"react": "18.2.0",
|
||||
"solid-js": "catalog:"
|
||||
},
|
||||
"exports": {
|
||||
"./*": "./emails/templates/*"
|
||||
},
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "email preview emails/templates"
|
||||
}
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-resource",
|
||||
"dependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250830.0"
|
||||
"@cloudflare/workers-types": "catalog:"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -11,5 +11,8 @@
|
||||
},
|
||||
"import": "./resource.node.ts"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "22.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-scripts",
|
||||
"version": "0.14.6",
|
||||
"version": "0.15.4",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../../CLAUDE.md
|
||||
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user