Compare commits
270
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac3d0cb5a3 | ||
|
|
06d69ab609 | ||
|
|
c2cc486c7d | ||
|
|
8a6b8e5339 | ||
|
|
cfd6a7ae96 | ||
|
|
4173ee0e0b | ||
|
|
22b5d7e570 | ||
|
|
f1ec28176f | ||
|
|
ab78a46396 | ||
|
|
2ed18ea1fe | ||
|
|
40eddce435 | ||
|
|
78f8cc9418 | ||
|
|
58f7da6e9f | ||
|
|
5a199b04cb | ||
|
|
eb968a6651 | ||
|
|
a813fcb41c | ||
|
|
a58d1be822 | ||
|
|
07dc8d8ce4 | ||
|
|
d377246491 | ||
|
|
7030f49a74 | ||
|
|
c4e4f2a058 | ||
|
|
2729705594 | ||
|
|
ea13b6e8aa | ||
|
|
85ab9798c6 | ||
|
|
33290c54cd | ||
|
|
5d613a038d | ||
|
|
db78a59f03 | ||
|
|
7c3eeeb0fa | ||
|
|
e8357a87b0 | ||
|
|
06c543e938 | ||
|
|
759ce8fb8e | ||
|
|
38847e13bb | ||
|
|
e0c6459faa | ||
|
|
80b278ddab | ||
|
|
ef7ef6538e | ||
|
|
d23c21023a | ||
|
|
dfa2a9f225 | ||
|
|
6f78a71fa7 | ||
|
|
f8f1f46a4f | ||
|
|
ab705dacfa | ||
|
|
d1b93616f7 | ||
|
|
69215d456c | ||
|
|
54e52896a4 | ||
|
|
b18fb16e9c | ||
|
|
1250486ddf | ||
|
|
d645e8bbe1 | ||
|
|
cad415872e | ||
|
|
e8746ddb1d | ||
|
|
80020ade2e | ||
|
|
08ef97b162 | ||
|
|
1aedb265dd | ||
|
|
5c13b209aa | ||
|
|
43a9c50389 | ||
|
|
55224d64a2 | ||
|
|
c325aa1142 | ||
|
|
6e020ef9ef | ||
|
|
aca1eb6b5b | ||
|
|
3d095e7fe7 | ||
|
|
632f20558a | ||
|
|
f96c4badd8 | ||
|
|
cbe1c81470 | ||
|
|
c25155586c | ||
|
|
08b94a6890 | ||
|
|
8cddc9ea55 | ||
|
|
578239e0d0 | ||
|
|
626fa1462b | ||
|
|
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 | ||
|
|
99a1e73fa1 | ||
|
|
ba4c86448b | ||
|
|
b36837ae93 | ||
|
|
e03932e586 | ||
|
|
6b019a125a | ||
|
|
6a2fed7042 | ||
|
|
74baae597a | ||
|
|
d78d31430d | ||
|
|
096e14d787 | ||
|
|
bbb3120b59 | ||
|
|
9e4438f5bf | ||
|
|
87438fb38e | ||
|
|
6b6d6e9e07 | ||
|
|
207a59aad4 | ||
|
|
b3ae1931fc | ||
|
|
4d08123ca0 | ||
|
|
7d3c7a9f65 | ||
|
|
50dfa9caf3 | ||
|
|
1f86aa8bb9 | ||
|
|
d83756eaaf | ||
|
|
c0b43d3cb4 | ||
|
|
3206ed47e0 | ||
|
|
346c5e0da6 | ||
|
|
5b431c36f8 | ||
|
|
44d24d42b8 | ||
|
|
3a9e6b558c | ||
|
|
9d92ae7530 | ||
|
|
e6e7eaf6e0 | ||
|
|
8ce5c2b900 | ||
|
|
78be8fecdc |
@@ -9,6 +9,7 @@ on:
|
|||||||
- "nix/**"
|
- "nix/**"
|
||||||
- "packages/app/**"
|
- "packages/app/**"
|
||||||
- "packages/desktop/**"
|
- "packages/desktop/**"
|
||||||
|
- ".github/workflows/nix-desktop.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "flake.nix"
|
- "flake.nix"
|
||||||
@@ -16,6 +17,7 @@ on:
|
|||||||
- "nix/**"
|
- "nix/**"
|
||||||
- "packages/app/**"
|
- "packages/app/**"
|
||||||
- "packages/desktop/**"
|
- "packages/desktop/**"
|
||||||
|
- ".github/workflows/nix-desktop.yml"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -25,6 +27,8 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- blacksmith-4vcpu-ubuntu-2404
|
- blacksmith-4vcpu-ubuntu-2404
|
||||||
|
- blacksmith-4vcpu-ubuntu-2404-arm
|
||||||
|
- macos-15-intel
|
||||||
- macos-latest
|
- macos-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
@@ -33,7 +37,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Nix
|
- name: Setup Nix
|
||||||
uses: DeterminateSystems/nix-installer-action@v21
|
uses: nixbuild/nix-quick-install-action@v34
|
||||||
|
|
||||||
- name: Build desktop via flake
|
- name: Build desktop via flake
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -10,22 +10,24 @@ on:
|
|||||||
- "bun.lock"
|
- "bun.lock"
|
||||||
- "package.json"
|
- "package.json"
|
||||||
- "packages/*/package.json"
|
- "packages/*/package.json"
|
||||||
|
- ".github/workflows/update-nix-hashes.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "bun.lock"
|
- "bun.lock"
|
||||||
- "package.json"
|
- "package.json"
|
||||||
- "packages/*/package.json"
|
- "packages/*/package.json"
|
||||||
|
- ".github/workflows/update-nix-hashes.yml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-linux:
|
update-flake:
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
env:
|
env:
|
||||||
SYSTEM: x86_64-linux
|
TITLE: flake.lock
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -33,39 +35,32 @@ jobs:
|
|||||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||||
|
|
||||||
- name: Setup Nix
|
- name: Setup Nix
|
||||||
uses: DeterminateSystems/nix-installer-action@v20
|
uses: nixbuild/nix-quick-install-action@v34
|
||||||
|
|
||||||
- name: Configure git
|
- name: Configure git
|
||||||
run: |
|
run: |
|
||||||
git config --global user.email "action@github.com"
|
git config --global user.email "action@github.com"
|
||||||
git config --global user.name "Github Action"
|
git config --global user.name "Github Action"
|
||||||
|
|
||||||
- name: Update flake.lock
|
- name: Update ${{ env.TITLE }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
echo "📦 Updating flake.lock..."
|
echo "Updating $TITLE..."
|
||||||
nix flake update
|
nix flake update
|
||||||
echo "✅ flake.lock updated successfully"
|
echo "$TITLE updated successfully"
|
||||||
|
|
||||||
- name: Update node_modules hash for x86_64-linux
|
- name: Commit ${{ env.TITLE }} changes
|
||||||
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
|
|
||||||
env:
|
env:
|
||||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "🔍 Checking for changes in tracked Nix files..."
|
echo "Checking for changes in tracked files..."
|
||||||
|
|
||||||
summarize() {
|
summarize() {
|
||||||
local status="$1"
|
local status="$1"
|
||||||
{
|
{
|
||||||
echo "### Nix Hash Update (x86_64-linux)"
|
echo "### Nix $TITLE"
|
||||||
echo ""
|
echo ""
|
||||||
echo "- ref: ${GITHUB_REF_NAME}"
|
echo "- ref: ${GITHUB_REF_NAME}"
|
||||||
echo "- status: ${status}"
|
echo "- status: ${status}"
|
||||||
@@ -75,42 +70,53 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
}
|
}
|
||||||
|
FILES=(flake.lock flake.nix)
|
||||||
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
|
|
||||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||||
if [ -z "$STATUS" ]; then
|
if [ -z "$STATUS" ]; then
|
||||||
echo "✅ No changes detected. Hashes are already up to date."
|
echo "No changes detected."
|
||||||
summarize "no changes"
|
summarize "no changes"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "📝 Changes detected:"
|
echo "Changes detected:"
|
||||||
echo "$STATUS"
|
echo "$STATUS"
|
||||||
echo "🔗 Staging files..."
|
echo "Staging files..."
|
||||||
git add "${FILES[@]}"
|
git add "${FILES[@]}"
|
||||||
echo "💾 Committing changes..."
|
echo "Committing changes..."
|
||||||
git commit -m "Update Nix flake.lock and x86_64-linux hash"
|
git commit -m "Update $TITLE"
|
||||||
echo "✅ Changes committed"
|
echo "Changes committed"
|
||||||
|
|
||||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||||
echo "🌳 Pulling latest from branch: $BRANCH"
|
echo "Pulling latest from branch: $BRANCH"
|
||||||
git pull --rebase origin "$BRANCH"
|
git pull --rebase --autostash origin "$BRANCH"
|
||||||
echo "🚀 Pushing changes to branch: $BRANCH"
|
echo "Pushing changes to branch: $BRANCH"
|
||||||
git push origin HEAD:"$BRANCH"
|
git push origin HEAD:"$BRANCH"
|
||||||
echo "✅ Changes pushed successfully"
|
echo "Changes pushed successfully"
|
||||||
|
|
||||||
summarize "committed $(git rev-parse --short HEAD)"
|
summarize "committed $(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
update-macos:
|
compute-node-modules-hash:
|
||||||
needs: update-linux
|
needs: update-flake
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
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:
|
env:
|
||||||
SYSTEM: aarch64-darwin
|
SYSTEM: ${{ matrix.system }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -118,7 +124,105 @@ jobs:
|
|||||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||||
|
|
||||||
- name: Setup Nix
|
- name: Setup Nix
|
||||||
uses: DeterminateSystems/nix-installer-action@v20
|
uses: nixbuild/nix-quick-install-action@v34
|
||||||
|
|
||||||
|
- name: Compute node_modules hash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||||
|
HASH_FILE="nix/hashes.json"
|
||||||
|
OUTPUT_FILE="hash-${SYSTEM}.txt"
|
||||||
|
|
||||||
|
export NIX_KEEP_OUTPUTS=1
|
||||||
|
export NIX_KEEP_DERIVATIONS=1
|
||||||
|
|
||||||
|
BUILD_LOG=$(mktemp)
|
||||||
|
TMP_JSON=$(mktemp)
|
||||||
|
trap 'rm -f "$BUILD_LOG" "$TMP_JSON"' EXIT
|
||||||
|
|
||||||
|
if [ ! -f "$HASH_FILE" ]; then
|
||||||
|
mkdir -p "$(dirname "$HASH_FILE")"
|
||||||
|
echo '{"nodeModules":{}}' > "$HASH_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set dummy hash to force nix to rebuild and reveal correct hash
|
||||||
|
jq --arg system "$SYSTEM" --arg value "$DUMMY" \
|
||||||
|
'.nodeModules = (.nodeModules // {}) | .nodeModules[$system] = $value' "$HASH_FILE" > "$TMP_JSON"
|
||||||
|
mv "$TMP_JSON" "$HASH_FILE"
|
||||||
|
|
||||||
|
MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules"
|
||||||
|
DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")"
|
||||||
|
|
||||||
|
echo "Building node_modules for ${SYSTEM} to discover correct hash..."
|
||||||
|
echo "Attempting to realize derivation: ${DRV_PATH}"
|
||||||
|
REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true)
|
||||||
|
|
||||||
|
BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true)
|
||||||
|
CORRECT_HASH=""
|
||||||
|
|
||||||
|
if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then
|
||||||
|
echo "Realized node_modules output: $BUILD_PATH"
|
||||||
|
CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to extract hash from build log
|
||||||
|
if [ -z "$CORRECT_HASH" ]; then
|
||||||
|
CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$CORRECT_HASH" ]; then
|
||||||
|
CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to hash from kept failed build directory
|
||||||
|
if [ -z "$CORRECT_HASH" ]; then
|
||||||
|
KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1 || true)
|
||||||
|
if [ -z "$KEPT_DIR" ]; then
|
||||||
|
KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1 || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then
|
||||||
|
HASH_PATH="$KEPT_DIR"
|
||||||
|
[ -d "$KEPT_DIR/build" ] && HASH_PATH="$KEPT_DIR/build"
|
||||||
|
|
||||||
|
if [ -d "$HASH_PATH/node_modules" ]; then
|
||||||
|
CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$CORRECT_HASH" ]; then
|
||||||
|
echo "Failed to determine correct node_modules hash for ${SYSTEM}."
|
||||||
|
cat "$BUILD_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$CORRECT_HASH" > "$OUTPUT_FILE"
|
||||||
|
echo "Hash for ${SYSTEM}: $CORRECT_HASH"
|
||||||
|
|
||||||
|
- name: Upload hash artifact
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: hash-${{ matrix.system }}
|
||||||
|
path: hash-${{ matrix.system }}.txt
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
commit-node-modules-hashes:
|
||||||
|
needs: compute-node-modules-hash
|
||||||
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
env:
|
||||||
|
TITLE: node_modules hashes
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.head_ref || github.ref_name }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||||
|
|
||||||
- name: Configure git
|
- name: Configure git
|
||||||
run: |
|
run: |
|
||||||
@@ -130,27 +234,71 @@ jobs:
|
|||||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||||
git pull origin "$BRANCH"
|
git pull --rebase --autostash origin "$BRANCH"
|
||||||
|
|
||||||
- name: Update node_modules hash for aarch64-darwin
|
- name: Download all hash artifacts
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
pattern: hash-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Merge hashes into hashes.json
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
echo "🔄 Updating node_modules hash for aarch64-darwin..."
|
|
||||||
nix/scripts/update-hashes.sh
|
|
||||||
echo "✅ node_modules hash for aarch64-darwin updated successfully"
|
|
||||||
|
|
||||||
- name: Commit macOS hash changes
|
HASH_FILE="nix/hashes.json"
|
||||||
|
|
||||||
|
if [ ! -f "$HASH_FILE" ]; then
|
||||||
|
mkdir -p "$(dirname "$HASH_FILE")"
|
||||||
|
echo '{"nodeModules":{}}' > "$HASH_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Merging hashes into ${HASH_FILE}..."
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
files=(hash-*.txt)
|
||||||
|
if [ ${#files[@]} -eq 0 ]; then
|
||||||
|
echo "No hash files found, nothing to update"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXPECTED_SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin"
|
||||||
|
for sys in $EXPECTED_SYSTEMS; do
|
||||||
|
if [ ! -f "hash-${sys}.txt" ]; then
|
||||||
|
echo "WARNING: Missing hash file for $sys"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
system="${f#hash-}"
|
||||||
|
system="${system%.txt}"
|
||||||
|
hash=$(cat "$f")
|
||||||
|
if [ -z "$hash" ]; then
|
||||||
|
echo "WARNING: Empty hash for $system, skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo " $system: $hash"
|
||||||
|
jq --arg sys "$system" --arg h "$hash" \
|
||||||
|
'.nodeModules = (.nodeModules // {}) | .nodeModules[$sys] = $h' "$HASH_FILE" > "${HASH_FILE}.tmp"
|
||||||
|
mv "${HASH_FILE}.tmp" "$HASH_FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "All hashes merged:"
|
||||||
|
cat "$HASH_FILE"
|
||||||
|
|
||||||
|
- name: Commit ${{ env.TITLE }} changes
|
||||||
env:
|
env:
|
||||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "🔍 Checking for changes in tracked Nix files..."
|
HASH_FILE="nix/hashes.json"
|
||||||
|
echo "Checking for changes..."
|
||||||
|
|
||||||
summarize() {
|
summarize() {
|
||||||
local status="$1"
|
local status="$1"
|
||||||
{
|
{
|
||||||
echo "### Nix Hash Update (aarch64-darwin)"
|
echo "### Nix $TITLE"
|
||||||
echo ""
|
echo ""
|
||||||
echo "- ref: ${GITHUB_REF_NAME}"
|
echo "- ref: ${GITHUB_REF_NAME}"
|
||||||
echo "- status: ${status}"
|
echo "- status: ${status}"
|
||||||
@@ -161,27 +309,22 @@ jobs:
|
|||||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
}
|
}
|
||||||
|
|
||||||
FILES=(nix/hashes.json)
|
FILES=("$HASH_FILE")
|
||||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||||
if [ -z "$STATUS" ]; then
|
if [ -z "$STATUS" ]; then
|
||||||
echo "✅ No changes detected. Hash is already up to date."
|
echo "No changes detected."
|
||||||
summarize "no changes"
|
summarize "no changes"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "📝 Changes detected:"
|
echo "Changes detected:"
|
||||||
echo "$STATUS"
|
echo "$STATUS"
|
||||||
echo "🔗 Staging files..."
|
|
||||||
git add "${FILES[@]}"
|
git add "${FILES[@]}"
|
||||||
echo "💾 Committing changes..."
|
git commit -m "Update $TITLE"
|
||||||
git commit -m "Update aarch64-darwin hash"
|
|
||||||
echo "✅ Changes committed"
|
|
||||||
|
|
||||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||||
echo "🌳 Pulling latest from branch: $BRANCH"
|
git pull --rebase --autostash origin "$BRANCH"
|
||||||
git pull --rebase origin "$BRANCH"
|
|
||||||
echo "🚀 Pushing changes to branch: $BRANCH"
|
|
||||||
git push origin HEAD:"$BRANCH"
|
git push origin HEAD:"$BRANCH"
|
||||||
echo "✅ Changes pushed successfully"
|
echo "Changes pushed successfully"
|
||||||
|
|
||||||
summarize "committed $(git rev-parse --short HEAD)"
|
summarize "committed $(git rev-parse --short HEAD)"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ opencode.json
|
|||||||
a.out
|
a.out
|
||||||
target
|
target
|
||||||
.scripts
|
.scripts
|
||||||
|
.direnv/
|
||||||
|
|
||||||
# Local dev files
|
# Local dev files
|
||||||
opencode-dev
|
opencode-dev
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
description: "Bump AI sdk dependencies minor / patch versions only"
|
||||||
|
---
|
||||||
|
|
||||||
|
Please read @package.json and @packages/opencode/package.json.
|
||||||
|
|
||||||
|
Your job is to look into AI SDK dependencies, figure out if they have versions that can be upgraded (minor or patch versions ONLY no major ignore major changes).
|
||||||
|
|
||||||
|
I want a report of every dependency and the version that can be upgraded to.
|
||||||
|
What would be even better is if you can give me links to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added.
|
||||||
|
|
||||||
|
Consider using subagents for each dep to save your context window.
|
||||||
|
|
||||||
|
Here is a short list of some deps (please be comprehensive tho):
|
||||||
|
|
||||||
|
- "ai"
|
||||||
|
- "@ai-sdk/openai"
|
||||||
|
- "@ai-sdk/anthropic"
|
||||||
|
- "@openrouter/ai-sdk-provider"
|
||||||
|
- etc, etc
|
||||||
|
|
||||||
|
DO NOT upgrade the dependencies yet, just make a list of all dependencies and their versions that can be upgraded to minor or patch versions only.
|
||||||
|
|
||||||
|
Write up your findings to ai-sdk-updates.md
|
||||||
+29
@@ -1,3 +1,32 @@
|
|||||||
|
# Security
|
||||||
|
|
||||||
|
## Threat Model
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
OpenCode is an AI-powered coding assistant that runs locally on your machine. It provides an agent system with access to powerful tools including shell execution, file operations, and web access.
|
||||||
|
|
||||||
|
### No Sandbox
|
||||||
|
|
||||||
|
OpenCode does **not** sandbox the agent. The permission system exists as a UX feature to help users stay aware of what actions the agent is taking - it prompts for confirmation before executing commands, writing files, etc. However, it is not designed to provide security isolation.
|
||||||
|
|
||||||
|
If you need true isolation, run OpenCode inside a Docker container or VM.
|
||||||
|
|
||||||
|
### Server Mode
|
||||||
|
|
||||||
|
Server mode is opt-in only. When enabled, set `OPENCODE_SERVER_PASSWORD` to require HTTP Basic Auth. Without this, the server runs unauthenticated (with a warning). It is the end user's responsibility to secure the server - any functionality it provides is not a vulnerability.
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
|
||||||
|
| Category | Rationale |
|
||||||
|
| ------------------------------- | ----------------------------------------------------------------------- |
|
||||||
|
| **Server access when opted-in** | If you enable server mode, API access is expected behavior |
|
||||||
|
| **Sandbox escapes** | The permission system is not a sandbox (see above) |
|
||||||
|
| **LLM provider data handling** | Data sent to your configured LLM provider is governed by their policies |
|
||||||
|
| **MCP server behavior** | External MCP servers you configure are outside our trust boundary |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Reporting Security Issues
|
# Reporting Security Issues
|
||||||
|
|
||||||
We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
|
We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
|
||||||
|
|||||||
@@ -1,203 +1,205 @@
|
|||||||
# Download Stats
|
# Download Stats
|
||||||
|
|
||||||
| Date | GitHub Downloads | npm Downloads | Total |
|
| Date | GitHub Downloads | npm Downloads | Total |
|
||||||
| ---------- | -------------------- | ------------------- | -------------------- |
|
| ---------- | -------------------- | -------------------- | -------------------- |
|
||||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
| 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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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 | 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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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) |
|
| 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-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-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-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-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-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-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-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-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-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-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-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-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-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-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) |
|
||||||
|
| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) |
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -81,6 +81,8 @@
|
|||||||
"@opencode-ai/console-mail": "workspace:*",
|
"@opencode-ai/console-mail": "workspace:*",
|
||||||
"@opencode-ai/console-resource": "workspace:*",
|
"@opencode-ai/console-resource": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
|
"@smithy/eventstream-codec": "4.2.7",
|
||||||
|
"@smithy/util-utf8": "4.2.0",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
"@solidjs/start": "catalog:",
|
"@solidjs/start": "catalog:",
|
||||||
@@ -95,13 +97,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"@webgpu/types": "0.1.54",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"wrangler": "4.50.0",
|
"wrangler": "4.50.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -128,7 +131,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -152,7 +155,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -176,7 +179,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -205,7 +208,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -234,7 +237,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -250,7 +253,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -290,8 +293,8 @@
|
|||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||||
"@opentui/core": "0.1.72",
|
"@opentui/core": "0.1.74",
|
||||||
"@opentui/solid": "0.1.72",
|
"@opentui/solid": "0.1.74",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -354,7 +357,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -374,9 +377,9 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.88.1",
|
"@hey-api/openapi-ts": "0.90.4",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
@@ -385,7 +388,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -398,7 +401,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -410,7 +413,7 @@
|
|||||||
"@solid-primitives/resize-observer": "2.1.3",
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"dompurify": "catalog:",
|
"dompurify": "3.3.1",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"katex": "0.16.27",
|
"katex": "0.16.27",
|
||||||
"luxon": "catalog:",
|
"luxon": "catalog:",
|
||||||
@@ -421,6 +424,7 @@
|
|||||||
"shiki": "catalog:",
|
"shiki": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"solid-list": "catalog:",
|
"solid-list": "catalog:",
|
||||||
|
"strip-ansi": "7.1.2",
|
||||||
"virtua": "catalog:",
|
"virtua": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -438,7 +442,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -449,7 +453,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -505,7 +509,7 @@
|
|||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@types/bun": "1.3.4",
|
"@types/bun": "1.3.5",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
@@ -919,11 +923,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=="],
|
"@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/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=="],
|
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
|
||||||
|
|
||||||
@@ -1215,21 +1219,21 @@
|
|||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@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=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
@@ -1521,7 +1525,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/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=="],
|
"@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 +1777,7 @@
|
|||||||
|
|
||||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
|
"@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=="],
|
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
|
|
||||||
@@ -1903,7 +1907,7 @@
|
|||||||
|
|
||||||
"@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
"@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=="],
|
"@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
|
||||||
|
|
||||||
@@ -2075,7 +2079,7 @@
|
|||||||
|
|
||||||
"bun-pty": ["bun-pty@0.4.4", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="],
|
"bun-pty": ["bun-pty@0.4.4", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
|
"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=="],
|
"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 +2095,7 @@
|
|||||||
|
|
||||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
"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=="],
|
"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 +3495,7 @@
|
|||||||
|
|
||||||
"selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
|
"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=="],
|
"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 +3969,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/@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": ["@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=="],
|
"@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 +4241,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=="],
|
"@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/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=="],
|
"@solidjs/start/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||||
@@ -4271,6 +4281,8 @@
|
|||||||
|
|
||||||
"astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
|
"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/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=="],
|
"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 +4303,10 @@
|
|||||||
|
|
||||||
"body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
"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=="],
|
"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=="],
|
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||||
@@ -4307,6 +4323,8 @@
|
|||||||
|
|
||||||
"editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="],
|
"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=="],
|
"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=="],
|
"es-get-iterator/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
@@ -4333,6 +4351,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=="],
|
"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=="],
|
"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=="],
|
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
@@ -4347,6 +4367,8 @@
|
|||||||
|
|
||||||
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
"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=="],
|
"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=="],
|
"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 +4453,8 @@
|
|||||||
|
|
||||||
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"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/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=="],
|
"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 +4933,8 @@
|
|||||||
|
|
||||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"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=="],
|
"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=="],
|
"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": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768302833,
|
"lastModified": 1768456270,
|
||||||
"narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
|
"narHash": "sha256-NgaL2CCiUR6nsqUIY4yxkzz07iQUlUCany44CFv+OxY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "61db79b0c6b838d9894923920b612048e1201926",
|
"rev": "f4606b01b39e09065df37905a2133905246db9ed",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{
|
{
|
||||||
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
@@ -107,33 +108,10 @@
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = opencodePkg;
|
default = self.packages.${system}.opencode;
|
||||||
|
opencode = opencodePkg;
|
||||||
desktop = desktopPkg;
|
desktop = desktopPkg;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
apps = forEachSystem (
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
pkgs = pkgsFor system;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
opencode-dev = {
|
|
||||||
type = "app";
|
|
||||||
meta = {
|
|
||||||
description = "Nix devshell shell for OpenCode";
|
|
||||||
runtimeInputs = [ pkgs.bun ];
|
|
||||||
};
|
|
||||||
program = "${
|
|
||||||
pkgs.writeShellApplication {
|
|
||||||
name = "opencode-dev";
|
|
||||||
text = ''
|
|
||||||
exec bun run dev "$@"
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
}/bin/opencode-dev";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ const ZEN_MODELS = [
|
|||||||
new sst.Secret("ZEN_MODELS5"),
|
new sst.Secret("ZEN_MODELS5"),
|
||||||
new sst.Secret("ZEN_MODELS6"),
|
new sst.Secret("ZEN_MODELS6"),
|
||||||
new sst.Secret("ZEN_MODELS7"),
|
new sst.Secret("ZEN_MODELS7"),
|
||||||
|
new sst.Secret("ZEN_MODELS8"),
|
||||||
]
|
]
|
||||||
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
|
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
|
||||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ case $current_shell in
|
|||||||
config_files="$HOME/.config/fish/config.fish"
|
config_files="$HOME/.config/fish/config.fish"
|
||||||
;;
|
;;
|
||||||
zsh)
|
zsh)
|
||||||
config_files="$HOME/.zshrc $HOME/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
|
config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
|
||||||
;;
|
;;
|
||||||
bash)
|
bash)
|
||||||
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
|
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
cargo,
|
cargo,
|
||||||
rustc,
|
rustc,
|
||||||
makeBinaryWrapper,
|
makeBinaryWrapper,
|
||||||
|
copyDesktopItems,
|
||||||
|
makeDesktopItem,
|
||||||
nodejs,
|
nodejs,
|
||||||
jq,
|
jq,
|
||||||
}:
|
}:
|
||||||
@@ -57,12 +59,28 @@ rustPlatform.buildRustPackage rec {
|
|||||||
pkg-config
|
pkg-config
|
||||||
bun
|
bun
|
||||||
makeBinaryWrapper
|
makeBinaryWrapper
|
||||||
|
copyDesktopItems
|
||||||
cargo
|
cargo
|
||||||
rustc
|
rustc
|
||||||
nodejs
|
nodejs
|
||||||
jq
|
jq
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# based on packages/desktop/src-tauri/release/appstream.metainfo.xml
|
||||||
|
desktopItems = lib.optionals stdenv.isLinux [
|
||||||
|
(makeDesktopItem {
|
||||||
|
name = "ai.opencode.opencode";
|
||||||
|
desktopName = "OpenCode";
|
||||||
|
comment = "Open source AI coding agent";
|
||||||
|
exec = "opencode-desktop";
|
||||||
|
icon = "opencode";
|
||||||
|
terminal = false;
|
||||||
|
type = "Application";
|
||||||
|
categories = [ "Development" "IDE" ];
|
||||||
|
startupWMClass = "opencode";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
openssl
|
openssl
|
||||||
]
|
]
|
||||||
@@ -121,6 +139,10 @@ rustPlatform.buildRustPackage rec {
|
|||||||
# It looks for them in the location specified in tauri.conf.json.
|
# It looks for them in the location specified in tauri.conf.json.
|
||||||
|
|
||||||
postInstall = lib.optionalString stdenv.isLinux ''
|
postInstall = lib.optionalString stdenv.isLinux ''
|
||||||
|
# Install icon
|
||||||
|
mkdir -p $out/share/icons/hicolor/128x128/apps
|
||||||
|
cp ../../../packages/desktop/src-tauri/icons/prod/128x128.png $out/share/icons/hicolor/128x128/apps/opencode.png
|
||||||
|
|
||||||
# Wrap the binary to ensure it finds the libraries
|
# Wrap the binary to ensure it finds the libraries
|
||||||
wrapProgram $out/bin/opencode-desktop \
|
wrapProgram $out/bin/opencode-desktop \
|
||||||
--prefix LD_LIBRARY_PATH : ${
|
--prefix LD_LIBRARY_PATH : ${
|
||||||
|
|||||||
+4
-2
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-1l4twtOi/7YYy1KFJME1XazAgTETAfbxB3EOv2qQeVs=",
|
"x86_64-linux": "sha256-4zchRpxzvHnPMcwumgL9yaX0deIXS5IGPp131eYsSvg=",
|
||||||
"aarch64-darwin": "sha256-jdZI3BA/v35er4xgWkI2rHo54D1TDNVhMX83b5BcIvk="
|
"aarch64-linux": "sha256-3/BSRsl5pI0Iz3qAFZxIkOehFLZ2Ox9UsbdDHYzqlVg=",
|
||||||
|
"aarch64-darwin": "sha256-86d/G1q6xiHSSlm+/irXoKLb/yLQbV348uuSrBV70+Q=",
|
||||||
|
"x86_64-darwin": "sha256-WYaP44PWRGtoG1DIuUJUH4DvuaCuFhlJZ9fPzGsiIfE="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
|
||||||
SYSTEM=${SYSTEM:-x86_64-linux}
|
|
||||||
DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json}
|
|
||||||
HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE}
|
|
||||||
|
|
||||||
if [ ! -f "$HASH_FILE" ]; then
|
|
||||||
cat >"$HASH_FILE" <<EOF
|
|
||||||
{
|
|
||||||
"nodeModules": {}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
||||||
if ! git ls-files --error-unmatch "$HASH_FILE" >/dev/null 2>&1; then
|
|
||||||
git add -N "$HASH_FILE" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
export DUMMY
|
|
||||||
export NIX_KEEP_OUTPUTS=1
|
|
||||||
export NIX_KEEP_DERIVATIONS=1
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -f "${JSON_OUTPUT:-}" "${BUILD_LOG:-}" "${TMP_EXPR:-}"
|
|
||||||
}
|
|
||||||
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
write_node_modules_hash() {
|
|
||||||
local value="$1"
|
|
||||||
local system="${2:-$SYSTEM}"
|
|
||||||
local temp
|
|
||||||
temp=$(mktemp)
|
|
||||||
|
|
||||||
if jq -e '.nodeModules | type == "object"' "$HASH_FILE" >/dev/null 2>&1; then
|
|
||||||
jq --arg system "$system" --arg value "$value" '.nodeModules[$system] = $value' "$HASH_FILE" >"$temp"
|
|
||||||
else
|
|
||||||
jq --arg system "$system" --arg value "$value" '.nodeModules = {($system): $value}' "$HASH_FILE" >"$temp"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mv "$temp" "$HASH_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
TARGET="packages.${SYSTEM}.default"
|
|
||||||
MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules"
|
|
||||||
CORRECT_HASH=""
|
|
||||||
|
|
||||||
DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")"
|
|
||||||
|
|
||||||
echo "Setting dummy node_modules outputHash for ${SYSTEM}..."
|
|
||||||
write_node_modules_hash "$DUMMY"
|
|
||||||
|
|
||||||
BUILD_LOG=$(mktemp)
|
|
||||||
JSON_OUTPUT=$(mktemp)
|
|
||||||
|
|
||||||
echo "Building node_modules for ${SYSTEM} to discover correct outputHash..."
|
|
||||||
echo "Attempting to realize derivation: ${DRV_PATH}"
|
|
||||||
REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true)
|
|
||||||
|
|
||||||
BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true)
|
|
||||||
if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then
|
|
||||||
echo "Realized node_modules output: $BUILD_PATH"
|
|
||||||
CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$CORRECT_HASH" ]; then
|
|
||||||
CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)"
|
|
||||||
|
|
||||||
if [ -z "$CORRECT_HASH" ]; then
|
|
||||||
CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$CORRECT_HASH" ]; then
|
|
||||||
echo "Searching for kept failed build directory..."
|
|
||||||
KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1)
|
|
||||||
|
|
||||||
if [ -z "$KEPT_DIR" ]; then
|
|
||||||
KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then
|
|
||||||
echo "Found kept build directory: $KEPT_DIR"
|
|
||||||
if [ -d "$KEPT_DIR/build" ]; then
|
|
||||||
HASH_PATH="$KEPT_DIR/build"
|
|
||||||
else
|
|
||||||
HASH_PATH="$KEPT_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Attempting to hash: $HASH_PATH"
|
|
||||||
ls -la "$HASH_PATH" || true
|
|
||||||
|
|
||||||
if [ -d "$HASH_PATH/node_modules" ]; then
|
|
||||||
CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true)
|
|
||||||
echo "Computed hash from kept build: $CORRECT_HASH"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$CORRECT_HASH" ]; then
|
|
||||||
echo "Failed to determine correct node_modules hash for ${SYSTEM}."
|
|
||||||
echo "Build log:"
|
|
||||||
cat "$BUILD_LOG"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
write_node_modules_hash "$CORRECT_HASH"
|
|
||||||
|
|
||||||
jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null
|
|
||||||
|
|
||||||
echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH"
|
|
||||||
|
|
||||||
rm -f "$BUILD_LOG"
|
|
||||||
unset BUILD_LOG
|
|
||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@types/bun": "1.3.4",
|
"@types/bun": "1.3.5",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
|
|||||||
@@ -13,12 +13,11 @@
|
|||||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||||
<meta property="og:image" content="/social-share.png" />
|
<meta property="og:image" content="/social-share.png" />
|
||||||
<meta property="twitter: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>
|
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<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>
|
<script src="/src/entry.tsx" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { useParams } from "@solidjs/router"
|
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 { useLayout } from "@/context/layout"
|
||||||
import { useFile } from "@/context/file"
|
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() {
|
export function DialogSelectFile() {
|
||||||
|
const command = useCommand()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
@@ -16,35 +32,148 @@ export function DialogSelectFile() {
|
|||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||||
const view = createMemo(() => layout.view(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 (
|
return (
|
||||||
<Dialog title="Select file">
|
<Dialog class="pt-3 pb-0 !max-h-[480px]">
|
||||||
<List
|
<List
|
||||||
search={{ placeholder: "Search files", autofocus: true }}
|
search={{ placeholder: "Search files and commands", autofocus: true, hideIcon: true, class: "pl-3 pr-2 !mb-0" }}
|
||||||
emptyMessage="No files found"
|
emptyMessage="No results found"
|
||||||
items={file.searchFiles}
|
items={items}
|
||||||
key={(x) => x}
|
key={(item) => item.id}
|
||||||
onSelect={(path) => {
|
filterKeys={["title", "description", "category"]}
|
||||||
if (path) {
|
groupBy={(item) => item.category}
|
||||||
const value = file.tab(path)
|
onMove={handleMove}
|
||||||
tabs().open(value)
|
onSelect={handleSelect}
|
||||||
file.load(path)
|
|
||||||
view().reviewPanel.open()
|
|
||||||
}
|
|
||||||
dialog.close()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(item) => (
|
||||||
<div class="w-full flex items-center justify-between rounded-md">
|
<Show
|
||||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
when={item.type === "command"}
|
||||||
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
fallback={
|
||||||
<div class="flex items-center text-14-regular">
|
<div class="w-full flex items-center justify-between rounded-md">
|
||||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||||
{getDirectory(i)}
|
<FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
|
||||||
</span>
|
<div class="flex items-center text-14-regular">
|
||||||
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
|
<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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="w-full flex items-center justify-between gap-4 pl-1">
|
||||||
|
<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}>
|
||||||
|
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,267 +1,203 @@
|
|||||||
import { createMemo, createResource, Show } from "solid-js"
|
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 { useLayout } from "@/context/layout"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useServer } from "@/context/server"
|
// import { useServer } from "@/context/server"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
// import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
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 { iife } from "@opencode-ai/util/iife"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
|
||||||
import { Popover } from "@opencode-ai/ui/popover"
|
import { Popover } from "@opencode-ai/ui/popover"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
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() {
|
export function SessionHeader() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const navigate = useNavigate()
|
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const server = useServer()
|
// const server = useServer()
|
||||||
const dialog = useDialog()
|
// const dialog = useDialog()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
|
||||||
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||||
|
const project = createMemo(() => {
|
||||||
const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
|
const directory = projectDirectory()
|
||||||
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
if (!directory) return
|
||||||
const parentSession = createMemo(() => {
|
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
|
||||||
const current = currentSession()
|
|
||||||
if (!current?.parentID) return undefined
|
|
||||||
return sync.data.session.find((s) => s.id === current.parentID)
|
|
||||||
})
|
})
|
||||||
|
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 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 sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const view = createMemo(() => layout.view(sessionKey()))
|
const view = createMemo(() => layout.view(sessionKey()))
|
||||||
|
|
||||||
function navigateToProject(directory: string) {
|
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
||||||
navigate(`/${base64Encode(directory)}`)
|
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
||||||
}
|
|
||||||
|
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex">
|
<>
|
||||||
<button
|
<Show when={centerMount()}>
|
||||||
type="button"
|
{(mount) => (
|
||||||
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"
|
<Portal mount={mount()}>
|
||||||
onClick={layout.mobileSidebar.toggle}
|
<button
|
||||||
>
|
type="button"
|
||||||
<Icon name="menu" size="small" />
|
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"
|
||||||
</button>
|
onClick={() => command.trigger("file.open")}
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2">
|
||||||
<Select
|
<Icon name="magnifying-glass" size="normal" class="icon-base" />
|
||||||
options={sessions()}
|
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate h-3.5 flex items-center overflow-visible">
|
||||||
current={parentSession()}
|
Search {name()}
|
||||||
placeholder="Back to parent session"
|
</span>
|
||||||
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>
|
</div>
|
||||||
</Show>
|
|
||||||
</div>
|
<Show when={hotkey()}>{(keybind) => <Keybind>{keybind()}</Keybind>}</Show>
|
||||||
<Show when={currentSession() && !parentSession()}>
|
</button>
|
||||||
<TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
|
</Portal>
|
||||||
<IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
|
)}
|
||||||
</TooltipKeybind>
|
</Show>
|
||||||
</Show>
|
<Show when={rightMount()}>
|
||||||
</div>
|
{(mount) => (
|
||||||
<div class="flex items-center gap-3">
|
<Portal mount={mount()}>
|
||||||
<div class="hidden md:flex items-center gap-1">
|
<div class="flex items-center gap-3">
|
||||||
<Button
|
{/* <div class="hidden md:flex items-center gap-1"> */}
|
||||||
size="small"
|
{/* <Button */}
|
||||||
variant="ghost"
|
{/* size="small" */}
|
||||||
onClick={() => {
|
{/* variant="ghost" */}
|
||||||
dialog.show(() => <DialogSelectServer />)
|
{/* onClick={() => { */}
|
||||||
}}
|
{/* dialog.show(() => <DialogSelectServer />) */}
|
||||||
>
|
{/* }} */}
|
||||||
<div
|
{/* > */}
|
||||||
classList={{
|
{/* <div */}
|
||||||
"size-1.5 rounded-full": true,
|
{/* classList={{ */}
|
||||||
"bg-icon-success-base": server.healthy() === true,
|
{/* "size-1.5 rounded-full": true, */}
|
||||||
"bg-icon-critical-base": server.healthy() === false,
|
{/* "bg-icon-success-base": server.healthy() === true, */}
|
||||||
"bg-border-weak-base": server.healthy() === undefined,
|
{/* "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>
|
{/* <Icon name="server" size="small" class="text-icon-weak" /> */}
|
||||||
</Button>
|
{/* <span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span> */}
|
||||||
<SessionLspIndicator />
|
{/* </Button> */}
|
||||||
<SessionMcpIndicator />
|
{/* <SessionLspIndicator /> */}
|
||||||
</div>
|
{/* <SessionMcpIndicator /> */}
|
||||||
<div class="flex items-center gap-1">
|
{/* </div> */}
|
||||||
<Show when={currentSession()?.summary?.files}>
|
<div class="flex items-center gap-1">
|
||||||
<TooltipKeybind
|
<Show when={currentSession()?.summary?.files}>
|
||||||
class="hidden md:block shrink-0"
|
<TooltipKeybind
|
||||||
title="Toggle review"
|
class="hidden md:block shrink-0"
|
||||||
keybind={command.keybind("review.toggle")}
|
title="Toggle review"
|
||||||
>
|
keybind={command.keybind("review.toggle")}
|
||||||
<Button
|
>
|
||||||
variant="ghost"
|
<Button
|
||||||
class="group/review-toggle size-6 p-0"
|
variant="ghost"
|
||||||
onClick={() => view().reviewPanel.toggle()}
|
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">
|
<Button
|
||||||
<Icon
|
variant="ghost"
|
||||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
|
class="group/terminal-toggle size-6 p-0"
|
||||||
size="small"
|
onClick={() => view().terminal.toggle()}
|
||||||
class="group-hover/review-toggle:hidden"
|
>
|
||||||
/>
|
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||||
<Icon
|
<Icon
|
||||||
name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
|
size="small"
|
||||||
size="small"
|
name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||||
class="hidden group-hover/review-toggle:inline-block"
|
class="group-hover/terminal-toggle:hidden"
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
|
size="small"
|
||||||
size="small"
|
name="layout-bottom-partial"
|
||||||
class="hidden group-active/review-toggle:inline-block"
|
class="hidden group-hover/terminal-toggle:inline-block"
|
||||||
/>
|
/>
|
||||||
</div>
|
<Icon
|
||||||
</Button>
|
size="small"
|
||||||
</TooltipKeybind>
|
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||||
</Show>
|
class="hidden group-active/terminal-toggle:inline-block"
|
||||||
<TooltipKeybind
|
/>
|
||||||
class="hidden md:block shrink-0"
|
</div>
|
||||||
title="Toggle terminal"
|
</Button>
|
||||||
keybind={command.keybind("terminal.toggle")}
|
</TooltipKeybind>
|
||||||
>
|
</div>
|
||||||
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={() => view().terminal.toggle()}>
|
<Show when={shareEnabled() && currentSession()}>
|
||||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
<Popover
|
||||||
<Icon
|
title="Share session"
|
||||||
size="small"
|
trigger={
|
||||||
name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
<Tooltip class="shrink-0" value="Share session">
|
||||||
class="group-hover/terminal-toggle:hidden"
|
<IconButton icon="share" variant="ghost" class="" />
|
||||||
/>
|
</Tooltip>
|
||||||
<Icon
|
}
|
||||||
size="small"
|
>
|
||||||
name="layout-bottom-partial"
|
{iife(() => {
|
||||||
class="hidden group-hover/terminal-toggle:inline-block"
|
const [url] = createResource(
|
||||||
/>
|
() => currentSession(),
|
||||||
<Icon
|
async (session) => {
|
||||||
size="small"
|
if (!session) return
|
||||||
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
let shareURL = session.share?.url
|
||||||
class="hidden group-active/terminal-toggle:inline-block"
|
if (!shareURL) {
|
||||||
/>
|
shareURL = await globalSDK.client.session
|
||||||
</div>
|
.share({ sessionID: session.id, directory: projectDirectory() })
|
||||||
</Button>
|
.then((r) => r.data?.share?.url)
|
||||||
</TooltipKeybind>
|
.catch((e) => {
|
||||||
</div>
|
console.error("Failed to share session", e)
|
||||||
<Show when={shareEnabled() && currentSession()}>
|
return undefined
|
||||||
<Popover
|
})
|
||||||
title="Share session"
|
}
|
||||||
trigger={
|
return shareURL
|
||||||
<Tooltip class="shrink-0" value="Share session">
|
},
|
||||||
<IconButton icon="share" variant="ghost" class="" />
|
{ initialValue: "" },
|
||||||
</Tooltip>
|
)
|
||||||
}
|
return (
|
||||||
>
|
<Show when={url.latest}>
|
||||||
{iife(() => {
|
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
|
||||||
const [url] = createResource(
|
</Show>
|
||||||
() => currentSession(),
|
)
|
||||||
async (session) => {
|
})}
|
||||||
if (!session) return
|
</Popover>
|
||||||
let shareURL = session.share?.url
|
</Show>
|
||||||
if (!shareURL) {
|
</div>
|
||||||
shareURL = await globalSDK.client.session
|
</Portal>
|
||||||
.share({ sessionID: session.id, directory: projectDirectory() })
|
)}
|
||||||
.then((r) => r.data?.share?.url)
|
</Show>
|
||||||
.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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 @@
|
|||||||
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 { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
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)
|
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||||
|
|
||||||
@@ -107,74 +105,27 @@ export function formatKeybind(config: string): string {
|
|||||||
if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
|
if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
|
||||||
|
|
||||||
if (kb.key) {
|
if (kb.key) {
|
||||||
const displayKey = kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1)
|
const arrows: Record<string, string> = {
|
||||||
|
arrowup: "↑",
|
||||||
|
arrowdown: "↓",
|
||||||
|
arrowleft: "←",
|
||||||
|
arrowright: "→",
|
||||||
|
}
|
||||||
|
const displayKey =
|
||||||
|
arrows[kb.key.toLowerCase()] ??
|
||||||
|
(kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1))
|
||||||
parts.push(displayKey)
|
parts.push(displayKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return IS_MAC ? parts.join("") : parts.join("+")
|
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({
|
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
|
||||||
name: "Command",
|
name: "Command",
|
||||||
init: () => {
|
init: () => {
|
||||||
|
const dialog = useDialog()
|
||||||
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
||||||
const [suspendCount, setSuspendCount] = createSignal(0)
|
const [suspendCount, setSuspendCount] = createSignal(0)
|
||||||
const dialog = useDialog()
|
|
||||||
|
|
||||||
const options = createMemo(() => {
|
const options = createMemo(() => {
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
@@ -202,14 +153,21 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
|
|
||||||
const suspended = () => suspendCount() > 0
|
const suspended = () => suspendCount() > 0
|
||||||
|
|
||||||
const showPalette = () => {
|
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
|
||||||
if (!dialog.active) {
|
for (const option of options()) {
|
||||||
dialog.show(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
|
if (option.id === id || option.id === "suggested." + id) {
|
||||||
|
option.onSelect?.(source)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showPalette = () => {
|
||||||
|
run("file.open", "palette")
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (suspended()) return
|
if (suspended() || dialog.active) return
|
||||||
|
|
||||||
const paletteKeybinds = parseKeybind("mod+shift+p")
|
const paletteKeybinds = parseKeybind("mod+shift+p")
|
||||||
if (matchKeybind(paletteKeybinds, event)) {
|
if (matchKeybind(paletteKeybinds, event)) {
|
||||||
@@ -248,12 +206,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
||||||
for (const option of options()) {
|
run(id, source)
|
||||||
if (option.id === id || option.id === "suggested." + id) {
|
|
||||||
option.onSelect?.(source)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
keybind(id: string) {
|
keybind(id: string) {
|
||||||
const option = options().find((x) => x.id === id || x.id === "suggested." + id)
|
const option = options().find((x) => x.id === id || x.id === "suggested." + id)
|
||||||
|
|||||||
@@ -19,15 +19,29 @@ import {
|
|||||||
type QuestionRequest,
|
type QuestionRequest,
|
||||||
createOpencodeClient,
|
createOpencodeClient,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} 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 { Binary } from "@opencode-ai/util/binary"
|
||||||
import { retry } from "@opencode-ai/util/retry"
|
import { retry } from "@opencode-ai/util/retry"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import { ErrorPage, type InitError } from "../pages/error"
|
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 { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { usePlatform } from "./platform"
|
import { usePlatform } from "./platform"
|
||||||
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
status: "loading" | "partial" | "complete"
|
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() {
|
function createGlobalSync() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const platform = usePlatform()
|
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<{
|
const [globalStore, setGlobalStore] = createStore<{
|
||||||
ready: boolean
|
ready: boolean
|
||||||
error?: InitError
|
error?: InitError
|
||||||
@@ -86,35 +109,51 @@ function createGlobalSync() {
|
|||||||
provider_auth: {},
|
provider_auth: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||||
|
|
||||||
function child(directory: string) {
|
function child(directory: string) {
|
||||||
if (!directory) console.error("No directory provided")
|
if (!directory) console.error("No directory provided")
|
||||||
if (!children[directory]) {
|
if (!children[directory]) {
|
||||||
children[directory] = createStore<State>({
|
const cache = runWithOwner(owner, () =>
|
||||||
project: "",
|
persisted(
|
||||||
provider: { all: [], connected: [], default: {} },
|
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
||||||
config: {},
|
createStore({ value: undefined as VcsInfo | undefined }),
|
||||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
),
|
||||||
status: "loading" as const,
|
)
|
||||||
agent: [],
|
if (!cache) throw new Error("Failed to create persisted cache")
|
||||||
command: [],
|
vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })
|
||||||
session: [],
|
|
||||||
sessionTotal: 0,
|
const init = () => {
|
||||||
session_status: {},
|
children[directory] = createStore<State>({
|
||||||
session_diff: {},
|
project: "",
|
||||||
todo: {},
|
provider: { all: [], connected: [], default: {} },
|
||||||
permission: {},
|
config: {},
|
||||||
question: {},
|
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||||
mcp: {},
|
status: "loading" as const,
|
||||||
lsp: [],
|
agent: [],
|
||||||
vcs: undefined,
|
command: [],
|
||||||
limit: 5,
|
session: [],
|
||||||
message: {},
|
sessionTotal: 0,
|
||||||
part: {},
|
session_status: {},
|
||||||
})
|
session_diff: {},
|
||||||
bootstrapInstance(directory)
|
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) {
|
async function loadSessions(directory: string) {
|
||||||
@@ -124,12 +163,19 @@ function createGlobalSync() {
|
|||||||
return globalSDK.client.session
|
return globalSDK.client.session
|
||||||
.list({ directory, roots: true })
|
.list({ directory, roots: true })
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
|
|
||||||
const nonArchived = (x.data ?? [])
|
const nonArchived = (x.data ?? [])
|
||||||
.filter((s) => !!s?.id)
|
.filter((s) => !!s?.id)
|
||||||
.filter((s) => !s.time?.archived)
|
.filter((s) => !s.time?.archived)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
.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
|
// Include up to the limit, plus any updated in the last 4 hours
|
||||||
const sessions = nonArchived.filter((s, i) => {
|
const sessions = nonArchived.filter((s, i) => {
|
||||||
if (i < limit) return true
|
if (i < limit) return true
|
||||||
@@ -150,6 +196,8 @@ function createGlobalSync() {
|
|||||||
async function bootstrapInstance(directory: string) {
|
async function bootstrapInstance(directory: string) {
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
const [store, setStore] = child(directory)
|
const [store, setStore] = child(directory)
|
||||||
|
const cache = vcsCache.get(directory)
|
||||||
|
if (!cache) return
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: globalSDK.url,
|
baseUrl: globalSDK.url,
|
||||||
fetch: platform.fetch,
|
fetch: platform.fetch,
|
||||||
@@ -157,6 +205,13 @@ function createGlobalSync() {
|
|||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!cache.ready()) return
|
||||||
|
const cached = cache.store.value
|
||||||
|
if (!cached?.branch) return
|
||||||
|
setStore("vcs", (value) => value ?? cached)
|
||||||
|
})
|
||||||
|
|
||||||
const blockingRequests = {
|
const blockingRequests = {
|
||||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||||
provider: () =>
|
provider: () =>
|
||||||
@@ -186,7 +241,11 @@ function createGlobalSync() {
|
|||||||
loadSessions(directory),
|
loadSessions(directory),
|
||||||
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||||
sdk.lsp.status().then((x) => setStore("lsp", 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) => {
|
sdk.permission.list().then((x) => {
|
||||||
const grouped: Record<string, PermissionRequest[]> = {}
|
const grouped: Record<string, PermissionRequest[]> = {}
|
||||||
for (const perm of x.data ?? []) {
|
for (const perm of x.data ?? []) {
|
||||||
@@ -292,6 +351,23 @@ function createGlobalSync() {
|
|||||||
bootstrapInstance(directory)
|
bootstrapInstance(directory)
|
||||||
break
|
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": {
|
case "session.updated": {
|
||||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||||
if (event.properties.info.time.archived) {
|
if (event.properties.info.time.archived) {
|
||||||
@@ -303,6 +379,8 @@ function createGlobalSync() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (event.properties.info.parentID) break
|
||||||
|
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
@@ -399,7 +477,10 @@ function createGlobalSync() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "vcs.branch.updated": {
|
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
|
break
|
||||||
}
|
}
|
||||||
case "permission.asked": {
|
case "permission.asked": {
|
||||||
|
|||||||
@@ -47,12 +47,34 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
const globalSdk = useGlobalSDK()
|
const globalSdk = useGlobalSDK()
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const server = useServer()
|
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(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.global("layout", ["layout.v6"]),
|
{ ...target, migrate },
|
||||||
createStore({
|
createStore({
|
||||||
sidebar: {
|
sidebar: {
|
||||||
opened: false,
|
opened: false,
|
||||||
width: 280,
|
width: 344,
|
||||||
|
workspaces: {} as Record<string, boolean>,
|
||||||
|
workspacesDefault: false,
|
||||||
},
|
},
|
||||||
terminal: {
|
terminal: {
|
||||||
height: 280,
|
height: 280,
|
||||||
@@ -304,6 +326,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
resize(width: number) {
|
resize(width: number) {
|
||||||
setStore("sidebar", "width", width)
|
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: {
|
terminal: {
|
||||||
height: createMemo(() => store.terminal.height),
|
height: createMemo(() => store.terminal.height),
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export type Platform = {
|
|||||||
/** Platform discriminator */
|
/** Platform discriminator */
|
||||||
platform: "web" | "desktop"
|
platform: "web" | "desktop"
|
||||||
|
|
||||||
|
/** Desktop OS (Tauri only) */
|
||||||
|
os?: "macos" | "windows" | "linux"
|
||||||
|
|
||||||
/** App version */
|
/** App version */
|
||||||
version?: string
|
version?: string
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
createStore({
|
createStore({
|
||||||
list: [] as string[],
|
list: [] as string[],
|
||||||
projects: {} as Record<string, StoredProject[]>,
|
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)
|
result.splice(toIndex, 0, item)
|
||||||
setStore("projects", key, result)
|
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 sdk = useSDK()
|
||||||
const [store, setStore] = globalSync.child(sdk.directory)
|
const [store, setStore] = globalSync.child(sdk.directory)
|
||||||
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
|
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
|
||||||
const chunk = 200
|
const chunk = 400
|
||||||
const inflight = new Map<string, Promise<void>>()
|
const inflight = new Map<string, Promise<void>>()
|
||||||
const inflightDiff = new Map<string, Promise<void>>()
|
const inflightDiff = new Map<string, Promise<void>>()
|
||||||
const inflightTodo = new Map<string, Promise<void>>()
|
const inflightTodo = new Map<string, Promise<void>>()
|
||||||
|
|||||||
@@ -5,3 +5,7 @@
|
|||||||
cursor: default;
|
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 { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Logo } from "@opencode-ai/ui/logo"
|
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 { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const sync = useGlobalSync()
|
const sync = useGlobalSync()
|
||||||
@@ -24,6 +24,7 @@ export default function Home() {
|
|||||||
|
|
||||||
function openProject(directory: string) {
|
function openProject(directory: string) {
|
||||||
layout.projects.open(directory)
|
layout.projects.open(directory)
|
||||||
|
server.projects.touch(directory)
|
||||||
navigate(`/${base64Encode(directory)}`)
|
navigate(`/${base64Encode(directory)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1024
-540
File diff suppressed because it is too large
Load Diff
@@ -419,7 +419,6 @@ export default function Page() {
|
|||||||
{
|
{
|
||||||
id: "session.new",
|
id: "session.new",
|
||||||
title: "New session",
|
title: "New session",
|
||||||
description: "Create a new session",
|
|
||||||
category: "Session",
|
category: "Session",
|
||||||
keybind: "mod+shift+s",
|
keybind: "mod+shift+s",
|
||||||
slash: "new",
|
slash: "new",
|
||||||
@@ -428,7 +427,7 @@ export default function Page() {
|
|||||||
{
|
{
|
||||||
id: "file.open",
|
id: "file.open",
|
||||||
title: "Open file",
|
title: "Open file",
|
||||||
description: "Search and open a file",
|
description: "Search files and commands",
|
||||||
category: "File",
|
category: "File",
|
||||||
keybind: "mod+p",
|
keybind: "mod+p",
|
||||||
slash: "open",
|
slash: "open",
|
||||||
@@ -437,7 +436,7 @@ export default function Page() {
|
|||||||
{
|
{
|
||||||
id: "terminal.toggle",
|
id: "terminal.toggle",
|
||||||
title: "Toggle terminal",
|
title: "Toggle terminal",
|
||||||
description: "Show or hide the terminal",
|
description: "",
|
||||||
category: "View",
|
category: "View",
|
||||||
keybind: "ctrl+`",
|
keybind: "ctrl+`",
|
||||||
slash: "terminal",
|
slash: "terminal",
|
||||||
@@ -446,7 +445,7 @@ export default function Page() {
|
|||||||
{
|
{
|
||||||
id: "review.toggle",
|
id: "review.toggle",
|
||||||
title: "Toggle review",
|
title: "Toggle review",
|
||||||
description: "Show or hide the review panel",
|
description: "",
|
||||||
category: "View",
|
category: "View",
|
||||||
keybind: "mod+shift+r",
|
keybind: "mod+shift+r",
|
||||||
onSelect: () => view().reviewPanel.toggle(),
|
onSelect: () => view().reviewPanel.toggle(),
|
||||||
@@ -885,6 +884,19 @@ export default function Page() {
|
|||||||
window.history.replaceState(null, "", `#${anchor(id)}`)
|
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") => {
|
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||||
setActiveMessage(message)
|
setActiveMessage(message)
|
||||||
|
|
||||||
@@ -896,7 +908,7 @@ export default function Page() {
|
|||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const el = document.getElementById(anchor(message.id))
|
const el = document.getElementById(anchor(message.id))
|
||||||
if (el) el.scrollIntoView({ behavior, block: "start" })
|
if (el) scrollToElement(el, behavior)
|
||||||
})
|
})
|
||||||
|
|
||||||
updateHash(message.id)
|
updateHash(message.id)
|
||||||
@@ -904,7 +916,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const el = document.getElementById(anchor(message.id))
|
const el = document.getElementById(anchor(message.id))
|
||||||
if (el) el.scrollIntoView({ behavior, block: "start" })
|
if (el) scrollToElement(el, behavior)
|
||||||
updateHash(message.id)
|
updateHash(message.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -956,7 +968,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const hashTarget = document.getElementById(hash)
|
const hashTarget = document.getElementById(hash)
|
||||||
if (hashTarget) {
|
if (hashTarget) {
|
||||||
hashTarget.scrollIntoView({ behavior: "auto", block: "start" })
|
scrollToElement(hashTarget, "auto")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -20,6 +20,8 @@
|
|||||||
"@opencode-ai/console-mail": "workspace:*",
|
"@opencode-ai/console-mail": "workspace:*",
|
||||||
"@opencode-ai/console-resource": "workspace:*",
|
"@opencode-ai/console-resource": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
|
"@smithy/eventstream-codec": "4.2.7",
|
||||||
|
"@smithy/util-utf8": "4.2.0",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
"@solidjs/start": "catalog:",
|
"@solidjs/start": "catalog:",
|
||||||
@@ -34,6 +36,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"@webgpu/types": "0.1.54",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"wrangler": "4.50.0"
|
"wrangler": "4.50.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../ui/src/assets/images/social-share-black.png
|
||||||
@@ -24,6 +24,9 @@ export function Footer() {
|
|||||||
<div data-slot="cell">
|
<div data-slot="cell">
|
||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Docs</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div data-slot="cell">
|
||||||
|
<a href="/changelog">Changelog</a>
|
||||||
|
</div>
|
||||||
<div data-slot="cell">
|
<div data-slot="cell">
|
||||||
<a href="/discord">Discord</a>
|
<a href="/discord">Discord</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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: {
|
github: {
|
||||||
repoUrl: "https://github.com/anomalyco/opencode",
|
repoUrl: "https://github.com/anomalyco/opencode",
|
||||||
starsFormatted: {
|
starsFormatted: {
|
||||||
compact: "60K",
|
compact: "70K",
|
||||||
full: "60,000",
|
full: "70,000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export const config = {
|
|||||||
// Static stats (used on landing page)
|
// Static stats (used on landing page)
|
||||||
stats: {
|
stats: {
|
||||||
contributors: "500",
|
contributors: "500",
|
||||||
commits: "6,500",
|
commits: "7,000",
|
||||||
monthlyUsers: "650,000",
|
monthlyUsers: "650,000",
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ export const github = query(async () => {
|
|||||||
fetch(`${apiBaseUrl}/releases`, { headers }).then((res) => res.json()),
|
fetch(`${apiBaseUrl}/releases`, { headers }).then((res) => res.json()),
|
||||||
fetch(`${apiBaseUrl}/contributors?per_page=1`, { headers }),
|
fetch(`${apiBaseUrl}/contributors?per_page=1`, { headers }),
|
||||||
])
|
])
|
||||||
|
if (!Array.isArray(releases) || releases.length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
const [release] = releases
|
const [release] = releases
|
||||||
const contributorCount = Number.parseInt(
|
const linkHeader = contributors.headers.get("Link")
|
||||||
contributors.headers
|
const contributorCount = linkHeader
|
||||||
.get("Link")!
|
? Number.parseInt(linkHeader.match(/&page=(\d+)>; rel="last"/)?.at(1) ?? "0")
|
||||||
.match(/&page=(\d+)>; rel="last"/)!
|
: 0
|
||||||
.at(1)!,
|
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
stars: meta.stargazers_count,
|
stars: meta.stargazers_count,
|
||||||
release: {
|
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"] {
|
[data-page="black"] {
|
||||||
background: #000;
|
background: #000;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -8,13 +119,18 @@
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: #fff;
|
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;
|
position: absolute;
|
||||||
top: 0;
|
inset: 0 0 auto 0;
|
||||||
left: 0;
|
height: 30dvh;
|
||||||
width: 100%;
|
pointer-events: none;
|
||||||
height: 288px;
|
z-index: 0;
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="header"] {
|
[data-component="header"] {
|
||||||
@@ -24,9 +140,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 40px;
|
padding-top: 40px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
/* [data-component="header-logo"] { */
|
|
||||||
/* } */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="content"] {
|
[data-component="content"] {
|
||||||
@@ -51,27 +164,35 @@
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: rgba(255, 255, 255, 0.92);
|
color: rgba(255, 255, 255, 0.92);
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 160%;
|
line-height: 1.45;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: rgba(255, 255, 255, 0.59);
|
color: rgba(255, 255, 255, 0.59);
|
||||||
font-size: 15px;
|
font-size: 16px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 160%;
|
line-height: 1.45;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,6 +200,7 @@
|
|||||||
[data-slot="hero-black"] {
|
[data-slot="hero-black"] {
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
margin-top: 60px;
|
margin-top: 60px;
|
||||||
@@ -86,9 +208,29 @@
|
|||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 540px;
|
max-width: 590px;
|
||||||
height: auto;
|
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-base"] {
|
||||||
|
fill: url(#hero-black-fill-gradient);
|
||||||
|
stroke: url(#hero-black-stroke-gradient);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="black-glow"] {
|
||||||
|
fill: url(#hero-black-top-glow);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="black-shimmer"] {
|
||||||
|
fill: url(#hero-black-shimmer-gradient);
|
||||||
|
pointer-events: none;
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +253,7 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 160%; /* 28.8px */
|
line-height: 160%;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -172,7 +314,7 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||||
color: rgba(255, 255, 255, 0.59);
|
color: rgba(255, 255, 255, 0.59);
|
||||||
font-family: "JetBrains Mono Nerd Font";
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -185,7 +327,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 540px;
|
max-width: 660px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
@@ -197,16 +339,21 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||||
|
background: black;
|
||||||
|
background-clip: padding-box;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: border-color 0.15s ease;
|
transition: border-color 0.15s ease;
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:hover {
|
@media (max-width: 480px) {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:active) {
|
||||||
border-color: rgba(255, 255, 255, 0.35);
|
border-color: rgba(255, 255, 255, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,19 +394,27 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 32px;
|
gap: 32px;
|
||||||
width: fit-content;
|
width: 100%;
|
||||||
max-width: calc(100% - 40px);
|
max-width: 660px;
|
||||||
margin: 0 auto;
|
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"] {
|
[data-slot="selected-card"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
width: fit-content;
|
width: 100%;
|
||||||
|
|
||||||
[data-slot="icon"] {
|
[data-slot="icon"] {
|
||||||
color: rgba(255, 255, 255, 0.59);
|
color: rgba(255, 255, 255, 0.59);
|
||||||
@@ -299,16 +454,15 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
color: rgba(255, 255, 255, 0.59);
|
color: rgba(255, 255, 255, 0.59);
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "▪";
|
content: "▪";
|
||||||
@@ -316,6 +470,10 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
color: rgba(255, 255, 255, 0.39);
|
color: rgba(255, 255, 255, 0.39);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,22 +499,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="cancel"] {
|
[data-slot="cancel"] {
|
||||||
background: transparent;
|
background: rgba(255, 255, 255, 0.05);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||||
color: rgba(255, 255, 255, 0.92);
|
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 {
|
&:hover {
|
||||||
border-color: rgba(255, 255, 255, 0.35);
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="continue"] {
|
[data-slot="continue"] {
|
||||||
background: rgba(255, 255, 255, 0.17);
|
background: rgb(255, 255, 255);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
color: rgb(0, 0, 0);
|
||||||
color: rgba(255, 255, 255, 0.59);
|
transition: background-color 150ms cubic-bezier(0.25, 0, 0.5, 1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(255, 255, 255, 0.25);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,7 +548,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: -18px;
|
margin-top: -18px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 540px;
|
max-width: 660px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
@@ -419,6 +581,8 @@
|
|||||||
|
|
||||||
[data-slot="icon"] {
|
[data-slot="icon"] {
|
||||||
color: rgba(255, 255, 255, 0.59);
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
isolation: isolate;
|
||||||
|
transform: translateZ(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="price"] {
|
[data-slot="price"] {
|
||||||
@@ -460,39 +624,6 @@
|
|||||||
font-weight: 400;
|
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"] {
|
[data-slot="checkout-form"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -533,52 +664,6 @@
|
|||||||
text-align: center;
|
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"] {
|
[data-slot="loading"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -595,6 +680,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
view-transition-name: fine-print;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: rgba(255, 255, 255, 0.39);
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2,8 +2,8 @@ import { Match, Switch } from "solid-js"
|
|||||||
|
|
||||||
export const plans = [
|
export const plans = [
|
||||||
{ id: "20", multiplier: null },
|
{ id: "20", multiplier: null },
|
||||||
{ id: "100", multiplier: "6x more usage than Black 20" },
|
{ id: "100", multiplier: "5x more usage than Black 20" },
|
||||||
{ id: "200", multiplier: "21x more usage than Black 20" },
|
{ id: "200", multiplier: "20x more usage than Black 20" },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type PlanID = (typeof plans)[number]["id"]
|
export type PlanID = (typeof plans)[number]["id"]
|
||||||
@@ -14,28 +14,47 @@ export function PlanIcon(props: { plan: string }) {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.plan === "20"}>
|
<Match when={props.plan === "20"}>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect x="4" y="4" width="16" height="16" rx="2" stroke="currentColor" stroke-width="1.5" />
|
<title>Black 20 plan</title>
|
||||||
|
<rect x="0.5" y="0.5" width="23" height="23" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.plan === "100"}>
|
<Match when={props.plan === "100"}>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect x="3" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
|
<title>Black 100 plan</title>
|
||||||
<rect x="14" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
|
<rect x="0.5" y="0.5" width="9" height="9" stroke="currentColor" />
|
||||||
<rect x="3" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
|
<rect x="0.5" y="14.5" width="9" height="9" stroke="currentColor" />
|
||||||
<rect x="14" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
|
<rect x="14.5" y="0.5" width="9" height="9" stroke="currentColor" />
|
||||||
|
<rect x="14.5" y="14.5" width="9" height="9" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.plan === "200"}>
|
<Match when={props.plan === "200"}>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect x="2" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
<title>Black 200 plan</title>
|
||||||
<rect x="10" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
<rect x="0.5" y="0.5" width="3" height="3" stroke="currentColor" />
|
||||||
<rect x="18" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
<rect x="0.5" y="5.5" width="3" height="3" stroke="currentColor" />
|
||||||
<rect x="2" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
<rect x="0.5" y="10.5" width="3" height="3" stroke="currentColor" />
|
||||||
<rect x="10" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
<rect x="0.5" y="15.5" width="3" height="3" stroke="currentColor" />
|
||||||
<rect x="18" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
<rect x="0.5" y="20.5" width="3" height="3" stroke="currentColor" />
|
||||||
<rect x="2" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
<rect x="5.5" y="0.5" width="3" height="3" stroke="currentColor" />
|
||||||
<rect x="10" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
<rect x="5.5" y="5.5" width="3" height="3" stroke="currentColor" />
|
||||||
<rect x="18" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
<rect x="5.5" y="10.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="5.5" y="15.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="5.5" y="20.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="10.5" y="0.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="10.5" y="5.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="10.5" y="10.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="10.5" y="15.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="10.5" y="20.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="15.5" y="0.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="15.5" y="5.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="15.5" y="10.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="15.5" y="15.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="15.5" y="20.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="20.5" y="0.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="20.5" y="5.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="20.5" y="10.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="20.5" y="15.5" width="3" height="3" stroke="currentColor" />
|
||||||
|
<rect x="20.5" y="20.5" width="3" height="3" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -1,13 +1,39 @@
|
|||||||
import { A, useSearchParams } from "@solidjs/router"
|
import { A, useSearchParams } from "@solidjs/router"
|
||||||
import { Title } from "@solidjs/meta"
|
import { Title } from "@solidjs/meta"
|
||||||
import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
|
import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js"
|
||||||
import { PlanIcon, plans } from "./common"
|
import { PlanIcon, plans } from "./common"
|
||||||
|
|
||||||
export default function Black() {
|
export default function Black() {
|
||||||
const [params] = useSearchParams()
|
const [params] = useSearchParams()
|
||||||
const [selected, setSelected] = createSignal<string | null>((params.plan as string) || null)
|
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()))
|
const selectedPlan = createMemo(() => plans.find((p) => p.id === selected()))
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
requestAnimationFrame(() => setMounted(true))
|
||||||
|
})
|
||||||
|
|
||||||
|
const transition = (action: () => void) => {
|
||||||
|
if (mounted() && "startViewTransition" in document) {
|
||||||
|
;(document as any).startViewTransition(action)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = (planId: string) => {
|
||||||
|
if (selected() === planId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transition(() => setSelected(planId))
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
transition(() => setSelected(null))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>opencode</Title>
|
<Title>opencode</Title>
|
||||||
@@ -17,7 +43,12 @@ export default function Black() {
|
|||||||
<div data-slot="pricing">
|
<div data-slot="pricing">
|
||||||
<For each={plans}>
|
<For each={plans}>
|
||||||
{(plan) => (
|
{(plan) => (
|
||||||
<button type="button" onClick={() => setSelected(plan.id)} data-slot="pricing-card">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => select(plan.id)}
|
||||||
|
data-slot="pricing-card"
|
||||||
|
style={{ "view-transition-name": `card-${plan.id}` }}
|
||||||
|
>
|
||||||
<div data-slot="icon">
|
<div data-slot="icon">
|
||||||
<PlanIcon plan={plan.id} />
|
<PlanIcon plan={plan.id} />
|
||||||
</div>
|
</div>
|
||||||
@@ -31,14 +62,11 @@ export default function Black() {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<p data-slot="fine-print">
|
|
||||||
Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
|
|
||||||
</p>
|
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={selectedPlan()}>
|
<Match when={selectedPlan()}>
|
||||||
{(plan) => (
|
{(plan) => (
|
||||||
<div data-slot="selected-plan">
|
<div data-slot="selected-plan">
|
||||||
<div data-slot="selected-card">
|
<div data-slot="selected-card" style={{ "view-transition-name": `card-${plan().id}` }}>
|
||||||
<div data-slot="icon">
|
<div data-slot="icon">
|
||||||
<PlanIcon plan={plan().id} />
|
<PlanIcon plan={plan().id} />
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +77,7 @@ export default function Black() {
|
|||||||
<span data-slot="multiplier">{plan().multiplier}</span>
|
<span data-slot="multiplier">{plan().multiplier}</span>
|
||||||
</Show>
|
</Show>
|
||||||
</p>
|
</p>
|
||||||
<ul data-slot="terms">
|
<ul data-slot="terms" style={{ "view-transition-name": `terms-${plan().id}` }}>
|
||||||
<li>Your subscription will not start immediately</li>
|
<li>Your subscription will not start immediately</li>
|
||||||
<li>You will be added to the waitlist and activated soon</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>Your card will be only charged when your subscription is activated</li>
|
||||||
@@ -58,8 +86,8 @@ export default function Black() {
|
|||||||
<li>Limits may be adjusted and plans may be discontinued in the future</li>
|
<li>Limits may be adjusted and plans may be discontinued in the future</li>
|
||||||
<li>Cancel your subscription at anytime</li>
|
<li>Cancel your subscription at anytime</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div data-slot="actions">
|
<div data-slot="actions" style={{ "view-transition-name": `actions-${plan().id}` }}>
|
||||||
<button type="button" onClick={() => setSelected(null)} data-slot="cancel">
|
<button type="button" onClick={() => cancel()} data-slot="cancel">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<a href={`/black/subscribe/${plan().id}`} data-slot="continue">
|
<a href={`/black/subscribe/${plan().id}`} data-slot="continue">
|
||||||
@@ -67,13 +95,13 @@ export default function Black() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p data-slot="fine-print">
|
|
||||||
Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</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>
|
</section>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import { Billing } from "@opencode-ai/console-core/billing.js"
|
|||||||
const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record<PlanID, (typeof plans)[number]>
|
const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record<PlanID, (typeof plans)[number]>
|
||||||
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!)
|
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!)
|
||||||
|
|
||||||
const getWorkspaces = query(async () => {
|
const getWorkspaces = query(async (plan: string) => {
|
||||||
"use server"
|
"use server"
|
||||||
const actor = await getActor()
|
const actor = await getActor()
|
||||||
if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe")
|
if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe/" + plan)
|
||||||
return withActor(async () => {
|
return withActor(async () => {
|
||||||
return Database.use((tx) =>
|
return Database.use((tx) =>
|
||||||
tx
|
tx
|
||||||
@@ -258,15 +258,16 @@ function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BlackSubscribe() {
|
export default function BlackSubscribe() {
|
||||||
const workspaces = createAsync(() => getWorkspaces())
|
const params = useParams()
|
||||||
|
const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
|
||||||
|
const plan = planData.id
|
||||||
|
|
||||||
|
const workspaces = createAsync(() => getWorkspaces(plan))
|
||||||
const [selectedWorkspace, setSelectedWorkspace] = createSignal<string | undefined>(undefined)
|
const [selectedWorkspace, setSelectedWorkspace] = createSignal<string | undefined>(undefined)
|
||||||
const [success, setSuccess] = createSignal<SuccessData | undefined>(undefined)
|
const [success, setSuccess] = createSignal<SuccessData | undefined>(undefined)
|
||||||
const [failure, setFailure] = createSignal<string | undefined>(undefined)
|
const [failure, setFailure] = createSignal<string | undefined>(undefined)
|
||||||
const [clientSecret, setClientSecret] = createSignal<string | undefined>(undefined)
|
const [clientSecret, setClientSecret] = createSignal<string | undefined>(undefined)
|
||||||
const [stripe, setStripe] = createSignal<Stripe | undefined>(undefined)
|
const [stripe, setStripe] = createSignal<Stripe | undefined>(undefined)
|
||||||
const params = useParams()
|
|
||||||
const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
|
|
||||||
const plan = planData.id
|
|
||||||
|
|
||||||
// Resolve stripe promise once
|
// Resolve stripe promise once
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|||||||
@@ -0,0 +1,477 @@
|
|||||||
|
::selection {
|
||||||
|
background: var(--color-background-interactive);
|
||||||
|
color: var(--color-text-strong);
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background: var(--color-background-interactive);
|
||||||
|
color: var(--color-text-inverted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="changelog"] {
|
||||||
|
--color-background: hsl(0, 20%, 99%);
|
||||||
|
--color-background-weak: hsl(0, 8%, 97%);
|
||||||
|
--color-background-weak-hover: hsl(0, 8%, 94%);
|
||||||
|
--color-background-strong: hsl(0, 5%, 12%);
|
||||||
|
--color-background-strong-hover: hsl(0, 5%, 18%);
|
||||||
|
--color-background-interactive: hsl(62, 84%, 88%);
|
||||||
|
--color-background-interactive-weaker: hsl(64, 74%, 95%);
|
||||||
|
|
||||||
|
--color-text: hsl(0, 1%, 39%);
|
||||||
|
--color-text-weak: hsl(0, 1%, 60%);
|
||||||
|
--color-text-weaker: hsl(30, 2%, 81%);
|
||||||
|
--color-text-strong: hsl(0, 5%, 12%);
|
||||||
|
--color-text-inverted: hsl(0, 20%, 99%);
|
||||||
|
|
||||||
|
--color-border: hsl(30, 2%, 81%);
|
||||||
|
--color-border-weak: hsl(0, 1%, 85%);
|
||||||
|
|
||||||
|
--color-icon: hsl(0, 1%, 55%);
|
||||||
|
|
||||||
|
background: var(--color-background);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--color-text);
|
||||||
|
padding-bottom: 5rem;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
--color-background: hsl(0, 9%, 7%);
|
||||||
|
--color-background-weak: hsl(0, 6%, 10%);
|
||||||
|
--color-background-weak-hover: hsl(0, 6%, 15%);
|
||||||
|
--color-background-strong: hsl(0, 15%, 94%);
|
||||||
|
--color-background-strong-hover: hsl(0, 15%, 97%);
|
||||||
|
--color-background-interactive: hsl(62, 100%, 90%);
|
||||||
|
--color-background-interactive-weaker: hsl(60, 20%, 8%);
|
||||||
|
|
||||||
|
--color-text: hsl(0, 4%, 71%);
|
||||||
|
--color-text-weak: hsl(0, 2%, 49%);
|
||||||
|
--color-text-weaker: hsl(0, 3%, 28%);
|
||||||
|
--color-text-strong: hsl(0, 15%, 94%);
|
||||||
|
--color-text-inverted: hsl(0, 9%, 7%);
|
||||||
|
|
||||||
|
--color-border: hsl(0, 3%, 28%);
|
||||||
|
--color-border-weak: hsl(0, 4%, 23%);
|
||||||
|
|
||||||
|
--color-icon: hsl(10, 3%, 43%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header styles - copied from download */
|
||||||
|
[data-component="top"] {
|
||||||
|
padding: 24px 5rem;
|
||||||
|
height: 80px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--color-background);
|
||||||
|
border-bottom: 1px solid var(--color-border-weak);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
@media (max-width: 60rem) {
|
||||||
|
padding: 24px 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 34px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="nav-desktop"] {
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 48px;
|
||||||
|
|
||||||
|
@media (max-width: 55rem) {
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 48rem) {
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
span {
|
||||||
|
color: var(--color-text-weak);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
}
|
||||||
|
[data-slot="cta-button"] {
|
||||||
|
background: var(--color-background-strong);
|
||||||
|
color: var(--color-text-inverted);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
@media (max-width: 55rem) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[data-slot="cta-button"]:hover {
|
||||||
|
background: var(--color-background-strong-hover);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="nav-mobile"] {
|
||||||
|
button > svg {
|
||||||
|
color: var(--color-icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="nav-mobile-toggle"] {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
outline: none;
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="nav-mobile-toggle"]:hover {
|
||||||
|
background: var(--color-background-weak);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="nav-mobile"] {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
[data-component="nav-mobile-icon"] {
|
||||||
|
cursor: pointer;
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="nav-mobile-menu-list"] {
|
||||||
|
position: fixed;
|
||||||
|
background: var(--color-background);
|
||||||
|
top: 80px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 20px;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--color-text-weak);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
background: var(--color-background-weak);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="logo dark"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
[data-slot="logo light"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
[data-slot="logo dark"] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="footer"] {
|
||||||
|
border-top: 1px solid var(--color-border-weak);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 4rem;
|
||||||
|
|
||||||
|
@media (max-width: 65rem) {
|
||||||
|
border-bottom: 1px solid var(--color-border-weak);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="cell"] {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 2rem 0;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--color-text-weak);
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
background: var(--color-background-weak);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="cell"] + [data-slot="cell"] {
|
||||||
|
border-left: 1px solid var(--color-border-weak);
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 25rem) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
[data-slot="cell"] {
|
||||||
|
flex: 1 0 100%;
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid var(--color-border-weak);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="cell"]:nth-child(1) {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="container"] {
|
||||||
|
max-width: 67.5rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 1px solid var(--color-border-weak);
|
||||||
|
border-top: none;
|
||||||
|
|
||||||
|
@media (max-width: 65rem) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="content"] {
|
||||||
|
padding: 6rem 5rem;
|
||||||
|
|
||||||
|
@media (max-width: 60rem) {
|
||||||
|
padding: 4rem 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="legal"] {
|
||||||
|
color: var(--color-text-weak);
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem 5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 32px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (max-width: 60rem) {
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-text-weak);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Changelog Hero */
|
||||||
|
[data-component="changelog-hero"] {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
border-bottom: 1px solid var(--color-border-weak);
|
||||||
|
|
||||||
|
@media (max-width: 50rem) {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-strong);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Releases */
|
||||||
|
[data-component="releases"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="release"] {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 180px 1fr;
|
||||||
|
gap: 3rem;
|
||||||
|
padding: 2rem 0;
|
||||||
|
border-bottom: 1px solid var(--color-border-weak);
|
||||||
|
|
||||||
|
@media (max-width: 50rem) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
@media (max-width: 50rem) {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="version"] {
|
||||||
|
a {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-strong);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time {
|
||||||
|
color: var(--color-text-weak);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="content"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="section"] {
|
||||||
|
h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-strong);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-left: 16px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "-";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--color-text-weak);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="author"] {
|
||||||
|
color: var(--color-text-weak);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-left: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="contributors"] {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-weak);
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--color-text-weak);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-text);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-text-strong);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import "./index.css"
|
||||||
|
import { Title, Meta, Link } from "@solidjs/meta"
|
||||||
|
import { createAsync, query } from "@solidjs/router"
|
||||||
|
import { Header } from "~/component/header"
|
||||||
|
import { Footer } from "~/component/footer"
|
||||||
|
import { Legal } from "~/component/legal"
|
||||||
|
import { config } from "~/config"
|
||||||
|
import { For, Show } from "solid-js"
|
||||||
|
|
||||||
|
type Release = {
|
||||||
|
tag_name: string
|
||||||
|
name: string
|
||||||
|
body: string
|
||||||
|
published_at: string
|
||||||
|
html_url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const getReleases = query(async () => {
|
||||||
|
"use server"
|
||||||
|
const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github.v3+json",
|
||||||
|
"User-Agent": "OpenCode-Console",
|
||||||
|
},
|
||||||
|
cf: {
|
||||||
|
cacheTtl: 60 * 5,
|
||||||
|
cacheEverything: true,
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
if (!response.ok) return []
|
||||||
|
return response.json() as Promise<Release[]>
|
||||||
|
}, "releases.get")
|
||||||
|
|
||||||
|
function formatDate(dateString: string) {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMarkdown(body: string) {
|
||||||
|
const lines = body.split("\n")
|
||||||
|
const sections: { title: string; items: string[] }[] = []
|
||||||
|
let current: { title: string; items: string[] } | null = null
|
||||||
|
let skip = false
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith("## ")) {
|
||||||
|
if (current) sections.push(current)
|
||||||
|
const title = line.slice(3).trim()
|
||||||
|
current = { title, items: [] }
|
||||||
|
skip = false
|
||||||
|
} else if (line.startsWith("**Thank you")) {
|
||||||
|
skip = true
|
||||||
|
} else if (line.startsWith("- ") && !skip) {
|
||||||
|
current?.items.push(line.slice(2).trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current) sections.push(current)
|
||||||
|
|
||||||
|
return { sections }
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReleaseItem(props: { item: string }) {
|
||||||
|
const parts = () => {
|
||||||
|
const match = props.item.match(/^(.+?)(\s*\(@([\w-]+)\))?$/)
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
text: match[1],
|
||||||
|
username: match[3],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { text: props.item, username: undefined }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<span>{parts().text}</span>
|
||||||
|
<Show when={parts().username}>
|
||||||
|
<a data-slot="author" href={`https://github.com/${parts().username}`} target="_blank" rel="noopener noreferrer">
|
||||||
|
(@{parts().username})
|
||||||
|
</a>
|
||||||
|
</Show>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Changelog() {
|
||||||
|
const releases = createAsync(() => getReleases())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main data-page="changelog">
|
||||||
|
<Title>OpenCode | Changelog</Title>
|
||||||
|
<Link rel="canonical" href={`${config.baseUrl}/changelog`} />
|
||||||
|
<Meta name="description" content="OpenCode release notes and changelog" />
|
||||||
|
|
||||||
|
<div data-component="container">
|
||||||
|
<Header hideGetStarted />
|
||||||
|
|
||||||
|
<div data-component="content">
|
||||||
|
<section data-component="changelog-hero">
|
||||||
|
<h1>Changelog</h1>
|
||||||
|
<p>New updates and improvements to OpenCode</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section data-component="releases">
|
||||||
|
<For each={releases()}>
|
||||||
|
{(release) => {
|
||||||
|
const parsed = () => parseMarkdown(release.body || "")
|
||||||
|
return (
|
||||||
|
<article data-component="release">
|
||||||
|
<header>
|
||||||
|
<div data-slot="version">
|
||||||
|
<a href={release.html_url} target="_blank" rel="noopener noreferrer">
|
||||||
|
{release.tag_name}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<time dateTime={release.published_at}>{formatDate(release.published_at)}</time>
|
||||||
|
</header>
|
||||||
|
<div data-slot="content">
|
||||||
|
<For each={parsed().sections}>
|
||||||
|
{(section) => (
|
||||||
|
<div data-component="section">
|
||||||
|
<h3>{section.title}</h3>
|
||||||
|
<ul>
|
||||||
|
<For each={section.items}>{(item) => <ReleaseItem item={item} />}</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Legal />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -34,7 +34,6 @@
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
padding-bottom: 5rem;
|
padding-bottom: 5rem;
|
||||||
overflow-x: hidden;
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
--color-background: hsl(0, 9%, 7%);
|
--color-background: hsl(0, 9%, 7%);
|
||||||
|
|||||||
@@ -693,7 +693,7 @@ export default function Home() {
|
|||||||
<li>
|
<li>
|
||||||
<Faq question="Can I only use OpenCode in the terminal?">
|
<Faq question="Can I only use OpenCode in the terminal?">
|
||||||
Not anymore! OpenCode is now available as an app for your <a href="/download">desktop</a> and{" "}
|
Not anymore! OpenCode is now available as an app for your <a href="/download">desktop</a> and{" "}
|
||||||
<a href="/docs/cli/#web">web</a>!
|
<a href="/docs/web">web</a>!
|
||||||
</Faq>
|
</Faq>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -183,7 +183,12 @@ export async function POST(input: APIEvent) {
|
|||||||
.set({
|
.set({
|
||||||
customerID,
|
customerID,
|
||||||
subscriptionID,
|
subscriptionID,
|
||||||
subscriptionCouponID: couponID,
|
subscription: {
|
||||||
|
status: "subscribed",
|
||||||
|
coupon: couponID,
|
||||||
|
seats: 1,
|
||||||
|
plan: "200",
|
||||||
|
},
|
||||||
paymentMethodID: paymentMethod.id,
|
paymentMethodID: paymentMethod.id,
|
||||||
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
|
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
|
||||||
paymentMethodType: paymentMethod.type,
|
paymentMethodType: paymentMethod.type,
|
||||||
@@ -408,7 +413,7 @@ export async function POST(input: APIEvent) {
|
|||||||
await Database.transaction(async (tx) => {
|
await Database.transaction(async (tx) => {
|
||||||
await tx
|
await tx
|
||||||
.update(BillingTable)
|
.update(BillingTable)
|
||||||
.set({ subscriptionID: null, subscriptionCouponID: null })
|
.set({ subscriptionID: null, subscription: null })
|
||||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
|
||||||
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
|
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
|
||||||
|
|||||||
@@ -5,4 +5,58 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-slot="usage"] {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-6);
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="usage-item"] {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="usage-header"] {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="usage-label"] {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="usage-value"] {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="progress"] {
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--color-bg-surface);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="progress-bar"] {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="reset-time"] {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,58 @@
|
|||||||
import { action, useParams, useAction, useSubmission, json } from "@solidjs/router"
|
import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
import { Show } from "solid-js"
|
||||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||||
|
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||||
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
|
import { Black } from "@opencode-ai/console-core/black.js"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { queryBillingInfo } from "../../common"
|
import { queryBillingInfo } from "../../common"
|
||||||
import styles from "./black-section.module.css"
|
import styles from "./black-section.module.css"
|
||||||
|
|
||||||
|
const querySubscription = query(async (workspaceID: string) => {
|
||||||
|
"use server"
|
||||||
|
return withActor(async () => {
|
||||||
|
const row = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
rollingUsage: SubscriptionTable.rollingUsage,
|
||||||
|
fixedUsage: SubscriptionTable.fixedUsage,
|
||||||
|
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
|
||||||
|
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
|
||||||
|
})
|
||||||
|
.from(SubscriptionTable)
|
||||||
|
.where(and(eq(SubscriptionTable.workspaceID, Actor.workspace()), isNull(SubscriptionTable.timeDeleted)))
|
||||||
|
.then((r) => r[0]),
|
||||||
|
)
|
||||||
|
if (!row) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
rollingUsage: Black.analyzeRollingUsage({
|
||||||
|
usage: row.rollingUsage ?? 0,
|
||||||
|
timeUpdated: row.timeRollingUpdated ?? new Date(),
|
||||||
|
}),
|
||||||
|
weeklyUsage: Black.analyzeWeeklyUsage({
|
||||||
|
usage: row.fixedUsage ?? 0,
|
||||||
|
timeUpdated: row.timeFixedUpdated ?? new Date(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}, workspaceID)
|
||||||
|
}, "subscription.get")
|
||||||
|
|
||||||
|
function formatResetTime(seconds: number) {
|
||||||
|
const days = Math.floor(seconds / 86400)
|
||||||
|
if (days >= 1) {
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600)
|
||||||
|
return `${days} ${days === 1 ? "day" : "days"} ${hours} ${hours === 1 ? "hour" : "hours"}`
|
||||||
|
}
|
||||||
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
if (hours >= 1) return `${hours} ${hours === 1 ? "hour" : "hours"} ${minutes} ${minutes === 1 ? "minute" : "minutes"}`
|
||||||
|
if (minutes === 0) return "a few seconds"
|
||||||
|
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`
|
||||||
|
}
|
||||||
|
|
||||||
const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
|
const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
|
||||||
"use server"
|
"use server"
|
||||||
return json(
|
return json(
|
||||||
@@ -26,6 +74,7 @@ export function BlackSection() {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const sessionAction = useAction(createSessionUrl)
|
const sessionAction = useAction(createSessionUrl)
|
||||||
const sessionSubmission = useSubmission(createSessionUrl)
|
const sessionSubmission = useSubmission(createSessionUrl)
|
||||||
|
const subscription = createAsync(() => querySubscription(params.id!))
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
sessionRedirecting: false,
|
sessionRedirecting: false,
|
||||||
})
|
})
|
||||||
@@ -53,6 +102,32 @@ export function BlackSection() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={subscription()}>
|
||||||
|
{(sub) => (
|
||||||
|
<div data-slot="usage">
|
||||||
|
<div data-slot="usage-item">
|
||||||
|
<div data-slot="usage-header">
|
||||||
|
<span data-slot="usage-label">5-hour Usage</span>
|
||||||
|
<span data-slot="usage-value">{sub().rollingUsage.usagePercent}%</span>
|
||||||
|
</div>
|
||||||
|
<div data-slot="progress">
|
||||||
|
<div data-slot="progress-bar" style={{ width: `${sub().rollingUsage.usagePercent}%` }} />
|
||||||
|
</div>
|
||||||
|
<span data-slot="reset-time">Resets in {formatResetTime(sub().rollingUsage.resetInSec)}</span>
|
||||||
|
</div>
|
||||||
|
<div data-slot="usage-item">
|
||||||
|
<div data-slot="usage-header">
|
||||||
|
<span data-slot="usage-label">Weekly Usage</span>
|
||||||
|
<span data-slot="usage-value">{sub().weeklyUsage.usagePercent}%</span>
|
||||||
|
</div>
|
||||||
|
<div data-slot="progress">
|
||||||
|
<div data-slot="progress-bar" style={{ width: `${sub().weeklyUsage.usagePercent}%` }} />
|
||||||
|
</div>
|
||||||
|
<span data-slot="reset-time">Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
|
|||||||
import { action, json, query } from "@solidjs/router"
|
import { action, json, query } from "@solidjs/router"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||||
import { User } from "@opencode-ai/console-core/user.js"
|
|
||||||
import { and, Database, desc, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
import { and, Database, desc, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||||
@@ -96,11 +95,22 @@ export const queryBillingInfo = query(async (workspaceID: string) => {
|
|||||||
return withActor(async () => {
|
return withActor(async () => {
|
||||||
const billing = await Billing.get()
|
const billing = await Billing.get()
|
||||||
return {
|
return {
|
||||||
...billing,
|
customerID: billing.customerID,
|
||||||
|
paymentMethodID: billing.paymentMethodID,
|
||||||
|
paymentMethodType: billing.paymentMethodType,
|
||||||
|
paymentMethodLast4: billing.paymentMethodLast4,
|
||||||
|
balance: billing.balance,
|
||||||
|
reload: billing.reload,
|
||||||
reloadAmount: billing.reloadAmount ?? Billing.RELOAD_AMOUNT,
|
reloadAmount: billing.reloadAmount ?? Billing.RELOAD_AMOUNT,
|
||||||
reloadAmountMin: Billing.RELOAD_AMOUNT_MIN,
|
reloadAmountMin: Billing.RELOAD_AMOUNT_MIN,
|
||||||
reloadTrigger: billing.reloadTrigger ?? Billing.RELOAD_TRIGGER,
|
reloadTrigger: billing.reloadTrigger ?? Billing.RELOAD_TRIGGER,
|
||||||
reloadTriggerMin: Billing.RELOAD_TRIGGER_MIN,
|
reloadTriggerMin: Billing.RELOAD_TRIGGER_MIN,
|
||||||
|
monthlyLimit: billing.monthlyLimit,
|
||||||
|
monthlyUsage: billing.monthlyUsage,
|
||||||
|
timeMonthlyUsageUpdated: billing.timeMonthlyUsageUpdated,
|
||||||
|
reloadError: billing.reloadError,
|
||||||
|
timeReloadError: billing.timeReloadError,
|
||||||
|
subscriptionID: billing.subscriptionID,
|
||||||
}
|
}
|
||||||
}, workspaceID)
|
}, workspaceID)
|
||||||
}, "billing.get")
|
}, "billing.get")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js"
|
|||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||||
import { BlackData } from "@opencode-ai/console-core/black.js"
|
import { Black, BlackData } from "@opencode-ai/console-core/black.js"
|
||||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||||
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
|
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
|
||||||
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
|
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
|
||||||
@@ -81,12 +81,13 @@ export async function handler(
|
|||||||
const isTrial = await trialLimiter?.isTrial()
|
const isTrial = await trialLimiter?.isTrial()
|
||||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
|
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
|
||||||
await rateLimiter?.check()
|
await rateLimiter?.check()
|
||||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider ?? false, sessionId)
|
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||||
const stickyProvider = await stickyTracker?.get()
|
const stickyProvider = await stickyTracker?.get()
|
||||||
const authInfo = await authenticate(modelInfo)
|
const authInfo = await authenticate(modelInfo)
|
||||||
|
|
||||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||||
const providerInfo = selectProvider(
|
const providerInfo = selectProvider(
|
||||||
|
model,
|
||||||
zenData,
|
zenData,
|
||||||
authInfo,
|
authInfo,
|
||||||
modelInfo,
|
modelInfo,
|
||||||
@@ -101,7 +102,7 @@ export async function handler(
|
|||||||
logger.metric({ provider: providerInfo.id })
|
logger.metric({ provider: providerInfo.id })
|
||||||
|
|
||||||
const startTimestamp = Date.now()
|
const startTimestamp = Date.now()
|
||||||
const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream)
|
const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)
|
||||||
const reqBody = JSON.stringify(
|
const reqBody = JSON.stringify(
|
||||||
providerInfo.modifyBody({
|
providerInfo.modifyBody({
|
||||||
...createBodyConverter(opts.format, providerInfo.format)(body),
|
...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.
|
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||||
res.status !== 404 &&
|
res.status !== 404 &&
|
||||||
// ie. cannot change codex model providers mid-session
|
// ie. cannot change codex model providers mid-session
|
||||||
!modelInfo.stickyProvider &&
|
modelInfo.stickyProvider !== "strict" &&
|
||||||
modelInfo.fallbackProvider &&
|
modelInfo.fallbackProvider &&
|
||||||
providerInfo.id !== modelInfo.fallbackProvider
|
providerInfo.id !== modelInfo.fallbackProvider
|
||||||
) {
|
) {
|
||||||
@@ -194,17 +195,19 @@ export async function handler(
|
|||||||
// Handle streaming response
|
// Handle streaming response
|
||||||
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
|
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
|
||||||
const usageParser = providerInfo.createUsageParser()
|
const usageParser = providerInfo.createUsageParser()
|
||||||
|
const binaryDecoder = providerInfo.createBinaryStreamDecoder()
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
start(c) {
|
start(c) {
|
||||||
const reader = res.body?.getReader()
|
const reader = res.body?.getReader()
|
||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
const encoder = new TextEncoder()
|
const encoder = new TextEncoder()
|
||||||
|
|
||||||
let buffer = ""
|
let buffer = ""
|
||||||
let responseLength = 0
|
let responseLength = 0
|
||||||
|
|
||||||
function pump(): Promise<void> {
|
function pump(): Promise<void> {
|
||||||
return (
|
return (
|
||||||
reader?.read().then(async ({ done, value }) => {
|
reader?.read().then(async ({ done, value: rawValue }) => {
|
||||||
if (done) {
|
if (done) {
|
||||||
logger.metric({
|
logger.metric({
|
||||||
response_length: responseLength,
|
response_length: responseLength,
|
||||||
@@ -230,6 +233,10 @@ export async function handler(
|
|||||||
"timestamp.first_byte": now,
|
"timestamp.first_byte": now,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const value = binaryDecoder ? binaryDecoder(rawValue) : rawValue
|
||||||
|
if (!value) return
|
||||||
|
|
||||||
responseLength += value.length
|
responseLength += value.length
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
dataDumper?.provideStream(buffer)
|
dataDumper?.provideStream(buffer)
|
||||||
@@ -331,6 +338,7 @@ export async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectProvider(
|
function selectProvider(
|
||||||
|
reqModel: string,
|
||||||
zenData: ZenData,
|
zenData: ZenData,
|
||||||
authInfo: AuthInfo,
|
authInfo: AuthInfo,
|
||||||
modelInfo: ModelInfo,
|
modelInfo: ModelInfo,
|
||||||
@@ -339,7 +347,7 @@ export async function handler(
|
|||||||
retry: RetryOptions,
|
retry: RetryOptions,
|
||||||
stickyProvider: string | undefined,
|
stickyProvider: string | undefined,
|
||||||
) {
|
) {
|
||||||
const provider = (() => {
|
const modelProvider = (() => {
|
||||||
if (authInfo?.provider?.credentials) {
|
if (authInfo?.provider?.credentials) {
|
||||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
|
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
|
||||||
}
|
}
|
||||||
@@ -372,18 +380,19 @@ export async function handler(
|
|||||||
return providers[index || 0]
|
return providers[index || 0]
|
||||||
})()
|
})()
|
||||||
|
|
||||||
if (!provider) throw new ModelError("No provider available")
|
if (!modelProvider) throw new ModelError("No provider available")
|
||||||
if (!(provider.id in zenData.providers)) throw new ModelError(`Provider ${provider.id} not supported`)
|
if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...provider,
|
...modelProvider,
|
||||||
...zenData.providers[provider.id],
|
...zenData.providers[modelProvider.id],
|
||||||
...(() => {
|
...(() => {
|
||||||
const format = zenData.providers[provider.id].format
|
const format = zenData.providers[modelProvider.id].format
|
||||||
if (format === "anthropic") return anthropicHelper
|
const providerModel = modelProvider.model
|
||||||
if (format === "google") return googleHelper
|
if (format === "anthropic") return anthropicHelper({ reqModel, providerModel })
|
||||||
if (format === "openai") return openaiHelper
|
if (format === "google") return googleHelper({ reqModel, providerModel })
|
||||||
return oaCompatHelper
|
if (format === "openai") return openaiHelper({ reqModel, providerModel })
|
||||||
|
return oaCompatHelper({ reqModel, providerModel })
|
||||||
})(),
|
})(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,27 +504,28 @@ export async function handler(
|
|||||||
|
|
||||||
// Check weekly limit
|
// Check weekly limit
|
||||||
if (sub.fixedUsage && sub.timeFixedUpdated) {
|
if (sub.fixedUsage && sub.timeFixedUpdated) {
|
||||||
const week = getWeekBounds(now)
|
const result = Black.analyzeWeeklyUsage({
|
||||||
if (sub.timeFixedUpdated >= week.start && sub.fixedUsage >= centsToMicroCents(black.fixedLimit * 100)) {
|
usage: sub.fixedUsage,
|
||||||
const retryAfter = Math.ceil((week.end.getTime() - now.getTime()) / 1000)
|
timeUpdated: sub.timeFixedUpdated,
|
||||||
|
})
|
||||||
|
if (result.status === "rate-limited")
|
||||||
throw new SubscriptionError(
|
throw new SubscriptionError(
|
||||||
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
|
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||||
retryAfter,
|
result.resetInSec,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check rolling limit
|
// Check rolling limit
|
||||||
if (sub.rollingUsage && sub.timeRollingUpdated) {
|
if (sub.rollingUsage && sub.timeRollingUpdated) {
|
||||||
const rollingWindowMs = black.rollingWindow * 3600 * 1000
|
const result = Black.analyzeRollingUsage({
|
||||||
const windowStart = new Date(now.getTime() - rollingWindowMs)
|
usage: sub.rollingUsage,
|
||||||
if (sub.timeRollingUpdated >= windowStart && sub.rollingUsage >= centsToMicroCents(black.rollingLimit * 100)) {
|
timeUpdated: sub.timeRollingUpdated,
|
||||||
const retryAfter = Math.ceil((sub.timeRollingUpdated.getTime() + rollingWindowMs - now.getTime()) / 1000)
|
})
|
||||||
|
if (result.status === "rate-limited")
|
||||||
throw new SubscriptionError(
|
throw new SubscriptionError(
|
||||||
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
|
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||||
retryAfter,
|
result.resetInSec,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { EventStreamCodec } from "@smithy/eventstream-codec"
|
||||||
import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider"
|
import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider"
|
||||||
|
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
|
||||||
|
|
||||||
type Usage = {
|
type Usage = {
|
||||||
cache_creation?: {
|
cache_creation?: {
|
||||||
@@ -14,65 +16,169 @@ type Usage = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const anthropicHelper = {
|
export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => {
|
||||||
format: "anthropic",
|
const isBedrockModelArn = providerModel.startsWith("arn:aws:bedrock:")
|
||||||
modifyUrl: (providerApi: string) => providerApi + "/messages",
|
const isBedrockModelID = providerModel.startsWith("global.anthropic.")
|
||||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
const isBedrock = isBedrockModelArn || isBedrockModelID
|
||||||
headers.set("x-api-key", apiKey)
|
const isSonnet = reqModel.includes("sonnet")
|
||||||
headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
|
return {
|
||||||
if (body.model.startsWith("claude-sonnet-")) {
|
format: "anthropic",
|
||||||
headers.set("anthropic-beta", "context-1m-2025-08-07")
|
modifyUrl: (providerApi: string, isStream?: boolean) =>
|
||||||
}
|
isBedrock
|
||||||
},
|
? `${providerApi}/model/${isBedrockModelArn ? encodeURIComponent(providerModel) : providerModel}/${isStream ? "invoke-with-response-stream" : "invoke"}`
|
||||||
modifyBody: (body: Record<string, any>) => {
|
: providerApi + "/messages",
|
||||||
return {
|
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,
|
...body,
|
||||||
service_tier: "standard_only",
|
...(isBedrock
|
||||||
}
|
? {
|
||||||
},
|
anthropic_version: "bedrock-2023-05-31",
|
||||||
streamSeparator: "\n\n",
|
anthropic_beta: isSonnet ? "context-1m-2025-08-07" : undefined,
|
||||||
createUsageParser: () => {
|
model: undefined,
|
||||||
let usage: Usage
|
stream: undefined,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
service_tier: "standard_only",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
createBinaryStreamDecoder: () => {
|
||||||
|
if (!isBedrock) return undefined
|
||||||
|
|
||||||
return {
|
const decoder = new TextDecoder()
|
||||||
parse: (chunk: string) => {
|
const encoder = new TextEncoder()
|
||||||
const data = chunk.split("\n")[1]
|
const codec = new EventStreamCodec(toUtf8, fromUtf8)
|
||||||
if (!data.startsWith("data: ")) return
|
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
|
const messages = []
|
||||||
try {
|
while (buffer.length >= 4) {
|
||||||
json = JSON.parse(data.slice(6))
|
// first 4 bytes are the total length (big-endian)
|
||||||
} catch (e) {
|
const totalLength = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint32(0, false)
|
||||||
return
|
|
||||||
|
// 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
|
Decoded bytes
|
||||||
if (!usageUpdate) return
|
```
|
||||||
usage = {
|
{
|
||||||
...usage,
|
type: 'message_start',
|
||||||
...usageUpdate,
|
message: {
|
||||||
cache_creation: {
|
model: 'claude-opus-4-5-20251101',
|
||||||
...usage?.cache_creation,
|
id: 'msg_bdrk_0125FttFoid4ipZfxK6LnKqx',
|
||||||
...usageUpdate.cache_creation,
|
type: 'message',
|
||||||
},
|
role: 'assistant',
|
||||||
server_tool_use: {
|
content: [],
|
||||||
...usage?.server_tool_use,
|
stop_reason: null,
|
||||||
...usageUpdate.server_tool_use,
|
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,
|
*/
|
||||||
}
|
|
||||||
},
|
/* Example of Anthropic data
|
||||||
normalizeUsage: (usage: Usage) => ({
|
```
|
||||||
inputTokens: usage.input_tokens ?? 0,
|
event: message_delta
|
||||||
outputTokens: usage.output_tokens ?? 0,
|
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"}}}
|
||||||
reasoningTokens: undefined,
|
```
|
||||||
cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
|
*/
|
||||||
cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
|
if (decoded.headers[":message-type"]?.value === "event") {
|
||||||
cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
|
const data = decoder.decode(decoded.body, { stream: true })
|
||||||
}),
|
|
||||||
} satisfies ProviderHelper
|
const parsedDataResult = JSON.parse(data)
|
||||||
|
delete parsedDataResult.p
|
||||||
|
const binary = atob(parsedDataResult.bytes)
|
||||||
|
const uint8 = Uint8Array.from(binary, (c) => c.charCodeAt(0))
|
||||||
|
const bytes = decoder.decode(uint8)
|
||||||
|
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 {
|
export function fromAnthropicRequest(body: any): CommonRequest {
|
||||||
if (!body || typeof body !== "object") return body
|
if (!body || typeof body !== "object") return body
|
||||||
|
|||||||
@@ -26,16 +26,17 @@ type Usage = {
|
|||||||
thoughtsTokenCount?: number
|
thoughtsTokenCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const googleHelper = {
|
export const googleHelper: ProviderHelper = ({ providerModel }) => ({
|
||||||
format: "google",
|
format: "google",
|
||||||
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) =>
|
modifyUrl: (providerApi: string, isStream?: boolean) =>
|
||||||
`${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
|
`${providerApi}/models/${providerModel}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
|
||||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||||
headers.set("x-goog-api-key", apiKey)
|
headers.set("x-goog-api-key", apiKey)
|
||||||
},
|
},
|
||||||
modifyBody: (body: Record<string, any>) => {
|
modifyBody: (body: Record<string, any>) => {
|
||||||
return body
|
return body
|
||||||
},
|
},
|
||||||
|
createBinaryStreamDecoder: () => undefined,
|
||||||
streamSeparator: "\r\n\r\n",
|
streamSeparator: "\r\n\r\n",
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
@@ -71,4 +72,4 @@ export const googleHelper = {
|
|||||||
cacheWrite1hTokens: undefined,
|
cacheWrite1hTokens: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} satisfies ProviderHelper
|
})
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type Usage = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const oaCompatHelper = {
|
export const oaCompatHelper: ProviderHelper = () => ({
|
||||||
format: "oa-compat",
|
format: "oa-compat",
|
||||||
modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
|
modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
|
||||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||||
@@ -33,6 +33,7 @@ export const oaCompatHelper = {
|
|||||||
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
createBinaryStreamDecoder: () => undefined,
|
||||||
streamSeparator: "\n\n",
|
streamSeparator: "\n\n",
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
@@ -68,7 +69,7 @@ export const oaCompatHelper = {
|
|||||||
cacheWrite1hTokens: undefined,
|
cacheWrite1hTokens: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} satisfies ProviderHelper
|
})
|
||||||
|
|
||||||
export function fromOaCompatibleRequest(body: any): CommonRequest {
|
export function fromOaCompatibleRequest(body: any): CommonRequest {
|
||||||
if (!body || typeof body !== "object") return body
|
if (!body || typeof body !== "object") return body
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type Usage = {
|
|||||||
total_tokens?: number
|
total_tokens?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openaiHelper = {
|
export const openaiHelper: ProviderHelper = () => ({
|
||||||
format: "openai",
|
format: "openai",
|
||||||
modifyUrl: (providerApi: string) => providerApi + "/responses",
|
modifyUrl: (providerApi: string) => providerApi + "/responses",
|
||||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||||
@@ -21,6 +21,7 @@ export const openaiHelper = {
|
|||||||
modifyBody: (body: Record<string, any>) => {
|
modifyBody: (body: Record<string, any>) => {
|
||||||
return body
|
return body
|
||||||
},
|
},
|
||||||
|
createBinaryStreamDecoder: () => undefined,
|
||||||
streamSeparator: "\n\n",
|
streamSeparator: "\n\n",
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
@@ -58,7 +59,7 @@ export const openaiHelper = {
|
|||||||
cacheWrite1hTokens: undefined,
|
cacheWrite1hTokens: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} satisfies ProviderHelper
|
})
|
||||||
|
|
||||||
export function fromOpenaiRequest(body: any): CommonRequest {
|
export function fromOpenaiRequest(body: any): CommonRequest {
|
||||||
if (!body || typeof body !== "object") return body
|
if (!body || typeof body !== "object") return body
|
||||||
|
|||||||
@@ -33,11 +33,12 @@ export type UsageInfo = {
|
|||||||
cacheWrite1hTokens?: number
|
cacheWrite1hTokens?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProviderHelper = {
|
export type ProviderHelper = (input: { reqModel: string; providerModel: string }) => {
|
||||||
format: ZenData.Format
|
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
|
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
|
||||||
modifyBody: (body: Record<string, any>) => Record<string, any>
|
modifyBody: (body: Record<string, any>) => Record<string, any>
|
||||||
|
createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined
|
||||||
streamSeparator: string
|
streamSeparator: string
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
parse: (chunk: string) => void
|
parse: (chunk: string) => void
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Resource } from "@opencode-ai/console-resource"
|
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 (!stickyProvider) return
|
||||||
if (!session) return
|
if (!session) return
|
||||||
const key = `sticky:${session}`
|
const key = `sticky:${session}`
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client", "@webgpu/types"],
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `billing` ADD `subscription` json;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `billing` DROP COLUMN `subscription_coupon_id`;
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -372,6 +372,20 @@
|
|||||||
"when": 1768343920467,
|
"when": 1768343920467,
|
||||||
"tag": "0052_aromatic_agent_zero",
|
"tag": "0052_aromatic_agent_zero",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 53,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1768599366758,
|
||||||
|
"tag": "0053_gigantic_hardball",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 54,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1768603665356,
|
||||||
|
"tag": "0054_numerous_annihilus",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
"promote-models-to-dev": "script/promote-models.ts dev",
|
"promote-models-to-dev": "script/promote-models.ts dev",
|
||||||
"promote-models-to-prod": "script/promote-models.ts production",
|
"promote-models-to-prod": "script/promote-models.ts production",
|
||||||
"pull-models-from-dev": "script/pull-models.ts dev",
|
"pull-models-from-dev": "script/pull-models.ts dev",
|
||||||
|
"pull-models-from-prod": "script/pull-models.ts production",
|
||||||
"update-black": "script/update-black.ts",
|
"update-black": "script/update-black.ts",
|
||||||
"promote-black-to-dev": "script/promote-black.ts dev",
|
"promote-black-to-dev": "script/promote-black.ts dev",
|
||||||
"promote-black-to-prod": "script/promote-black.ts production",
|
"promote-black-to-prod": "script/promote-black.ts production",
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { subscribe } from "diagnostics_channel"
|
||||||
|
import { Billing } from "../src/billing.js"
|
||||||
|
import { and, Database, eq } from "../src/drizzle/index.js"
|
||||||
|
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
|
||||||
|
|
||||||
|
const workspaceID = process.argv[2]
|
||||||
|
|
||||||
|
if (!workspaceID) {
|
||||||
|
console.error("Usage: bun script/foo.ts <workspaceID>")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Removing from Black waitlist`)
|
||||||
|
|
||||||
|
const billing = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
subscriptionPlan: BillingTable.subscriptionPlan,
|
||||||
|
timeSubscriptionBooked: BillingTable.timeSubscriptionBooked,
|
||||||
|
})
|
||||||
|
.from(BillingTable)
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!billing?.timeSubscriptionBooked) {
|
||||||
|
console.error(`Error: Workspace is not on the waitlist`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.update(BillingTable)
|
||||||
|
.set({
|
||||||
|
subscriptionPlan: null,
|
||||||
|
timeSubscriptionBooked: null,
|
||||||
|
})
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID)),
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(`Done`)
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { Billing } from "../src/billing.js"
|
||||||
|
import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js"
|
||||||
|
import { UserTable } from "../src/schema/user.sql.js"
|
||||||
|
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
|
||||||
|
import { Identifier } from "../src/identifier.js"
|
||||||
|
import { centsToMicroCents } from "../src/util/price.js"
|
||||||
|
import { AuthTable } from "../src/schema/auth.sql.js"
|
||||||
|
|
||||||
|
const plan = "200"
|
||||||
|
const workspaceID = process.argv[2]
|
||||||
|
const seats = parseInt(process.argv[3])
|
||||||
|
|
||||||
|
console.log(`Gifting ${seats} seats of Black to workspace ${workspaceID}`)
|
||||||
|
|
||||||
|
if (!workspaceID || !seats) throw new Error("Usage: bun foo.ts <workspaceID> <seats>")
|
||||||
|
|
||||||
|
// Get workspace user
|
||||||
|
const users = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
id: UserTable.id,
|
||||||
|
role: UserTable.role,
|
||||||
|
email: AuthTable.subject,
|
||||||
|
})
|
||||||
|
.from(UserTable)
|
||||||
|
.leftJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email")))
|
||||||
|
.where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
|
||||||
|
)
|
||||||
|
if (users.length === 0) throw new Error(`Error: No users found in workspace ${workspaceID}`)
|
||||||
|
if (users.length !== seats)
|
||||||
|
throw new Error(`Error: Workspace ${workspaceID} has ${users.length} users, expected ${seats}`)
|
||||||
|
const adminUser = users.find((user) => user.role === "admin")
|
||||||
|
if (!adminUser) throw new Error(`Error: No admin user found in workspace ${workspaceID}`)
|
||||||
|
if (!adminUser.email) throw new Error(`Error: Admin user ${adminUser.id} has no email`)
|
||||||
|
|
||||||
|
// Get 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) throw new Error(`Error: Workspace ${workspaceID} has no billing record`)
|
||||||
|
if (billing.subscriptionID) throw new Error(`Error: Workspace ${workspaceID} already has a subscription`)
|
||||||
|
|
||||||
|
// Look up the Stripe customer by email
|
||||||
|
const customerID =
|
||||||
|
billing.customerID ??
|
||||||
|
(await (() =>
|
||||||
|
Billing.stripe()
|
||||||
|
.customers.create({
|
||||||
|
email: adminUser.email,
|
||||||
|
metadata: {
|
||||||
|
workspaceID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((customer) => customer.id))())
|
||||||
|
console.log(`Customer ID: ${customerID}`)
|
||||||
|
|
||||||
|
const couponID = "JAIr0Pe1"
|
||||||
|
const subscription = await Billing.stripe().subscriptions.create({
|
||||||
|
customer: customerID!,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
price: `price_1SmfyI2StuRr0lbXovxJNeZn`,
|
||||||
|
discounts: [{ coupon: couponID }],
|
||||||
|
quantity: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
console.log(`Subscription ID: ${subscription.id}`)
|
||||||
|
|
||||||
|
await Database.transaction(async (tx) => {
|
||||||
|
// Set customer id, subscription id, and payment method on workspace billing
|
||||||
|
await tx
|
||||||
|
.update(BillingTable)
|
||||||
|
.set({
|
||||||
|
customerID,
|
||||||
|
subscriptionID: subscription.id,
|
||||||
|
subscription: { status: "subscribed", coupon: couponID, seats, plan },
|
||||||
|
})
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
|
||||||
|
// Create a row in subscription table
|
||||||
|
for (const user of users) {
|
||||||
|
await tx.insert(SubscriptionTable).values({
|
||||||
|
workspaceID,
|
||||||
|
id: Identifier.create("subscription"),
|
||||||
|
userID: user.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// // Create a row in payments table
|
||||||
|
// await tx.insert(PaymentTable).values({
|
||||||
|
// workspaceID,
|
||||||
|
// id: Identifier.create("payment"),
|
||||||
|
// amount: centsToMicroCents(amountInCents),
|
||||||
|
// customerID,
|
||||||
|
// invoiceID,
|
||||||
|
// paymentID,
|
||||||
|
// enrichment: {
|
||||||
|
// type: "subscription",
|
||||||
|
// couponID,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`done`)
|
||||||
+8
-3
@@ -12,7 +12,7 @@ const email = process.argv[3]
|
|||||||
console.log(`Onboarding workspace ${workspaceID} for email ${email}`)
|
console.log(`Onboarding workspace ${workspaceID} for email ${email}`)
|
||||||
|
|
||||||
if (!workspaceID || !email) {
|
if (!workspaceID || !email) {
|
||||||
console.error("Usage: bun onboard-zen-black.ts <workspaceID> <email>")
|
console.error("Usage: bun foo.ts <workspaceID> <email>")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ const existingSubscription = await Database.use((tx) =>
|
|||||||
tx
|
tx
|
||||||
.select({ workspaceID: BillingTable.workspaceID })
|
.select({ workspaceID: BillingTable.workspaceID })
|
||||||
.from(BillingTable)
|
.from(BillingTable)
|
||||||
.where(eq(BillingTable.subscriptionID, subscriptionID))
|
.where(sql`JSON_EXTRACT(${BillingTable.subscription}, '$.id') = ${subscriptionID}`)
|
||||||
.then((rows) => rows[0]),
|
.then((rows) => rows[0]),
|
||||||
)
|
)
|
||||||
if (existingSubscription) {
|
if (existingSubscription) {
|
||||||
@@ -128,10 +128,15 @@ await Database.transaction(async (tx) => {
|
|||||||
.set({
|
.set({
|
||||||
customerID,
|
customerID,
|
||||||
subscriptionID,
|
subscriptionID,
|
||||||
subscriptionCouponID: couponID,
|
|
||||||
paymentMethodID,
|
paymentMethodID,
|
||||||
paymentMethodLast4,
|
paymentMethodLast4,
|
||||||
paymentMethodType,
|
paymentMethodType,
|
||||||
|
subscription: {
|
||||||
|
status: "subscribed",
|
||||||
|
coupon: couponID,
|
||||||
|
seats: 1,
|
||||||
|
plan: "200",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
subscription: BillingTable.subscription,
|
||||||
|
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,
|
||||||
|
subscription: 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,
|
||||||
|
subscription: fromBilling.subscription,
|
||||||
|
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`)
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Billing } from "../src/billing.js"
|
import { Billing } from "../src/billing.js"
|
||||||
|
import { Database, eq } from "../src/drizzle/index.js"
|
||||||
|
import { WorkspaceTable } from "../src/schema/workspace.sql.js"
|
||||||
|
|
||||||
// get input from command line
|
// get input from command line
|
||||||
const workspaceID = process.argv[2]
|
const workspaceID = process.argv[2]
|
||||||
@@ -9,6 +11,19 @@ if (!workspaceID || !dollarAmount) {
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check workspace exists
|
||||||
|
const workspace = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(WorkspaceTable)
|
||||||
|
.where(eq(WorkspaceTable.id, workspaceID))
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
)
|
||||||
|
if (!workspace) {
|
||||||
|
console.error("Error: Workspace not found")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
const amountInDollars = parseFloat(dollarAmount)
|
const amountInDollars = parseFloat(dollarAmount)
|
||||||
if (isNaN(amountInDollars) || amountInDollars <= 0) {
|
if (isNaN(amountInDollars) || amountInDollars <= 0) {
|
||||||
console.error("Error: dollarAmount must be a positive number")
|
console.error("Error: dollarAmount must be a positive number")
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ if (identifier.startsWith("wrk_")) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get all payments for these workspaces
|
for (const user of users) {
|
||||||
await Promise.all(users.map((u: { workspaceID: string }) => printWorkspace(u.workspaceID)))
|
await printWorkspace(user.workspaceID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function printWorkspace(workspaceID: string) {
|
async function printWorkspace(workspaceID: string) {
|
||||||
@@ -113,8 +114,13 @@ async function printWorkspace(workspaceID: string) {
|
|||||||
.select({
|
.select({
|
||||||
balance: BillingTable.balance,
|
balance: BillingTable.balance,
|
||||||
customerID: BillingTable.customerID,
|
customerID: BillingTable.customerID,
|
||||||
|
reload: BillingTable.reload,
|
||||||
subscriptionID: BillingTable.subscriptionID,
|
subscriptionID: BillingTable.subscriptionID,
|
||||||
subscriptionCouponID: BillingTable.subscriptionCouponID,
|
subscription: {
|
||||||
|
plan: BillingTable.subscriptionPlan,
|
||||||
|
booked: BillingTable.timeSubscriptionBooked,
|
||||||
|
enrichment: BillingTable.subscription,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.from(BillingTable)
|
.from(BillingTable)
|
||||||
.where(eq(BillingTable.workspaceID, workspace.id))
|
.where(eq(BillingTable.workspaceID, workspace.id))
|
||||||
@@ -123,6 +129,16 @@ async function printWorkspace(workspaceID: string) {
|
|||||||
rows.map((row) => ({
|
rows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
balance: `$${(row.balance / 100000000).toFixed(2)}`,
|
balance: `$${(row.balance / 100000000).toFixed(2)}`,
|
||||||
|
subscription: row.subscriptionID
|
||||||
|
? [
|
||||||
|
`Black ${row.subscription.enrichment!.plan}`,
|
||||||
|
row.subscription.enrichment!.seats > 1 ? `X ${row.subscription.enrichment!.seats} seats` : "",
|
||||||
|
row.subscription.enrichment!.coupon ? `(coupon: ${row.subscription.enrichment!.coupon})` : "",
|
||||||
|
`(ref: ${row.subscriptionID})`,
|
||||||
|
].join(" ")
|
||||||
|
: row.subscription.booked
|
||||||
|
? `Waitlist ${row.subscription.plan} plan`
|
||||||
|
: undefined,
|
||||||
}))[0],
|
}))[0],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -133,6 +149,7 @@ async function printWorkspace(workspaceID: string) {
|
|||||||
amount: PaymentTable.amount,
|
amount: PaymentTable.amount,
|
||||||
paymentID: PaymentTable.paymentID,
|
paymentID: PaymentTable.paymentID,
|
||||||
invoiceID: PaymentTable.invoiceID,
|
invoiceID: PaymentTable.invoiceID,
|
||||||
|
customerID: PaymentTable.customerID,
|
||||||
timeCreated: PaymentTable.timeCreated,
|
timeCreated: PaymentTable.timeCreated,
|
||||||
timeRefunded: PaymentTable.timeRefunded,
|
timeRefunded: PaymentTable.timeRefunded,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,33 +8,25 @@ const stage = process.argv[2]
|
|||||||
if (!stage) throw new Error("Stage is required")
|
if (!stage) throw new Error("Stage is required")
|
||||||
|
|
||||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||||
|
const PARTS = 8
|
||||||
|
|
||||||
// read the secret
|
// read the secret
|
||||||
const ret = await $`bun sst secret list`.cwd(root).text()
|
const ret = await $`bun sst secret list`.cwd(root).text()
|
||||||
const lines = ret.split("\n")
|
const lines = ret.split("\n")
|
||||||
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
const values = Array.from({ length: PARTS }, (_, i) => {
|
||||||
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
const value = lines
|
||||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
|
||||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
?.split("=")
|
||||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
.slice(1)
|
||||||
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
.join("=")
|
||||||
const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
|
||||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
return value
|
||||||
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")
|
|
||||||
|
|
||||||
// validate value
|
// validate value
|
||||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
|
ZenData.validate(JSON.parse(values.join("")))
|
||||||
|
|
||||||
// update the secret
|
// update the secret
|
||||||
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
|
for (let i = 0; i < PARTS; i++) {
|
||||||
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
|
await $`bun sst secret set ZEN_MODELS${i + 1} --stage ${stage} -- ${values[i]}`
|
||||||
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}`
|
|
||||||
|
|||||||
@@ -8,32 +8,25 @@ const stage = process.argv[2]
|
|||||||
if (!stage) throw new Error("Stage is required")
|
if (!stage) throw new Error("Stage is required")
|
||||||
|
|
||||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||||
|
const PARTS = 8
|
||||||
|
|
||||||
// read the secret
|
// read the secret
|
||||||
const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()
|
const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()
|
||||||
const lines = ret.split("\n")
|
const lines = ret.split("\n")
|
||||||
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
const values = Array.from({ length: PARTS }, (_, i) => {
|
||||||
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
const value = lines
|
||||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
|
||||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
?.split("=")
|
||||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
.slice(1)
|
||||||
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
.join("=")
|
||||||
const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
|
||||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
return value
|
||||||
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")
|
|
||||||
// validate value
|
// validate value
|
||||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
|
ZenData.validate(JSON.parse(values.join("")))
|
||||||
|
|
||||||
// update the secret
|
// update the secret
|
||||||
await $`bun sst secret set ZEN_MODELS1 ${value1}`
|
for (let i = 0; i < PARTS; i++) {
|
||||||
await $`bun sst secret set ZEN_MODELS2 ${value2}`
|
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${values[i]}`
|
||||||
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}`
|
|
||||||
|
|||||||
@@ -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 root = path.resolve(process.cwd(), "..", "..", "..")
|
||||||
const models = await $`bun sst secret list`.cwd(root).text()
|
const models = await $`bun sst secret list`.cwd(root).text()
|
||||||
|
const PARTS = 8
|
||||||
|
|
||||||
// read the line starting with "ZEN_MODELS"
|
// read the line starting with "ZEN_MODELS"
|
||||||
const lines = models.split("\n")
|
const lines = models.split("\n")
|
||||||
const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
const oldValues = Array.from({ length: PARTS }, (_, i) => {
|
||||||
const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
const value = lines
|
||||||
const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
|
||||||
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
?.split("=")
|
||||||
const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
.slice(1)
|
||||||
const oldValue6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
.join("=")
|
||||||
const oldValue7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
|
||||||
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
|
return value
|
||||||
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")
|
|
||||||
|
|
||||||
// store the prettified json to a temp file
|
// store the prettified json to a temp file
|
||||||
const filename = `models-${Date.now()}.json`
|
const filename = `models-${Date.now()}.json`
|
||||||
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
||||||
await tempFile.write(
|
await tempFile.write(JSON.stringify(JSON.parse(oldValues.join("")), null, 2))
|
||||||
JSON.stringify(
|
|
||||||
JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5 + oldValue6 + oldValue7),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
console.log("tempFile", tempFile.name)
|
console.log("tempFile", tempFile.name)
|
||||||
|
|
||||||
// open temp file in vim and read the file on close
|
// 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))
|
ZenData.validate(JSON.parse(newValue))
|
||||||
|
|
||||||
// update the secret
|
// update the secret
|
||||||
const chunk = Math.ceil(newValue.length / 7)
|
const chunk = Math.ceil(newValue.length / PARTS)
|
||||||
const newValue1 = newValue.slice(0, chunk)
|
const newValues = Array.from({ length: PARTS }, (_, i) =>
|
||||||
const newValue2 = newValue.slice(chunk, chunk * 2)
|
newValue.slice(chunk * i, i === PARTS - 1 ? undefined : chunk * (i + 1)),
|
||||||
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)
|
|
||||||
|
|
||||||
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
|
for (let i = 0; i < PARTS; i++) {
|
||||||
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
|
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${newValues[i]}`
|
||||||
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}`
|
|
||||||
|
|||||||
@@ -25,22 +25,7 @@ export namespace Billing {
|
|||||||
export const get = async () => {
|
export const get = async () => {
|
||||||
return Database.use(async (tx) =>
|
return Database.use(async (tx) =>
|
||||||
tx
|
tx
|
||||||
.select({
|
.select()
|
||||||
customerID: BillingTable.customerID,
|
|
||||||
subscriptionID: BillingTable.subscriptionID,
|
|
||||||
paymentMethodID: BillingTable.paymentMethodID,
|
|
||||||
paymentMethodType: BillingTable.paymentMethodType,
|
|
||||||
paymentMethodLast4: BillingTable.paymentMethodLast4,
|
|
||||||
balance: BillingTable.balance,
|
|
||||||
reload: BillingTable.reload,
|
|
||||||
reloadAmount: BillingTable.reloadAmount,
|
|
||||||
reloadTrigger: BillingTable.reloadTrigger,
|
|
||||||
monthlyLimit: BillingTable.monthlyLimit,
|
|
||||||
monthlyUsage: BillingTable.monthlyUsage,
|
|
||||||
timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
|
|
||||||
reloadError: BillingTable.reloadError,
|
|
||||||
timeReloadError: BillingTable.timeReloadError,
|
|
||||||
})
|
|
||||||
.from(BillingTable)
|
.from(BillingTable)
|
||||||
.where(eq(BillingTable.workspaceID, Actor.workspace()))
|
.where(eq(BillingTable.workspaceID, Actor.workspace()))
|
||||||
.then((r) => r[0]),
|
.then((r) => r[0]),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { fn } from "./util/fn"
|
import { fn } from "./util/fn"
|
||||||
import { Resource } from "@opencode-ai/console-resource"
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
|
import { centsToMicroCents } from "./util/price"
|
||||||
|
import { getWeekBounds } from "./util/date"
|
||||||
|
|
||||||
export namespace BlackData {
|
export namespace BlackData {
|
||||||
const Schema = z.object({
|
const Schema = z.object({
|
||||||
@@ -18,3 +20,73 @@ export namespace BlackData {
|
|||||||
return Schema.parse(json)
|
return Schema.parse(json)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace Black {
|
||||||
|
export const analyzeRollingUsage = fn(
|
||||||
|
z.object({
|
||||||
|
usage: z.number().int(),
|
||||||
|
timeUpdated: z.date(),
|
||||||
|
}),
|
||||||
|
({ usage, timeUpdated }) => {
|
||||||
|
const now = new Date()
|
||||||
|
const black = BlackData.get()
|
||||||
|
const rollingWindowMs = black.rollingWindow * 3600 * 1000
|
||||||
|
const rollingLimitInMicroCents = centsToMicroCents(black.rollingLimit * 100)
|
||||||
|
const windowStart = new Date(now.getTime() - rollingWindowMs)
|
||||||
|
if (timeUpdated < windowStart) {
|
||||||
|
return {
|
||||||
|
status: "ok" as const,
|
||||||
|
resetInSec: black.rollingWindow * 3600,
|
||||||
|
usagePercent: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowEnd = new Date(timeUpdated.getTime() + rollingWindowMs)
|
||||||
|
if (usage < rollingLimitInMicroCents) {
|
||||||
|
return {
|
||||||
|
status: "ok" as const,
|
||||||
|
resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000),
|
||||||
|
usagePercent: Math.ceil(Math.min(100, (usage / rollingLimitInMicroCents) * 100)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: "rate-limited" as const,
|
||||||
|
resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000),
|
||||||
|
usagePercent: 100,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const analyzeWeeklyUsage = fn(
|
||||||
|
z.object({
|
||||||
|
usage: z.number().int(),
|
||||||
|
timeUpdated: z.date(),
|
||||||
|
}),
|
||||||
|
({ usage, timeUpdated }) => {
|
||||||
|
const black = BlackData.get()
|
||||||
|
const now = new Date()
|
||||||
|
const week = getWeekBounds(now)
|
||||||
|
const fixedLimitInMicroCents = centsToMicroCents(black.fixedLimit * 100)
|
||||||
|
if (timeUpdated < week.start) {
|
||||||
|
return {
|
||||||
|
status: "ok" as const,
|
||||||
|
resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
|
||||||
|
usagePercent: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (usage < fixedLimitInMicroCents) {
|
||||||
|
return {
|
||||||
|
status: "ok" as const,
|
||||||
|
resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
|
||||||
|
usagePercent: Math.ceil(Math.min(100, (usage / fixedLimitInMicroCents) * 100)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "rate-limited" as const,
|
||||||
|
resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
|
||||||
|
usagePercent: 100,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export namespace ZenData {
|
|||||||
cost200K: ModelCostSchema.optional(),
|
cost200K: ModelCostSchema.optional(),
|
||||||
allowAnonymous: z.boolean().optional(),
|
allowAnonymous: z.boolean().optional(),
|
||||||
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
|
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
|
||||||
stickyProvider: z.boolean().optional(),
|
stickyProvider: z.enum(["strict", "prefer"]).optional(),
|
||||||
trial: TrialSchema.optional(),
|
trial: TrialSchema.optional(),
|
||||||
rateLimit: z.number().optional(),
|
rateLimit: z.number().optional(),
|
||||||
fallbackProvider: z.string().optional(),
|
fallbackProvider: z.string().optional(),
|
||||||
@@ -74,7 +74,8 @@ export namespace ZenData {
|
|||||||
Resource.ZEN_MODELS4.value +
|
Resource.ZEN_MODELS4.value +
|
||||||
Resource.ZEN_MODELS5.value +
|
Resource.ZEN_MODELS5.value +
|
||||||
Resource.ZEN_MODELS6.value +
|
Resource.ZEN_MODELS6.value +
|
||||||
Resource.ZEN_MODELS7.value,
|
Resource.ZEN_MODELS7.value +
|
||||||
|
Resource.ZEN_MODELS8.value,
|
||||||
)
|
)
|
||||||
return ModelsSchema.parse(json)
|
return ModelsSchema.parse(json)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,8 +21,13 @@ export const BillingTable = mysqlTable(
|
|||||||
reloadError: varchar("reload_error", { length: 255 }),
|
reloadError: varchar("reload_error", { length: 255 }),
|
||||||
timeReloadError: utc("time_reload_error"),
|
timeReloadError: utc("time_reload_error"),
|
||||||
timeReloadLockedTill: utc("time_reload_locked_till"),
|
timeReloadLockedTill: utc("time_reload_locked_till"),
|
||||||
|
subscription: json("subscription").$type<{
|
||||||
|
status: "subscribed"
|
||||||
|
coupon?: string
|
||||||
|
seats: number
|
||||||
|
plan: "20" | "100" | "200"
|
||||||
|
}>(),
|
||||||
subscriptionID: varchar("subscription_id", { length: 28 }),
|
subscriptionID: varchar("subscription_id", { length: 28 }),
|
||||||
subscriptionCouponID: varchar("subscription_coupon_id", { length: 28 }),
|
|
||||||
subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const),
|
subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const),
|
||||||
timeSubscriptionBooked: utc("time_subscription_booked"),
|
timeSubscriptionBooked: utc("time_subscription_booked"),
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
+4
@@ -134,6 +134,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_MODELS8": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"ZEN_SESSION_SECRET": {
|
"ZEN_SESSION_SECRET": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+4
@@ -134,6 +134,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_MODELS8": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"ZEN_SESSION_SECRET": {
|
"ZEN_SESSION_SECRET": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
+4
@@ -134,6 +134,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_MODELS8": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"ZEN_SESSION_SECRET": {
|
"ZEN_SESSION_SECRET": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<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>
|
<script src="/src/index.tsx" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ semver = "1.0.27"
|
|||||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||||
uuid = { version = "1.19.0", features = ["v4"] }
|
uuid = { version = "1.19.0", features = ["v4"] }
|
||||||
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
gtk = "0.18.2"
|
gtk = "0.18.2"
|
||||||
webkit2gtk = "=2.0.1"
|
webkit2gtk = "=2.0.1"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-set-theme",
|
||||||
"core:webview:allow-set-webview-zoom",
|
"core:webview:allow-set-webview-zoom",
|
||||||
"core:window:allow-is-focused",
|
"core:window:allow-is-focused",
|
||||||
"core:window:allow-show",
|
"core:window:allow-show",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use std::{
|
|||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
time::{Duration, Instant},
|
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_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
|
||||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||||
use tauri_plugin_store::StoreExt;
|
use tauri_plugin_store::StoreExt;
|
||||||
@@ -223,7 +223,7 @@ async fn check_server_health(url: &str, password: Option<&str>) -> bool {
|
|||||||
pub fn run() {
|
pub fn run() {
|
||||||
let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
|
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")
|
let _ = std::process::Command::new("killall")
|
||||||
.arg("opencode-cli")
|
.arg("opencode-cli")
|
||||||
.output();
|
.output();
|
||||||
@@ -237,7 +237,14 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.plugin(tauri_plugin_os::init())
|
.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_store::Builder::new().build())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
@@ -268,29 +275,30 @@ pub fn run() {
|
|||||||
.map(|m| m.size().to_logical(m.scale_factor()))
|
.map(|m| m.size().to_logical(m.scale_factor()))
|
||||||
.unwrap_or(LogicalSize::new(1920, 1080));
|
.unwrap_or(LogicalSize::new(1920, 1080));
|
||||||
|
|
||||||
#[allow(unused_mut)]
|
let config = app
|
||||||
let mut window_builder =
|
.config()
|
||||||
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
|
.app
|
||||||
.title("OpenCode")
|
.windows
|
||||||
.inner_size(size.width as f64, size.height as f64)
|
.iter()
|
||||||
.decorations(true)
|
.find(|w| w.label == "main")
|
||||||
.zoom_hotkeys_enabled(true)
|
.expect("main window config missing");
|
||||||
.disable_drag_drop_handler()
|
|
||||||
.initialization_script(format!(
|
let window_builder = WebviewWindowBuilder::from_config(&app, config)
|
||||||
r#"
|
.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__ ??= {{}};
|
||||||
window.__OPENCODE__.updaterEnabled = {updater_enabled};
|
window.__OPENCODE__.updaterEnabled = {updater_enabled};
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
let window_builder = window_builder
|
||||||
window_builder = window_builder
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
.hidden_title(true);
|
||||||
.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();
|
let (tx, rx) = oneshot::channel();
|
||||||
app.manage(ServerState::new(None, rx));
|
app.manage(ServerState::new(None, rx));
|
||||||
|
|||||||
@@ -11,6 +11,20 @@
|
|||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
},
|
},
|
||||||
"app": {
|
"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,
|
"withGlobalTauri": true,
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": null
|
||||||
|
|||||||
@@ -2,6 +2,27 @@
|
|||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "OpenCode",
|
"productName": "OpenCode",
|
||||||
"identifier": "ai.opencode.desktop",
|
"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": {
|
"bundle": {
|
||||||
"createUpdaterArtifacts": true,
|
"createUpdaterArtifacts": true,
|
||||||
"icon": [
|
"icon": [
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { AsyncStorage } from "@solid-primitives/storage"
|
|||||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
|
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
|
||||||
import { Store } from "@tauri-apps/plugin-store"
|
import { Store } from "@tauri-apps/plugin-store"
|
||||||
import { Logo } from "@opencode-ai/ui/logo"
|
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 { UPDATER_ENABLED } from "./updater"
|
||||||
import { createMenu } from "./menu"
|
import { createMenu } from "./menu"
|
||||||
@@ -26,10 +26,27 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isWindows = ostype() === "windows"
|
||||||
|
if (isWindows) {
|
||||||
|
const originalGetComputedStyle = window.getComputedStyle
|
||||||
|
window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => {
|
||||||
|
if (!(elt instanceof Element)) {
|
||||||
|
// WebView2 can call into Floating UI with non-elements; fall back to a safe element.
|
||||||
|
return originalGetComputedStyle(document.documentElement, pseudoElt ?? undefined)
|
||||||
|
}
|
||||||
|
return originalGetComputedStyle(elt, pseudoElt ?? undefined)
|
||||||
|
}) as typeof window.getComputedStyle
|
||||||
|
}
|
||||||
|
|
||||||
let update: Update | null = null
|
let update: Update | null = null
|
||||||
|
|
||||||
const createPlatform = (password: Accessor<string | null>): Platform => ({
|
const createPlatform = (password: Accessor<string | null>): Platform => ({
|
||||||
platform: "desktop",
|
platform: "desktop",
|
||||||
|
os: (() => {
|
||||||
|
const type = ostype()
|
||||||
|
if (type === "macos" || type === "windows" || type === "linux") return type
|
||||||
|
return undefined
|
||||||
|
})(),
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
|
|
||||||
async openDirectoryPickerDialog(opts) {
|
async openDirectoryPickerDialog(opts) {
|
||||||
@@ -296,12 +313,24 @@ render(() => {
|
|||||||
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
|
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
|
||||||
const platform = createPlatform(() => serverPassword())
|
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 (
|
return (
|
||||||
<PlatformProvider value={platform}>
|
<PlatformProvider value={platform}>
|
||||||
<AppBaseProviders>
|
<AppBaseProviders>
|
||||||
{ostype() === "macos" && (
|
|
||||||
<div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
|
|
||||||
)}
|
|
||||||
<ServerGate>
|
<ServerGate>
|
||||||
{(data) => {
|
{(data) => {
|
||||||
setServerPassword(data().password)
|
setServerPassword(data().password)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
Vendored
+4
@@ -134,6 +134,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_MODELS8": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"ZEN_SESSION_SECRET": {
|
"ZEN_SESSION_SECRET": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.1.20"
|
version = "1.1.25"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.20/opencode-darwin-arm64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.20/opencode-darwin-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.20/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.20/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.20/opencode-windows-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
Vendored
+4
@@ -134,6 +134,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_MODELS8": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"ZEN_SESSION_SECRET": {
|
"ZEN_SESSION_SECRET": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.1.20",
|
"version": "1.1.25",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -82,8 +82,8 @@
|
|||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||||
"@opentui/core": "0.1.72",
|
"@opentui/core": "0.1.74",
|
||||||
"@opentui/solid": "0.1.72",
|
"@opentui/solid": "0.1.74",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
|||||||
@@ -731,6 +731,9 @@ export namespace ACP {
|
|||||||
const defaultAgentName = await AgentModule.defaultAgent()
|
const defaultAgentName = await AgentModule.defaultAgent()
|
||||||
const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id
|
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> = {}
|
const mcpServers: Record<string, Config.Mcp> = {}
|
||||||
for (const server of params.mcpServers) {
|
for (const server of params.mcpServers) {
|
||||||
if ("type" in server) {
|
if ("type" in server) {
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ export namespace Agent {
|
|||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
"*": "deny",
|
"*": "deny",
|
||||||
".opencode/plans/*.md": "allow",
|
[path.join(".opencode", "plans", "*.md")]: "allow",
|
||||||
[path.relative(Instance.worktree, path.join(Global.Path.data, "plans/*.md"))]: "allow",
|
[path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
user,
|
user,
|
||||||
@@ -255,7 +255,20 @@ export namespace Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function defaultAgent() {
|
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 } }) {
|
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 { Global } from "../global"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { Filesystem } from "../util/filesystem"
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
import { readableStreamToText } from "bun"
|
import { readableStreamToText } from "bun"
|
||||||
import { createRequire } from "module"
|
import { createRequire } from "module"
|
||||||
@@ -71,7 +72,10 @@ export namespace BunProc {
|
|||||||
await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
|
await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
|
||||||
return result
|
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 = !!(
|
const proxied = !!(
|
||||||
process.env.HTTP_PROXY ||
|
process.env.HTTP_PROXY ||
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user