Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
968239bb76 | ||
|
|
8c24879246 | ||
|
|
9127055ae7 | ||
|
|
f5a6a4af7f | ||
|
|
6e00348bd7 | ||
|
|
95f7403daf | ||
|
|
14d1e20287 | ||
|
|
b8e2895dfc | ||
|
|
6e028ec2dc | ||
|
|
8e0ddd1ac9 | ||
|
|
da78b758d4 | ||
|
|
360765c591 | ||
|
|
db0078bf17 | ||
|
|
98578d3a7b | ||
|
|
bc3616d9c6 | ||
|
|
71306cbd1f | ||
|
|
0866034946 | ||
|
|
2ccaa10e79 | ||
|
|
e92d5b592c | ||
|
|
00ec29dae6 | ||
|
|
438916de5f | ||
|
|
8d4a67324e | ||
|
|
0d683eaa8e | ||
|
|
8fd1b92e6e | ||
|
|
22e3240296 | ||
|
|
e1d0b2ba6e | ||
|
|
ccc27e23df | ||
|
|
ad4bdd9f0f | ||
|
|
5479928a9d | ||
|
|
40836e9683 | ||
|
|
9a48f8e9e3 | ||
|
|
91b8ba2186 | ||
|
|
d075c097ac | ||
|
|
88fd6a294b | ||
|
|
ea8ef37d50 | ||
|
|
d510bd52a4 | ||
|
|
e0a854f035 | ||
|
|
bd914a8c06 | ||
|
|
a49102db01 | ||
|
|
21012fab4b | ||
|
|
9a71a73f50 | ||
|
|
2190e8c656 | ||
|
|
1fd496a5e2 | ||
|
|
74d584af34 | ||
|
|
46f415ecb0 | ||
|
|
d0399045da | ||
|
|
4be0ba19ca | ||
|
|
d5a5e6e062 | ||
|
|
e8dad85233 | ||
|
|
f197b8a0cd | ||
|
|
efaf854e09 | ||
|
|
704276753b | ||
|
|
2c5437791b | ||
|
|
94ab87ffad | ||
|
|
416f419a81 | ||
|
|
3ba03a97dc | ||
|
|
b1a22e08f5 | ||
|
|
c551a4b6e3 | ||
|
|
524ea95a00 | ||
|
|
0e9664d300 | ||
|
|
fffa718f5e | ||
|
|
cce4f64e0b | ||
|
|
ce6e9a822d | ||
|
|
f66e6d7033 | ||
|
|
de2de099b4 | ||
|
|
0233dd1b39 | ||
|
|
40b275d7e6 | ||
|
|
e4a34beb8b | ||
|
|
ac54535486 | ||
|
|
1a43e5fe87 | ||
|
|
46be47d0be | ||
|
|
4af9defb89 | ||
|
|
12b621068a | ||
|
|
07e7ebdb8e | ||
|
|
5092b5f07b | ||
|
|
d8ef9f808d | ||
|
|
d7192d6af9 | ||
|
|
68e6c540bb | ||
|
|
b572c68100 | ||
|
|
25cb03dbe5 | ||
|
|
d47510785a | ||
|
|
657f3d5089 | ||
|
|
49939c4d8d | ||
|
|
529eb6e147 | ||
|
|
a7cae8f674 | ||
|
|
12ae80856e | ||
|
|
7e619a9302 | ||
|
|
2abafbcd2f | ||
|
|
8b08d340ac | ||
|
|
81983d4a2e | ||
|
|
7443b99295 | ||
|
|
306fc05c00 | ||
|
|
9d8d0e97ec | ||
|
|
f4086ac459 | ||
|
|
b9b5d42bd8 | ||
|
|
83ed1adcbd | ||
|
|
9b57db30d1 | ||
|
|
df8e6e6014 | ||
|
|
472a6cc83e | ||
|
|
47d43aaf2d | ||
|
|
da3dea0429 | ||
|
|
9862303eed | ||
|
|
b14622352e | ||
|
|
ea643f1e3f | ||
|
|
f5fd54598f | ||
|
|
0f7b17b1b4 | ||
|
|
4d3e983edb | ||
|
|
d3fc29bdec | ||
|
|
fe58c649cb | ||
|
|
af2a09940c | ||
|
|
7e016fdda6 | ||
|
|
beb97d21ff | ||
|
|
b0345284f9 | ||
|
|
d71153eae6 | ||
|
|
e60ded01df | ||
|
|
4b2a14c154 | ||
|
|
b4717d8092 | ||
|
|
dc8f8cc567 | ||
|
|
99110d12c4 | ||
|
|
74b1349cf6 | ||
|
|
3b3505cfe8 | ||
|
|
55bd6e487e | ||
|
|
1ee916a3c3 | ||
|
|
a5d47f076e | ||
|
|
acd1eb574d | ||
|
|
a71dcc189e | ||
|
|
3789a31423 | ||
|
|
bb6e350d68 | ||
|
|
f9a441d4f4 | ||
|
|
1c05ebaea2 | ||
|
|
520c47e81d | ||
|
|
e5b08da0f1 | ||
|
|
fe2cc0cff1 | ||
|
|
fbc8f6eba9 | ||
|
|
8cba7d7f53 | ||
|
|
6450ba1b79 | ||
|
|
dc1c25cff5 | ||
|
|
3f3550a16e | ||
|
|
161e3db795 | ||
|
|
5a8a0f6a56 | ||
|
|
37f30993fa | ||
|
|
ebc194ca9a | ||
|
|
4edb4fa4fa | ||
|
|
63176bb049 | ||
|
|
216a2d87cf | ||
|
|
dd1f981d23 | ||
|
|
bfc9b24b48 | ||
|
|
2691e1e666 | ||
|
|
3f16e0d89f | ||
|
|
994c55f709 | ||
|
|
2f32f2ceb5 | ||
|
|
076dfb3752 | ||
|
|
60aa0cb96e | ||
|
|
e5973e2860 | ||
|
|
dbd1987f0a | ||
|
|
f270ea65c5 | ||
|
|
1698448016 | ||
|
|
564d3edfac | ||
|
|
679270d9e0 | ||
|
|
9f66a45970 | ||
|
|
779610d668 | ||
|
|
1fb611ef0a | ||
|
|
972f5ecc7d | ||
|
|
8d720f9463 | ||
|
|
92931437c4 | ||
|
|
08ca1237cc | ||
|
|
6473e15793 | ||
|
|
16cac69a72 | ||
|
|
b2da41cfad | ||
|
|
fcf2da9571 | ||
|
|
253b7ea784 | ||
|
|
3a9fd1bb36 | ||
|
|
f84ac697dc | ||
|
|
76a79284d2 |
@@ -25,6 +25,8 @@ jobs:
|
||||
matrix:
|
||||
os:
|
||||
- blacksmith-4vcpu-ubuntu-2404
|
||||
- blacksmith-4vcpu-ubuntu-2404-arm
|
||||
- macos-15
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
@@ -33,7 +35,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v21
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
|
||||
- name: Build desktop via flake
|
||||
run: |
|
||||
|
||||
@@ -17,11 +17,11 @@ on:
|
||||
- "packages/*/package.json"
|
||||
|
||||
jobs:
|
||||
update-linux:
|
||||
update-flake:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
env:
|
||||
SYSTEM: x86_64-linux
|
||||
TITLE: flake.lock
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -33,39 +33,32 @@ jobs:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v20
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "Github Action"
|
||||
|
||||
- name: Update flake.lock
|
||||
- name: Update ${{ env.TITLE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "📦 Updating flake.lock..."
|
||||
echo "📦 Updating $TITLE..."
|
||||
nix flake update
|
||||
echo "✅ flake.lock updated successfully"
|
||||
echo "✅ $TITLE updated successfully"
|
||||
|
||||
- name: Update node_modules hash for x86_64-linux
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "🔄 Updating node_modules hash for x86_64-linux..."
|
||||
nix/scripts/update-hashes.sh
|
||||
echo "✅ node_modules hash for x86_64-linux updated successfully"
|
||||
|
||||
- name: Commit Linux hash changes
|
||||
- name: Commit ${{ env.TITLE }} changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "🔍 Checking for changes in tracked Nix files..."
|
||||
echo "🔍 Checking for changes in tracked files..."
|
||||
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
echo "### Nix Hash Update (x86_64-linux)"
|
||||
echo "### Nix $TITLE"
|
||||
echo ""
|
||||
echo "- ref: ${GITHUB_REF_NAME}"
|
||||
echo "- status: ${status}"
|
||||
@@ -75,11 +68,10 @@ jobs:
|
||||
fi
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
|
||||
FILES=(flake.lock flake.nix)
|
||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||
if [ -z "$STATUS" ]; then
|
||||
echo "✅ No changes detected. Hashes are already up to date."
|
||||
echo "✅ No changes detected."
|
||||
summarize "no changes"
|
||||
exit 0
|
||||
fi
|
||||
@@ -89,7 +81,7 @@ jobs:
|
||||
echo "🔗 Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
echo "💾 Committing changes..."
|
||||
git commit -m "Update Nix flake.lock and x86_64-linux hash"
|
||||
git commit -m "Update $TITLE"
|
||||
echo "✅ Changes committed"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
@@ -101,12 +93,25 @@ jobs:
|
||||
|
||||
summarize "committed $(git rev-parse --short HEAD)"
|
||||
|
||||
update-macos:
|
||||
needs: update-linux
|
||||
update-node-modules-hash:
|
||||
needs: update-flake
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- system: x86_64-linux
|
||||
host: blacksmith-4vcpu-ubuntu-2404
|
||||
- system: aarch64-linux
|
||||
host: blacksmith-4vcpu-ubuntu-2404-arm
|
||||
- system: x86_64-darwin
|
||||
host: macos-15-intel
|
||||
- system: aarch64-darwin
|
||||
host: macos-latest
|
||||
runs-on: ${{ matrix.host }}
|
||||
env:
|
||||
SYSTEM: aarch64-darwin
|
||||
SYSTEM: ${{ matrix.system }}
|
||||
TITLE: node_modules hash (${{ matrix.system }})
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -118,7 +123,7 @@ jobs:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v20
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
@@ -132,25 +137,25 @@ jobs:
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
git pull origin "$BRANCH"
|
||||
|
||||
- name: Update node_modules hash for aarch64-darwin
|
||||
- name: Update ${{ env.TITLE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "🔄 Updating node_modules hash for aarch64-darwin..."
|
||||
echo "🔄 Updating $TITLE..."
|
||||
nix/scripts/update-hashes.sh
|
||||
echo "✅ node_modules hash for aarch64-darwin updated successfully"
|
||||
echo "✅ $TITLE updated successfully"
|
||||
|
||||
- name: Commit macOS hash changes
|
||||
- name: Commit ${{ env.TITLE }} changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "🔍 Checking for changes in tracked Nix files..."
|
||||
echo "🔍 Checking for changes in tracked files..."
|
||||
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
echo "### Nix Hash Update (aarch64-darwin)"
|
||||
echo "### Nix $TITLE"
|
||||
echo ""
|
||||
echo "- ref: ${GITHUB_REF_NAME}"
|
||||
echo "- status: ${status}"
|
||||
@@ -164,7 +169,7 @@ jobs:
|
||||
FILES=(nix/hashes.json)
|
||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||
if [ -z "$STATUS" ]; then
|
||||
echo "✅ No changes detected. Hash is already up to date."
|
||||
echo "✅ No changes detected."
|
||||
summarize "no changes"
|
||||
exit 0
|
||||
fi
|
||||
@@ -174,7 +179,7 @@ jobs:
|
||||
echo "🔗 Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
echo "💾 Committing changes..."
|
||||
git commit -m "Update aarch64-darwin hash"
|
||||
git commit -m "Update $TITLE"
|
||||
echo "✅ Changes committed"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
|
||||
@@ -1,203 +1,204 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | -------------------- | ------------------- | -------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
||||
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
|
||||
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
|
||||
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
|
||||
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
|
||||
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
|
||||
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
|
||||
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
|
||||
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
|
||||
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
|
||||
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
||||
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
||||
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
||||
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
||||
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | -------------------- | -------------------- | -------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
||||
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
|
||||
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
|
||||
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
|
||||
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
|
||||
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
|
||||
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
|
||||
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
|
||||
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
|
||||
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
|
||||
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
||||
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
||||
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
||||
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
||||
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
|
||||
| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -81,6 +81,8 @@
|
||||
"@opencode-ai/console-mail": "workspace:*",
|
||||
"@opencode-ai/console-resource": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.7",
|
||||
"@smithy/util-utf8": "4.2.0",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
@@ -95,13 +97,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@webgpu/types": "0.1.54",
|
||||
"typescript": "catalog:",
|
||||
"wrangler": "4.50.0",
|
||||
},
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -128,7 +131,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -152,7 +155,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -176,7 +179,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -205,7 +208,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -234,7 +237,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -250,7 +253,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -290,8 +293,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.72",
|
||||
"@opentui/solid": "0.1.72",
|
||||
"@opentui/core": "0.1.74",
|
||||
"@opentui/solid": "0.1.74",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -354,7 +357,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -374,9 +377,9 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@hey-api/openapi-ts": "0.90.4",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
@@ -385,7 +388,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -398,7 +401,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -410,7 +413,7 @@
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"dompurify": "catalog:",
|
||||
"dompurify": "3.3.1",
|
||||
"fuzzysort": "catalog:",
|
||||
"katex": "0.16.27",
|
||||
"luxon": "catalog:",
|
||||
@@ -438,7 +441,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -449,7 +452,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -505,7 +508,7 @@
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.6",
|
||||
"@types/bun": "1.3.5",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
@@ -919,11 +922,11 @@
|
||||
|
||||
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
|
||||
|
||||
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="],
|
||||
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.2", "", { "dependencies": { "ansi-colors": "4.1.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-88cqrrB2cLXN8nMOHidQTcVOnZsJ5kebEbBefjMCifaUCwTA30ouSSWvTZqrOX4O104zjJyu7M8Gcv/NNYQuaA=="],
|
||||
|
||||
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="],
|
||||
|
||||
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.1", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-x/nDTupOnV9VuSeNIiJpgIpc915GHduhyseJeMTnI0JMsXaObmpa0rgPr3ASVEYMLgpvqozIEG1RTOOnal6zLQ=="],
|
||||
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.4", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.2", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-9l++kjcb0ui4JqPlueZ6OZ9zKn6eK/8//Z2jHcIXb5MRwDRgubOOSpTU5llEv3uvWfT10VzcMp99dySWq0AASw=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
|
||||
|
||||
@@ -1215,21 +1218,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.72", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.72", "@opentui/core-darwin-x64": "0.1.72", "@opentui/core-linux-arm64": "0.1.72", "@opentui/core-linux-x64": "0.1.72", "@opentui/core-win32-arm64": "0.1.72", "@opentui/core-win32-x64": "0.1.72", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-l4WQzubBJ80Q0n77Lxuodjwwm8qj/sOa7IXxEAzzDDXY/7bsIhdSpVhRTt+KevBRlok5J+w/KMKYr8UzkA4/hA=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.74", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.74", "@opentui/core-darwin-x64": "0.1.74", "@opentui/core-linux-arm64": "0.1.74", "@opentui/core-linux-x64": "0.1.74", "@opentui/core-win32-arm64": "0.1.74", "@opentui/core-win32-x64": "0.1.74", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g4W16ymv12JdgZ+9B4t7mpIICvzWy2+eHERfmDf80ALduOQCUedKQdULcBFhVCYUXIkDRtIy6CID5thMAah3FA=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.72", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RoU48kOrhLZYDBiXaDu1LXS2bwRdlJlFle8eUQiqJjLRbMIY34J/srBuL0JnAS3qKW4J34NepUQa0l0/S43Q3w=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.74", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rfmlDLtm/u17CnuhJgCxPeYMvOST+A2MOdVOk46IurtHO849bdYqK6iudKNlFRs1FOrymgSKF9GlWBHAOKeRjg=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.72", "", { "os": "darwin", "cpu": "x64" }, "sha512-hHUQw8i2LWPToRW1rjAiRqmNf34iJPS9ve9CJDygvFs5JOqUxN5yrfLfKfE+1bQjfFDHnpqW1HUk96iLhkPj8Q=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.74", "", { "os": "darwin", "cpu": "x64" }, "sha512-WAD8orsDV0ZdW/5GwjOOB4FY96772xbkz+rcV7WRzEFUVaqoBaC04IuqYzS9d5s+cjkbT5Cpj47hrVYkkVQKng=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.72", "", { "os": "linux", "cpu": "arm64" }, "sha512-63yml0OQ8tVa0JuDF9lBAWiChX6Q+iDO7lKv7c2n0352n/WyPr3iAgq4uSoH49HXuKeAXY/VwHGjvPzjXD/SDA=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.74", "", { "os": "linux", "cpu": "arm64" }, "sha512-lgmHzrzLy4e+rgBS+lhtsMLLgIMLbtLNMm6EzVPyYVDlLDGjM7+ulXMem7AtpaRrWrUUl4REiG9BoQUsCFDwYA=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.72", "", { "os": "linux", "cpu": "x64" }, "sha512-51veiQXNLvzDsFzsEvt71uK7WhiRe2DnvlJSGBSe6aRRHHxjCFYHzYi7t6bitJqtDTUj+EaMPbH81oZ6xy7tyg=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.74", "", { "os": "linux", "cpu": "x64" }, "sha512-8Mn2WbdBQ29xCThuPZezjDhd1N3+fXwKkGvCBOdTI0le6h2A/vCNbfUVjwfr/EGZSRXxCG+Yapol34BAULGpOA=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.72", "", { "os": "win32", "cpu": "arm64" }, "sha512-1Ep6OcaYTy1RlLOln+LNN7DL1iNyLwLjG2M8aO0pVJKFvxeD5P7rdRzY065E4uhkHeJIHuduUqxvUjD0dyuwbw=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.74", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvYUXz03avnI6ZluyLp00HPmR0UT/IE/6QS97XBsgJlUTtpnbKkBtB5jD1NHwWkElaRj1Qv2QP36ngFoJqbl9g=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.72", "", { "os": "win32", "cpu": "x64" }, "sha512-5QUv91UkOINlkEaPky3kaxmJvshcJMBAX7LZtIroduaKBGpWRA1aogNhPZzp+30WkvgOU7aOtUktAZuFXb9WdQ=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.74", "", { "os": "win32", "cpu": "x64" }, "sha512-3wfWXaAKOIlDQz6ZZIESf2M+YGZ7uFHijjTEM8w/STRlLw8Y6+QyGYi1myHSM4d6RSO+/s2EMDxvjDf899W9vQ=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.72", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.72", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-hytoLPboL/MTY/BQUnf/HlBuNXTVONney0X+PIQI82wT7kMx7+HHI2wnowpM3dyvA7l6NfORSud2cs9kIUBFBw=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.74", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.74", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-Vz82cI8T9YeJjGsVg4ULp6ral4N+xyt1j9A6Tbu3aaQgEKiB74LW03EXREehfjPr1irOFxtKfWPbx5NKH0Upag=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -1521,7 +1524,7 @@
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="],
|
||||
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
|
||||
|
||||
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.5", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw=="],
|
||||
|
||||
@@ -1773,7 +1776,7 @@
|
||||
|
||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
@@ -1903,7 +1906,7 @@
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
||||
|
||||
"@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
|
||||
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
|
||||
|
||||
"@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
|
||||
|
||||
@@ -2075,7 +2078,7 @@
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.4", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||
|
||||
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
||||
|
||||
@@ -2091,7 +2094,7 @@
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"c12": ["c12@3.3.2", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A=="],
|
||||
"c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
@@ -3491,7 +3494,7 @@
|
||||
|
||||
"selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||
|
||||
@@ -3965,6 +3968,8 @@
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
|
||||
@@ -4235,6 +4240,10 @@
|
||||
|
||||
"@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
|
||||
|
||||
"@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="],
|
||||
|
||||
"@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
|
||||
|
||||
"@solidjs/start/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"@solidjs/start/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
@@ -4271,6 +4280,8 @@
|
||||
|
||||
"astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
|
||||
|
||||
"astro/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"astro/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
|
||||
|
||||
"astro/unstorage": ["unstorage@1.17.3", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q=="],
|
||||
@@ -4291,6 +4302,10 @@
|
||||
|
||||
"body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||
|
||||
"bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
|
||||
|
||||
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
@@ -4307,6 +4322,8 @@
|
||||
|
||||
"editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="],
|
||||
|
||||
"editorconfig/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"es-get-iterator/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
@@ -4333,6 +4350,8 @@
|
||||
|
||||
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"gel/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@@ -4347,6 +4366,8 @@
|
||||
|
||||
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||
|
||||
"jsonwebtoken/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
@@ -4431,6 +4452,8 @@
|
||||
|
||||
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"shiki/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
||||
|
||||
"shiki/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
||||
@@ -4909,6 +4932,8 @@
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1768302833,
|
||||
"narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
|
||||
"lastModified": 1768395095,
|
||||
"narHash": "sha256-ZhuYJbwbZT32QA95tSkXd9zXHcdZj90EzHpEXBMabaw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "61db79b0c6b838d9894923920b612048e1201926",
|
||||
"rev": "13868c071cc73a5e9f610c47d7bb08e5da64fdd5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -119,6 +119,7 @@ const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS5"),
|
||||
new sst.Secret("ZEN_MODELS6"),
|
||||
new sst.Secret("ZEN_MODELS7"),
|
||||
new sst.Secret("ZEN_MODELS8"),
|
||||
]
|
||||
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-GKdu7nan/9ioBtgL3cUeuVLNKUDio10LeQrn7BPgbng=",
|
||||
"aarch64-darwin": "sha256-STLB1J65VjauvPM+BqCyTQQkHPoVmUhDvVEdH3WTJP4="
|
||||
"x86_64-linux": "sha256-07XxcHLuToM4QfWVyaPLACxjPZ93ZM7gtpX2o08Lp18=",
|
||||
"aarch64-linux": "sha256-0Im52dLeZ0ZtaPJr/U4m7+IRtOfziHNJI/Bu/V6cPho=",
|
||||
"aarch64-darwin": "sha256-U2UvE70nM0OI0VhIku8qnX+ptPbA+Q/y1BGXbFMcyt4=",
|
||||
"x86_64-darwin": "sha256-CpZFHBMPJSib2Vqs6oC8HQjQtviPUMa/qezHAe22N/A="
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.6",
|
||||
"packageManager": "bun@1.3.5",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
@@ -21,7 +21,7 @@
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@types/bun": "1.3.6",
|
||||
"@types/bun": "1.3.5",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"ulid": "3.0.1",
|
||||
|
||||
@@ -13,12 +13,11 @@
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
<!-- Theme preload script - applies cached theme to avoid FOUC -->
|
||||
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-dvh"></div>
|
||||
<div id="root" class="flex flex-col h-dvh p-px"></div>
|
||||
<script src="/src/entry.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -4,11 +4,26 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createMemo } from "solid-js"
|
||||
import { createMemo, createSignal, onCleanup, Show } from "solid-js"
|
||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useFile } from "@/context/file"
|
||||
|
||||
type EntryType = "command" | "file"
|
||||
|
||||
type Entry = {
|
||||
id: string
|
||||
type: EntryType
|
||||
title: string
|
||||
description?: string
|
||||
keybind?: string
|
||||
category: "Commands" | "Files"
|
||||
option?: CommandOption
|
||||
path?: string
|
||||
}
|
||||
|
||||
export function DialogSelectFile() {
|
||||
const command = useCommand()
|
||||
const layout = useLayout()
|
||||
const file = useFile()
|
||||
const dialog = useDialog()
|
||||
@@ -16,35 +31,148 @@ export function DialogSelectFile() {
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const [grouped, setGrouped] = createSignal(false)
|
||||
const common = ["session.new", "session.previous", "session.next", "terminal.toggle", "review.toggle"]
|
||||
const limit = 5
|
||||
|
||||
const allowed = createMemo(() =>
|
||||
command.options.filter(
|
||||
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
),
|
||||
)
|
||||
|
||||
const commandItem = (option: CommandOption): Entry => ({
|
||||
id: "command:" + option.id,
|
||||
type: "command",
|
||||
title: option.title,
|
||||
description: option.description,
|
||||
keybind: option.keybind,
|
||||
category: "Commands",
|
||||
option,
|
||||
})
|
||||
|
||||
const fileItem = (path: string): Entry => ({
|
||||
id: "file:" + path,
|
||||
type: "file",
|
||||
title: path,
|
||||
category: "Files",
|
||||
path,
|
||||
})
|
||||
|
||||
const list = createMemo(() => allowed().map(commandItem))
|
||||
|
||||
const picks = createMemo(() => {
|
||||
const all = allowed()
|
||||
const order = new Map(common.map((id, index) => [id, index]))
|
||||
const picked = all.filter((option) => order.has(option.id))
|
||||
const base = picked.length ? picked : all.slice(0, limit)
|
||||
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
||||
return sorted.map(commandItem)
|
||||
})
|
||||
|
||||
const recent = createMemo(() => {
|
||||
const all = tabs().all()
|
||||
const active = tabs().active()
|
||||
const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
||||
const seen = new Set<string>()
|
||||
const items: Entry[] = []
|
||||
|
||||
for (const item of order) {
|
||||
const path = file.pathFromTab(item)
|
||||
if (!path) continue
|
||||
if (seen.has(path)) continue
|
||||
seen.add(path)
|
||||
items.push(fileItem(path))
|
||||
}
|
||||
|
||||
return items.slice(0, limit)
|
||||
})
|
||||
|
||||
const items = async (filter: string) => {
|
||||
const query = filter.trim()
|
||||
setGrouped(query.length > 0)
|
||||
if (!query) return [...picks(), ...recent()]
|
||||
const files = await file.searchFiles(query)
|
||||
const entries = files.map(fileItem)
|
||||
return [...list(), ...entries]
|
||||
}
|
||||
|
||||
const handleMove = (item: Entry | undefined) => {
|
||||
state.cleanup?.()
|
||||
if (!item) return
|
||||
if (item.type !== "command") return
|
||||
state.cleanup = item.option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const open = (path: string) => {
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
view().reviewPanel.open()
|
||||
}
|
||||
|
||||
const handleSelect = (item: Entry | undefined) => {
|
||||
if (!item) return
|
||||
state.committed = true
|
||||
state.cleanup = undefined
|
||||
dialog.close()
|
||||
|
||||
if (item.type === "command") {
|
||||
item.option?.onSelect?.("palette")
|
||||
return
|
||||
}
|
||||
|
||||
if (!item.path) return
|
||||
open(item.path)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (state.committed) return
|
||||
state.cleanup?.()
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog title="Select file">
|
||||
<Dialog title="Search">
|
||||
<List
|
||||
search={{ placeholder: "Search files", autofocus: true }}
|
||||
emptyMessage="No files found"
|
||||
items={file.searchFiles}
|
||||
key={(x) => x}
|
||||
onSelect={(path) => {
|
||||
if (path) {
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
view().reviewPanel.open()
|
||||
}
|
||||
dialog.close()
|
||||
}}
|
||||
search={{ placeholder: "Search files and commands", autofocus: true }}
|
||||
emptyMessage="No results found"
|
||||
items={items}
|
||||
key={(item) => item.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
groupBy={(item) => (grouped() ? item.category : "")}
|
||||
onMove={handleMove}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(i)}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
|
||||
{(item) => (
|
||||
<Show
|
||||
when={item.type === "command"}
|
||||
fallback={
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(item.path ?? "")}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
||||
<Show when={item.description}>
|
||||
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={item.keybind}>
|
||||
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(item.keybind ?? "")}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,267 +1,211 @@
|
||||
import { createMemo, createResource, Show } from "solid-js"
|
||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
// import { useServer } from "@/context/server"
|
||||
// import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Popover } from "@opencode-ai/ui/popover"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
|
||||
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
|
||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { same } from "@/utils/same"
|
||||
|
||||
export function SessionHeader() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const command = useCommand()
|
||||
const server = useServer()
|
||||
const dialog = useDialog()
|
||||
// const server = useServer()
|
||||
// const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
|
||||
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
|
||||
const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
|
||||
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
||||
const parentSession = createMemo(() => {
|
||||
const current = currentSession()
|
||||
if (!current?.parentID) return undefined
|
||||
return sync.data.session.find((s) => s.id === current.parentID)
|
||||
const project = createMemo(() => {
|
||||
const directory = projectDirectory()
|
||||
if (!directory) return
|
||||
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
|
||||
})
|
||||
const name = createMemo(() => {
|
||||
const current = project()
|
||||
if (current) return current.name || getFilename(current.worktree)
|
||||
return getFilename(projectDirectory())
|
||||
})
|
||||
const hotkey = createMemo(() => command.keybind("file.open"))
|
||||
|
||||
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||
const worktrees = createMemo(() => layout.projects.list().map((p) => p.worktree), [], { equals: same })
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
|
||||
function navigateToProject(directory: string) {
|
||||
navigate(`/${base64Encode(directory)}`)
|
||||
}
|
||||
|
||||
function navigateToSession(session: Session | undefined) {
|
||||
if (!session) return
|
||||
// Only navigate if we're actually changing to a different session
|
||||
if (session.id === params.id) return
|
||||
navigate(`/${params.dir}/session/${session.id}`)
|
||||
}
|
||||
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
||||
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
||||
|
||||
return (
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex">
|
||||
<button
|
||||
type="button"
|
||||
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
>
|
||||
<Icon name="menu" size="small" />
|
||||
</button>
|
||||
<div class="px-4 flex items-center justify-between gap-4 w-full">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div class="hidden xl:flex items-center gap-2">
|
||||
<Select
|
||||
options={worktrees()}
|
||||
current={sync.project?.worktree ?? projectDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
</div>
|
||||
<Show
|
||||
when={parentSession()}
|
||||
fallback={
|
||||
<>
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
placeholder="New session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={navigateToSession}
|
||||
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
<>
|
||||
<Show when={centerMount()}>
|
||||
{(mount) => (
|
||||
<Portal mount={mount()}>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={parentSession()}
|
||||
placeholder="Back to parent session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={(session) => {
|
||||
// Only navigate if selecting a different session than current parent
|
||||
const currentParent = parentSession()
|
||||
if (session && currentParent && session.id !== currentParent.id) {
|
||||
navigateToSession(session)
|
||||
}
|
||||
}}
|
||||
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
<div class="text-text-weaker">/</div>
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
<Tooltip value="Back to parent session">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center gap-1 p-1 rounded hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors flex-shrink-0"
|
||||
onClick={() => navigateToSession(parentSession())}
|
||||
>
|
||||
<Icon name="arrow-left" size="small" class="text-icon-base" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="magnifying-glass" size="normal" class="icon-base" />
|
||||
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate" style={{ "line-height": 1 }}>
|
||||
Search {name()}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={currentSession() && !parentSession()}>
|
||||
<TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
|
||||
<IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="hidden md:flex items-center gap-1">
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
dialog.show(() => <DialogSelectServer />)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full": true,
|
||||
"bg-icon-success-base": server.healthy() === true,
|
||||
"bg-icon-critical-base": server.healthy() === false,
|
||||
"bg-border-weak-base": server.healthy() === undefined,
|
||||
}}
|
||||
/>
|
||||
<Icon name="server" size="small" class="text-icon-weak" />
|
||||
<span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
|
||||
</Button>
|
||||
<SessionLspIndicator />
|
||||
<SessionMcpIndicator />
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Show when={currentSession()?.summary?.files}>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle review"
|
||||
keybind={command.keybind("review.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/review-toggle size-6 p-0"
|
||||
onClick={() => view().reviewPanel.toggle()}
|
||||
|
||||
<Show when={hotkey()}>
|
||||
{(keybind) => (
|
||||
<span
|
||||
class="shrink-0 flex items-center justify-center h-5 px-2 rounded-[2px] bg-surface-base text-12-medium text-text-weak"
|
||||
style={{ "box-shadow": "var(--shadow-xxs-border)" }}
|
||||
>
|
||||
{keybind()}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</button>
|
||||
</Portal>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={rightMount()}>
|
||||
{(mount) => (
|
||||
<Portal mount={mount()}>
|
||||
<div class="flex items-center gap-3">
|
||||
{/* <div class="hidden md:flex items-center gap-1"> */}
|
||||
{/* <Button */}
|
||||
{/* size="small" */}
|
||||
{/* variant="ghost" */}
|
||||
{/* onClick={() => { */}
|
||||
{/* dialog.show(() => <DialogSelectServer />) */}
|
||||
{/* }} */}
|
||||
{/* > */}
|
||||
{/* <div */}
|
||||
{/* classList={{ */}
|
||||
{/* "size-1.5 rounded-full": true, */}
|
||||
{/* "bg-icon-success-base": server.healthy() === true, */}
|
||||
{/* "bg-icon-critical-base": server.healthy() === false, */}
|
||||
{/* "bg-border-weak-base": server.healthy() === undefined, */}
|
||||
{/* }} */}
|
||||
{/* /> */}
|
||||
{/* <Icon name="server" size="small" class="text-icon-weak" /> */}
|
||||
{/* <span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span> */}
|
||||
{/* </Button> */}
|
||||
{/* <SessionLspIndicator /> */}
|
||||
{/* <SessionMcpIndicator /> */}
|
||||
{/* </div> */}
|
||||
<div class="flex items-center gap-1">
|
||||
<Show when={currentSession()?.summary?.files}>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle review"
|
||||
keybind={command.keybind("review.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/review-toggle size-6 p-0"
|
||||
onClick={() => view().reviewPanel.toggle()}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
|
||||
size="small"
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
|
||||
size="small"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
|
||||
size="small"
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle terminal"
|
||||
keybind={command.keybind("terminal.toggle")}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
|
||||
size="small"
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
|
||||
size="small"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
|
||||
size="small"
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle terminal"
|
||||
keybind={command.keybind("terminal.toggle")}
|
||||
>
|
||||
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={() => view().terminal.toggle()}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<Show when={shareEnabled() && currentSession()}>
|
||||
<Popover
|
||||
title="Share session"
|
||||
trigger={
|
||||
<Tooltip class="shrink-0" value="Share session">
|
||||
<IconButton icon="share" variant="ghost" class="" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{iife(() => {
|
||||
const [url] = createResource(
|
||||
() => currentSession(),
|
||||
async (session) => {
|
||||
if (!session) return
|
||||
let shareURL = session.share?.url
|
||||
if (!shareURL) {
|
||||
shareURL = await globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: projectDirectory() })
|
||||
.then((r) => r.data?.share?.url)
|
||||
.catch((e) => {
|
||||
console.error("Failed to share session", e)
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
return shareURL
|
||||
},
|
||||
{ initialValue: "" },
|
||||
)
|
||||
return (
|
||||
<Show when={url.latest}>
|
||||
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
|
||||
</Show>
|
||||
)
|
||||
})}
|
||||
</Popover>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/terminal-toggle size-6 p-0"
|
||||
onClick={() => view().terminal.toggle()}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<Show when={shareEnabled() && currentSession()}>
|
||||
<Popover
|
||||
title="Share session"
|
||||
trigger={
|
||||
<Tooltip class="shrink-0" value="Share session">
|
||||
<IconButton icon="share" variant="ghost" class="" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{iife(() => {
|
||||
const [url] = createResource(
|
||||
() => currentSession(),
|
||||
async (session) => {
|
||||
if (!session) return
|
||||
let shareURL = session.share?.url
|
||||
if (!shareURL) {
|
||||
shareURL = await globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: projectDirectory() })
|
||||
.then((r) => r.data?.share?.url)
|
||||
.catch((e) => {
|
||||
console.error("Failed to share session", e)
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
return shareURL
|
||||
},
|
||||
{ initialValue: "" },
|
||||
)
|
||||
return (
|
||||
<Show when={url.latest}>
|
||||
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
|
||||
</Show>
|
||||
)
|
||||
})}
|
||||
</Popover>
|
||||
</Show>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createEffect, createMemo, Show } from "solid-js"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { useTheme } from "@opencode-ai/ui/theme"
|
||||
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useCommand } from "@/context/command"
|
||||
|
||||
export function Titlebar() {
|
||||
const layout = useLayout()
|
||||
const platform = usePlatform()
|
||||
const command = useCommand()
|
||||
const theme = useTheme()
|
||||
|
||||
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
||||
const reserve = createMemo(
|
||||
() => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"),
|
||||
)
|
||||
const web = createMemo(() => platform.platform === "web")
|
||||
|
||||
const getWin = () => {
|
||||
if (platform.platform !== "desktop") return
|
||||
|
||||
const tauri = (
|
||||
window as unknown as {
|
||||
__TAURI__?: { window?: { getCurrentWindow?: () => { startDragging?: () => Promise<void> } } }
|
||||
}
|
||||
).__TAURI__
|
||||
if (!tauri?.window?.getCurrentWindow) return
|
||||
|
||||
return tauri.window.getCurrentWindow()
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (platform.platform !== "desktop") return
|
||||
|
||||
const scheme = theme.colorScheme()
|
||||
const value = scheme === "system" ? null : scheme
|
||||
|
||||
const tauri = (window as unknown as { __TAURI__?: { webviewWindow?: { getCurrentWebviewWindow?: () => unknown } } })
|
||||
.__TAURI__
|
||||
const get = tauri?.webviewWindow?.getCurrentWebviewWindow
|
||||
if (!get) return
|
||||
|
||||
const win = get() as { setTheme?: (theme?: "light" | "dark" | null) => Promise<void> }
|
||||
if (!win.setTheme) return
|
||||
|
||||
void win.setTheme(value).catch(() => undefined)
|
||||
})
|
||||
|
||||
const interactive = (target: EventTarget | null) => {
|
||||
if (!(target instanceof Element)) return false
|
||||
|
||||
const selector =
|
||||
"button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']"
|
||||
|
||||
return !!target.closest(selector)
|
||||
}
|
||||
|
||||
const drag = (e: MouseEvent) => {
|
||||
if (platform.platform !== "desktop") return
|
||||
if (e.buttons !== 1) return
|
||||
if (interactive(e.target)) return
|
||||
|
||||
const win = getWin()
|
||||
if (!win?.startDragging) return
|
||||
|
||||
e.preventDefault()
|
||||
void win.startDragging().catch(() => undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<header class="h-10 shrink-0 bg-background-base flex items-center relative">
|
||||
<div
|
||||
classList={{
|
||||
"flex items-center w-full min-w-0 pr-2": true,
|
||||
"pl-2": !mac(),
|
||||
}}
|
||||
onMouseDown={drag}
|
||||
>
|
||||
<Show when={mac()}>
|
||||
<div class="w-[72px] h-full shrink-0" data-tauri-drag-region />
|
||||
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
||||
<IconButton icon="menu" variant="ghost" class="size-8 rounded-md" onClick={layout.mobileSidebar.toggle} />
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!mac()}>
|
||||
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
|
||||
<IconButton icon="menu" variant="ghost" class="size-8 rounded-md" onClick={layout.mobileSidebar.toggle} />
|
||||
</div>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0"}
|
||||
placement="bottom"
|
||||
title="Toggle sidebar"
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
>
|
||||
<IconButton
|
||||
icon={layout.sidebar.opened() ? "layout-left" : "layout-right"}
|
||||
variant="ghost"
|
||||
class="size-8 rounded-md"
|
||||
onClick={layout.sidebar.toggle}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
<div class="flex-1 h-full" data-tauri-drag-region />
|
||||
<div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0" />
|
||||
<Show when={reserve()}>
|
||||
<div class="w-[120px] h-full shrink-0" data-tauri-drag-region />
|
||||
</Show>
|
||||
</div>
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div id="opencode-titlebar-center" class="pointer-events-auto" />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import { createMemo, createSignal, onCleanup, onMount, Show, type Accessor } from "solid-js"
|
||||
import { createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
|
||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||
|
||||
@@ -114,67 +111,11 @@ export function formatKeybind(config: string): string {
|
||||
return IS_MAC ? parts.join("") : parts.join("+")
|
||||
}
|
||||
|
||||
function DialogCommand(props: { options: CommandOption[] }) {
|
||||
const dialog = useDialog()
|
||||
let cleanup: (() => void) | void
|
||||
let committed = false
|
||||
|
||||
const handleMove = (option: CommandOption | undefined) => {
|
||||
cleanup?.()
|
||||
cleanup = option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const handleSelect = (option: CommandOption | undefined) => {
|
||||
if (option) {
|
||||
committed = true
|
||||
cleanup = undefined
|
||||
dialog.close()
|
||||
option.onSelect?.("palette")
|
||||
}
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (!committed) {
|
||||
cleanup?.()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog title="Commands">
|
||||
<List
|
||||
search={{ placeholder: "Search commands", autofocus: true }}
|
||||
emptyMessage="No commands found"
|
||||
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
|
||||
key={(x) => x?.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
groupBy={(x) => x.category ?? ""}
|
||||
onMove={handleMove}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{(option) => (
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{option.title}</span>
|
||||
<Show when={option.description}>
|
||||
<span class="text-14-regular text-text-weak truncate">{option.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={option.keybind}>
|
||||
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(option.keybind!)}</span>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
|
||||
name: "Command",
|
||||
init: () => {
|
||||
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
||||
const [suspendCount, setSuspendCount] = createSignal(0)
|
||||
const dialog = useDialog()
|
||||
|
||||
const options = createMemo(() => {
|
||||
const seen = new Set<string>()
|
||||
@@ -202,12 +143,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
|
||||
const suspended = () => suspendCount() > 0
|
||||
|
||||
const showPalette = () => {
|
||||
if (!dialog.active) {
|
||||
dialog.show(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
|
||||
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
|
||||
for (const option of options()) {
|
||||
if (option.id === id || option.id === "suggested." + id) {
|
||||
option.onSelect?.(source)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showPalette = () => {
|
||||
run("file.open", "palette")
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (suspended()) return
|
||||
|
||||
@@ -248,12 +196,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
})
|
||||
},
|
||||
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
||||
for (const option of options()) {
|
||||
if (option.id === id || option.id === "suggested." + id) {
|
||||
option.onSelect?.(source)
|
||||
return
|
||||
}
|
||||
}
|
||||
run(id, source)
|
||||
},
|
||||
keybind(id: string) {
|
||||
const option = options().find((x) => x.id === id || x.id === "suggested." + id)
|
||||
|
||||
@@ -19,15 +19,29 @@ import {
|
||||
type QuestionRequest,
|
||||
createOpencodeClient,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { createStore, produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { retry } from "@opencode-ai/util/retry"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { ErrorPage, type InitError } from "../pages/error"
|
||||
import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js"
|
||||
import {
|
||||
batch,
|
||||
createContext,
|
||||
createEffect,
|
||||
getOwner,
|
||||
runWithOwner,
|
||||
useContext,
|
||||
onCleanup,
|
||||
onMount,
|
||||
type Accessor,
|
||||
type ParentProps,
|
||||
Switch,
|
||||
Match,
|
||||
} from "solid-js"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { usePlatform } from "./platform"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
type State = {
|
||||
status: "loading" | "partial" | "complete"
|
||||
@@ -68,9 +82,18 @@ type State = {
|
||||
}
|
||||
}
|
||||
|
||||
type VcsCache = {
|
||||
store: Store<{ value: VcsInfo | undefined }>
|
||||
setStore: SetStoreFunction<{ value: VcsInfo | undefined }>
|
||||
ready: Accessor<boolean>
|
||||
}
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const platform = usePlatform()
|
||||
const owner = getOwner()
|
||||
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||
const vcsCache = new Map<string, VcsCache>()
|
||||
const [globalStore, setGlobalStore] = createStore<{
|
||||
ready: boolean
|
||||
error?: InitError
|
||||
@@ -86,35 +109,51 @@ function createGlobalSync() {
|
||||
provider_auth: {},
|
||||
})
|
||||
|
||||
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||
|
||||
function child(directory: string) {
|
||||
if (!directory) console.error("No directory provided")
|
||||
if (!children[directory]) {
|
||||
children[directory] = createStore<State>({
|
||||
project: "",
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
status: "loading" as const,
|
||||
agent: [],
|
||||
command: [],
|
||||
session: [],
|
||||
sessionTotal: 0,
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
permission: {},
|
||||
question: {},
|
||||
mcp: {},
|
||||
lsp: [],
|
||||
vcs: undefined,
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
})
|
||||
bootstrapInstance(directory)
|
||||
const cache = runWithOwner(owner, () =>
|
||||
persisted(
|
||||
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
||||
createStore({ value: undefined as VcsInfo | undefined }),
|
||||
),
|
||||
)
|
||||
if (!cache) throw new Error("Failed to create persisted cache")
|
||||
vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })
|
||||
|
||||
const init = () => {
|
||||
children[directory] = createStore<State>({
|
||||
project: "",
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
status: "loading" as const,
|
||||
agent: [],
|
||||
command: [],
|
||||
session: [],
|
||||
sessionTotal: 0,
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
permission: {},
|
||||
question: {},
|
||||
mcp: {},
|
||||
lsp: [],
|
||||
vcs: cache[0].value,
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
})
|
||||
bootstrapInstance(directory)
|
||||
}
|
||||
|
||||
runWithOwner(owner, init)
|
||||
}
|
||||
return children[directory]
|
||||
const childStore = children[directory]
|
||||
if (!childStore) throw new Error("Failed to create store")
|
||||
return childStore
|
||||
}
|
||||
|
||||
async function loadSessions(directory: string) {
|
||||
@@ -124,12 +163,19 @@ function createGlobalSync() {
|
||||
return globalSDK.client.session
|
||||
.list({ directory, roots: true })
|
||||
.then((x) => {
|
||||
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
|
||||
const nonArchived = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.filter((s) => !s.time?.archived)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory))
|
||||
if (sandboxWorkspace) {
|
||||
setStore("session", reconcile(nonArchived, { key: "id" }))
|
||||
return
|
||||
}
|
||||
|
||||
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
|
||||
// Include up to the limit, plus any updated in the last 4 hours
|
||||
const sessions = nonArchived.filter((s, i) => {
|
||||
if (i < limit) return true
|
||||
@@ -150,6 +196,8 @@ function createGlobalSync() {
|
||||
async function bootstrapInstance(directory: string) {
|
||||
if (!directory) return
|
||||
const [store, setStore] = child(directory)
|
||||
const cache = vcsCache.get(directory)
|
||||
if (!cache) return
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
fetch: platform.fetch,
|
||||
@@ -157,6 +205,13 @@ function createGlobalSync() {
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!cache.ready()) return
|
||||
const cached = cache.store.value
|
||||
if (!cached?.branch) return
|
||||
setStore("vcs", (value) => value ?? cached)
|
||||
})
|
||||
|
||||
const blockingRequests = {
|
||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () =>
|
||||
@@ -186,7 +241,11 @@ function createGlobalSync() {
|
||||
loadSessions(directory),
|
||||
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
|
||||
sdk.vcs.get().then((x) => setStore("vcs", x.data)),
|
||||
sdk.vcs.get().then((x) => {
|
||||
const next = x.data ?? store.vcs
|
||||
setStore("vcs", next)
|
||||
if (next?.branch) cache.setStore("value", next)
|
||||
}),
|
||||
sdk.permission.list().then((x) => {
|
||||
const grouped: Record<string, PermissionRequest[]> = {}
|
||||
for (const perm of x.data ?? []) {
|
||||
@@ -292,6 +351,23 @@ function createGlobalSync() {
|
||||
bootstrapInstance(directory)
|
||||
break
|
||||
}
|
||||
case "session.created": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore("session", result.index, reconcile(event.properties.info))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
}),
|
||||
)
|
||||
if (!event.properties.info.parentID) {
|
||||
setStore("sessionTotal", store.sessionTotal + 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "session.updated": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (event.properties.info.time.archived) {
|
||||
@@ -399,7 +475,10 @@ function createGlobalSync() {
|
||||
break
|
||||
}
|
||||
case "vcs.branch.updated": {
|
||||
setStore("vcs", { branch: event.properties.branch })
|
||||
const next = { branch: event.properties.branch }
|
||||
setStore("vcs", next)
|
||||
const cache = vcsCache.get(directory)
|
||||
if (cache) cache.setStore("value", next)
|
||||
break
|
||||
}
|
||||
case "permission.asked": {
|
||||
|
||||
@@ -47,12 +47,34 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
const globalSdk = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const server = useServer()
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
|
||||
const migrate = (value: unknown) => {
|
||||
if (!isRecord(value)) return value
|
||||
const sidebar = value.sidebar
|
||||
if (!isRecord(sidebar)) return value
|
||||
if (typeof sidebar.workspaces !== "boolean") return value
|
||||
return {
|
||||
...value,
|
||||
sidebar: {
|
||||
...sidebar,
|
||||
workspaces: {},
|
||||
workspacesDefault: sidebar.workspaces,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const target = Persist.global("layout", ["layout.v6"])
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("layout", ["layout.v6"]),
|
||||
{ ...target, migrate },
|
||||
createStore({
|
||||
sidebar: {
|
||||
opened: false,
|
||||
width: 280,
|
||||
width: 344,
|
||||
workspaces: {} as Record<string, boolean>,
|
||||
workspacesDefault: false,
|
||||
},
|
||||
terminal: {
|
||||
height: 280,
|
||||
@@ -304,6 +326,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
resize(width: number) {
|
||||
setStore("sidebar", "width", width)
|
||||
},
|
||||
workspaces(directory: string) {
|
||||
return createMemo(() => store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false)
|
||||
},
|
||||
setWorkspaces(directory: string, value: boolean) {
|
||||
setStore("sidebar", "workspaces", directory, value)
|
||||
},
|
||||
toggleWorkspaces(directory: string) {
|
||||
const current = store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false
|
||||
setStore("sidebar", "workspaces", directory, !current)
|
||||
},
|
||||
},
|
||||
terminal: {
|
||||
height: createMemo(() => store.terminal.height),
|
||||
|
||||
@@ -5,6 +5,9 @@ export type Platform = {
|
||||
/** Platform discriminator */
|
||||
platform: "web" | "desktop"
|
||||
|
||||
/** Desktop OS (Tauri only) */
|
||||
os?: "macos" | "windows" | "linux"
|
||||
|
||||
/** App version */
|
||||
version?: string
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
createStore({
|
||||
list: [] as string[],
|
||||
projects: {} as Record<string, StoredProject[]>,
|
||||
lastProject: {} as Record<string, string>,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -197,6 +198,16 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
result.splice(toIndex, 0, item)
|
||||
setStore("projects", key, result)
|
||||
},
|
||||
last() {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
return store.lastProject[key]
|
||||
},
|
||||
touch(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
setStore("lastProject", key, directory)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const sdk = useSDK()
|
||||
const [store, setStore] = globalSync.child(sdk.directory)
|
||||
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
|
||||
const chunk = 200
|
||||
const chunk = 400
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const inflightDiff = new Map<string, Promise<void>>()
|
||||
const inflightTodo = new Map<string, Promise<void>>()
|
||||
|
||||
@@ -5,3 +5,7 @@
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
*[data-tauri-drag-region] {
|
||||
app-region: drag;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
@@ -12,6 +11,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
|
||||
export default function Home() {
|
||||
const sync = useGlobalSync()
|
||||
@@ -24,6 +24,7 @@ export default function Home() {
|
||||
|
||||
function openProject(directory: string) {
|
||||
layout.projects.open(directory)
|
||||
server.projects.touch(directory)
|
||||
navigate(`/${base64Encode(directory)}`)
|
||||
}
|
||||
|
||||
|
||||
+1024
-540
File diff suppressed because it is too large
Load Diff
@@ -428,7 +428,7 @@ export default function Page() {
|
||||
{
|
||||
id: "file.open",
|
||||
title: "Open file",
|
||||
description: "Search and open a file",
|
||||
description: "Search files and commands",
|
||||
category: "File",
|
||||
keybind: "mod+p",
|
||||
slash: "open",
|
||||
@@ -885,6 +885,19 @@ export default function Page() {
|
||||
window.history.replaceState(null, "", `#${anchor(id)}`)
|
||||
}
|
||||
|
||||
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
|
||||
const root = scroller
|
||||
if (!root) {
|
||||
el.scrollIntoView({ behavior, block: "start" })
|
||||
return
|
||||
}
|
||||
|
||||
const a = el.getBoundingClientRect()
|
||||
const b = root.getBoundingClientRect()
|
||||
const top = a.top - b.top + root.scrollTop
|
||||
root.scrollTo({ top, behavior })
|
||||
}
|
||||
|
||||
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||
setActiveMessage(message)
|
||||
|
||||
@@ -896,7 +909,7 @@ export default function Page() {
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById(anchor(message.id))
|
||||
if (el) el.scrollIntoView({ behavior, block: "start" })
|
||||
if (el) scrollToElement(el, behavior)
|
||||
})
|
||||
|
||||
updateHash(message.id)
|
||||
@@ -904,7 +917,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const el = document.getElementById(anchor(message.id))
|
||||
if (el) el.scrollIntoView({ behavior, block: "start" })
|
||||
if (el) scrollToElement(el, behavior)
|
||||
updateHash(message.id)
|
||||
}
|
||||
|
||||
@@ -956,7 +969,7 @@ export default function Page() {
|
||||
|
||||
const hashTarget = document.getElementById(hash)
|
||||
if (hashTarget) {
|
||||
hashTarget.scrollIntoView({ behavior: "auto", block: "start" })
|
||||
scrollToElement(hashTarget, "auto")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@@ -20,6 +20,8 @@
|
||||
"@opencode-ai/console-mail": "workspace:*",
|
||||
"@opencode-ai/console-resource": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.7",
|
||||
"@smithy/util-utf8": "4.2.0",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
@@ -34,6 +36,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@webgpu/types": "0.1.54",
|
||||
"typescript": "catalog:",
|
||||
"wrangler": "4.50.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.spotlight-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50dvh;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spotlight-container canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,820 @@
|
||||
import { createSignal, createEffect, onMount, onCleanup, Accessor } from "solid-js"
|
||||
import "./spotlight.css"
|
||||
|
||||
export interface ParticlesConfig {
|
||||
enabled: boolean
|
||||
amount: number
|
||||
size: [number, number]
|
||||
speed: number
|
||||
opacity: number
|
||||
drift: number
|
||||
}
|
||||
|
||||
export interface SpotlightConfig {
|
||||
placement: [number, number]
|
||||
color: string
|
||||
speed: number
|
||||
spread: number
|
||||
length: number
|
||||
width: number
|
||||
pulsating: false | [number, number]
|
||||
distance: number
|
||||
saturation: number
|
||||
noiseAmount: number
|
||||
distortion: number
|
||||
opacity: number
|
||||
particles: ParticlesConfig
|
||||
}
|
||||
|
||||
export const defaultConfig: SpotlightConfig = {
|
||||
placement: [0.5, -0.15],
|
||||
color: "#ffffff",
|
||||
speed: 0.8,
|
||||
spread: 0.5,
|
||||
length: 4.0,
|
||||
width: 0.15,
|
||||
pulsating: [0.95, 1.1],
|
||||
distance: 3.5,
|
||||
saturation: 0.35,
|
||||
noiseAmount: 0.15,
|
||||
distortion: 0.05,
|
||||
opacity: 0.325,
|
||||
particles: {
|
||||
enabled: true,
|
||||
amount: 70,
|
||||
size: [1.25, 1.5],
|
||||
speed: 0.75,
|
||||
opacity: 0.9,
|
||||
drift: 1.5,
|
||||
},
|
||||
}
|
||||
|
||||
export interface SpotlightAnimationState {
|
||||
time: number
|
||||
intensity: number
|
||||
pulseValue: number
|
||||
}
|
||||
|
||||
interface SpotlightProps {
|
||||
config: Accessor<SpotlightConfig>
|
||||
class?: string
|
||||
onAnimationFrame?: (state: SpotlightAnimationState) => void
|
||||
}
|
||||
|
||||
const hexToRgb = (hex: string): [number, number, number] => {
|
||||
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1]
|
||||
}
|
||||
|
||||
const getAnchorAndDir = (
|
||||
placement: [number, number],
|
||||
w: number,
|
||||
h: number,
|
||||
): { anchor: [number, number]; dir: [number, number] } => {
|
||||
const [px, py] = placement
|
||||
const outside = 0.2
|
||||
|
||||
let anchorX = px * w
|
||||
let anchorY = py * h
|
||||
let dirX = 0
|
||||
let dirY = 0
|
||||
|
||||
const centerX = 0.5
|
||||
const centerY = 0.5
|
||||
|
||||
if (py <= 0.25) {
|
||||
anchorY = -outside * h + py * h
|
||||
dirY = 1
|
||||
dirX = (centerX - px) * 0.5
|
||||
} else if (py >= 0.75) {
|
||||
anchorY = (1 + outside) * h - (1 - py) * h
|
||||
dirY = -1
|
||||
dirX = (centerX - px) * 0.5
|
||||
} else if (px <= 0.25) {
|
||||
anchorX = -outside * w + px * w
|
||||
dirX = 1
|
||||
dirY = (centerY - py) * 0.5
|
||||
} else if (px >= 0.75) {
|
||||
anchorX = (1 + outside) * w - (1 - px) * w
|
||||
dirX = -1
|
||||
dirY = (centerY - py) * 0.5
|
||||
} else {
|
||||
dirY = 1
|
||||
}
|
||||
|
||||
const len = Math.sqrt(dirX * dirX + dirY * dirY)
|
||||
if (len > 0) {
|
||||
dirX /= len
|
||||
dirY /= len
|
||||
}
|
||||
|
||||
return { anchor: [anchorX, anchorY], dir: [dirX, dirY] }
|
||||
}
|
||||
|
||||
interface UniformData {
|
||||
iTime: number
|
||||
iResolution: [number, number]
|
||||
lightPos: [number, number]
|
||||
lightDir: [number, number]
|
||||
color: [number, number, number]
|
||||
speed: number
|
||||
lightSpread: number
|
||||
lightLength: number
|
||||
sourceWidth: number
|
||||
pulsating: number
|
||||
pulsatingMin: number
|
||||
pulsatingMax: number
|
||||
fadeDistance: number
|
||||
saturation: number
|
||||
noiseAmount: number
|
||||
distortion: number
|
||||
particlesEnabled: number
|
||||
particleAmount: number
|
||||
particleSizeMin: number
|
||||
particleSizeMax: number
|
||||
particleSpeed: number
|
||||
particleOpacity: number
|
||||
particleDrift: number
|
||||
}
|
||||
|
||||
const WGSL_SHADER = `
|
||||
struct Uniforms {
|
||||
iTime: f32,
|
||||
_pad0: f32,
|
||||
iResolution: vec2<f32>,
|
||||
lightPos: vec2<f32>,
|
||||
lightDir: vec2<f32>,
|
||||
color: vec3<f32>,
|
||||
speed: f32,
|
||||
lightSpread: f32,
|
||||
lightLength: f32,
|
||||
sourceWidth: f32,
|
||||
pulsating: f32,
|
||||
pulsatingMin: f32,
|
||||
pulsatingMax: f32,
|
||||
fadeDistance: f32,
|
||||
saturation: f32,
|
||||
noiseAmount: f32,
|
||||
distortion: f32,
|
||||
particlesEnabled: f32,
|
||||
particleAmount: f32,
|
||||
particleSizeMin: f32,
|
||||
particleSizeMax: f32,
|
||||
particleSpeed: f32,
|
||||
particleOpacity: f32,
|
||||
particleDrift: f32,
|
||||
_pad1: f32,
|
||||
_pad2: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) vUv: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
||||
var positions = array<vec2<f32>, 3>(
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>(3.0, -1.0),
|
||||
vec2<f32>(-1.0, 3.0)
|
||||
);
|
||||
|
||||
var output: VertexOutput;
|
||||
let pos = positions[vertexIndex];
|
||||
output.position = vec4<f32>(pos, 0.0, 1.0);
|
||||
output.vUv = pos * 0.5 + 0.5;
|
||||
return output;
|
||||
}
|
||||
|
||||
fn hash(p: vec2<f32>) -> f32 {
|
||||
let p3 = fract(p.xyx * 0.1031);
|
||||
return fract((p3.x + p3.y) * p3.z + dot(p3, p3.yzx + 33.33));
|
||||
}
|
||||
|
||||
fn hash2(p: vec2<f32>) -> vec2<f32> {
|
||||
let n = sin(dot(p, vec2<f32>(41.0, 289.0)));
|
||||
return fract(vec2<f32>(n * 262144.0, n * 32768.0));
|
||||
}
|
||||
|
||||
fn fastNoise(st: vec2<f32>) -> f32 {
|
||||
return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
fn lightStrengthCombined(lightSource: vec2<f32>, lightRefDirection: vec2<f32>, coord: vec2<f32>) -> f32 {
|
||||
let sourceToCoord = coord - lightSource;
|
||||
let distSq = dot(sourceToCoord, sourceToCoord);
|
||||
let distance = sqrt(distSq);
|
||||
|
||||
let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
|
||||
let maxDistance = max(baseSize * uniforms.lightLength, 0.001);
|
||||
if (distance > maxDistance) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let invDist = 1.0 / max(distance, 0.001);
|
||||
let dirNorm = sourceToCoord * invDist;
|
||||
let cosAngle = dot(dirNorm, lightRefDirection);
|
||||
|
||||
if (cosAngle < 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let side = dot(dirNorm, vec2<f32>(-lightRefDirection.y, lightRefDirection.x));
|
||||
let time = uniforms.iTime;
|
||||
let speed = uniforms.speed;
|
||||
|
||||
let asymNoise = fastNoise(vec2<f32>(side * 6.0 + time * 0.12, distance * 0.004 + cosAngle * 2.0));
|
||||
let asymShift = (asymNoise - 0.5) * uniforms.distortion * 0.6;
|
||||
|
||||
let distortPhase = time * 1.4 + distance * 0.006 + cosAngle * 4.5 + side * 1.7;
|
||||
let distortedAngle = cosAngle + uniforms.distortion * sin(distortPhase) * 0.22 + asymShift;
|
||||
|
||||
let flickerSeed = cosAngle * 9.0 + side * 4.0 + time * speed * 0.35;
|
||||
let flicker = 0.86 + fastNoise(vec2<f32>(flickerSeed, distance * 0.01)) * 0.28;
|
||||
|
||||
let asymSpread = max(uniforms.lightSpread * (0.9 + (asymNoise - 0.5) * 0.25), 0.001);
|
||||
let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / asymSpread);
|
||||
let lengthFalloff = clamp(1.0 - distance / maxDistance, 0.0, 1.0);
|
||||
|
||||
let fadeMaxDist = max(baseSize * uniforms.fadeDistance, 0.001);
|
||||
let fadeFalloff = clamp((fadeMaxDist - distance) / fadeMaxDist, 0.0, 1.0);
|
||||
|
||||
var pulse: f32 = 1.0;
|
||||
if (uniforms.pulsating > 0.5) {
|
||||
let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5;
|
||||
let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5;
|
||||
pulse = pulseCenter + pulseAmplitude * sin(time * speed * 3.0);
|
||||
}
|
||||
|
||||
let timeSpeed = time * speed;
|
||||
let wave = 0.5
|
||||
+ 0.25 * sin(cosAngle * 28.0 + side * 8.0 + timeSpeed * 1.2)
|
||||
+ 0.18 * cos(cosAngle * 22.0 - timeSpeed * 0.95 + side * 6.0)
|
||||
+ 0.12 * sin(cosAngle * 35.0 + timeSpeed * 1.6 + asymNoise * 3.0);
|
||||
let minStrength = 0.14 + asymNoise * 0.06;
|
||||
let baseStrength = max(clamp(wave * (0.85 + asymNoise * 0.3), 0.0, 1.0), minStrength);
|
||||
|
||||
let lightStrength = baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse * flicker;
|
||||
let ambientLight = (0.06 + asymNoise * 0.04) * lengthFalloff * fadeFalloff * spreadFactor;
|
||||
|
||||
return max(lightStrength, ambientLight);
|
||||
}
|
||||
|
||||
fn particle(coord: vec2<f32>, particlePos: vec2<f32>, size: f32) -> f32 {
|
||||
let delta = coord - particlePos;
|
||||
let distSq = dot(delta, delta);
|
||||
let sizeSq = size * size;
|
||||
|
||||
if (distSq > sizeSq * 9.0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let d = sqrt(distSq);
|
||||
let core = smoothstep(size, size * 0.35, d);
|
||||
let glow = smoothstep(size * 3.0, 0.0, d) * 0.55;
|
||||
return core + glow;
|
||||
}
|
||||
|
||||
fn renderParticles(coord: vec2<f32>, lightSource: vec2<f32>, lightDir: vec2<f32>) -> f32 {
|
||||
if (uniforms.particlesEnabled < 0.5 || uniforms.particleAmount < 1.0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
var particleSum: f32 = 0.0;
|
||||
let particleCount = i32(uniforms.particleAmount);
|
||||
let time = uniforms.iTime * uniforms.particleSpeed;
|
||||
let perpDir = vec2<f32>(-lightDir.y, lightDir.x);
|
||||
let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
|
||||
let maxDist = max(baseSize * uniforms.lightLength, 1.0);
|
||||
let spreadScale = uniforms.lightSpread * baseSize * 0.65;
|
||||
let coneHalfWidth = uniforms.lightSpread * baseSize * 0.55;
|
||||
|
||||
for (var i: i32 = 0; i < particleCount; i = i + 1) {
|
||||
let fi = f32(i);
|
||||
let seed = vec2<f32>(fi * 127.1, fi * 311.7);
|
||||
let rnd = hash2(seed);
|
||||
|
||||
let lifeDuration = 2.0 + hash(seed + vec2<f32>(19.0, 73.0)) * 3.0;
|
||||
let lifeOffset = hash(seed + vec2<f32>(91.0, 37.0)) * lifeDuration;
|
||||
let lifeProgress = fract((time + lifeOffset) / lifeDuration);
|
||||
|
||||
let fadeIn = smoothstep(0.0, 0.2, lifeProgress);
|
||||
let fadeOut = 1.0 - smoothstep(0.8, 1.0, lifeProgress);
|
||||
let lifeFade = fadeIn * fadeOut;
|
||||
if (lifeFade < 0.01) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let alongLight = rnd.x * maxDist * 0.8;
|
||||
let perpOffset = (rnd.y - 0.5) * spreadScale;
|
||||
|
||||
let floatPhase = rnd.y * 6.28318 + fi * 0.37;
|
||||
let floatSpeed = 0.35 + rnd.x * 0.9;
|
||||
let drift = vec2<f32>(
|
||||
sin(time * floatSpeed + floatPhase),
|
||||
cos(time * floatSpeed * 0.85 + floatPhase * 1.3)
|
||||
) * uniforms.particleDrift * baseSize * 0.08;
|
||||
|
||||
let wobble = vec2<f32>(
|
||||
sin(time * 1.4 + floatPhase * 2.1),
|
||||
cos(time * 1.1 + floatPhase * 1.6)
|
||||
) * uniforms.particleDrift * baseSize * 0.03;
|
||||
|
||||
let flowOffset = (rnd.x - 0.5) * baseSize * 0.12 + fract(time * 0.06 + rnd.y) * baseSize * 0.1;
|
||||
|
||||
let basePos = lightSource + lightDir * (alongLight + flowOffset) + perpDir * perpOffset + drift + wobble;
|
||||
|
||||
let toParticle = basePos - lightSource;
|
||||
let projLen = dot(toParticle, lightDir);
|
||||
if (projLen < 0.0 || projLen > maxDist) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sideDist = abs(dot(toParticle, perpDir));
|
||||
if (sideDist > coneHalfWidth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let size = mix(uniforms.particleSizeMin, uniforms.particleSizeMax, rnd.x);
|
||||
let twinkle = 0.7 + 0.3 * sin(time * (1.5 + rnd.y * 2.0) + floatPhase);
|
||||
let distFade = 1.0 - smoothstep(maxDist * 0.2, maxDist * 0.95, projLen);
|
||||
if (distFade < 0.01) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let p = particle(coord, basePos, size);
|
||||
if (p > 0.0) {
|
||||
particleSum = particleSum + p * lifeFade * twinkle * distFade * uniforms.particleOpacity;
|
||||
if (particleSum >= 1.0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return min(particleSum, 1.0);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragmentMain(@builtin(position) fragCoord: vec4<f32>, @location(0) vUv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
let coord = vec2<f32>(fragCoord.x, fragCoord.y);
|
||||
|
||||
let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5;
|
||||
let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x;
|
||||
|
||||
let perpDir = vec2<f32>(-uniforms.lightDir.y, uniforms.lightDir.x);
|
||||
let adjustedLightPos = uniforms.lightPos + perpDir * widthOffset;
|
||||
|
||||
let lightValue = lightStrengthCombined(adjustedLightPos, uniforms.lightDir, coord);
|
||||
|
||||
if (lightValue < 0.001) {
|
||||
let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
|
||||
if (particles < 0.001) {
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
let particleBrightness = particles * 1.8;
|
||||
return vec4<f32>(uniforms.color * particleBrightness, particles * 0.9);
|
||||
}
|
||||
|
||||
var fragColor = vec4<f32>(lightValue, lightValue, lightValue, lightValue);
|
||||
|
||||
if (uniforms.noiseAmount > 0.01) {
|
||||
let n = fastNoise(coord * 0.5 + uniforms.iTime * 0.5);
|
||||
let grain = mix(1.0, n, uniforms.noiseAmount * 0.5);
|
||||
fragColor = vec4<f32>(fragColor.rgb * grain, fragColor.a);
|
||||
}
|
||||
|
||||
let brightness = 1.0 - (coord.y / uniforms.iResolution.y);
|
||||
fragColor = vec4<f32>(
|
||||
fragColor.x * (0.15 + brightness * 0.85),
|
||||
fragColor.y * (0.35 + brightness * 0.65),
|
||||
fragColor.z * (0.55 + brightness * 0.45),
|
||||
fragColor.a
|
||||
);
|
||||
|
||||
if (abs(uniforms.saturation - 1.0) > 0.01) {
|
||||
let gray = dot(fragColor.rgb, vec3<f32>(0.299, 0.587, 0.114));
|
||||
fragColor = vec4<f32>(mix(vec3<f32>(gray), fragColor.rgb, uniforms.saturation), fragColor.a);
|
||||
}
|
||||
|
||||
fragColor = vec4<f32>(fragColor.rgb * uniforms.color, fragColor.a);
|
||||
|
||||
let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
|
||||
if (particles > 0.001) {
|
||||
let particleBrightness = particles * 1.8;
|
||||
fragColor = vec4<f32>(fragColor.rgb + uniforms.color * particleBrightness, max(fragColor.a, particles * 0.9));
|
||||
}
|
||||
|
||||
return fragColor;
|
||||
}
|
||||
`
|
||||
|
||||
const UNIFORM_BUFFER_SIZE = 144
|
||||
|
||||
function updateUniformBuffer(buffer: Float32Array, data: UniformData): void {
|
||||
buffer[0] = data.iTime
|
||||
buffer[2] = data.iResolution[0]
|
||||
buffer[3] = data.iResolution[1]
|
||||
buffer[4] = data.lightPos[0]
|
||||
buffer[5] = data.lightPos[1]
|
||||
buffer[6] = data.lightDir[0]
|
||||
buffer[7] = data.lightDir[1]
|
||||
buffer[8] = data.color[0]
|
||||
buffer[9] = data.color[1]
|
||||
buffer[10] = data.color[2]
|
||||
buffer[11] = data.speed
|
||||
buffer[12] = data.lightSpread
|
||||
buffer[13] = data.lightLength
|
||||
buffer[14] = data.sourceWidth
|
||||
buffer[15] = data.pulsating
|
||||
buffer[16] = data.pulsatingMin
|
||||
buffer[17] = data.pulsatingMax
|
||||
buffer[18] = data.fadeDistance
|
||||
buffer[19] = data.saturation
|
||||
buffer[20] = data.noiseAmount
|
||||
buffer[21] = data.distortion
|
||||
buffer[22] = data.particlesEnabled
|
||||
buffer[23] = data.particleAmount
|
||||
buffer[24] = data.particleSizeMin
|
||||
buffer[25] = data.particleSizeMax
|
||||
buffer[26] = data.particleSpeed
|
||||
buffer[27] = data.particleOpacity
|
||||
buffer[28] = data.particleDrift
|
||||
}
|
||||
|
||||
export default function Spotlight(props: SpotlightProps) {
|
||||
let containerRef: HTMLDivElement | undefined
|
||||
let canvasRef: HTMLCanvasElement | null = null
|
||||
let deviceRef: GPUDevice | null = null
|
||||
let contextRef: GPUCanvasContext | null = null
|
||||
let pipelineRef: GPURenderPipeline | null = null
|
||||
let uniformBufferRef: GPUBuffer | null = null
|
||||
let bindGroupRef: GPUBindGroup | null = null
|
||||
let animationIdRef: number | null = null
|
||||
let cleanupFunctionRef: (() => void) | null = null
|
||||
let uniformDataRef: UniformData | null = null
|
||||
let uniformArrayRef: Float32Array | null = null
|
||||
let configRef: SpotlightConfig = props.config()
|
||||
let frameCount = 0
|
||||
|
||||
const [isVisible, setIsVisible] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
configRef = props.config()
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (!containerRef) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
setIsVisible(entry.isIntersecting)
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
)
|
||||
|
||||
observer.observe(containerRef)
|
||||
|
||||
onCleanup(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const visible = isVisible()
|
||||
const config = props.config()
|
||||
if (!visible || !containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
if (cleanupFunctionRef) {
|
||||
cleanupFunctionRef()
|
||||
cleanupFunctionRef = null
|
||||
}
|
||||
|
||||
const initializeWebGPU = async () => {
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!navigator.gpu) {
|
||||
console.warn("WebGPU is not supported in this browser")
|
||||
return
|
||||
}
|
||||
|
||||
const adapter = await navigator.gpu.requestAdapter({
|
||||
powerPreference: "high-performance",
|
||||
})
|
||||
if (!adapter) {
|
||||
console.warn("Failed to get WebGPU adapter")
|
||||
return
|
||||
}
|
||||
|
||||
const device = await adapter.requestDevice()
|
||||
deviceRef = device
|
||||
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.style.width = "100%"
|
||||
canvas.style.height = "100%"
|
||||
canvasRef = canvas
|
||||
|
||||
while (containerRef.firstChild) {
|
||||
containerRef.removeChild(containerRef.firstChild)
|
||||
}
|
||||
containerRef.appendChild(canvas)
|
||||
|
||||
const context = canvas.getContext("webgpu")
|
||||
if (!context) {
|
||||
console.warn("Failed to get WebGPU context")
|
||||
return
|
||||
}
|
||||
contextRef = context
|
||||
|
||||
const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
|
||||
context.configure({
|
||||
device,
|
||||
format: presentationFormat,
|
||||
alphaMode: "premultiplied",
|
||||
})
|
||||
|
||||
const shaderModule = device.createShaderModule({
|
||||
code: WGSL_SHADER,
|
||||
})
|
||||
|
||||
const uniformBuffer = device.createBuffer({
|
||||
size: UNIFORM_BUFFER_SIZE,
|
||||
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||
})
|
||||
uniformBufferRef = uniformBuffer
|
||||
|
||||
const bindGroupLayout = device.createBindGroupLayout({
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
||||
buffer: { type: "uniform" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const bindGroup = device.createBindGroup({
|
||||
layout: bindGroupLayout,
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: { buffer: uniformBuffer },
|
||||
},
|
||||
],
|
||||
})
|
||||
bindGroupRef = bindGroup
|
||||
|
||||
const pipelineLayout = device.createPipelineLayout({
|
||||
bindGroupLayouts: [bindGroupLayout],
|
||||
})
|
||||
|
||||
const pipeline = device.createRenderPipeline({
|
||||
layout: pipelineLayout,
|
||||
vertex: {
|
||||
module: shaderModule,
|
||||
entryPoint: "vertexMain",
|
||||
},
|
||||
fragment: {
|
||||
module: shaderModule,
|
||||
entryPoint: "fragmentMain",
|
||||
targets: [
|
||||
{
|
||||
format: presentationFormat,
|
||||
blend: {
|
||||
color: {
|
||||
srcFactor: "src-alpha",
|
||||
dstFactor: "one-minus-src-alpha",
|
||||
operation: "add",
|
||||
},
|
||||
alpha: {
|
||||
srcFactor: "one",
|
||||
dstFactor: "one-minus-src-alpha",
|
||||
operation: "add",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: "triangle-list",
|
||||
},
|
||||
})
|
||||
pipelineRef = pipeline
|
||||
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const w = wCSS * dpr
|
||||
const h = hCSS * dpr
|
||||
const { anchor, dir } = getAnchorAndDir(config.placement, w, h)
|
||||
|
||||
uniformDataRef = {
|
||||
iTime: 0,
|
||||
iResolution: [w, h],
|
||||
lightPos: anchor,
|
||||
lightDir: dir,
|
||||
color: hexToRgb(config.color),
|
||||
speed: config.speed,
|
||||
lightSpread: config.spread,
|
||||
lightLength: config.length,
|
||||
sourceWidth: config.width,
|
||||
pulsating: config.pulsating !== false ? 1.0 : 0.0,
|
||||
pulsatingMin: config.pulsating !== false ? config.pulsating[0] : 1.0,
|
||||
pulsatingMax: config.pulsating !== false ? config.pulsating[1] : 1.0,
|
||||
fadeDistance: config.distance,
|
||||
saturation: config.saturation,
|
||||
noiseAmount: config.noiseAmount,
|
||||
distortion: config.distortion,
|
||||
particlesEnabled: config.particles.enabled ? 1.0 : 0.0,
|
||||
particleAmount: config.particles.amount,
|
||||
particleSizeMin: config.particles.size[0],
|
||||
particleSizeMax: config.particles.size[1],
|
||||
particleSpeed: config.particles.speed,
|
||||
particleOpacity: config.particles.opacity,
|
||||
particleDrift: config.particles.drift,
|
||||
}
|
||||
|
||||
const updatePlacement = () => {
|
||||
if (!containerRef || !canvasRef || !uniformDataRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const w = Math.floor(wCSS * dpr)
|
||||
const h = Math.floor(hCSS * dpr)
|
||||
|
||||
canvasRef.width = w
|
||||
canvasRef.height = h
|
||||
|
||||
uniformDataRef.iResolution = [w, h]
|
||||
|
||||
const { anchor, dir } = getAnchorAndDir(configRef.placement, w, h)
|
||||
uniformDataRef.lightPos = anchor
|
||||
uniformDataRef.lightDir = dir
|
||||
}
|
||||
|
||||
const loop = (t: number) => {
|
||||
if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeSeconds = t * 0.001
|
||||
uniformDataRef.iTime = timeSeconds
|
||||
frameCount++
|
||||
|
||||
if (props.onAnimationFrame && frameCount % 2 === 0) {
|
||||
const pulsatingMin = configRef.pulsating !== false ? configRef.pulsating[0] : 1.0
|
||||
const pulsatingMax = configRef.pulsating !== false ? configRef.pulsating[1] : 1.0
|
||||
const pulseCenter = (pulsatingMin + pulsatingMax) * 0.5
|
||||
const pulseAmplitude = (pulsatingMax - pulsatingMin) * 0.5
|
||||
const pulseValue =
|
||||
configRef.pulsating !== false
|
||||
? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * configRef.speed * 3.0)
|
||||
: 1.0
|
||||
|
||||
const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * configRef.speed * 1.5)
|
||||
const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * configRef.speed * 1.1)
|
||||
const intensity = Math.max((baseIntensity1 + baseIntensity2) * pulseValue, 0.55)
|
||||
|
||||
props.onAnimationFrame({
|
||||
time: timeSeconds,
|
||||
intensity,
|
||||
pulseValue: Math.max(pulseValue, 0.9),
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
if (!uniformArrayRef) {
|
||||
uniformArrayRef = new Float32Array(36)
|
||||
}
|
||||
updateUniformBuffer(uniformArrayRef, uniformDataRef)
|
||||
deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformArrayRef.buffer)
|
||||
|
||||
const commandEncoder = deviceRef.createCommandEncoder()
|
||||
|
||||
const textureView = contextRef.getCurrentTexture().createView()
|
||||
|
||||
const renderPass = commandEncoder.beginRenderPass({
|
||||
colorAttachments: [
|
||||
{
|
||||
view: textureView,
|
||||
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
||||
loadOp: "clear",
|
||||
storeOp: "store",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
renderPass.setPipeline(pipelineRef)
|
||||
renderPass.setBindGroup(0, bindGroupRef)
|
||||
renderPass.draw(3)
|
||||
renderPass.end()
|
||||
|
||||
deviceRef.queue.submit([commandEncoder.finish()])
|
||||
|
||||
animationIdRef = requestAnimationFrame(loop)
|
||||
} catch (error) {
|
||||
console.warn("WebGPU rendering error:", error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", updatePlacement)
|
||||
updatePlacement()
|
||||
animationIdRef = requestAnimationFrame(loop)
|
||||
|
||||
cleanupFunctionRef = () => {
|
||||
if (animationIdRef) {
|
||||
cancelAnimationFrame(animationIdRef)
|
||||
animationIdRef = null
|
||||
}
|
||||
|
||||
window.removeEventListener("resize", updatePlacement)
|
||||
|
||||
if (uniformBufferRef) {
|
||||
uniformBufferRef.destroy()
|
||||
uniformBufferRef = null
|
||||
}
|
||||
|
||||
if (deviceRef) {
|
||||
deviceRef.destroy()
|
||||
deviceRef = null
|
||||
}
|
||||
|
||||
if (canvasRef && canvasRef.parentNode) {
|
||||
canvasRef.parentNode.removeChild(canvasRef)
|
||||
}
|
||||
|
||||
canvasRef = null
|
||||
contextRef = null
|
||||
pipelineRef = null
|
||||
bindGroupRef = null
|
||||
uniformDataRef = null
|
||||
}
|
||||
}
|
||||
|
||||
initializeWebGPU()
|
||||
|
||||
onCleanup(() => {
|
||||
if (cleanupFunctionRef) {
|
||||
cleanupFunctionRef()
|
||||
cleanupFunctionRef = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!uniformDataRef || !containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const config = props.config()
|
||||
|
||||
uniformDataRef.color = hexToRgb(config.color)
|
||||
uniformDataRef.speed = config.speed
|
||||
uniformDataRef.lightSpread = config.spread
|
||||
uniformDataRef.lightLength = config.length
|
||||
uniformDataRef.sourceWidth = config.width
|
||||
uniformDataRef.pulsating = config.pulsating !== false ? 1.0 : 0.0
|
||||
uniformDataRef.pulsatingMin = config.pulsating !== false ? config.pulsating[0] : 1.0
|
||||
uniformDataRef.pulsatingMax = config.pulsating !== false ? config.pulsating[1] : 1.0
|
||||
uniformDataRef.fadeDistance = config.distance
|
||||
uniformDataRef.saturation = config.saturation
|
||||
uniformDataRef.noiseAmount = config.noiseAmount
|
||||
uniformDataRef.distortion = config.distortion
|
||||
uniformDataRef.particlesEnabled = config.particles.enabled ? 1.0 : 0.0
|
||||
uniformDataRef.particleAmount = config.particles.amount
|
||||
uniformDataRef.particleSizeMin = config.particles.size[0]
|
||||
uniformDataRef.particleSizeMax = config.particles.size[1]
|
||||
uniformDataRef.particleSpeed = config.particles.speed
|
||||
uniformDataRef.particleOpacity = config.particles.opacity
|
||||
uniformDataRef.particleDrift = config.particles.drift
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const { anchor, dir } = getAnchorAndDir(config.placement, wCSS * dpr, hCSS * dpr)
|
||||
uniformDataRef.lightPos = anchor
|
||||
uniformDataRef.lightDir = dir
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={`spotlight-container ${props.class ?? ""}`.trim()}
|
||||
style={{ opacity: props.config().opacity }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -9,8 +9,8 @@ export const config = {
|
||||
github: {
|
||||
repoUrl: "https://github.com/anomalyco/opencode",
|
||||
starsFormatted: {
|
||||
compact: "60K",
|
||||
full: "60,000",
|
||||
compact: "70K",
|
||||
full: "70,000",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ export const config = {
|
||||
// Static stats (used on landing page)
|
||||
stats: {
|
||||
contributors: "500",
|
||||
commits: "6,500",
|
||||
commits: "7,000",
|
||||
monthlyUsers: "650,000",
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -14,13 +14,14 @@ export const github = query(async () => {
|
||||
fetch(`${apiBaseUrl}/releases`, { headers }).then((res) => res.json()),
|
||||
fetch(`${apiBaseUrl}/contributors?per_page=1`, { headers }),
|
||||
])
|
||||
if (!Array.isArray(releases) || releases.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
const [release] = releases
|
||||
const contributorCount = Number.parseInt(
|
||||
contributors.headers
|
||||
.get("Link")!
|
||||
.match(/&page=(\d+)>; rel="last"/)!
|
||||
.at(1)!,
|
||||
)
|
||||
const linkHeader = contributors.headers.get("Link")
|
||||
const contributorCount = linkHeader
|
||||
? Number.parseInt(linkHeader.match(/&page=(\d+)>; rel="last"/)?.at(1) ?? "0")
|
||||
: 0
|
||||
return {
|
||||
stars: meta.stargazers_count,
|
||||
release: {
|
||||
|
||||
@@ -1,3 +1,114 @@
|
||||
::view-transition-group(*) {
|
||||
animation-duration: 250ms;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation-duration: 250ms;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
::view-transition-image-pair(root) {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reveal-terms {
|
||||
from {
|
||||
mask-position: 0% 200%;
|
||||
}
|
||||
to {
|
||||
mask-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hide-terms {
|
||||
from {
|
||||
mask-position: 0% 50%;
|
||||
}
|
||||
to {
|
||||
mask-position: 0% 200%;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(terms-20),
|
||||
::view-transition-old(terms-100),
|
||||
::view-transition-old(terms-200) {
|
||||
mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent);
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 100% 200%;
|
||||
animation: hide-terms 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
|
||||
}
|
||||
|
||||
::view-transition-new(terms-20),
|
||||
::view-transition-new(terms-100),
|
||||
::view-transition-new(terms-200) {
|
||||
mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent);
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 0% 200%;
|
||||
mask-size: 100% 200%;
|
||||
animation: reveal-terms 300ms cubic-bezier(0.25, 0, 0.5, 1) 50ms forwards;
|
||||
}
|
||||
|
||||
::view-transition-old(actions-20),
|
||||
::view-transition-old(actions-100),
|
||||
::view-transition-old(actions-200) {
|
||||
animation: fade-out 80ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
::view-transition-new(actions-20),
|
||||
::view-transition-new(actions-100),
|
||||
::view-transition-new(actions-200) {
|
||||
animation: fade-in-up 300ms cubic-bezier(0.16, 1, 0.3, 1) 300ms forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
::view-transition-group(card-20),
|
||||
::view-transition-group(card-100),
|
||||
::view-transition-group(card-200) {
|
||||
animation-duration: 250ms;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
[data-page="black"] {
|
||||
background: #000;
|
||||
min-height: 100vh;
|
||||
@@ -8,13 +119,18 @@
|
||||
font-family: var(--font-mono);
|
||||
color: #fff;
|
||||
|
||||
[data-component="header-gradient"] {
|
||||
[data-component="header-logo"] {
|
||||
filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.25)) drop-shadow(0 4px 16px rgba(0, 0, 0, 0.1));
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header-light-rays {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 288px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
inset: 0 0 auto 0;
|
||||
height: 30dvh;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
[data-component="header"] {
|
||||
@@ -48,27 +164,35 @@
|
||||
|
||||
h1 {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
line-height: 1.45;
|
||||
margin: 0;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
line-height: 1.45;
|
||||
margin: 0;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,30 +200,36 @@
|
||||
[data-slot="hero-black"] {
|
||||
margin-top: 40px;
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
svg {
|
||||
--hero-black-fill-from: hsl(0 0% 100%);
|
||||
--hero-black-fill-to: hsl(0 0% 100% / 0%);
|
||||
--hero-black-stroke-from: hsl(0 0% 100% / 60%);
|
||||
--hero-black-stroke-to: hsl(0 0% 100% / 0%);
|
||||
|
||||
width: 100%;
|
||||
max-width: 590px;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.1));
|
||||
overflow: visible;
|
||||
filter: drop-shadow(0 0 20px rgba(255, 255, 255, calc(0.1 + var(--hero-black-glow-intensity, 0) * 0.15)))
|
||||
drop-shadow(0 -5px 30px rgba(255, 255, 255, calc(var(--hero-black-glow-intensity, 0) * 0.2)));
|
||||
mask-image: linear-gradient(to bottom, black, transparent);
|
||||
stroke-width: 1.5;
|
||||
|
||||
[data-slot="black-fill"] {
|
||||
[data-slot="black-base"] {
|
||||
fill: url(#hero-black-fill-gradient);
|
||||
stroke: url(#hero-black-stroke-gradient);
|
||||
}
|
||||
|
||||
[data-slot="black-stroke"] {
|
||||
fill: url(#hero-black-stroke-gradient);
|
||||
[data-slot="black-glow"] {
|
||||
fill: url(#hero-black-top-glow);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-slot="black-shimmer"] {
|
||||
fill: url(#hero-black-shimmer-gradient);
|
||||
pointer-events: none;
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,14 +237,14 @@
|
||||
[data-slot="cta"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-top: -32px;
|
||||
margin-top: -40px;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: -16px;
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
[data-slot="heading"] {
|
||||
@@ -129,7 +259,6 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="subheading"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 15px;
|
||||
@@ -142,7 +271,6 @@
|
||||
line-height: 160%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="button"] {
|
||||
display: inline-flex;
|
||||
height: 40px;
|
||||
@@ -154,7 +282,7 @@
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
font-family: var(--font-mono);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
@@ -168,16 +296,14 @@
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="back-soon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
line-height: 160%; /* 20.8px */
|
||||
}
|
||||
|
||||
[data-slot="follow-us"] {
|
||||
display: inline-flex;
|
||||
height: 40px;
|
||||
@@ -201,98 +327,38 @@
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
max-width: 660px;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="pricing-card"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
border-radius: 5px;
|
||||
background: black;
|
||||
background-clip: padding-box;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
background: #000;
|
||||
transition: border-color 0.15s ease;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
transition: border-color 200ms ease;
|
||||
|
||||
&:hover:not([data-selected="true"]) {
|
||||
@media (max-width: 480px) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
&:hover:not(:active) {
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
[data-slot="card-trigger"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
transition: padding 200ms ease;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-selected="true"] {
|
||||
[data-slot="amount"] {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
[data-slot="terms"] {
|
||||
animation: reveal 500ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
|
||||
}
|
||||
|
||||
[data-slot="actions"] {
|
||||
[data-slot="continue"] {
|
||||
animation-delay: 200ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-collapsed="true"] {
|
||||
[data-slot="card-trigger"] {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
[data-slot="plan-header"] {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-selected="false"][data-collapsed="false"] {
|
||||
[data-slot="amount"] {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
[data-slot="period"],
|
||||
[data-slot="multiplier"] {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="plan-header"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
transition: gap 200ms ease;
|
||||
}
|
||||
|
||||
[data-slot="plan-icon"] {
|
||||
[data-slot="icon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="price"] {
|
||||
@@ -300,31 +366,81 @@
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
line-height: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="content"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="period"],
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
}
|
||||
|
||||
[data-slot="billing"] {
|
||||
[data-slot="period"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: "·";
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="selected-plan"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
z-index: 1;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
margin: 0 20px;
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="selected-card"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
|
||||
[data-slot="icon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
}
|
||||
|
||||
[data-slot="price"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="period"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: "·";
|
||||
@@ -334,32 +450,30 @@
|
||||
|
||||
[data-slot="terms"] {
|
||||
list-style: none;
|
||||
padding: 0 24px 24px 24px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 100%);
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 100% 200%;
|
||||
mask-position: 0% 320%;
|
||||
}
|
||||
|
||||
[data-slot="terms"] li {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 13px;
|
||||
line-height: 1.2;
|
||||
padding-left: 16px;
|
||||
position: relative;
|
||||
li {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding-left: 16px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "▪";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
&::before {
|
||||
content: "▪";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,48 +481,45 @@
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
padding: 0 24px 24px 24px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="actions"] button,
|
||||
[data-slot="actions"] a {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition-property: background-color, border-color;
|
||||
transition-duration: 200ms;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
|
||||
}
|
||||
|
||||
[data-slot="cancel"] {
|
||||
border: 1px solid var(--border-base, rgba(255, 255, 255, 0.17));
|
||||
background: var(--surface-raised-base, rgba(255, 255, 255, 0.06));
|
||||
background-clip: border-box;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
|
||||
&:hover {
|
||||
background: var(--surface-raised-base, rgba(255, 255, 255, 0.08));
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
button,
|
||||
a {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="continue"] {
|
||||
background: rgb(255, 255, 255);
|
||||
color: rgb(0, 0, 0);
|
||||
[data-slot="cancel"] {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
transition-property: background-color, border-color;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
|
||||
|
||||
&:hover {
|
||||
background: rgb(255, 255, 255, 0.9);
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="continue"] {
|
||||
background: rgb(255, 255, 255);
|
||||
color: rgb(0, 0, 0);
|
||||
transition: background-color 150ms cubic-bezier(0.25, 0, 0.5, 1);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,7 +530,8 @@
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
line-height: 160%; /* 20.8px */
|
||||
font-style: italic;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
@@ -436,7 +548,7 @@
|
||||
align-items: center;
|
||||
margin-top: -18px;
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
max-width: 660px;
|
||||
padding: 0 20px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -469,6 +581,8 @@
|
||||
|
||||
[data-slot="icon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
isolation: isolate;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
[data-slot="price"] {
|
||||
@@ -491,7 +605,7 @@
|
||||
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: "·";
|
||||
@@ -510,39 +624,6 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
[data-slot="tax-id-section"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
[data-slot="label"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="input"] {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0 12px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="checkout-form"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -583,52 +664,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-slot="success"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
[data-slot="title"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot="details"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
dd {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="charge-notice"] {
|
||||
color: #d4a500;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="loading"] {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -645,6 +680,7 @@
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
view-transition-name: fine-print;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
@@ -739,7 +775,7 @@
|
||||
span,
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: var(--font-mono);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
@@ -749,7 +785,7 @@
|
||||
|
||||
[data-slot="github-stars"] {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
font-family: var(--font-mono);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
@@ -764,10 +800,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="anomaly-alt"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: var(--font-mono);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
@@ -777,7 +812,7 @@
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: "JetBrains Mono Nerd Font", monospace;
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
@@ -791,15 +826,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-group(*) {
|
||||
animation-duration: 200ms;
|
||||
animation-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
|
||||
}
|
||||
|
||||
@keyframes reveal {
|
||||
100% {
|
||||
mask-position: 0% 0%;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { A, createAsync, RouteSectionProps } from "@solidjs/router"
|
||||
import { Title, Meta, Link } from "@solidjs/meta"
|
||||
import { createMemo } from "solid-js"
|
||||
import { createMemo, createSignal } from "solid-js"
|
||||
import { github } from "~/lib/github"
|
||||
import { config } from "~/config"
|
||||
import Spotlight, { defaultConfig, type SpotlightAnimationState } from "~/component/spotlight"
|
||||
import "./black.css"
|
||||
|
||||
export default function BlackLayout(props: RouteSectionProps) {
|
||||
@@ -16,6 +17,50 @@ export default function BlackLayout(props: RouteSectionProps) {
|
||||
: config.github.starsFormatted.compact,
|
||||
)
|
||||
|
||||
const [spotlightAnimationState, setSpotlightAnimationState] = createSignal<SpotlightAnimationState>({
|
||||
time: 0,
|
||||
intensity: 0.5,
|
||||
pulseValue: 1,
|
||||
})
|
||||
|
||||
const svgLightingValues = createMemo(() => {
|
||||
const state = spotlightAnimationState()
|
||||
const t = state.time
|
||||
|
||||
const wave1 = Math.sin(t * 1.5) * 0.5 + 0.5
|
||||
const wave2 = Math.sin(t * 2.3 + 1.2) * 0.5 + 0.5
|
||||
const wave3 = Math.sin(t * 0.8 + 2.5) * 0.5 + 0.5
|
||||
|
||||
const shimmerPos = Math.sin(t * 0.7) * 0.5 + 0.5
|
||||
const glowIntensity = Math.max(state.intensity * state.pulseValue * 0.35, 0.15)
|
||||
const fillOpacity = Math.max(0.1 + wave1 * 0.08 * state.pulseValue, 0.12)
|
||||
const strokeBrightness = Math.max(55 + wave2 * 25 * state.pulseValue, 60)
|
||||
|
||||
const shimmerIntensity = Math.max(wave3 * 0.15 * state.pulseValue, 0.08)
|
||||
|
||||
return {
|
||||
glowIntensity,
|
||||
fillOpacity,
|
||||
strokeBrightness,
|
||||
shimmerPos,
|
||||
shimmerIntensity,
|
||||
}
|
||||
})
|
||||
|
||||
const svgLightingStyle = createMemo(() => {
|
||||
const values = svgLightingValues()
|
||||
return {
|
||||
"--hero-black-glow-intensity": values.glowIntensity.toFixed(3),
|
||||
"--hero-black-stroke-brightness": `${values.strokeBrightness.toFixed(0)}%`,
|
||||
} as Record<string, string>
|
||||
})
|
||||
|
||||
const handleAnimationFrame = (state: SpotlightAnimationState) => {
|
||||
setSpotlightAnimationState(state)
|
||||
}
|
||||
|
||||
const spotlightConfig = () => defaultConfig
|
||||
|
||||
return (
|
||||
<div data-page="black">
|
||||
<Title>OpenCode Black | Access all the world's best coding models</Title>
|
||||
@@ -39,7 +84,9 @@ export default function BlackLayout(props: RouteSectionProps) {
|
||||
content="Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans."
|
||||
/>
|
||||
<Meta name="twitter:image" content="/social-share-black.png" />
|
||||
<div data-component="header-gradient" />
|
||||
|
||||
<Spotlight config={spotlightConfig} class="header-spotlight" onAnimationFrame={handleAnimationFrame} />
|
||||
|
||||
<header data-component="header">
|
||||
<A href="/" data-component="header-logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="179" height="32" viewBox="0 0 179 32" fill="none">
|
||||
@@ -112,15 +159,8 @@ export default function BlackLayout(props: RouteSectionProps) {
|
||||
<h1>Access all the world's best coding models</h1>
|
||||
<p>Including Claude, GPT, Gemini and more</p>
|
||||
</div>
|
||||
<div data-slot="hero-black">
|
||||
<div data-slot="hero-black" style={svgLightingStyle()}>
|
||||
<svg width="591" height="90" viewBox="0 0 591 90" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M425.56 0.75C429.464 0.750017 432.877 1.27807 435.78 2.35645C438.656 3.42455 441.138 4.86975 443.215 6.69727C445.268 8.50382 446.995 10.5587 448.394 12.8604C449.77 15.0464 450.986 17.2741 452.04 19.5439L452.357 20.2275L451.672 20.542L443.032 24.502L442.311 24.833L442.021 24.0938C441.315 22.2906 440.494 20.6079 439.557 19.0459L439.552 19.0391L439.548 19.0322C438.626 17.419 437.517 16.0443 436.223 14.9023L436.206 14.8867L436.189 14.8701C434.989 13.6697 433.518 12.7239 431.766 12.0381L431.755 12.0342V12.0332C430.111 11.3607 428.053 11.0098 425.56 11.0098C419.142 11.0098 414.433 13.4271 411.308 18.2295C408.212 23.109 406.629 29.6717 406.629 37.9805V51.6602C406.629 59.9731 408.214 66.5377 411.312 71.418C414.438 76.2157 419.145 78.6299 425.56 78.6299C428.054 78.6299 430.111 78.2782 431.756 77.6055L431.766 77.6016L432.413 77.333C433.893 76.6811 435.154 75.8593 436.206 74.873C437.512 73.644 438.625 72.2626 439.548 70.7275C440.489 69.0801 441.314 67.3534 442.021 65.5469L442.311 64.8076L443.032 65.1387L451.672 69.0986L452.348 69.4082L452.044 70.0869C450.99 72.439 449.773 74.7099 448.395 76.8994C446.995 79.1229 445.266 81.1379 443.215 82.9434C441.138 84.7708 438.656 86.2151 435.78 87.2832C432.877 88.3616 429.464 88.8896 425.56 88.8896C415.111 88.8896 407.219 85.0777 402.019 77.4004L402.016 77.3965C396.939 69.7818 394.449 58.891 394.449 44.8203C394.449 30.7495 396.939 19.8589 402.016 12.2441L402.019 12.2393C407.219 4.56202 415.111 0.75 425.56 0.75ZM29.9404 2.19043C37.2789 2.19051 43.125 4.19131 47.3799 8.2793C51.6307 12.3635 53.7305 17.8115 53.7305 24.54C53.7305 29.6953 52.4605 33.8451 49.835 36.8994L49.8359 36.9004C47.7064 39.4558 45.0331 41.367 41.835 42.6445C45.893 43.8751 49.3115 45.9006 52.0703 48.7295C55.2954 51.9546 56.8496 56.6143 56.8496 62.5801C56.8496 66.0251 56.2751 69.2753 55.1211 72.3252C53.9689 75.3702 52.3185 78.014 50.1689 80.249L50.1699 80.25C48.0996 82.4858 45.6172 84.2628 42.7314 85.582L42.7227 85.5859C39.9002 86.8312 36.8362 87.4502 33.54 87.4502H0.75V2.19043H29.9404ZM148.123 2.19043V77.1904H187.843V87.4502H136.543V2.19043H148.123ZM298.121 2.19043L298.283 2.71973L323.963 86.4805L324.261 87.4502H312.006L311.848 86.9131L304.927 63.5703H276.646L269.726 86.9131L269.566 87.4502H257.552L257.85 86.4805L283.529 2.71973L283.691 2.19043H298.121ZM539.782 2.19043V44.9209L549.845 32.2344L549.851 32.2275L549.855 32.2207L574.575 2.46094L574.801 2.19043H588.874L587.849 3.41992L558.795 38.2832L588.749 86.3027L589.464 87.4502H575.934L575.714 87.0938L550.937 46.9316L539.782 60.0947V87.4502H528.202V2.19043H539.782ZM12.3301 77.1904H30.54C35.0749 77.1904 38.5307 76.1729 40.9961 74.2305C43.4059 72.3317 44.6699 69.3811 44.6699 65.2197V60.2998C44.6699 56.2239 43.4093 53.3106 40.9961 51.4092L40.9854 51.4004C38.5207 49.3838 35.0691 48.3301 30.54 48.3301H12.3301V77.1904ZM279.485 53.3096H302.087L290.786 14.4482L279.485 53.3096ZM12.3301 38.5498H28.8604C33 38.5498 36.1378 37.6505 38.3633 35.9443C40.5339 34.2015 41.6698 31.5679 41.6699 27.9004V23.2197C41.6699 19.5455 40.5299 16.9088 38.3516 15.166C36.1272 13.3865 32.9938 12.4502 28.8604 12.4502H12.3301V38.5498Z"
|
||||
fill="url(#hero-black-fill-gradient)"
|
||||
fill-opacity="0.1"
|
||||
stroke="url(#hero-black-stroke-gradient)"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="hero-black-fill-gradient"
|
||||
@@ -130,9 +170,10 @@ export default function BlackLayout(props: RouteSectionProps) {
|
||||
y2="87.0326"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--hero-black-fill-from)" />
|
||||
<stop offset="1" stop-color="var(--hero-black-fill-to)" />
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-stroke-gradient"
|
||||
x1="290.82"
|
||||
@@ -141,10 +182,80 @@ export default function BlackLayout(props: RouteSectionProps) {
|
||||
y2="87.0325"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--hero-black-stroke-from)" />
|
||||
<stop offset="1" stop-color="var(--hero-black-stroke-to)" />
|
||||
<stop stop-color={`hsl(0 0% ${svgLightingValues().strokeBrightness}%)`} />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-shimmer-gradient"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="591"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset={Math.max(0, svgLightingValues().shimmerPos - 0.12)} stop-color="transparent" />
|
||||
<stop
|
||||
offset={svgLightingValues().shimmerPos}
|
||||
stop-color={`rgba(255, 255, 255, ${svgLightingValues().shimmerIntensity})`}
|
||||
/>
|
||||
<stop offset={Math.min(1, svgLightingValues().shimmerPos + 0.12)} stop-color="transparent" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-top-glow"
|
||||
x1="290.82"
|
||||
y1="0"
|
||||
x2="290.82"
|
||||
y2="45"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color={`rgba(255, 255, 255, ${svgLightingValues().glowIntensity})`} />
|
||||
<stop offset="1" stop-color="transparent" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-shimmer-mask"
|
||||
x1="290.82"
|
||||
y1="0"
|
||||
x2="290.82"
|
||||
y2="50"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color="white" />
|
||||
<stop offset="0.8" stop-color="white" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
|
||||
<mask id="shimmer-top-mask">
|
||||
<rect x="0" y="0" width="591" height="90" fill="url(#hero-black-shimmer-mask)" />
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<path
|
||||
d="M425.56 0.75C429.464 0.750017 432.877 1.27807 435.78 2.35645C438.656 3.42455 441.138 4.86975 443.215 6.69727C445.268 8.50382 446.995 10.5587 448.394 12.8604C449.77 15.0464 450.986 17.2741 452.04 19.5439L452.357 20.2275L451.672 20.542L443.032 24.502L442.311 24.833L442.021 24.0938C441.315 22.2906 440.494 20.6079 439.557 19.0459L439.552 19.0391L439.548 19.0322C438.626 17.419 437.517 16.0443 436.223 14.9023L436.206 14.8867L436.189 14.8701C434.989 13.6697 433.518 12.7239 431.766 12.0381L431.755 12.0342V12.0332C430.111 11.3607 428.053 11.0098 425.56 11.0098C419.142 11.0098 414.433 13.4271 411.308 18.2295C408.212 23.109 406.629 29.6717 406.629 37.9805V51.6602C406.629 59.9731 408.214 66.5377 411.312 71.418C414.438 76.2157 419.145 78.6299 425.56 78.6299C428.054 78.6299 430.111 78.2782 431.756 77.6055L431.766 77.6016L432.413 77.333C433.893 76.6811 435.154 75.8593 436.206 74.873C437.512 73.644 438.625 72.2626 439.548 70.7275C440.489 69.0801 441.314 67.3534 442.021 65.5469L442.311 64.8076L443.032 65.1387L451.672 69.0986L452.348 69.4082L452.044 70.0869C450.99 72.439 449.773 74.7099 448.395 76.8994C446.995 79.1229 445.266 81.1379 443.215 82.9434C441.138 84.7708 438.656 86.2151 435.78 87.2832C432.877 88.3616 429.464 88.8896 425.56 88.8896C415.111 88.8896 407.219 85.0777 402.019 77.4004L402.016 77.3965C396.939 69.7818 394.449 58.891 394.449 44.8203C394.449 30.7495 396.939 19.8589 402.016 12.2441L402.019 12.2393C407.219 4.56202 415.111 0.75 425.56 0.75ZM29.9404 2.19043C37.2789 2.19051 43.125 4.19131 47.3799 8.2793C51.6307 12.3635 53.7305 17.8115 53.7305 24.54C53.7305 29.6953 52.4605 33.8451 49.835 36.8994L49.8359 36.9004C47.7064 39.4558 45.0331 41.367 41.835 42.6445C45.893 43.8751 49.3115 45.9006 52.0703 48.7295C55.2954 51.9546 56.8496 56.6143 56.8496 62.5801C56.8496 66.0251 56.2751 69.2753 55.1211 72.3252C53.9689 75.3702 52.3185 78.014 50.1689 80.249L50.1699 80.25C48.0996 82.4858 45.6172 84.2628 42.7314 85.582L42.7227 85.5859C39.9002 86.8312 36.8362 87.4502 33.54 87.4502H0.75V2.19043H29.9404ZM148.123 2.19043V77.1904H187.843V87.4502H136.543V2.19043H148.123ZM298.121 2.19043L298.283 2.71973L323.963 86.4805L324.261 87.4502H312.006L311.848 86.9131L304.927 63.5703H276.646L269.726 86.9131L269.566 87.4502H257.552L257.85 86.4805L283.529 2.71973L283.691 2.19043H298.121ZM539.782 2.19043V44.9209L549.845 32.2344L549.851 32.2275L549.855 32.2207L574.575 2.46094L574.801 2.19043H588.874L587.849 3.41992L558.795 38.2832L588.749 86.3027L589.464 87.4502H575.934L575.714 87.0938L550.937 46.9316L539.782 60.0947V87.4502H528.202V2.19043H539.782ZM12.3301 77.1904H30.54C35.0749 77.1904 38.5307 76.1729 40.9961 74.2305C43.4059 72.3317 44.6699 69.3811 44.6699 65.2197V60.2998C44.6699 56.2239 43.4093 53.3106 40.9961 51.4092L40.9854 51.4004C38.5207 49.3838 35.0691 48.3301 30.54 48.3301H12.3301V77.1904ZM279.485 53.3096H302.087L290.786 14.4482L279.485 53.3096ZM12.3301 38.5498H28.8604C33 38.5498 36.1378 37.6505 38.3633 35.9443C40.5339 34.2015 41.6698 31.5679 41.6699 27.9004V23.2197C41.6699 19.5455 40.5299 16.9088 38.3516 15.166C36.1272 13.3865 32.9938 12.4502 28.8604 12.4502H12.3301V38.5498Z"
|
||||
fill="url(#hero-black-fill-gradient)"
|
||||
fill-opacity={svgLightingValues().fillOpacity}
|
||||
stroke="url(#hero-black-stroke-gradient)"
|
||||
stroke-width="1.5"
|
||||
data-slot="black-base"
|
||||
/>
|
||||
|
||||
<path
|
||||
d="M425.56 0.75C429.464 0.750017 432.877 1.27807 435.78 2.35645C438.656 3.42455 441.138 4.86975 443.215 6.69727C445.268 8.50382 446.995 10.5587 448.394 12.8604C449.77 15.0464 450.986 17.2741 452.04 19.5439L452.357 20.2275L451.672 20.542L443.032 24.502L442.311 24.833L442.021 24.0938C441.315 22.2906 440.494 20.6079 439.557 19.0459L439.552 19.0391L439.548 19.0322C438.626 17.419 437.517 16.0443 436.223 14.9023L436.206 14.8867L436.189 14.8701C434.989 13.6697 433.518 12.7239 431.766 12.0381L431.755 12.0342V12.0332C430.111 11.3607 428.053 11.0098 425.56 11.0098C419.142 11.0098 414.433 13.4271 411.308 18.2295C408.212 23.109 406.629 29.6717 406.629 37.9805V51.6602C406.629 59.9731 408.214 66.5377 411.312 71.418C414.438 76.2157 419.145 78.6299 425.56 78.6299C428.054 78.6299 430.111 78.2782 431.756 77.6055L431.766 77.6016L432.413 77.333C433.893 76.6811 435.154 75.8593 436.206 74.873C437.512 73.644 438.625 72.2626 439.548 70.7275C440.489 69.0801 441.314 67.3534 442.021 65.5469L442.311 64.8076L443.032 65.1387L451.672 69.0986L452.348 69.4082L452.044 70.0869C450.99 72.439 449.773 74.7099 448.395 76.8994C446.995 79.1229 445.266 81.1379 443.215 82.9434C441.138 84.7708 438.656 86.2151 435.78 87.2832C432.877 88.3616 429.464 88.8896 425.56 88.8896C415.111 88.8896 407.219 85.0777 402.019 77.4004L402.016 77.3965C396.939 69.7818 394.449 58.891 394.449 44.8203C394.449 30.7495 396.939 19.8589 402.016 12.2441L402.019 12.2393C407.219 4.56202 415.111 0.75 425.56 0.75ZM29.9404 2.19043C37.2789 2.19051 43.125 4.19131 47.3799 8.2793C51.6307 12.3635 53.7305 17.8115 53.7305 24.54C53.7305 29.6953 52.4605 33.8451 49.835 36.8994L49.8359 36.9004C47.7064 39.4558 45.0331 41.367 41.835 42.6445C45.893 43.8751 49.3115 45.9006 52.0703 48.7295C55.2954 51.9546 56.8496 56.6143 56.8496 62.5801C56.8496 66.0251 56.2751 69.2753 55.1211 72.3252C53.9689 75.3702 52.3185 78.014 50.1689 80.249L50.1699 80.25C48.0996 82.4858 45.6172 84.2628 42.7314 85.582L42.7227 85.5859C39.9002 86.8312 36.8362 87.4502 33.54 87.4502H0.75V2.19043H29.9404ZM148.123 2.19043V77.1904H187.843V87.4502H136.543V2.19043H148.123ZM298.121 2.19043L298.283 2.71973L323.963 86.4805L324.261 87.4502H312.006L311.848 86.9131L304.927 63.5703H276.646L269.726 86.9131L269.566 87.4502H257.552L257.85 86.4805L283.529 2.71973L283.691 2.19043H298.121ZM539.782 2.19043V44.9209L549.845 32.2344L549.851 32.2275L549.855 32.2207L574.575 2.46094L574.801 2.19043H588.874L587.849 3.41992L558.795 38.2832L588.749 86.3027L589.464 87.4502H575.934L575.714 87.0938L550.937 46.9316L539.782 60.0947V87.4502H528.202V2.19043H539.782ZM12.3301 77.1904H30.54C35.0749 77.1904 38.5307 76.1729 40.9961 74.2305C43.4059 72.3317 44.6699 69.3811 44.6699 65.2197V60.2998C44.6699 56.2239 43.4093 53.3106 40.9961 51.4092L40.9854 51.4004C38.5207 49.3838 35.0691 48.3301 30.54 48.3301H12.3301V77.1904ZM279.485 53.3096H302.087L290.786 14.4482L279.485 53.3096ZM12.3301 38.5498H28.8604C33 38.5498 36.1378 37.6505 38.3633 35.9443C40.5339 34.2015 41.6698 31.5679 41.6699 27.9004V23.2197C41.6699 19.5455 40.5299 16.9088 38.3516 15.166C36.1272 13.3865 32.9938 12.4502 28.8604 12.4502H12.3301V38.5498Z"
|
||||
fill="url(#hero-black-top-glow)"
|
||||
stroke="none"
|
||||
data-slot="black-glow"
|
||||
/>
|
||||
|
||||
<path
|
||||
d="M425.56 0.75C429.464 0.750017 432.877 1.27807 435.78 2.35645C438.656 3.42455 441.138 4.86975 443.215 6.69727C445.268 8.50382 446.995 10.5587 448.394 12.8604C449.77 15.0464 450.986 17.2741 452.04 19.5439L452.357 20.2275L451.672 20.542L443.032 24.502L442.311 24.833L442.021 24.0938C441.315 22.2906 440.494 20.6079 439.557 19.0459L439.552 19.0391L439.548 19.0322C438.626 17.419 437.517 16.0443 436.223 14.9023L436.206 14.8867L436.189 14.8701C434.989 13.6697 433.518 12.7239 431.766 12.0381L431.755 12.0342V12.0332C430.111 11.3607 428.053 11.0098 425.56 11.0098C419.142 11.0098 414.433 13.4271 411.308 18.2295C408.212 23.109 406.629 29.6717 406.629 37.9805V51.6602C406.629 59.9731 408.214 66.5377 411.312 71.418C414.438 76.2157 419.145 78.6299 425.56 78.6299C428.054 78.6299 430.111 78.2782 431.756 77.6055L431.766 77.6016L432.413 77.333C433.893 76.6811 435.154 75.8593 436.206 74.873C437.512 73.644 438.625 72.2626 439.548 70.7275C440.489 69.0801 441.314 67.3534 442.021 65.5469L442.311 64.8076L443.032 65.1387L451.672 69.0986L452.348 69.4082L452.044 70.0869C450.99 72.439 449.773 74.7099 448.395 76.8994C446.995 79.1229 445.266 81.1379 443.215 82.9434C441.138 84.7708 438.656 86.2151 435.78 87.2832C432.877 88.3616 429.464 88.8896 425.56 88.8896C415.111 88.8896 407.219 85.0777 402.019 77.4004L402.016 77.3965C396.939 69.7818 394.449 58.891 394.449 44.8203C394.449 30.7495 396.939 19.8589 402.016 12.2441L402.019 12.2393C407.219 4.56202 415.111 0.75 425.56 0.75ZM29.9404 2.19043C37.2789 2.19051 43.125 4.19131 47.3799 8.2793C51.6307 12.3635 53.7305 17.8115 53.7305 24.54C53.7305 29.6953 52.4605 33.8451 49.835 36.8994L49.8359 36.9004C47.7064 39.4558 45.0331 41.367 41.835 42.6445C45.893 43.8751 49.3115 45.9006 52.0703 48.7295C55.2954 51.9546 56.8496 56.6143 56.8496 62.5801C56.8496 66.0251 56.2751 69.2753 55.1211 72.3252C53.9689 75.3702 52.3185 78.014 50.1689 80.249L50.1699 80.25C48.0996 82.4858 45.6172 84.2628 42.7314 85.582L42.7227 85.5859C39.9002 86.8312 36.8362 87.4502 33.54 87.4502H0.75V2.19043H29.9404ZM148.123 2.19043V77.1904H187.843V87.4502H136.543V2.19043H148.123ZM298.121 2.19043L298.283 2.71973L323.963 86.4805L324.261 87.4502H312.006L311.848 86.9131L304.927 63.5703H276.646L269.726 86.9131L269.566 87.4502H257.552L257.85 86.4805L283.529 2.71973L283.691 2.19043H298.121ZM539.782 2.19043V44.9209L549.845 32.2344L549.851 32.2275L549.855 32.2207L574.575 2.46094L574.801 2.19043H588.874L587.849 3.41992L558.795 38.2832L588.749 86.3027L589.464 87.4502H575.934L575.714 87.0938L550.937 46.9316L539.782 60.0947V87.4502H528.202V2.19043H539.782ZM12.3301 77.1904H30.54C35.0749 77.1904 38.5307 76.1729 40.9961 74.2305C43.4059 72.3317 44.6699 69.3811 44.6699 65.2197V60.2998C44.6699 56.2239 43.4093 53.3106 40.9961 51.4092L40.9854 51.4004C38.5207 49.3838 35.0691 48.3301 30.54 48.3301H12.3301V77.1904ZM279.485 53.3096H302.087L290.786 14.4482L279.485 53.3096ZM12.3301 38.5498H28.8604C33 38.5498 36.1378 37.6505 38.3633 35.9443C40.5339 34.2015 41.6698 31.5679 41.6699 27.9004V23.2197C41.6699 19.5455 40.5299 16.9088 38.3516 15.166C36.1272 13.3865 32.9938 12.4502 28.8604 12.4502H12.3301V38.5498Z"
|
||||
fill="url(#hero-black-shimmer-gradient)"
|
||||
stroke="none"
|
||||
data-slot="black-shimmer"
|
||||
mask="url(#shimmer-top-mask)"
|
||||
style={{ "mix-blend-mode": "overlay" }}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{props.children}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { A, useSearchParams } from "@solidjs/router"
|
||||
import { Title } from "@solidjs/meta"
|
||||
import { createMemo, createSignal, For, onMount, Show } from "solid-js"
|
||||
import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js"
|
||||
import { PlanIcon, plans } from "./common"
|
||||
|
||||
export default function Black() {
|
||||
const [params] = useSearchParams()
|
||||
const [selected, setSelected] = createSignal<string | null>((params.plan as string) || null)
|
||||
const [mounted, setMounted] = createSignal(false)
|
||||
const selectedPlan = createMemo(() => plans.find((p) => p.id === selected()))
|
||||
|
||||
onMount(() => {
|
||||
requestAnimationFrame(() => setMounted(true))
|
||||
@@ -37,110 +38,68 @@ export default function Black() {
|
||||
<>
|
||||
<Title>opencode</Title>
|
||||
<section data-slot="cta">
|
||||
<div data-slot="pricing">
|
||||
<For each={plans}>
|
||||
{(plan) => {
|
||||
const isSelected = createMemo(() => selected() === plan.id)
|
||||
const isCollapsed = createMemo(() => selected() !== null && selected() !== plan.id)
|
||||
|
||||
return (
|
||||
<article
|
||||
data-slot="pricing-card"
|
||||
data-plan-id={plan.id}
|
||||
data-selected={isSelected() ? "true" : "false"}
|
||||
data-collapsed={isCollapsed() ? "true" : "false"}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={!selected()}>
|
||||
<div data-slot="pricing">
|
||||
<For each={plans}>
|
||||
{(plan) => (
|
||||
<button
|
||||
type="button"
|
||||
data-slot="card-trigger"
|
||||
onClick={() => select(plan.id)}
|
||||
disabled={isSelected()}
|
||||
data-slot="pricing-card"
|
||||
style={{ "view-transition-name": `card-${plan.id}` }}
|
||||
>
|
||||
<div
|
||||
data-slot="plan-header"
|
||||
style={{
|
||||
"view-transition-name": `plan-header-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
<div data-slot="plan-icon">
|
||||
<PlanIcon plan={plan.id} />
|
||||
</div>
|
||||
<p
|
||||
data-slot="price"
|
||||
style={{
|
||||
"view-transition-name": `price-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
data-slot="amount"
|
||||
style={{
|
||||
"view-transition-name": `amount-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
${plan.id}
|
||||
</span>
|
||||
<Show when={!isSelected()}>
|
||||
<span
|
||||
data-slot="period"
|
||||
style={{
|
||||
"view-transition-name": `period-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
per month
|
||||
</span>
|
||||
</Show>
|
||||
|
||||
<Show when={isSelected()}>
|
||||
<span
|
||||
data-slot="billing"
|
||||
style={{
|
||||
"view-transition-name": `billing-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
per person billed monthly
|
||||
</span>
|
||||
</Show>
|
||||
{plan.multiplier && (
|
||||
<span
|
||||
data-slot="multiplier"
|
||||
style={{
|
||||
"view-transition-name": `multiplier-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
{plan.multiplier}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<div data-slot="icon">
|
||||
<PlanIcon plan={plan.id} />
|
||||
</div>
|
||||
<p data-slot="price">
|
||||
<span data-slot="amount">${plan.id}</span> <span data-slot="period">per month</span>
|
||||
<Show when={plan.multiplier}>
|
||||
<span data-slot="multiplier">{plan.multiplier}</span>
|
||||
</Show>
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<Show when={isSelected()}>
|
||||
<div data-slot="content">
|
||||
<ul data-slot="terms">
|
||||
<li>You will be added to the waitlist and activated in batches</li>
|
||||
<li>Card won't be charged until subscription is active</li>
|
||||
<li>Not unlimited - limits apply and may be adjusted dynamically</li>
|
||||
<li>Heavily automated usage will hit limits quickly</li>
|
||||
<li>Plans may be discontinued</li>
|
||||
<li>Can cancel subscription at anytime</li>
|
||||
<li>Cannot issue refunds for consumed subscriptions</li>
|
||||
</ul>
|
||||
<div data-slot="actions">
|
||||
<button type="button" onClick={cancel} data-slot="cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<a href={`/black/subscribe/${plan.id}`} data-slot="continue">
|
||||
Continue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</article>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
<p data-slot="fine-print">
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={selectedPlan()}>
|
||||
{(plan) => (
|
||||
<div data-slot="selected-plan">
|
||||
<div data-slot="selected-card" style={{ "view-transition-name": `card-${plan().id}` }}>
|
||||
<div data-slot="icon">
|
||||
<PlanIcon plan={plan().id} />
|
||||
</div>
|
||||
<p data-slot="price">
|
||||
<span data-slot="amount">${plan().id}</span>{" "}
|
||||
<span data-slot="period">per person billed monthly</span>
|
||||
<Show when={plan().multiplier}>
|
||||
<span data-slot="multiplier">{plan().multiplier}</span>
|
||||
</Show>
|
||||
</p>
|
||||
<ul data-slot="terms" style={{ "view-transition-name": `terms-${plan().id}` }}>
|
||||
<li>Your subscription will not start immediately</li>
|
||||
<li>You will be added to the waitlist and activated soon</li>
|
||||
<li>Your card will be only charged when your subscription is activated</li>
|
||||
<li>Usage limits apply, heavily automated use may reach limits sooner</li>
|
||||
<li>Subscriptions for individuals, contact Enterprise for teams</li>
|
||||
<li>Limits may be adjusted and plans may be discontinued in the future</li>
|
||||
<li>Cancel your subscription at anytime</li>
|
||||
</ul>
|
||||
<div data-slot="actions" style={{ "view-transition-name": `actions-${plan().id}` }}>
|
||||
<button type="button" onClick={() => cancel()} data-slot="cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<a href={`/black/subscribe/${plan().id}`} data-slot="continue">
|
||||
Continue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
<p data-slot="fine-print" style={{ "view-transition-name": "fine-print" }}>
|
||||
Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-text);
|
||||
padding-bottom: 5rem;
|
||||
overflow-x: hidden;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--color-background: hsl(0, 9%, 7%);
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-text);
|
||||
padding-bottom: 5rem;
|
||||
overflow-x: hidden;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--color-background: hsl(0, 9%, 7%);
|
||||
|
||||
@@ -81,12 +81,13 @@ export async function handler(
|
||||
const isTrial = await trialLimiter?.isTrial()
|
||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
|
||||
await rateLimiter?.check()
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider ?? false, sessionId)
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||
const stickyProvider = await stickyTracker?.get()
|
||||
const authInfo = await authenticate(modelInfo)
|
||||
|
||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||
const providerInfo = selectProvider(
|
||||
model,
|
||||
zenData,
|
||||
authInfo,
|
||||
modelInfo,
|
||||
@@ -101,7 +102,7 @@ export async function handler(
|
||||
logger.metric({ provider: providerInfo.id })
|
||||
|
||||
const startTimestamp = Date.now()
|
||||
const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream)
|
||||
const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)
|
||||
const reqBody = JSON.stringify(
|
||||
providerInfo.modifyBody({
|
||||
...createBodyConverter(opts.format, providerInfo.format)(body),
|
||||
@@ -135,7 +136,7 @@ export async function handler(
|
||||
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||
res.status !== 404 &&
|
||||
// ie. cannot change codex model providers mid-session
|
||||
!modelInfo.stickyProvider &&
|
||||
modelInfo.stickyProvider !== "strict" &&
|
||||
modelInfo.fallbackProvider &&
|
||||
providerInfo.id !== modelInfo.fallbackProvider
|
||||
) {
|
||||
@@ -194,17 +195,19 @@ export async function handler(
|
||||
// Handle streaming response
|
||||
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
|
||||
const usageParser = providerInfo.createUsageParser()
|
||||
const binaryDecoder = providerInfo.createBinaryStreamDecoder()
|
||||
const stream = new ReadableStream({
|
||||
start(c) {
|
||||
const reader = res.body?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
let buffer = ""
|
||||
let responseLength = 0
|
||||
|
||||
function pump(): Promise<void> {
|
||||
return (
|
||||
reader?.read().then(async ({ done, value }) => {
|
||||
reader?.read().then(async ({ done, value: rawValue }) => {
|
||||
if (done) {
|
||||
logger.metric({
|
||||
response_length: responseLength,
|
||||
@@ -230,6 +233,10 @@ export async function handler(
|
||||
"timestamp.first_byte": now,
|
||||
})
|
||||
}
|
||||
|
||||
const value = binaryDecoder ? binaryDecoder(rawValue) : rawValue
|
||||
if (!value) return
|
||||
|
||||
responseLength += value.length
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
dataDumper?.provideStream(buffer)
|
||||
@@ -331,6 +338,7 @@ export async function handler(
|
||||
}
|
||||
|
||||
function selectProvider(
|
||||
reqModel: string,
|
||||
zenData: ZenData,
|
||||
authInfo: AuthInfo,
|
||||
modelInfo: ModelInfo,
|
||||
@@ -339,7 +347,7 @@ export async function handler(
|
||||
retry: RetryOptions,
|
||||
stickyProvider: string | undefined,
|
||||
) {
|
||||
const provider = (() => {
|
||||
const modelProvider = (() => {
|
||||
if (authInfo?.provider?.credentials) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
|
||||
}
|
||||
@@ -372,18 +380,19 @@ export async function handler(
|
||||
return providers[index || 0]
|
||||
})()
|
||||
|
||||
if (!provider) throw new ModelError("No provider available")
|
||||
if (!(provider.id in zenData.providers)) throw new ModelError(`Provider ${provider.id} not supported`)
|
||||
if (!modelProvider) throw new ModelError("No provider available")
|
||||
if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`)
|
||||
|
||||
return {
|
||||
...provider,
|
||||
...zenData.providers[provider.id],
|
||||
...modelProvider,
|
||||
...zenData.providers[modelProvider.id],
|
||||
...(() => {
|
||||
const format = zenData.providers[provider.id].format
|
||||
if (format === "anthropic") return anthropicHelper
|
||||
if (format === "google") return googleHelper
|
||||
if (format === "openai") return openaiHelper
|
||||
return oaCompatHelper
|
||||
const format = zenData.providers[modelProvider.id].format
|
||||
const providerModel = modelProvider.model
|
||||
if (format === "anthropic") return anthropicHelper({ reqModel, providerModel })
|
||||
if (format === "google") return googleHelper({ reqModel, providerModel })
|
||||
if (format === "openai") return openaiHelper({ reqModel, providerModel })
|
||||
return oaCompatHelper({ reqModel, providerModel })
|
||||
})(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { EventStreamCodec } from "@smithy/eventstream-codec"
|
||||
import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider"
|
||||
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
|
||||
|
||||
type Usage = {
|
||||
cache_creation?: {
|
||||
@@ -14,65 +16,168 @@ type Usage = {
|
||||
}
|
||||
}
|
||||
|
||||
export const anthropicHelper = {
|
||||
format: "anthropic",
|
||||
modifyUrl: (providerApi: string) => providerApi + "/messages",
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
headers.set("x-api-key", apiKey)
|
||||
headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
|
||||
if (body.model.startsWith("claude-sonnet-")) {
|
||||
headers.set("anthropic-beta", "context-1m-2025-08-07")
|
||||
}
|
||||
},
|
||||
modifyBody: (body: Record<string, any>) => {
|
||||
return {
|
||||
export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => {
|
||||
const isBedrockModelArn = providerModel.startsWith("arn:aws:bedrock:")
|
||||
const isBedrockModelID = providerModel.startsWith("global.anthropic.")
|
||||
const isBedrock = isBedrockModelArn || isBedrockModelID
|
||||
const isSonnet = reqModel.includes("sonnet")
|
||||
return {
|
||||
format: "anthropic",
|
||||
modifyUrl: (providerApi: string, isStream?: boolean) =>
|
||||
isBedrock
|
||||
? `${providerApi}/model/${isBedrockModelArn ? encodeURIComponent(providerModel) : providerModel}/${isStream ? "invoke-with-response-stream" : "invoke"}`
|
||||
: providerApi + "/messages",
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
if (isBedrock) {
|
||||
headers.set("Authorization", `Bearer ${apiKey}`)
|
||||
} else {
|
||||
headers.set("x-api-key", apiKey)
|
||||
headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
|
||||
if (body.model.startsWith("claude-sonnet-")) {
|
||||
headers.set("anthropic-beta", "context-1m-2025-08-07")
|
||||
}
|
||||
}
|
||||
},
|
||||
modifyBody: (body: Record<string, any>) => ({
|
||||
...body,
|
||||
service_tier: "standard_only",
|
||||
}
|
||||
},
|
||||
streamSeparator: "\n\n",
|
||||
createUsageParser: () => {
|
||||
let usage: Usage
|
||||
...(isBedrock
|
||||
? {
|
||||
anthropic_version: "bedrock-2023-05-31",
|
||||
anthropic_beta: isSonnet ? "context-1m-2025-08-07" : undefined,
|
||||
model: undefined,
|
||||
stream: undefined,
|
||||
}
|
||||
: {
|
||||
service_tier: "standard_only",
|
||||
}),
|
||||
}),
|
||||
createBinaryStreamDecoder: () => {
|
||||
if (!isBedrock) return undefined
|
||||
|
||||
return {
|
||||
parse: (chunk: string) => {
|
||||
const data = chunk.split("\n")[1]
|
||||
if (!data.startsWith("data: ")) return
|
||||
const decoder = new TextDecoder()
|
||||
const encoder = new TextEncoder()
|
||||
const codec = new EventStreamCodec(toUtf8, fromUtf8)
|
||||
let buffer = new Uint8Array(0)
|
||||
return (value: Uint8Array) => {
|
||||
const newBuffer = new Uint8Array(buffer.length + value.length)
|
||||
newBuffer.set(buffer)
|
||||
newBuffer.set(value, buffer.length)
|
||||
buffer = newBuffer
|
||||
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(data.slice(6))
|
||||
} catch (e) {
|
||||
return
|
||||
const messages = []
|
||||
|
||||
while (buffer.length >= 4) {
|
||||
// first 4 bytes are the total length (big-endian)
|
||||
const totalLength = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint32(0, false)
|
||||
|
||||
// wait for more chunks
|
||||
if (buffer.length < totalLength) break
|
||||
|
||||
try {
|
||||
const subView = buffer.subarray(0, totalLength)
|
||||
const decoded = codec.decode(subView)
|
||||
buffer = buffer.slice(totalLength)
|
||||
|
||||
/* Example of Bedrock data
|
||||
```
|
||||
{
|
||||
bytes: 'eyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtb3B1cy00LTUtMjAyNTExMDEiLCJpZCI6Im1zZ19iZHJrXzAxMjVGdHRGb2lkNGlwWmZ4SzZMbktxeCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo0LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjEsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMTk2MywiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MSwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjF9fX0=',
|
||||
p: '...'
|
||||
}
|
||||
```
|
||||
|
||||
const usageUpdate = json.usage ?? json.message?.usage
|
||||
if (!usageUpdate) return
|
||||
usage = {
|
||||
...usage,
|
||||
...usageUpdate,
|
||||
cache_creation: {
|
||||
...usage?.cache_creation,
|
||||
...usageUpdate.cache_creation,
|
||||
},
|
||||
server_tool_use: {
|
||||
...usage?.server_tool_use,
|
||||
...usageUpdate.server_tool_use,
|
||||
},
|
||||
Decoded bytes
|
||||
```
|
||||
{
|
||||
type: 'message_start',
|
||||
message: {
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
id: 'msg_bdrk_0125FttFoid4ipZfxK6LnKqx',
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: 4,
|
||||
cache_creation_input_tokens: 1,
|
||||
cache_read_input_tokens: 11963,
|
||||
cache_creation: [Object],
|
||||
output_tokens: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
retrieve: () => usage,
|
||||
}
|
||||
},
|
||||
normalizeUsage: (usage: Usage) => ({
|
||||
inputTokens: usage.input_tokens ?? 0,
|
||||
outputTokens: usage.output_tokens ?? 0,
|
||||
reasoningTokens: undefined,
|
||||
cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
|
||||
cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
|
||||
cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
|
||||
}),
|
||||
} satisfies ProviderHelper
|
||||
```
|
||||
*/
|
||||
|
||||
/* Example of Anthropic data
|
||||
```
|
||||
event: message_delta
|
||||
data: {"type":"message_start","message":{"model":"claude-opus-4-5-20251101","id":"msg_01ETvwVWSKULxzPdkQ1xAnk2","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11543,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":11543,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}}}
|
||||
```
|
||||
*/
|
||||
if (decoded.headers[":message-type"]?.value === "event") {
|
||||
const data = decoder.decode(decoded.body, { stream: true })
|
||||
|
||||
const parsedDataResult = JSON.parse(data)
|
||||
delete parsedDataResult.p
|
||||
const bytes = atob(parsedDataResult.bytes)
|
||||
const eventName = JSON.parse(bytes).type
|
||||
messages.push([`event: ${eventName}`, "\n", `data: ${bytes}`, "\n\n"].join(""))
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("@@@EE@@@")
|
||||
console.log(e)
|
||||
break
|
||||
}
|
||||
}
|
||||
return encoder.encode(messages.join(""))
|
||||
}
|
||||
},
|
||||
streamSeparator: "\n\n",
|
||||
createUsageParser: () => {
|
||||
let usage: Usage
|
||||
|
||||
return {
|
||||
parse: (chunk: string) => {
|
||||
const data = chunk.split("\n")[1]
|
||||
if (!data.startsWith("data: ")) return
|
||||
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(data.slice(6))
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
|
||||
const usageUpdate = json.usage ?? json.message?.usage
|
||||
if (!usageUpdate) return
|
||||
usage = {
|
||||
...usage,
|
||||
...usageUpdate,
|
||||
cache_creation: {
|
||||
...usage?.cache_creation,
|
||||
...usageUpdate.cache_creation,
|
||||
},
|
||||
server_tool_use: {
|
||||
...usage?.server_tool_use,
|
||||
...usageUpdate.server_tool_use,
|
||||
},
|
||||
}
|
||||
},
|
||||
retrieve: () => usage,
|
||||
}
|
||||
},
|
||||
normalizeUsage: (usage: Usage) => ({
|
||||
inputTokens: usage.input_tokens ?? 0,
|
||||
outputTokens: usage.output_tokens ?? 0,
|
||||
reasoningTokens: undefined,
|
||||
cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
|
||||
cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
|
||||
cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export function fromAnthropicRequest(body: any): CommonRequest {
|
||||
if (!body || typeof body !== "object") return body
|
||||
|
||||
@@ -26,16 +26,17 @@ type Usage = {
|
||||
thoughtsTokenCount?: number
|
||||
}
|
||||
|
||||
export const googleHelper = {
|
||||
export const googleHelper: ProviderHelper = ({ providerModel }) => ({
|
||||
format: "google",
|
||||
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) =>
|
||||
`${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
|
||||
modifyUrl: (providerApi: string, isStream?: boolean) =>
|
||||
`${providerApi}/models/${providerModel}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
headers.set("x-goog-api-key", apiKey)
|
||||
},
|
||||
modifyBody: (body: Record<string, any>) => {
|
||||
return body
|
||||
},
|
||||
createBinaryStreamDecoder: () => undefined,
|
||||
streamSeparator: "\r\n\r\n",
|
||||
createUsageParser: () => {
|
||||
let usage: Usage
|
||||
@@ -71,4 +72,4 @@ export const googleHelper = {
|
||||
cacheWrite1hTokens: undefined,
|
||||
}
|
||||
},
|
||||
} satisfies ProviderHelper
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ type Usage = {
|
||||
}
|
||||
}
|
||||
|
||||
export const oaCompatHelper = {
|
||||
export const oaCompatHelper: ProviderHelper = () => ({
|
||||
format: "oa-compat",
|
||||
modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
@@ -33,6 +33,7 @@ export const oaCompatHelper = {
|
||||
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
||||
}
|
||||
},
|
||||
createBinaryStreamDecoder: () => undefined,
|
||||
streamSeparator: "\n\n",
|
||||
createUsageParser: () => {
|
||||
let usage: Usage
|
||||
@@ -68,7 +69,7 @@ export const oaCompatHelper = {
|
||||
cacheWrite1hTokens: undefined,
|
||||
}
|
||||
},
|
||||
} satisfies ProviderHelper
|
||||
})
|
||||
|
||||
export function fromOaCompatibleRequest(body: any): CommonRequest {
|
||||
if (!body || typeof body !== "object") return body
|
||||
|
||||
@@ -12,7 +12,7 @@ type Usage = {
|
||||
total_tokens?: number
|
||||
}
|
||||
|
||||
export const openaiHelper = {
|
||||
export const openaiHelper: ProviderHelper = () => ({
|
||||
format: "openai",
|
||||
modifyUrl: (providerApi: string) => providerApi + "/responses",
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
@@ -21,6 +21,7 @@ export const openaiHelper = {
|
||||
modifyBody: (body: Record<string, any>) => {
|
||||
return body
|
||||
},
|
||||
createBinaryStreamDecoder: () => undefined,
|
||||
streamSeparator: "\n\n",
|
||||
createUsageParser: () => {
|
||||
let usage: Usage
|
||||
@@ -58,7 +59,7 @@ export const openaiHelper = {
|
||||
cacheWrite1hTokens: undefined,
|
||||
}
|
||||
},
|
||||
} satisfies ProviderHelper
|
||||
})
|
||||
|
||||
export function fromOpenaiRequest(body: any): CommonRequest {
|
||||
if (!body || typeof body !== "object") return body
|
||||
|
||||
@@ -33,11 +33,12 @@ export type UsageInfo = {
|
||||
cacheWrite1hTokens?: number
|
||||
}
|
||||
|
||||
export type ProviderHelper = {
|
||||
export type ProviderHelper = (input: { reqModel: string; providerModel: string }) => {
|
||||
format: ZenData.Format
|
||||
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string
|
||||
modifyUrl: (providerApi: string, isStream?: boolean) => string
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
|
||||
modifyBody: (body: Record<string, any>) => Record<string, any>
|
||||
createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined
|
||||
streamSeparator: string
|
||||
createUsageParser: () => {
|
||||
parse: (chunk: string) => void
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
|
||||
export function createStickyTracker(stickyProvider: boolean, session: string) {
|
||||
export function createStickyTracker(stickyProvider: "strict" | "prefer" | undefined, session: string) {
|
||||
if (!stickyProvider) return
|
||||
if (!session) return
|
||||
const key = `sticky:${session}`
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"types": ["vite/client"],
|
||||
"types": ["vite/client", "@webgpu/types"],
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { Billing } from "../src/billing.js"
|
||||
import { and, Database, desc, eq, isNotNull, lt, sql } from "../src/drizzle/index.js"
|
||||
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
|
||||
|
||||
const fromWrkID = process.argv[2]
|
||||
const toWrkID = process.argv[3]
|
||||
|
||||
if (!fromWrkID || !toWrkID) {
|
||||
console.error("Usage: bun foo.ts <fromWrkID> <toWrkID>")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`Transferring subscription from ${fromWrkID} to ${toWrkID}`)
|
||||
|
||||
// Look up the FROM workspace billing
|
||||
const fromBilling = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
customerID: BillingTable.customerID,
|
||||
subscriptionID: BillingTable.subscriptionID,
|
||||
subscriptionCouponID: BillingTable.subscriptionCouponID,
|
||||
paymentMethodID: BillingTable.paymentMethodID,
|
||||
paymentMethodType: BillingTable.paymentMethodType,
|
||||
paymentMethodLast4: BillingTable.paymentMethodLast4,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, fromWrkID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (!fromBilling) throw new Error(`Error: FROM workspace has no billing record`)
|
||||
if (!fromBilling.customerID) throw new Error(`Error: FROM workspace has no Stripe customer ID`)
|
||||
if (!fromBilling.subscriptionID) throw new Error(`Error: FROM workspace has no subscription`)
|
||||
|
||||
const fromSubscription = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ userID: SubscriptionTable.userID })
|
||||
.from(SubscriptionTable)
|
||||
.where(eq(SubscriptionTable.workspaceID, fromWrkID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (!fromSubscription) throw new Error(`Error: FROM workspace has no subscription`)
|
||||
|
||||
// Look up the previous customer ID in FROM workspace
|
||||
const subscriptionPayment = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
customerID: PaymentTable.customerID,
|
||||
timeCreated: PaymentTable.timeCreated,
|
||||
})
|
||||
.from(PaymentTable)
|
||||
.where(and(eq(PaymentTable.workspaceID, fromWrkID), sql`JSON_EXTRACT(enrichment, '$.type') = 'subscription'`))
|
||||
.then((rows) => {
|
||||
if (rows.length > 1) {
|
||||
console.error(`Error: Multiple subscription payments found for workspace ${fromWrkID}`)
|
||||
process.exit(1)
|
||||
}
|
||||
return rows[0]
|
||||
}),
|
||||
)
|
||||
const fromPrevPayment = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ customerID: PaymentTable.customerID })
|
||||
.from(PaymentTable)
|
||||
.where(
|
||||
and(
|
||||
eq(PaymentTable.workspaceID, fromWrkID),
|
||||
isNotNull(PaymentTable.customerID),
|
||||
lt(PaymentTable.timeCreated, subscriptionPayment.timeCreated),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(PaymentTable.timeCreated))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (!fromPrevPayment?.customerID) throw new Error(`Error: FROM workspace has no previous Stripe customer to revert to`)
|
||||
if (fromPrevPayment.customerID === fromBilling.customerID)
|
||||
throw new Error(`Error: FROM workspace has the same Stripe customer ID as the current one`)
|
||||
|
||||
const fromPrevPaymentMethods = await Billing.stripe().customers.listPaymentMethods(fromPrevPayment.customerID, {})
|
||||
if (fromPrevPaymentMethods.data.length === 0)
|
||||
throw new Error(`Error: FROM workspace has no previous Stripe payment methods`)
|
||||
|
||||
// Look up the TO workspace billing
|
||||
const toBilling = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
customerID: BillingTable.customerID,
|
||||
subscriptionID: BillingTable.subscriptionID,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, toWrkID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (!toBilling) throw new Error(`Error: TO workspace has no billing record`)
|
||||
if (toBilling.subscriptionID) throw new Error(`Error: TO workspace already has a subscription`)
|
||||
|
||||
console.log(`FROM:`)
|
||||
console.log(` Old Customer ID: ${fromBilling.customerID}`)
|
||||
console.log(` New Customer ID: ${fromPrevPayment.customerID}`)
|
||||
console.log(`TO:`)
|
||||
console.log(` Old Customer ID: ${toBilling.customerID}`)
|
||||
console.log(` New Customer ID: ${fromBilling.customerID}`)
|
||||
|
||||
// Clear workspaceID from Stripe customer metadata
|
||||
await Billing.stripe().customers.update(fromPrevPayment.customerID, {
|
||||
metadata: {
|
||||
workspaceID: fromWrkID,
|
||||
},
|
||||
})
|
||||
await Billing.stripe().customers.update(fromBilling.customerID, {
|
||||
metadata: {
|
||||
workspaceID: toWrkID,
|
||||
},
|
||||
})
|
||||
|
||||
await Database.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
customerID: fromPrevPayment.customerID,
|
||||
subscriptionID: null,
|
||||
subscriptionCouponID: null,
|
||||
paymentMethodID: fromPrevPaymentMethods.data[0].id,
|
||||
paymentMethodLast4: fromPrevPaymentMethods.data[0].card?.last4 ?? null,
|
||||
paymentMethodType: fromPrevPaymentMethods.data[0].type,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, fromWrkID))
|
||||
|
||||
await tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
customerID: fromBilling.customerID,
|
||||
subscriptionID: fromBilling.subscriptionID,
|
||||
subscriptionCouponID: fromBilling.subscriptionCouponID,
|
||||
paymentMethodID: fromBilling.paymentMethodID,
|
||||
paymentMethodLast4: fromBilling.paymentMethodLast4,
|
||||
paymentMethodType: fromBilling.paymentMethodType,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, toWrkID))
|
||||
|
||||
await tx
|
||||
.update(SubscriptionTable)
|
||||
.set({
|
||||
workspaceID: toWrkID,
|
||||
userID: fromSubscription.userID,
|
||||
})
|
||||
.where(eq(SubscriptionTable.workspaceID, fromWrkID))
|
||||
|
||||
await tx
|
||||
.update(PaymentTable)
|
||||
.set({
|
||||
workspaceID: toWrkID,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(PaymentTable.workspaceID, fromWrkID),
|
||||
sql`JSON_EXTRACT(enrichment, '$.type') = 'subscription'`,
|
||||
eq(PaymentTable.amount, 20000000000),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
console.log(`done`)
|
||||
@@ -143,6 +143,7 @@ async function printWorkspace(workspaceID: string) {
|
||||
amount: PaymentTable.amount,
|
||||
paymentID: PaymentTable.paymentID,
|
||||
invoiceID: PaymentTable.invoiceID,
|
||||
customerID: PaymentTable.customerID,
|
||||
timeCreated: PaymentTable.timeCreated,
|
||||
timeRefunded: PaymentTable.timeRefunded,
|
||||
})
|
||||
|
||||
@@ -8,33 +8,25 @@ const stage = process.argv[2]
|
||||
if (!stage) throw new Error("Stage is required")
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const PARTS = 8
|
||||
|
||||
// read the secret
|
||||
const ret = await $`bun sst secret list`.cwd(root).text()
|
||||
const lines = ret.split("\n")
|
||||
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!value6) throw new Error("ZEN_MODELS6 not found")
|
||||
if (!value7) throw new Error("ZEN_MODELS7 not found")
|
||||
const values = Array.from({ length: PARTS }, (_, i) => {
|
||||
const value = lines
|
||||
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
|
||||
?.split("=")
|
||||
.slice(1)
|
||||
.join("=")
|
||||
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
|
||||
return value
|
||||
})
|
||||
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
|
||||
ZenData.validate(JSON.parse(values.join("")))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${value6} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS7 ${value7} --stage ${stage}`
|
||||
for (let i = 0; i < PARTS; i++) {
|
||||
await $`bun sst secret set ZEN_MODELS${i + 1} --stage ${stage} -- ${values[i]}`
|
||||
}
|
||||
|
||||
@@ -8,32 +8,25 @@ const stage = process.argv[2]
|
||||
if (!stage) throw new Error("Stage is required")
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const PARTS = 8
|
||||
|
||||
// read the secret
|
||||
const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()
|
||||
const lines = ret.split("\n")
|
||||
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!value6) throw new Error("ZEN_MODELS6 not found")
|
||||
if (!value7) throw new Error("ZEN_MODELS7 not found")
|
||||
const values = Array.from({ length: PARTS }, (_, i) => {
|
||||
const value = lines
|
||||
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
|
||||
?.split("=")
|
||||
.slice(1)
|
||||
.join("=")
|
||||
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
|
||||
return value
|
||||
})
|
||||
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
|
||||
ZenData.validate(JSON.parse(values.join("")))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${value2}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${value3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${value6}`
|
||||
await $`bun sst secret set ZEN_MODELS7 ${value7}`
|
||||
for (let i = 0; i < PARTS; i++) {
|
||||
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${values[i]}`
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Billing } from "../src/billing.js"
|
||||
import { and, Database, eq } from "../src/drizzle/index.js"
|
||||
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
|
||||
|
||||
const workspaceID = process.argv[2]
|
||||
|
||||
if (!workspaceID) {
|
||||
console.error("Usage: bun remove-black.ts <workspaceID>")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`Removing subscription from workspace ${workspaceID}`)
|
||||
|
||||
// Look up the workspace billing
|
||||
const billing = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
customerID: BillingTable.customerID,
|
||||
subscriptionID: BillingTable.subscriptionID,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
if (!billing) {
|
||||
console.error(`Error: No billing record found for workspace ${workspaceID}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!billing.subscriptionID) {
|
||||
console.error(`Error: Workspace ${workspaceID} does not have a subscription`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(` Customer ID: ${billing.customerID}`)
|
||||
console.log(` Subscription ID: ${billing.subscriptionID}`)
|
||||
|
||||
// Clear workspaceID from Stripe customer metadata
|
||||
if (billing.customerID) {
|
||||
//await Billing.stripe().customers.update(billing.customerID, {
|
||||
// metadata: {
|
||||
// workspaceID: "",
|
||||
// },
|
||||
//})
|
||||
//console.log(`Cleared workspaceID from Stripe customer metadata`)
|
||||
}
|
||||
|
||||
await Database.transaction(async (tx) => {
|
||||
// Clear subscription-related fields from billing table
|
||||
await tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
// customerID: null,
|
||||
subscriptionID: null,
|
||||
subscriptionCouponID: null,
|
||||
// paymentMethodID: null,
|
||||
// paymentMethodLast4: null,
|
||||
// paymentMethodType: null,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
|
||||
// Delete from subscription table
|
||||
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
|
||||
|
||||
// Delete from payments table
|
||||
await tx
|
||||
.delete(PaymentTable)
|
||||
.where(
|
||||
and(
|
||||
eq(PaymentTable.workspaceID, workspaceID),
|
||||
eq(PaymentTable.enrichment, { type: "subscription" }),
|
||||
eq(PaymentTable.amount, 20000000000),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
console.log(`Successfully removed subscription from workspace ${workspaceID}`)
|
||||
@@ -7,34 +7,24 @@ import { ZenData } from "../src/model"
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const models = await $`bun sst secret list`.cwd(root).text()
|
||||
const PARTS = 8
|
||||
|
||||
// read the line starting with "ZEN_MODELS"
|
||||
const lines = models.split("\n")
|
||||
const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||
const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const oldValue6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
const oldValue7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
||||
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!oldValue5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!oldValue6) throw new Error("ZEN_MODELS6 not found")
|
||||
if (!oldValue7) throw new Error("ZEN_MODELS7 not found")
|
||||
const oldValues = Array.from({ length: PARTS }, (_, i) => {
|
||||
const value = lines
|
||||
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
|
||||
?.split("=")
|
||||
.slice(1)
|
||||
.join("=")
|
||||
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
|
||||
return value
|
||||
})
|
||||
|
||||
// store the prettified json to a temp file
|
||||
const filename = `models-${Date.now()}.json`
|
||||
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
||||
await tempFile.write(
|
||||
JSON.stringify(
|
||||
JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5 + oldValue6 + oldValue7),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
await tempFile.write(JSON.stringify(JSON.parse(oldValues.join("")), null, 2))
|
||||
console.log("tempFile", tempFile.name)
|
||||
|
||||
// open temp file in vim and read the file on close
|
||||
@@ -43,19 +33,11 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
||||
ZenData.validate(JSON.parse(newValue))
|
||||
|
||||
// update the secret
|
||||
const chunk = Math.ceil(newValue.length / 7)
|
||||
const newValue1 = newValue.slice(0, chunk)
|
||||
const newValue2 = newValue.slice(chunk, chunk * 2)
|
||||
const newValue3 = newValue.slice(chunk * 2, chunk * 3)
|
||||
const newValue4 = newValue.slice(chunk * 3, chunk * 4)
|
||||
const newValue5 = newValue.slice(chunk * 4, chunk * 5)
|
||||
const newValue6 = newValue.slice(chunk * 5, chunk * 6)
|
||||
const newValue7 = newValue.slice(chunk * 6)
|
||||
const chunk = Math.ceil(newValue.length / PARTS)
|
||||
const newValues = Array.from({ length: PARTS }, (_, i) =>
|
||||
newValue.slice(chunk * i, i === PARTS - 1 ? undefined : chunk * (i + 1)),
|
||||
)
|
||||
|
||||
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${newValue5}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${newValue6}`
|
||||
await $`bun sst secret set ZEN_MODELS7 ${newValue7}`
|
||||
for (let i = 0; i < PARTS; i++) {
|
||||
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${newValues[i]}`
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export namespace ZenData {
|
||||
cost200K: ModelCostSchema.optional(),
|
||||
allowAnonymous: z.boolean().optional(),
|
||||
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
|
||||
stickyProvider: z.boolean().optional(),
|
||||
stickyProvider: z.enum(["strict", "prefer"]).optional(),
|
||||
trial: TrialSchema.optional(),
|
||||
rateLimit: z.number().optional(),
|
||||
fallbackProvider: z.string().optional(),
|
||||
@@ -74,7 +74,8 @@ export namespace ZenData {
|
||||
Resource.ZEN_MODELS4.value +
|
||||
Resource.ZEN_MODELS5.value +
|
||||
Resource.ZEN_MODELS6.value +
|
||||
Resource.ZEN_MODELS7.value,
|
||||
Resource.ZEN_MODELS7.value +
|
||||
Resource.ZEN_MODELS8.value,
|
||||
)
|
||||
return ModelsSchema.parse(json)
|
||||
})
|
||||
|
||||
Vendored
+4
@@ -134,6 +134,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS8": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_SESSION_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
+4
@@ -134,6 +134,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS8": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_SESSION_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
+4
@@ -134,6 +134,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS8": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_SESSION_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-screen"></div>
|
||||
<div id="root" class="flex flex-col h-dvh"></div>
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -41,6 +41,7 @@ semver = "1.0.27"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||
uuid = { version = "1.19.0", features = ["v4"] }
|
||||
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
gtk = "0.18.2"
|
||||
webkit2gtk = "=2.0.1"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-set-theme",
|
||||
"core:webview:allow-set-webview-zoom",
|
||||
"core:window:allow-is-focused",
|
||||
"core:window:allow-show",
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, WebviewWindow};
|
||||
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewWindowBuilder};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
|
||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
@@ -223,7 +223,7 @@ async fn check_server_health(url: &str, password: Option<&str>) -> bool {
|
||||
pub fn run() {
|
||||
let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(all(target_os = "macos", not(debug_assertions)))]
|
||||
let _ = std::process::Command::new("killall")
|
||||
.arg("opencode-cli")
|
||||
.output();
|
||||
@@ -237,7 +237,14 @@ pub fn run() {
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_window_state::Builder::new().build())
|
||||
.plugin(
|
||||
tauri_plugin_window_state::Builder::new()
|
||||
.with_state_flags(
|
||||
tauri_plugin_window_state::StateFlags::all()
|
||||
- tauri_plugin_window_state::StateFlags::DECORATIONS,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
@@ -268,29 +275,30 @@ pub fn run() {
|
||||
.map(|m| m.size().to_logical(m.scale_factor()))
|
||||
.unwrap_or(LogicalSize::new(1920, 1080));
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut window_builder =
|
||||
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
|
||||
.title("OpenCode")
|
||||
.inner_size(size.width as f64, size.height as f64)
|
||||
.decorations(true)
|
||||
.zoom_hotkeys_enabled(true)
|
||||
.disable_drag_drop_handler()
|
||||
.initialization_script(format!(
|
||||
r#"
|
||||
let config = app
|
||||
.config()
|
||||
.app
|
||||
.windows
|
||||
.iter()
|
||||
.find(|w| w.label == "main")
|
||||
.expect("main window config missing");
|
||||
|
||||
let window_builder = WebviewWindowBuilder::from_config(&app, config)
|
||||
.expect("Failed to create window builder from config")
|
||||
.inner_size(size.width as f64, size.height as f64)
|
||||
.initialization_script(format!(
|
||||
r#"
|
||||
window.__OPENCODE__ ??= {{}};
|
||||
window.__OPENCODE__.updaterEnabled = {updater_enabled};
|
||||
"#
|
||||
));
|
||||
));
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
window_builder = window_builder
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.hidden_title(true);
|
||||
}
|
||||
let window_builder = window_builder
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.hidden_title(true);
|
||||
|
||||
window_builder.build().expect("Failed to create window");
|
||||
let _window = window_builder.build().expect("Failed to create window");
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
app.manage(ServerState::new(None, rx));
|
||||
|
||||
@@ -11,6 +11,20 @@
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"create": false,
|
||||
"title": "OpenCode",
|
||||
"url": "/",
|
||||
"decorations": true,
|
||||
"dragDropEnabled": false,
|
||||
"zoomHotkeysEnabled": true,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true,
|
||||
"trafficLightPosition": { "x": 12.0, "y": 18.0 }
|
||||
}
|
||||
],
|
||||
"withGlobalTauri": true,
|
||||
"security": {
|
||||
"csp": null
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpenCode",
|
||||
"identifier": "ai.opencode.desktop",
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"create": false,
|
||||
"title": "OpenCode",
|
||||
"url": "/",
|
||||
"decorations": true,
|
||||
"dragDropEnabled": false,
|
||||
"zoomHotkeysEnabled": true,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true,
|
||||
"trafficLightPosition": { "x": 12.0, "y": 18.0 }
|
||||
}
|
||||
],
|
||||
"withGlobalTauri": true,
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"macOSPrivateApi": true
|
||||
},
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": true,
|
||||
"icon": [
|
||||
|
||||
@@ -13,7 +13,7 @@ import { AsyncStorage } from "@solid-primitives/storage"
|
||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
|
||||
import { Store } from "@tauri-apps/plugin-store"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
import { createSignal, Show, Accessor, JSX, createResource } from "solid-js"
|
||||
import { createSignal, Show, Accessor, JSX, createResource, onMount, onCleanup } from "solid-js"
|
||||
|
||||
import { UPDATER_ENABLED } from "./updater"
|
||||
import { createMenu } from "./menu"
|
||||
@@ -30,6 +30,11 @@ let update: Update | null = null
|
||||
|
||||
const createPlatform = (password: Accessor<string | null>): Platform => ({
|
||||
platform: "desktop",
|
||||
os: (() => {
|
||||
const type = ostype()
|
||||
if (type === "macos" || type === "windows" || type === "linux") return type
|
||||
return undefined
|
||||
})(),
|
||||
version: pkg.version,
|
||||
|
||||
async openDirectoryPickerDialog(opts) {
|
||||
@@ -296,12 +301,24 @@ render(() => {
|
||||
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
|
||||
const platform = createPlatform(() => serverPassword())
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
|
||||
if (link?.href) {
|
||||
e.preventDefault()
|
||||
platform.openLink(link.href)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("click", handleClick)
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("click", handleClick)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<PlatformProvider value={platform}>
|
||||
<AppBaseProviders>
|
||||
{ostype() === "macos" && (
|
||||
<div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
|
||||
)}
|
||||
<ServerGate>
|
||||
{(data) => {
|
||||
setServerPassword(data().password)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
Vendored
+4
@@ -134,6 +134,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS8": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_SESSION_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.1.21"
|
||||
version = "1.1.25"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.21/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.21/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.21/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.21/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.21/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
Vendored
+4
@@ -134,6 +134,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS8": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_SESSION_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.25",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -82,8 +82,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.72",
|
||||
"@opentui/solid": "0.1.72",
|
||||
"@opentui/core": "0.1.74",
|
||||
"@opentui/solid": "0.1.74",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
|
||||
@@ -731,6 +731,9 @@ export namespace ACP {
|
||||
const defaultAgentName = await AgentModule.defaultAgent()
|
||||
const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id
|
||||
|
||||
// Persist the default mode so prompt() uses it immediately
|
||||
this.sessionManager.setMode(sessionId, currentModeId)
|
||||
|
||||
const mcpServers: Record<string, Config.Mcp> = {}
|
||||
for (const server of params.mcpServers) {
|
||||
if ("type" in server) {
|
||||
|
||||
@@ -255,7 +255,20 @@ export namespace Agent {
|
||||
}
|
||||
|
||||
export async function defaultAgent() {
|
||||
return state().then((x) => Object.keys(x)[0])
|
||||
const cfg = await Config.get()
|
||||
const agents = await state()
|
||||
|
||||
if (cfg.default_agent) {
|
||||
const agent = agents[cfg.default_agent]
|
||||
if (!agent) throw new Error(`default agent "${cfg.default_agent}" not found`)
|
||||
if (agent.mode === "subagent") throw new Error(`default agent "${cfg.default_agent}" is a subagent`)
|
||||
if (agent.hidden === true) throw new Error(`default agent "${cfg.default_agent}" is hidden`)
|
||||
return agent.name
|
||||
}
|
||||
|
||||
const primaryVisible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
|
||||
if (!primaryVisible) throw new Error("no primary visible agent found")
|
||||
return primaryVisible.name
|
||||
}
|
||||
|
||||
export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import z from "zod"
|
||||
import { Global } from "../global"
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { readableStreamToText } from "bun"
|
||||
import { createRequire } from "module"
|
||||
@@ -71,7 +72,10 @@ export namespace BunProc {
|
||||
await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
|
||||
return result
|
||||
})
|
||||
if (parsed.dependencies[pkg] === version) return mod
|
||||
const dependencies = parsed.dependencies ?? {}
|
||||
if (!parsed.dependencies) parsed.dependencies = dependencies
|
||||
const modExists = await Filesystem.exists(mod)
|
||||
if (dependencies[pkg] === version && modExists) return mod
|
||||
|
||||
const proxied = !!(
|
||||
process.env.HTTP_PROXY ||
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as prompts from "@clack/prompts"
|
||||
import { UI } from "../ui"
|
||||
import { MCP } from "../../mcp"
|
||||
import { McpAuth } from "../../mcp/auth"
|
||||
import { McpOAuthCallback } from "../../mcp/oauth-callback"
|
||||
import { McpOAuthProvider } from "../../mcp/oauth-provider"
|
||||
import { Config } from "../../config/config"
|
||||
import { Instance } from "../../project/instance"
|
||||
@@ -13,6 +14,7 @@ import { Installation } from "../../installation"
|
||||
import path from "path"
|
||||
import { Global } from "../../global"
|
||||
import { modify, applyEdits } from "jsonc-parser"
|
||||
import { Bus } from "../../bus"
|
||||
|
||||
function getAuthStatusIcon(status: MCP.AuthStatus): string {
|
||||
switch (status) {
|
||||
@@ -227,6 +229,16 @@ export const McpAuthCommand = cmd({
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Starting OAuth flow...")
|
||||
|
||||
// Subscribe to browser open failure events to show URL for manual opening
|
||||
const unsubscribe = Bus.subscribe(MCP.BrowserOpenFailed, (evt) => {
|
||||
if (evt.properties.mcpName === serverName) {
|
||||
spinner.stop("Could not open browser automatically")
|
||||
prompts.log.warn("Please open this URL in your browser to authenticate:")
|
||||
prompts.log.info(evt.properties.url)
|
||||
spinner.start("Waiting for authorization...")
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const status = await MCP.authenticate(serverName)
|
||||
|
||||
@@ -256,6 +268,8 @@ export const McpAuthCommand = cmd({
|
||||
} catch (error) {
|
||||
spinner.stop("Authentication failed", 1)
|
||||
prompts.log.error(error instanceof Error ? error.message : String(error))
|
||||
} finally {
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
prompts.outro("Done")
|
||||
@@ -669,6 +683,10 @@ export const McpDebugCommand = cmd({
|
||||
|
||||
// Try to discover OAuth metadata
|
||||
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
|
||||
|
||||
// Start callback server
|
||||
await McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri)
|
||||
|
||||
const authProvider = new McpOAuthProvider(
|
||||
serverName,
|
||||
serverConfig.url,
|
||||
@@ -676,6 +694,7 @@ export const McpDebugCommand = cmd({
|
||||
clientId: oauthConfig?.clientId,
|
||||
clientSecret: oauthConfig?.clientSecret,
|
||||
scope: oauthConfig?.scope,
|
||||
redirectUri: oauthConfig?.redirectUri,
|
||||
},
|
||||
{
|
||||
onRedirect: async () => {},
|
||||
|
||||
@@ -5,7 +5,7 @@ import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda"
|
||||
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
import * as fuzzysort from "fuzzysort"
|
||||
|
||||
export function useConnected() {
|
||||
@@ -19,6 +19,7 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
const local = useLocal()
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const keybind = useKeybind()
|
||||
const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
|
||||
const [query, setQuery] = createSignal("")
|
||||
|
||||
@@ -207,14 +208,14 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
<DialogSelect
|
||||
keybind={[
|
||||
{
|
||||
keybind: Keybind.parse("ctrl+a")[0],
|
||||
keybind: keybind.all.model_provider_list?.[0],
|
||||
title: connected() ? "Connect provider" : "View all providers",
|
||||
onTrigger() {
|
||||
dialog.replace(() => <DialogProvider />)
|
||||
},
|
||||
},
|
||||
{
|
||||
keybind: Keybind.parse("ctrl+f")[0],
|
||||
keybind: keybind.all.model_favorite_toggle?.[0],
|
||||
title: "Favorite",
|
||||
disabled: !connected(),
|
||||
onTrigger: (option) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRoute } from "@tui/context/route"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
@@ -14,9 +14,10 @@ import "opentui-spinner/solid"
|
||||
|
||||
export function DialogSessionList() {
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
const { theme } = useTheme()
|
||||
const route = useRoute()
|
||||
const sync = useSync()
|
||||
const keybind = useKeybind()
|
||||
const { theme } = useTheme()
|
||||
const sdk = useSDK()
|
||||
const kv = useKV()
|
||||
|
||||
@@ -29,8 +30,6 @@ export function DialogSessionList() {
|
||||
return result.data ?? []
|
||||
})
|
||||
|
||||
const deleteKeybind = "ctrl+d"
|
||||
|
||||
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
||||
|
||||
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||||
@@ -52,7 +51,7 @@ export function DialogSessionList() {
|
||||
const status = sync.data.session_status?.[x.id]
|
||||
const isWorking = status?.type === "busy"
|
||||
return {
|
||||
title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title,
|
||||
title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : x.title,
|
||||
bg: isDeleting ? theme.error : undefined,
|
||||
value: x.id,
|
||||
category,
|
||||
@@ -89,7 +88,7 @@ export function DialogSessionList() {
|
||||
}}
|
||||
keybind={[
|
||||
{
|
||||
keybind: Keybind.parse(deleteKeybind)[0],
|
||||
keybind: keybind.all.session_delete?.[0],
|
||||
title: "delete",
|
||||
onTrigger: async (option) => {
|
||||
if (toDelete() === option.value) {
|
||||
@@ -103,7 +102,7 @@ export function DialogSessionList() {
|
||||
},
|
||||
},
|
||||
{
|
||||
keybind: Keybind.parse("ctrl+r")[0],
|
||||
keybind: keybind.all.session_rename?.[0],
|
||||
title: "rename",
|
||||
onTrigger: async (option) => {
|
||||
dialog.replace(() => <DialogSessionRename session={option.value} />)
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { createMemo, createSignal } from "solid-js"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
import { usePromptStash, type StashEntry } from "./prompt/stash"
|
||||
|
||||
function getRelativeTime(timestamp: number): string {
|
||||
@@ -30,6 +30,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
|
||||
const dialog = useDialog()
|
||||
const stash = usePromptStash()
|
||||
const { theme } = useTheme()
|
||||
const keybind = useKeybind()
|
||||
|
||||
const [toDelete, setToDelete] = createSignal<number>()
|
||||
|
||||
@@ -41,7 +42,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
|
||||
const isDeleting = toDelete() === index
|
||||
const lineCount = (entry.input.match(/\n/g)?.length ?? 0) + 1
|
||||
return {
|
||||
title: isDeleting ? "Press ctrl+d again to confirm" : getStashPreview(entry.input),
|
||||
title: isDeleting ? `Press ${keybind.print("stash_delete")} again to confirm` : getStashPreview(entry.input),
|
||||
bg: isDeleting ? theme.error : undefined,
|
||||
value: index,
|
||||
description: getRelativeTime(entry.timestamp),
|
||||
@@ -69,7 +70,7 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
|
||||
}}
|
||||
keybind={[
|
||||
{
|
||||
keybind: Keybind.parse("ctrl+d")[0],
|
||||
keybind: keybind.all.stash_delete?.[0],
|
||||
title: "delete",
|
||||
onTrigger: (option) => {
|
||||
if (toDelete() === option.value) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TextAttributes } from "@opentui/core"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { For, Match, Switch, Show, createMemo } from "solid-js"
|
||||
import { Installation } from "@/installation"
|
||||
|
||||
export type DialogStatusProps = {}
|
||||
|
||||
@@ -44,6 +45,7 @@ export function DialogStatus() {
|
||||
</text>
|
||||
<text fg={theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<text fg={theme.textMuted}>OpenCode v{Installation.VERSION}</text>
|
||||
<Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
|
||||
<box>
|
||||
<text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
|
||||
|
||||
@@ -1,24 +1,85 @@
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { For } from "solid-js"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { TextAttributes, RGBA } from "@opentui/core"
|
||||
import { For, type JSX } from "solid-js"
|
||||
import { useTheme, tint } from "@tui/context/theme"
|
||||
|
||||
const LOGO_LEFT = [` `, `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`, `█░░█ █░░█ █▀▀▀ █░░█`, `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀`]
|
||||
// Shadow markers (rendered chars in parens):
|
||||
// _ = full shadow cell (space with bg=shadow)
|
||||
// ^ = letter top, shadow bottom (▀ with fg=letter, bg=shadow)
|
||||
// ~ = shadow top only (▀ with fg=shadow)
|
||||
const SHADOW_MARKER = /[_^~]/
|
||||
|
||||
const LOGO_RIGHT = [` ▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`, `█░░░ █░░█ █░░█ █▀▀▀`, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`]
|
||||
const LOGO_LEFT = [` `, `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`, `█__█ █__█ █^^^ █__█`, `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀`]
|
||||
|
||||
const LOGO_RIGHT = [` ▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`, `█___ █__█ █__█ █^^^`, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`]
|
||||
|
||||
export function Logo() {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
|
||||
const shadow = tint(theme.background, fg, 0.25)
|
||||
const attrs = bold ? TextAttributes.BOLD : undefined
|
||||
const elements: JSX.Element[] = []
|
||||
let i = 0
|
||||
|
||||
while (i < line.length) {
|
||||
const rest = line.slice(i)
|
||||
const markerIndex = rest.search(SHADOW_MARKER)
|
||||
|
||||
if (markerIndex === -1) {
|
||||
elements.push(
|
||||
<text fg={fg} attributes={attrs} selectable={false}>
|
||||
{rest}
|
||||
</text>,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
if (markerIndex > 0) {
|
||||
elements.push(
|
||||
<text fg={fg} attributes={attrs} selectable={false}>
|
||||
{rest.slice(0, markerIndex)}
|
||||
</text>,
|
||||
)
|
||||
}
|
||||
|
||||
const marker = rest[markerIndex]
|
||||
switch (marker) {
|
||||
case "_":
|
||||
elements.push(
|
||||
<text fg={fg} bg={shadow} attributes={attrs} selectable={false}>
|
||||
{" "}
|
||||
</text>,
|
||||
)
|
||||
break
|
||||
case "^":
|
||||
elements.push(
|
||||
<text fg={fg} bg={shadow} attributes={attrs} selectable={false}>
|
||||
▀
|
||||
</text>,
|
||||
)
|
||||
break
|
||||
case "~":
|
||||
elements.push(
|
||||
<text fg={shadow} attributes={attrs} selectable={false}>
|
||||
▀
|
||||
</text>,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
i += markerIndex + 1
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
return (
|
||||
<box>
|
||||
<For each={LOGO_LEFT}>
|
||||
{(line, index) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.textMuted} selectable={false}>
|
||||
{line}
|
||||
</text>
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD} selectable={false}>
|
||||
{LOGO_RIGHT[index()]}
|
||||
</text>
|
||||
<box flexDirection="row">{renderLine(line, theme.textMuted, false)}</box>
|
||||
<box flexDirection="row">{renderLine(LOGO_RIGHT[index()], theme.text, true)}</box>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { FilePart } from "@opencode-ai/sdk/v2"
|
||||
import { TuiEvent } from "../../event"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { formatDuration } from "@/util/format"
|
||||
import { createColors, createFrames } from "../../ui/spinner.ts"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
|
||||
@@ -1037,7 +1038,8 @@ export function Prompt(props: PromptProps) {
|
||||
if (!r) return ""
|
||||
const baseMessage = message()
|
||||
const truncatedHint = isTruncated() ? " (click to expand)" : ""
|
||||
const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]`
|
||||
const duration = formatDuration(seconds())
|
||||
const retryInfo = ` [retrying ${duration ? `in ${duration} ` : ""}attempt #${r.attempt}]`
|
||||
return baseMessage + truncatedHint + retryInfo
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { createMemo, createSignal, For } from "solid-js"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { DEFAULT_THEMES, useTheme } from "@tui/context/theme"
|
||||
|
||||
const themeCount = Object.keys(DEFAULT_THEMES).length
|
||||
const themeTip = `Use {highlight}/theme{/highlight} or {highlight}Ctrl+X T{/highlight} to switch between ${themeCount} built-in themes`
|
||||
|
||||
type TipPart = { text: string; highlight: boolean }
|
||||
|
||||
@@ -57,7 +60,7 @@ const TIPS = [
|
||||
"Press {highlight}Ctrl+X E{/highlight} or {highlight}/editor{/highlight} to compose messages in your external editor",
|
||||
"Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase",
|
||||
"Run {highlight}/models{/highlight} or {highlight}Ctrl+X M{/highlight} to see and switch between available AI models",
|
||||
"Use {highlight}/theme{/highlight} or {highlight}Ctrl+X T{/highlight} to switch between 50+ built-in themes",
|
||||
themeTip,
|
||||
"Press {highlight}Ctrl+X N{/highlight} or {highlight}/new{/highlight} to start a fresh conversation session",
|
||||
"Use {highlight}/sessions{/highlight} or {highlight}Ctrl+X L{/highlight} to list and continue previous conversations",
|
||||
"Run {highlight}/compact{/highlight} to summarize long sessions near context limits",
|
||||
|
||||
@@ -35,6 +35,7 @@ import tokyonight from "./theme/tokyonight.json" with { type: "json" }
|
||||
import vercel from "./theme/vercel.json" with { type: "json" }
|
||||
import vesper from "./theme/vesper.json" with { type: "json" }
|
||||
import zenburn from "./theme/zenburn.json" with { type: "json" }
|
||||
import carbonfox from "./theme/carbonfox.json" with { type: "json" }
|
||||
import { useKV } from "./kv"
|
||||
import { useRenderer } from "@opentui/solid"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
@@ -170,6 +171,7 @@ export const DEFAULT_THEMES: Record<string, ThemeJson> = {
|
||||
vesper,
|
||||
vercel,
|
||||
zenburn,
|
||||
carbonfox,
|
||||
}
|
||||
|
||||
function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
||||
@@ -417,6 +419,13 @@ async function getCustomThemes() {
|
||||
return result
|
||||
}
|
||||
|
||||
export function tint(base: RGBA, overlay: RGBA, alpha: number): RGBA {
|
||||
const r = base.r + (overlay.r - base.r) * alpha
|
||||
const g = base.g + (overlay.g - base.g) * alpha
|
||||
const b = base.b + (overlay.b - base.b) * alpha
|
||||
return RGBA.fromInts(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255))
|
||||
}
|
||||
|
||||
function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
|
||||
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
|
||||
const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
|
||||
@@ -428,13 +437,6 @@ function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJs
|
||||
return ansiToRgba(i)
|
||||
}
|
||||
|
||||
const tint = (base: RGBA, overlay: RGBA, alpha: number) => {
|
||||
const r = base.r + (overlay.r - base.r) * alpha
|
||||
const g = base.g + (overlay.g - base.g) * alpha
|
||||
const b = base.b + (overlay.b - base.b) * alpha
|
||||
return RGBA.fromInts(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255))
|
||||
}
|
||||
|
||||
// Generate gray scale based on terminal background
|
||||
const grays = generateGrayScale(bg, isDark)
|
||||
const textMuted = generateMutedTextColor(bg, isDark)
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"bg0": "#0d0d0d",
|
||||
"bg1": "#161616",
|
||||
"bg1a": "#1a1a1a",
|
||||
"bg2": "#1e1e1e",
|
||||
"bg3": "#262626",
|
||||
"bg4": "#303030",
|
||||
"fg0": "#ffffff",
|
||||
"fg1": "#f2f4f8",
|
||||
"fg2": "#a9afbc",
|
||||
"fg3": "#7d848f",
|
||||
"lbg0": "#ffffff",
|
||||
"lbg1": "#f4f4f4",
|
||||
"lbg2": "#e8e8e8",
|
||||
"lbg3": "#dcdcdc",
|
||||
"lfg0": "#000000",
|
||||
"lfg1": "#161616",
|
||||
"lfg2": "#525252",
|
||||
"lfg3": "#6f6f6f",
|
||||
"red": "#ee5396",
|
||||
"green": "#25be6a",
|
||||
"yellow": "#08bdba",
|
||||
"blue": "#78a9ff",
|
||||
"magenta": "#be95ff",
|
||||
"cyan": "#33b1ff",
|
||||
"white": "#dfdfe0",
|
||||
"orange": "#3ddbd9",
|
||||
"pink": "#ff7eb6",
|
||||
"blueBright": "#8cb6ff",
|
||||
"cyanBright": "#52c7ff",
|
||||
"greenBright": "#46c880",
|
||||
"redLight": "#9f1853",
|
||||
"greenLight": "#198038",
|
||||
"yellowLight": "#007d79",
|
||||
"blueLight": "#0043ce",
|
||||
"magentaLight": "#6929c4",
|
||||
"cyanLight": "#0072c3",
|
||||
"warning": "#f1c21b",
|
||||
"diffGreen": "#50fa7b",
|
||||
"diffRed": "#ff6b6b",
|
||||
"diffGreenBg": "#0f2418",
|
||||
"diffRedBg": "#2a1216"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "cyan",
|
||||
"light": "blueLight"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "blue",
|
||||
"light": "blueLight"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "pink",
|
||||
"light": "redLight"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "redLight"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "warning",
|
||||
"light": "yellowLight"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "greenLight"
|
||||
},
|
||||
"info": {
|
||||
"dark": "blue",
|
||||
"light": "blueLight"
|
||||
},
|
||||
"text": {
|
||||
"dark": "fg1",
|
||||
"light": "lfg1"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "fg3",
|
||||
"light": "lfg3"
|
||||
},
|
||||
"background": {
|
||||
"dark": "bg1",
|
||||
"light": "lbg0"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "bg1a",
|
||||
"light": "lbg1"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "bg2",
|
||||
"light": "lbg1"
|
||||
},
|
||||
"border": {
|
||||
"dark": "bg4",
|
||||
"light": "lbg3"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "cyan",
|
||||
"light": "blueLight"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "bg3",
|
||||
"light": "lbg2"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "diffGreen",
|
||||
"light": "greenLight"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "diffRed",
|
||||
"light": "redLight"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "fg3",
|
||||
"light": "lfg3"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "blue",
|
||||
"light": "blueLight"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "#7dffaa",
|
||||
"light": "greenLight"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "#ff9999",
|
||||
"light": "redLight"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "diffGreenBg",
|
||||
"light": "#defbe6"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "diffRedBg",
|
||||
"light": "#fff1f1"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "bg1",
|
||||
"light": "lbg1"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "fg3",
|
||||
"light": "lfg3"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "diffGreenBg",
|
||||
"light": "#defbe6"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "diffRedBg",
|
||||
"light": "#fff1f1"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "fg1",
|
||||
"light": "lfg1"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "blueBright",
|
||||
"light": "blueLight"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "blue",
|
||||
"light": "blueLight"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "cyan",
|
||||
"light": "cyanLight"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "greenLight"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "fg3",
|
||||
"light": "lfg3"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "magenta",
|
||||
"light": "magentaLight"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "fg0",
|
||||
"light": "lfg0"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "bg4",
|
||||
"light": "lbg3"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "cyan",
|
||||
"light": "cyanLight"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "cyan",
|
||||
"light": "cyanLight"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "blue",
|
||||
"light": "blueLight"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "cyan",
|
||||
"light": "cyanLight"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "fg2",
|
||||
"light": "lfg2"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "fg3",
|
||||
"light": "lfg3"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "magenta",
|
||||
"light": "magentaLight"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "blueBright",
|
||||
"light": "blueLight"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "white",
|
||||
"light": "lfg1"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "green",
|
||||
"light": "greenLight"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "orange",
|
||||
"light": "yellowLight"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "yellow",
|
||||
"light": "yellowLight"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "fg2",
|
||||
"light": "lfg2"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "fg2",
|
||||
"light": "lfg1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { SplitBorder } from "@tui/component/border"
|
||||
import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
|
||||
import { useCommandDialog } from "@tui/component/dialog-command"
|
||||
import { useKeybind } from "../../context/keybind"
|
||||
import { Installation } from "@/installation"
|
||||
|
||||
const Title = (props: { session: Accessor<Session> }) => {
|
||||
const { theme } = useTheme()
|
||||
@@ -113,13 +114,19 @@ export function Header() {
|
||||
</text>
|
||||
</box>
|
||||
<box flexGrow={1} flexShrink={1} />
|
||||
<ContextInfo context={context} cost={cost} />
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
<ContextInfo context={context} cost={cost} />
|
||||
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
|
||||
</box>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<box flexDirection="row" justifyContent="space-between" gap={1}>
|
||||
<Title session={session} />
|
||||
<ContextInfo context={context} cost={cost} />
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
<ContextInfo context={context} cost={cost} />
|
||||
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
|
||||
</box>
|
||||
</box>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createMemo, For, Show } from "solid-js"
|
||||
import { useKeyboard } from "@opentui/solid"
|
||||
import type { TextareaRenderable } from "@opentui/core"
|
||||
import { useKeybind } from "../../context/keybind"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { tint, useTheme } from "../../context/theme"
|
||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
import { SplitBorder } from "../../component/border"
|
||||
@@ -125,7 +125,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
// Skip processing if a dialog (e.g., command palette) is open
|
||||
if (dialog.stack.length > 0) return
|
||||
|
||||
// When editing "Other" textarea
|
||||
// When editing custom answer textarea
|
||||
if (store.editing && !confirm()) {
|
||||
if (evt.name === "escape") {
|
||||
evt.preventDefault()
|
||||
@@ -198,6 +198,12 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
selectTab((store.tab + 1) % tabs())
|
||||
}
|
||||
|
||||
if (evt.name === "tab") {
|
||||
evt.preventDefault()
|
||||
const direction = evt.shift ? -1 : 1
|
||||
selectTab((store.tab + direction + tabs()) % tabs())
|
||||
}
|
||||
|
||||
if (confirm()) {
|
||||
if (evt.name === "return") {
|
||||
evt.preventDefault()
|
||||
@@ -299,12 +305,15 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||
return (
|
||||
<box onMouseOver={() => moveTo(i())} onMouseUp={() => selectOption()}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<box flexDirection="row">
|
||||
<box backgroundColor={active() ? theme.backgroundElement : undefined} paddingRight={1}>
|
||||
<text fg={active() ? tint(theme.textMuted, theme.secondary, 0.6) : theme.textMuted}>
|
||||
{`${i() + 1}.`}
|
||||
</text>
|
||||
</box>
|
||||
<box backgroundColor={active() ? theme.backgroundElement : undefined}>
|
||||
<text fg={active() ? theme.secondary : picked() ? theme.success : theme.text}>
|
||||
{multi()
|
||||
? `${i() + 1}. [${picked() ? "✓" : " "}] ${opt.label}`
|
||||
: `${i() + 1}. ${opt.label}`}
|
||||
{multi() ? `[${picked() ? "✓" : " "}] ${opt.label}` : opt.label}
|
||||
</text>
|
||||
</box>
|
||||
<Show when={!multi()}>
|
||||
@@ -321,14 +330,18 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
</For>
|
||||
<Show when={custom()}>
|
||||
<box onMouseOver={() => moveTo(options().length)} onMouseUp={() => selectOption()}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
|
||||
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
|
||||
{multi()
|
||||
? `${options().length + 1}. [${customPicked() ? "✓" : " "}] Type your own answer`
|
||||
: `${options().length + 1}. Type your own answer`}
|
||||
<box flexDirection="row">
|
||||
<box backgroundColor={other() ? theme.backgroundElement : undefined} paddingRight={1}>
|
||||
<text fg={other() ? tint(theme.textMuted, theme.secondary, 0.6) : theme.textMuted}>
|
||||
{`${options().length + 1}.`}
|
||||
</text>
|
||||
</box>
|
||||
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
|
||||
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
|
||||
{multi() ? `[${customPicked() ? "✓" : " "}] Type your own answer` : "Type your own answer"}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
<Show when={!multi()}>
|
||||
<text fg={theme.success}>{customPicked() ? "✓" : ""}</text>
|
||||
</Show>
|
||||
|
||||
@@ -237,17 +237,10 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
<Show when={diff().length <= 2 || expanded.diff}>
|
||||
<For each={diff() || []}>
|
||||
{(item) => {
|
||||
const file = createMemo(() => {
|
||||
const splits = item.file.split(path.sep).filter(Boolean)
|
||||
const last = splits.at(-1)!
|
||||
const rest = splits.slice(0, -1).join(path.sep)
|
||||
if (!rest) return last
|
||||
return Locale.truncateMiddle(rest, 30 - last.length) + "/" + last
|
||||
})
|
||||
return (
|
||||
<box flexDirection="row" gap={1} justifyContent="space-between">
|
||||
<text fg={theme.textMuted} wrapMode="char">
|
||||
{file()}
|
||||
<text fg={theme.textMuted} wrapMode="none">
|
||||
{item.file}
|
||||
</text>
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
<Show when={item.additions}>
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface DialogSelectProps<T> {
|
||||
onSelect?: (option: DialogSelectOption<T>) => void
|
||||
skipFilter?: boolean
|
||||
keybind?: {
|
||||
keybind: Keybind.Info
|
||||
keybind?: Keybind.Info
|
||||
title: string
|
||||
disabled?: boolean
|
||||
onTrigger: (option: DialogSelectOption<T>) => void
|
||||
@@ -109,15 +109,16 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
|
||||
createEffect(
|
||||
on([() => store.filter, () => props.current], ([filter, current]) => {
|
||||
if (filter.length > 0) {
|
||||
setStore("selected", 0)
|
||||
} else if (current) {
|
||||
const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current))
|
||||
if (currentIndex >= 0) {
|
||||
setStore("selected", currentIndex)
|
||||
setTimeout(() => {
|
||||
if (filter.length > 0) {
|
||||
moveTo(0, true)
|
||||
} else if (current) {
|
||||
const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current))
|
||||
if (currentIndex >= 0) {
|
||||
moveTo(currentIndex, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
scroll?.scrollTo(0)
|
||||
}, 0)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -129,7 +130,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
moveTo(next)
|
||||
}
|
||||
|
||||
function moveTo(next: number) {
|
||||
function moveTo(next: number, center = false) {
|
||||
setStore("selected", next)
|
||||
props.onMove?.(selected()!)
|
||||
if (!scroll) return
|
||||
@@ -138,13 +139,18 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
})
|
||||
if (!target) return
|
||||
const y = target.y - scroll.y
|
||||
if (y >= scroll.height) {
|
||||
scroll.scrollBy(y - scroll.height + 1)
|
||||
}
|
||||
if (y < 0) {
|
||||
scroll.scrollBy(y)
|
||||
if (isDeepEqual(flat()[0].value, selected()?.value)) {
|
||||
scroll.scrollTo(0)
|
||||
if (center) {
|
||||
const centerOffset = Math.floor(scroll.height / 2)
|
||||
scroll.scrollBy(y - centerOffset)
|
||||
} else {
|
||||
if (y >= scroll.height) {
|
||||
scroll.scrollBy(y - scroll.height + 1)
|
||||
}
|
||||
if (y < 0) {
|
||||
scroll.scrollBy(y)
|
||||
if (isDeepEqual(flat()[0].value, selected()?.value)) {
|
||||
scroll.scrollTo(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +172,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
}
|
||||
|
||||
for (const item of props.keybind ?? []) {
|
||||
if (item.disabled) continue
|
||||
if (item.disabled || !item.keybind) continue
|
||||
if (Keybind.match(item.keybind, keybind.parse(evt))) {
|
||||
const s = selected()
|
||||
if (s) {
|
||||
@@ -188,7 +194,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
}
|
||||
props.ref?.(ref)
|
||||
|
||||
const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled) ?? [])
|
||||
const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled && x.keybind) ?? [])
|
||||
|
||||
return (
|
||||
<box gap={1} paddingBottom={1}>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function FormatError(input: unknown) {
|
||||
return `Directory "${input.data.dir}" in ${input.data.path} is not valid. Rename the directory to "${input.data.suggestion}" or remove it. This is a common typo.`
|
||||
}
|
||||
if (ConfigMarkdown.FrontmatterError.isInstance(input)) {
|
||||
return `Failed to parse frontmatter in ${input.data.path}:\n${input.data.message}`
|
||||
return input.data.message
|
||||
}
|
||||
if (Config.InvalidError.isInstance(input))
|
||||
return [
|
||||
|
||||
@@ -19,6 +19,7 @@ import { BunProc } from "@/bun"
|
||||
import { Installation } from "@/installation"
|
||||
import { ConfigMarkdown } from "./markdown"
|
||||
import { existsSync } from "fs"
|
||||
import { Bus } from "@/bus"
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
@@ -231,8 +232,16 @@ export namespace Config {
|
||||
dot: true,
|
||||
cwd: dir,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item)
|
||||
if (!md.data) continue
|
||||
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
? err.data.message
|
||||
: `Failed to parse command ${item}`
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
log.error("failed to load command", { command: item, err })
|
||||
return undefined
|
||||
})
|
||||
if (!md) continue
|
||||
|
||||
const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"]
|
||||
const file = rel(item, patterns) ?? path.basename(item)
|
||||
@@ -263,8 +272,16 @@ export namespace Config {
|
||||
dot: true,
|
||||
cwd: dir,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item)
|
||||
if (!md.data) continue
|
||||
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
? err.data.message
|
||||
: `Failed to parse agent ${item}`
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
log.error("failed to load agent", { agent: item, err })
|
||||
return undefined
|
||||
})
|
||||
if (!md) continue
|
||||
|
||||
const patterns = ["/.opencode/agent/", "/.opencode/agents/", "/agent/", "/agents/"]
|
||||
const file = rel(item, patterns) ?? path.basename(item)
|
||||
@@ -294,8 +311,16 @@ export namespace Config {
|
||||
dot: true,
|
||||
cwd: dir,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item)
|
||||
if (!md.data) continue
|
||||
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
? err.data.message
|
||||
: `Failed to parse mode ${item}`
|
||||
const { Session } = await import("@/session")
|
||||
Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
|
||||
log.error("failed to load mode", { mode: item, err })
|
||||
return undefined
|
||||
})
|
||||
if (!md) continue
|
||||
|
||||
const config = {
|
||||
name: path.basename(item, ".md"),
|
||||
@@ -395,9 +420,7 @@ export namespace Config {
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe(
|
||||
"Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
|
||||
),
|
||||
.describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
@@ -412,6 +435,10 @@ export namespace Config {
|
||||
.describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."),
|
||||
clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"),
|
||||
scope: z.string().optional().describe("OAuth scopes to request during authorization"),
|
||||
redirectUri: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
@@ -436,9 +463,7 @@ export namespace Config {
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe(
|
||||
"Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
|
||||
),
|
||||
.describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
@@ -621,7 +646,11 @@ export namespace Config {
|
||||
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
|
||||
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
|
||||
session_fork: z.string().optional().default("none").describe("Fork session from message"),
|
||||
session_rename: z.string().optional().default("none").describe("Rename session"),
|
||||
session_rename: z.string().optional().default("ctrl+r").describe("Rename session"),
|
||||
session_delete: z.string().optional().default("ctrl+d").describe("Delete session"),
|
||||
stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"),
|
||||
model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"),
|
||||
model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"),
|
||||
session_share: z.string().optional().default("none").describe("Share current session"),
|
||||
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
|
||||
session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
|
||||
@@ -915,7 +944,7 @@ export namespace Config {
|
||||
})
|
||||
.catchall(Agent)
|
||||
.optional()
|
||||
.describe("Agent configuration, see https://opencode.ai/docs/agent"),
|
||||
.describe("Agent configuration, see https://opencode.ai/docs/agents"),
|
||||
provider: z
|
||||
.record(z.string(), Provider)
|
||||
.optional()
|
||||
|
||||
@@ -14,8 +14,60 @@ export namespace ConfigMarkdown {
|
||||
return Array.from(template.matchAll(SHELL_REGEX))
|
||||
}
|
||||
|
||||
export function preprocessFrontmatter(content: string): string {
|
||||
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
||||
if (!match) return content
|
||||
|
||||
const frontmatter = match[1]
|
||||
const lines = frontmatter.split("\n")
|
||||
const result: string[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
// skip comments and empty lines
|
||||
if (line.trim().startsWith("#") || line.trim() === "") {
|
||||
result.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
// skip lines that are continuations (indented)
|
||||
if (line.match(/^\s+/)) {
|
||||
result.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
// match key: value pattern
|
||||
const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/)
|
||||
if (!kvMatch) {
|
||||
result.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
const key = kvMatch[1]
|
||||
const value = kvMatch[2].trim()
|
||||
|
||||
// skip if value is empty, already quoted, or uses block scalar
|
||||
if (value === "" || value === ">" || value === "|" || value.startsWith('"') || value.startsWith("'")) {
|
||||
result.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
// if value contains a colon, convert to block scalar
|
||||
if (value.includes(":")) {
|
||||
result.push(`${key}: |`)
|
||||
result.push(` ${value}`)
|
||||
continue
|
||||
}
|
||||
|
||||
result.push(line)
|
||||
}
|
||||
|
||||
const processed = result.join("\n")
|
||||
return content.replace(frontmatter, () => processed)
|
||||
}
|
||||
|
||||
export async function parse(filePath: string) {
|
||||
const template = await Bun.file(filePath).text()
|
||||
const raw = await Bun.file(filePath).text()
|
||||
const template = preprocessFrontmatter(raw)
|
||||
|
||||
try {
|
||||
const md = matter(template)
|
||||
@@ -24,7 +76,7 @@ export namespace ConfigMarkdown {
|
||||
throw new FrontmatterError(
|
||||
{
|
||||
path: filePath,
|
||||
message: `Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
|
||||
message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
|
||||
},
|
||||
{ cause: err },
|
||||
)
|
||||
|
||||
@@ -46,6 +46,14 @@ export namespace MCP {
|
||||
}),
|
||||
)
|
||||
|
||||
export const BrowserOpenFailed = BusEvent.define(
|
||||
"mcp.browser.open.failed",
|
||||
z.object({
|
||||
mcpName: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const Failed = NamedError.create(
|
||||
"MCPFailed",
|
||||
z.object({
|
||||
@@ -109,7 +117,7 @@ export namespace MCP {
|
||||
}
|
||||
|
||||
// Convert MCP tool definition to AI SDK Tool type
|
||||
async function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient): Promise<Tool> {
|
||||
async function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Promise<Tool> {
|
||||
const inputSchema = mcpTool.inputSchema
|
||||
|
||||
// Spread first, then override type to ensure it's always "object"
|
||||
@@ -119,7 +127,6 @@ export namespace MCP {
|
||||
properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"],
|
||||
additionalProperties: false,
|
||||
}
|
||||
const config = await Config.get()
|
||||
|
||||
return dynamicTool({
|
||||
description: mcpTool.description ?? "",
|
||||
@@ -133,7 +140,7 @@ export namespace MCP {
|
||||
CallToolResultSchema,
|
||||
{
|
||||
resetTimeoutOnProgress: true,
|
||||
timeout: config.experimental?.mcp_timeout,
|
||||
timeout,
|
||||
},
|
||||
)
|
||||
},
|
||||
@@ -301,6 +308,8 @@ export namespace MCP {
|
||||
let authProvider: McpOAuthProvider | undefined
|
||||
|
||||
if (!oauthDisabled) {
|
||||
await McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri)
|
||||
|
||||
authProvider = new McpOAuthProvider(
|
||||
key,
|
||||
mcp.url,
|
||||
@@ -308,6 +317,7 @@ export namespace MCP {
|
||||
clientId: oauthConfig?.clientId,
|
||||
clientSecret: oauthConfig?.clientSecret,
|
||||
scope: oauthConfig?.scope,
|
||||
redirectUri: oauthConfig?.redirectUri,
|
||||
},
|
||||
{
|
||||
onRedirect: async (url) => {
|
||||
@@ -337,6 +347,7 @@ export namespace MCP {
|
||||
|
||||
let lastError: Error | undefined
|
||||
const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT
|
||||
|
||||
for (const { name, transport } of transports) {
|
||||
try {
|
||||
const client = new Client({
|
||||
@@ -556,11 +567,15 @@ export namespace MCP {
|
||||
export async function tools() {
|
||||
const result: Record<string, Tool> = {}
|
||||
const s = await state()
|
||||
const cfg = await Config.get()
|
||||
const config = cfg.mcp ?? {}
|
||||
const clientsSnapshot = await clients()
|
||||
const defaultTimeout = cfg.experimental?.mcp_timeout
|
||||
|
||||
for (const [clientName, client] of Object.entries(clientsSnapshot)) {
|
||||
// Only include tools from connected MCPs (skip disabled ones)
|
||||
if (s.status[clientName]?.status !== "connected") {
|
||||
const clientStatus = s.status[clientName]?.status
|
||||
if (clientStatus !== "connected") {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -577,10 +592,13 @@ export namespace MCP {
|
||||
if (!toolsResult) {
|
||||
continue
|
||||
}
|
||||
const mcpConfig = config[clientName]
|
||||
const entry = isMcpConfigured(mcpConfig) ? mcpConfig : undefined
|
||||
const timeout = entry?.timeout ?? defaultTimeout
|
||||
for (const mcpTool of toolsResult.tools) {
|
||||
const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
const sanitizedToolName = mcpTool.name.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
result[sanitizedClientName + "_" + sanitizedToolName] = await convertMcpTool(mcpTool, client)
|
||||
result[sanitizedClientName + "_" + sanitizedToolName] = await convertMcpTool(mcpTool, client, timeout)
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -707,8 +725,10 @@ export namespace MCP {
|
||||
throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
|
||||
}
|
||||
|
||||
// Start the callback server
|
||||
await McpOAuthCallback.ensureRunning()
|
||||
// OAuth config is optional - if not provided, we'll use auto-discovery
|
||||
const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined
|
||||
|
||||
await McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri)
|
||||
|
||||
// Generate and store a cryptographically secure state parameter BEFORE creating the provider
|
||||
// The SDK will call provider.state() to read this value
|
||||
@@ -718,8 +738,6 @@ export namespace MCP {
|
||||
await McpAuth.updateOAuthState(mcpName, oauthState)
|
||||
|
||||
// Create a new auth provider for this flow
|
||||
// OAuth config is optional - if not provided, we'll use auto-discovery
|
||||
const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined
|
||||
let capturedUrl: URL | undefined
|
||||
const authProvider = new McpOAuthProvider(
|
||||
mcpName,
|
||||
@@ -728,6 +746,7 @@ export namespace MCP {
|
||||
clientId: oauthConfig?.clientId,
|
||||
clientSecret: oauthConfig?.clientSecret,
|
||||
scope: oauthConfig?.scope,
|
||||
redirectUri: oauthConfig?.redirectUri,
|
||||
},
|
||||
{
|
||||
onRedirect: async (url) => {
|
||||
@@ -756,6 +775,7 @@ export namespace MCP {
|
||||
pendingOAuthTransports.set(mcpName, transport)
|
||||
return { authorizationUrl: capturedUrl.toString() }
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -765,9 +785,9 @@ export namespace MCP {
|
||||
* Opens the browser and waits for callback.
|
||||
*/
|
||||
export async function authenticate(mcpName: string): Promise<Status> {
|
||||
const { authorizationUrl } = await startAuth(mcpName)
|
||||
const result = await startAuth(mcpName)
|
||||
|
||||
if (!authorizationUrl) {
|
||||
if (!result.authorizationUrl) {
|
||||
// Already authenticated
|
||||
const s = await state()
|
||||
return s.status[mcpName] ?? { status: "connected" }
|
||||
@@ -781,8 +801,33 @@ export namespace MCP {
|
||||
|
||||
// The SDK has already added the state parameter to the authorization URL
|
||||
// We just need to open the browser
|
||||
log.info("opening browser for oauth", { mcpName, url: authorizationUrl, state: oauthState })
|
||||
await open(authorizationUrl)
|
||||
log.info("opening browser for oauth", { mcpName, url: result.authorizationUrl, state: oauthState })
|
||||
try {
|
||||
const subprocess = await open(result.authorizationUrl)
|
||||
// The open package spawns a detached process and returns immediately.
|
||||
// We need to listen for errors which fire asynchronously:
|
||||
// - "error" event: command not found (ENOENT)
|
||||
// - "exit" with non-zero code: command exists but failed (e.g., no display)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// Give the process a moment to fail if it's going to
|
||||
const timeout = setTimeout(() => resolve(), 500)
|
||||
subprocess.on("error", (error) => {
|
||||
clearTimeout(timeout)
|
||||
reject(error)
|
||||
})
|
||||
subprocess.on("exit", (code) => {
|
||||
if (code !== null && code !== 0) {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error(`Browser open failed with exit code ${code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
// Browser opening failed (e.g., in remote/headless sessions like SSH, devcontainers)
|
||||
// Emit event so CLI can display the URL for manual opening
|
||||
log.warn("failed to open browser, user must open URL manually", { mcpName, error })
|
||||
Bus.publish(BrowserOpenFailed, { mcpName, url: result.authorizationUrl })
|
||||
}
|
||||
|
||||
// Wait for callback using the OAuth state parameter
|
||||
const code = await McpOAuthCallback.waitForCallback(oauthState)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Log } from "../util/log"
|
||||
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
|
||||
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider"
|
||||
|
||||
const log = Log.create({ service: "mcp.oauth-callback" })
|
||||
|
||||
// Current callback server configuration (may differ from defaults if custom redirectUri is used)
|
||||
let currentPort = OAUTH_CALLBACK_PORT
|
||||
let currentPath = OAUTH_CALLBACK_PATH
|
||||
|
||||
const HTML_SUCCESS = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -56,21 +60,33 @@ export namespace McpOAuthCallback {
|
||||
|
||||
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
export async function ensureRunning(): Promise<void> {
|
||||
export async function ensureRunning(redirectUri?: string): Promise<void> {
|
||||
// Parse the redirect URI to get port and path (uses defaults if not provided)
|
||||
const { port, path } = parseRedirectUri(redirectUri)
|
||||
|
||||
// If server is running on a different port/path, stop it first
|
||||
if (server && (currentPort !== port || currentPath !== path)) {
|
||||
log.info("stopping oauth callback server to reconfigure", { oldPort: currentPort, newPort: port })
|
||||
await stop()
|
||||
}
|
||||
|
||||
if (server) return
|
||||
|
||||
const running = await isPortInUse()
|
||||
const running = await isPortInUse(port)
|
||||
if (running) {
|
||||
log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
|
||||
log.info("oauth callback server already running on another instance", { port })
|
||||
return
|
||||
}
|
||||
|
||||
currentPort = port
|
||||
currentPath = path
|
||||
|
||||
server = Bun.serve({
|
||||
port: OAUTH_CALLBACK_PORT,
|
||||
port: currentPort,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
|
||||
if (url.pathname !== OAUTH_CALLBACK_PATH) {
|
||||
if (url.pathname !== currentPath) {
|
||||
return new Response("Not found", { status: 404 })
|
||||
}
|
||||
|
||||
@@ -133,7 +149,7 @@ export namespace McpOAuthCallback {
|
||||
},
|
||||
})
|
||||
|
||||
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
|
||||
log.info("oauth callback server started", { port: currentPort, path: currentPath })
|
||||
}
|
||||
|
||||
export function waitForCallback(oauthState: string): Promise<string> {
|
||||
@@ -158,11 +174,11 @@ export namespace McpOAuthCallback {
|
||||
}
|
||||
}
|
||||
|
||||
export async function isPortInUse(): Promise<boolean> {
|
||||
export async function isPortInUse(port: number = OAUTH_CALLBACK_PORT): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
Bun.connect({
|
||||
hostname: "127.0.0.1",
|
||||
port: OAUTH_CALLBACK_PORT,
|
||||
port,
|
||||
socket: {
|
||||
open(socket) {
|
||||
socket.end()
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface McpOAuthConfig {
|
||||
clientId?: string
|
||||
clientSecret?: string
|
||||
scope?: string
|
||||
redirectUri?: string
|
||||
}
|
||||
|
||||
export interface McpOAuthCallbacks {
|
||||
@@ -32,6 +33,10 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||
) {}
|
||||
|
||||
get redirectUrl(): string {
|
||||
// Use configured redirectUri if provided, otherwise use OpenCode defaults
|
||||
if (this.config.redirectUri) {
|
||||
return this.config.redirectUri
|
||||
}
|
||||
return `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`
|
||||
}
|
||||
|
||||
@@ -152,3 +157,22 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||
}
|
||||
|
||||
export { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH }
|
||||
|
||||
/**
|
||||
* Parse a redirect URI to extract port and path for the callback server.
|
||||
* Returns defaults if the URI can't be parsed.
|
||||
*/
|
||||
export function parseRedirectUri(redirectUri?: string): { port: number; path: string } {
|
||||
if (!redirectUri) {
|
||||
return { port: OAUTH_CALLBACK_PORT, path: OAUTH_CALLBACK_PATH }
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(redirectUri)
|
||||
const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80
|
||||
const path = url.pathname || OAUTH_CALLBACK_PATH
|
||||
return { port, path }
|
||||
} catch {
|
||||
return { port: OAUTH_CALLBACK_PORT, path: OAUTH_CALLBACK_PATH }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,38 +361,6 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
}
|
||||
}
|
||||
|
||||
if (!provider.models["gpt-5.2-codex"]) {
|
||||
const model = {
|
||||
id: "gpt-5.2-codex",
|
||||
providerID: "openai",
|
||||
api: {
|
||||
id: "gpt-5.2-codex",
|
||||
url: "https://chatgpt.com/backend-api/codex",
|
||||
npm: "@ai-sdk/openai",
|
||||
},
|
||||
name: "GPT-5.2 Codex",
|
||||
capabilities: {
|
||||
temperature: false,
|
||||
reasoning: true,
|
||||
attachment: true,
|
||||
toolcall: true,
|
||||
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
||||
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||
interleaved: false,
|
||||
},
|
||||
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
||||
limit: { context: 400000, output: 128000 },
|
||||
status: "active" as const,
|
||||
options: {},
|
||||
headers: {},
|
||||
release_date: "2025-12-18",
|
||||
variants: {} as Record<string, Record<string, any>>,
|
||||
family: "gpt-codex",
|
||||
}
|
||||
model.variants = ProviderTransform.variants(model)
|
||||
provider.models["gpt-5.2-codex"] = model
|
||||
}
|
||||
|
||||
// Zero out costs for Codex (included with ChatGPT subscription)
|
||||
for (const model of Object.values(provider.models)) {
|
||||
model.cost = {
|
||||
|
||||
@@ -272,7 +272,11 @@ export namespace Project {
|
||||
|
||||
export async function list() {
|
||||
const keys = await Storage.list(["project"])
|
||||
return await Promise.all(keys.map((x) => Storage.read<Info>(x)))
|
||||
const projects = await Promise.all(keys.map((x) => Storage.read<Info>(x)))
|
||||
return projects.map((project) => ({
|
||||
...project,
|
||||
sandboxes: project.sandboxes?.filter((x) => existsSync(x)),
|
||||
}))
|
||||
}
|
||||
|
||||
export const update = fn(
|
||||
|
||||
@@ -47,6 +47,7 @@ export namespace ModelsDev {
|
||||
.optional(),
|
||||
limit: z.object({
|
||||
context: z.number(),
|
||||
input: z.number().optional(),
|
||||
output: z.number(),
|
||||
}),
|
||||
modalities: z
|
||||
@@ -80,7 +81,11 @@ export namespace ModelsDev {
|
||||
const file = Bun.file(filepath)
|
||||
const result = await file.json().catch(() => {})
|
||||
if (result) return result as Record<string, Provider>
|
||||
const json = await data()
|
||||
if (typeof data === "function") {
|
||||
const json = await data()
|
||||
return JSON.parse(json) as Record<string, Provider>
|
||||
}
|
||||
const json = await fetch("https://models.dev/api.json").then((x) => x.text())
|
||||
return JSON.parse(json) as Record<string, Provider>
|
||||
}
|
||||
|
||||
|
||||
@@ -557,6 +557,7 @@ export namespace Provider {
|
||||
}),
|
||||
limit: z.object({
|
||||
context: z.number(),
|
||||
input: z.number().optional(),
|
||||
output: z.number(),
|
||||
}),
|
||||
status: z.enum(["alpha", "beta", "deprecated", "active"]),
|
||||
@@ -619,6 +620,7 @@ export namespace Provider {
|
||||
},
|
||||
limit: {
|
||||
context: model.limit.context,
|
||||
input: model.limit.input,
|
||||
output: model.limit.output,
|
||||
},
|
||||
capabilities: {
|
||||
|
||||
@@ -16,7 +16,38 @@ function mimeToModality(mime: string): Modality | undefined {
|
||||
}
|
||||
|
||||
export namespace ProviderTransform {
|
||||
function normalizeMessages(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
|
||||
function normalizeMessages(
|
||||
msgs: ModelMessage[],
|
||||
model: Provider.Model,
|
||||
options: Record<string, unknown>,
|
||||
): ModelMessage[] {
|
||||
// Strip openai itemId metadata following what codex does
|
||||
if (model.api.npm === "@ai-sdk/openai" || options.store === false) {
|
||||
msgs = msgs.map((msg) => {
|
||||
if (msg.providerOptions) {
|
||||
for (const options of Object.values(msg.providerOptions)) {
|
||||
if (options && typeof options === "object") {
|
||||
delete options["itemId"]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(msg.content)) {
|
||||
return msg
|
||||
}
|
||||
const content = msg.content.map((part) => {
|
||||
if (part.providerOptions) {
|
||||
for (const options of Object.values(part.providerOptions)) {
|
||||
if (options && typeof options === "object") {
|
||||
delete options["itemId"]
|
||||
}
|
||||
}
|
||||
}
|
||||
return part
|
||||
})
|
||||
return { ...msg, content } as typeof msg
|
||||
})
|
||||
}
|
||||
|
||||
// Anthropic rejects messages with empty content - filter out empty string messages
|
||||
// and remove empty text/reasoning parts from array content
|
||||
if (model.api.npm === "@ai-sdk/anthropic") {
|
||||
@@ -218,9 +249,9 @@ export namespace ProviderTransform {
|
||||
})
|
||||
}
|
||||
|
||||
export function message(msgs: ModelMessage[], model: Provider.Model) {
|
||||
export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
|
||||
msgs = unsupportedParts(msgs, model)
|
||||
msgs = normalizeMessages(msgs, model)
|
||||
msgs = normalizeMessages(msgs, model, options)
|
||||
if (
|
||||
model.providerID === "anthropic" ||
|
||||
model.api.id.includes("anthropic") ||
|
||||
@@ -453,64 +484,69 @@ export namespace ProviderTransform {
|
||||
return {}
|
||||
}
|
||||
|
||||
export function options(
|
||||
model: Provider.Model,
|
||||
sessionID: string,
|
||||
providerOptions?: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
export function options(input: {
|
||||
model: Provider.Model
|
||||
sessionID: string
|
||||
providerOptions?: Record<string, any>
|
||||
}): Record<string, any> {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
if (model.api.npm === "@openrouter/ai-sdk-provider") {
|
||||
// openai and providers using openai package should set store to false by default.
|
||||
if (input.model.providerID === "openai" || input.model.api.npm === "@ai-sdk/openai") {
|
||||
result["store"] = false
|
||||
}
|
||||
|
||||
if (input.model.api.npm === "@openrouter/ai-sdk-provider") {
|
||||
result["usage"] = {
|
||||
include: true,
|
||||
}
|
||||
if (model.api.id.includes("gemini-3")) {
|
||||
if (input.model.api.id.includes("gemini-3")) {
|
||||
result["reasoning"] = { effort: "high" }
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
model.providerID === "baseten" ||
|
||||
(model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(model.api.id))
|
||||
input.model.providerID === "baseten" ||
|
||||
(input.model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(input.model.api.id))
|
||||
) {
|
||||
result["chat_template_args"] = { enable_thinking: true }
|
||||
}
|
||||
|
||||
if (["zai", "zhipuai"].includes(model.providerID) && model.api.npm === "@ai-sdk/openai-compatible") {
|
||||
if (["zai", "zhipuai"].includes(input.model.providerID) && input.model.api.npm === "@ai-sdk/openai-compatible") {
|
||||
result["thinking"] = {
|
||||
type: "enabled",
|
||||
clear_thinking: false,
|
||||
}
|
||||
}
|
||||
|
||||
if (model.providerID === "openai" || providerOptions?.setCacheKey) {
|
||||
result["promptCacheKey"] = sessionID
|
||||
if (input.model.providerID === "openai" || input.providerOptions?.setCacheKey) {
|
||||
result["promptCacheKey"] = input.sessionID
|
||||
}
|
||||
|
||||
if (model.api.npm === "@ai-sdk/google" || model.api.npm === "@ai-sdk/google-vertex") {
|
||||
if (input.model.api.npm === "@ai-sdk/google" || input.model.api.npm === "@ai-sdk/google-vertex") {
|
||||
result["thinkingConfig"] = {
|
||||
includeThoughts: true,
|
||||
}
|
||||
if (model.api.id.includes("gemini-3")) {
|
||||
if (input.model.api.id.includes("gemini-3")) {
|
||||
result["thinkingConfig"]["thinkingLevel"] = "high"
|
||||
}
|
||||
}
|
||||
|
||||
if (model.api.id.includes("gpt-5") && !model.api.id.includes("gpt-5-chat")) {
|
||||
if (model.providerID.includes("codex")) {
|
||||
if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
|
||||
if (input.model.providerID.includes("codex")) {
|
||||
result["store"] = false
|
||||
}
|
||||
|
||||
if (!model.api.id.includes("codex") && !model.api.id.includes("gpt-5-pro")) {
|
||||
if (!input.model.api.id.includes("codex") && !input.model.api.id.includes("gpt-5-pro")) {
|
||||
result["reasoningEffort"] = "medium"
|
||||
}
|
||||
|
||||
if (model.api.id.endsWith("gpt-5.") && model.providerID !== "azure") {
|
||||
if (input.model.api.id.endsWith("gpt-5.") && input.model.providerID !== "azure") {
|
||||
result["textVerbosity"] = "low"
|
||||
}
|
||||
|
||||
if (model.providerID.startsWith("opencode")) {
|
||||
result["promptCacheKey"] = sessionID
|
||||
if (input.model.providerID.startsWith("opencode")) {
|
||||
result["promptCacheKey"] = input.sessionID
|
||||
result["include"] = ["reasoning.encrypted_content"]
|
||||
result["reasoningSummary"] = "auto"
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Hono } from "hono"
|
||||
import { describeRoute, validator } from "hono-openapi"
|
||||
import { resolver } from "hono-openapi"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Project } from "../project/project"
|
||||
import z from "zod"
|
||||
import { errors } from "./error"
|
||||
|
||||
export const ProjectRoute = new Hono()
|
||||
.get(
|
||||
"/",
|
||||
describeRoute({
|
||||
summary: "List all projects",
|
||||
description: "Get a list of projects that have been opened with OpenCode.",
|
||||
operationId: "project.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of projects",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Project.Info.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const projects = await Project.list()
|
||||
return c.json(projects)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/current",
|
||||
describeRoute({
|
||||
summary: "Get current project",
|
||||
description: "Retrieve the currently active project that OpenCode is working with.",
|
||||
operationId: "project.current",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Current project information",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Project.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(Instance.project)
|
||||
},
|
||||
)
|
||||
.patch(
|
||||
"/:projectID",
|
||||
describeRoute({
|
||||
summary: "Update project",
|
||||
description: "Update project properties such as name, icon and color.",
|
||||
operationId: "project.update",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated project information",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Project.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400, 404),
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ projectID: z.string() })),
|
||||
validator("json", Project.update.schema.omit({ projectID: true })),
|
||||
async (c) => {
|
||||
const projectID = c.req.valid("param").projectID
|
||||
const body = c.req.valid("json")
|
||||
const project = await Project.update({ ...body, projectID })
|
||||
return c.json(project)
|
||||
},
|
||||
)
|
||||
@@ -1,95 +0,0 @@
|
||||
import { Hono } from "hono"
|
||||
import { describeRoute, validator } from "hono-openapi"
|
||||
import { resolver } from "hono-openapi"
|
||||
import { Question } from "../question"
|
||||
import z from "zod"
|
||||
import { errors } from "./error"
|
||||
|
||||
export const QuestionRoute = new Hono()
|
||||
.get(
|
||||
"/",
|
||||
describeRoute({
|
||||
summary: "List pending questions",
|
||||
description: "Get all pending question requests across all sessions.",
|
||||
operationId: "question.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of pending questions",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Question.Request.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const questions = await Question.list()
|
||||
return c.json(questions)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/:requestID/reply",
|
||||
describeRoute({
|
||||
summary: "Reply to question request",
|
||||
description: "Provide answers to a question request from the AI assistant.",
|
||||
operationId: "question.reply",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Question answered successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400, 404),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
requestID: z.string(),
|
||||
}),
|
||||
),
|
||||
validator("json", Question.Reply),
|
||||
async (c) => {
|
||||
const params = c.req.valid("param")
|
||||
const json = c.req.valid("json")
|
||||
await Question.reply({
|
||||
requestID: params.requestID,
|
||||
answers: json.answers,
|
||||
})
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/:requestID/reject",
|
||||
describeRoute({
|
||||
summary: "Reject question request",
|
||||
description: "Reject a question request from the AI assistant.",
|
||||
operationId: "question.reject",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Question rejected successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400, 404),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
requestID: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const params = c.req.valid("param")
|
||||
await Question.reject(params.requestID)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Hono } from "hono"
|
||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import { Config } from "../../config/config"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { mapValues } from "remeda"
|
||||
import { errors } from "../error"
|
||||
import { Log } from "../../util/log"
|
||||
import { lazy } from "../../util/lazy"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
export const ConfigRoutes = lazy(() =>
|
||||
new Hono()
|
||||
.get(
|
||||
"/",
|
||||
describeRoute({
|
||||
summary: "Get configuration",
|
||||
description: "Retrieve the current OpenCode configuration settings and preferences.",
|
||||
operationId: "config.get",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Get config info",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Config.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await Config.get())
|
||||
},
|
||||
)
|
||||
.patch(
|
||||
"/",
|
||||
describeRoute({
|
||||
summary: "Update configuration",
|
||||
description: "Update OpenCode configuration settings and preferences.",
|
||||
operationId: "config.update",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully updated config",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Config.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator("json", Config.Info),
|
||||
async (c) => {
|
||||
const config = c.req.valid("json")
|
||||
await Config.update(config)
|
||||
return c.json(config)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/providers",
|
||||
describeRoute({
|
||||
summary: "List config providers",
|
||||
description: "Get a list of all configured AI providers and their default models.",
|
||||
operationId: "config.providers",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of providers",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
providers: Provider.Info.array(),
|
||||
default: z.record(z.string(), z.string()),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
using _ = log.time("providers")
|
||||
const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
|
||||
return c.json({
|
||||
providers: Object.values(providers),
|
||||
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,157 @@
|
||||
import { Hono } from "hono"
|
||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import { ToolRegistry } from "../../tool/registry"
|
||||
import { Worktree } from "../../worktree"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Project } from "../../project/project"
|
||||
import { MCP } from "../../mcp"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
|
||||
export const ExperimentalRoutes = lazy(() =>
|
||||
new Hono()
|
||||
.get(
|
||||
"/tool/ids",
|
||||
describeRoute({
|
||||
summary: "List tool IDs",
|
||||
description:
|
||||
"Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
|
||||
operationId: "tool.ids",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Tool IDs",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await ToolRegistry.ids())
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/tool",
|
||||
describeRoute({
|
||||
summary: "List tools",
|
||||
description:
|
||||
"Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
|
||||
operationId: "tool.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Tools",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
id: z.string(),
|
||||
description: z.string(),
|
||||
parameters: z.any(),
|
||||
})
|
||||
.meta({ ref: "ToolListItem" }),
|
||||
)
|
||||
.meta({ ref: "ToolList" }),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
provider: z.string(),
|
||||
model: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { provider } = c.req.valid("query")
|
||||
const tools = await ToolRegistry.tools(provider)
|
||||
return c.json(
|
||||
tools.map((t) => ({
|
||||
id: t.id,
|
||||
description: t.description,
|
||||
// Handle both Zod schemas and plain JSON schemas
|
||||
parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters,
|
||||
})),
|
||||
)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/worktree",
|
||||
describeRoute({
|
||||
summary: "Create worktree",
|
||||
description: "Create a new git worktree for the current project.",
|
||||
operationId: "worktree.create",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Worktree created",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Worktree.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator("json", Worktree.create.schema),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
const worktree = await Worktree.create(body)
|
||||
return c.json(worktree)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/worktree",
|
||||
describeRoute({
|
||||
summary: "List worktrees",
|
||||
description: "List all sandbox worktrees for the current project.",
|
||||
operationId: "worktree.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of worktree directories",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(z.string())),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const sandboxes = await Project.sandboxes(Instance.project.id)
|
||||
return c.json(sandboxes)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/resource",
|
||||
describeRoute({
|
||||
summary: "Get MCP resources",
|
||||
description: "Get all available MCP resources from connected servers. Optionally filter by name.",
|
||||
operationId: "experimental.resource.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "MCP resources",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.record(z.string(), MCP.Resource)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await MCP.resources())
|
||||
},
|
||||
),
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user