mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-09 18:34:21 +00:00
Compare commits
5 Commits
scheduler-
...
plan-mode
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6560f40073 | ||
|
|
69795efdcd | ||
|
|
a5da5aabf5 | ||
|
|
758e3465b9 | ||
|
|
caa0a28821 |
6
.github/workflows/nix-desktop.yml
vendored
6
.github/workflows/nix-desktop.yml
vendored
@@ -9,7 +9,6 @@ on:
|
||||
- "nix/**"
|
||||
- "packages/app/**"
|
||||
- "packages/desktop/**"
|
||||
- ".github/workflows/nix-desktop.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "flake.nix"
|
||||
@@ -17,7 +16,6 @@ on:
|
||||
- "nix/**"
|
||||
- "packages/app/**"
|
||||
- "packages/desktop/**"
|
||||
- ".github/workflows/nix-desktop.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -27,8 +25,6 @@ jobs:
|
||||
matrix:
|
||||
os:
|
||||
- blacksmith-4vcpu-ubuntu-2404
|
||||
- blacksmith-4vcpu-ubuntu-2404-arm
|
||||
- macos-15-intel
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
@@ -37,7 +33,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Nix
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
uses: DeterminateSystems/nix-installer-action@v21
|
||||
|
||||
- name: Build desktop via flake
|
||||
run: |
|
||||
|
||||
251
.github/workflows/update-nix-hashes.yml
vendored
251
.github/workflows/update-nix-hashes.yml
vendored
@@ -10,24 +10,22 @@ on:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
- ".github/workflows/update-nix-hashes.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
- ".github/workflows/update-nix-hashes.yml"
|
||||
|
||||
jobs:
|
||||
update-flake:
|
||||
update-linux:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
env:
|
||||
TITLE: flake.lock
|
||||
SYSTEM: x86_64-linux
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -35,32 +33,39 @@ jobs:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
|
||||
- name: Setup Nix
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
uses: DeterminateSystems/nix-installer-action@v20
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "Github Action"
|
||||
|
||||
- name: Update ${{ env.TITLE }}
|
||||
- name: Update flake.lock
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Updating $TITLE..."
|
||||
echo "📦 Updating flake.lock..."
|
||||
nix flake update
|
||||
echo "$TITLE updated successfully"
|
||||
echo "✅ flake.lock updated successfully"
|
||||
|
||||
- name: Commit ${{ env.TITLE }} changes
|
||||
- name: Update node_modules hash for x86_64-linux
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "🔄 Updating node_modules hash for x86_64-linux..."
|
||||
nix/scripts/update-hashes.sh
|
||||
echo "✅ node_modules hash for x86_64-linux updated successfully"
|
||||
|
||||
- name: Commit Linux hash changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Checking for changes in tracked files..."
|
||||
echo "🔍 Checking for changes in tracked Nix files..."
|
||||
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
echo "### Nix $TITLE"
|
||||
echo "### Nix Hash Update (x86_64-linux)"
|
||||
echo ""
|
||||
echo "- ref: ${GITHUB_REF_NAME}"
|
||||
echo "- status: ${status}"
|
||||
@@ -70,53 +75,42 @@ jobs:
|
||||
fi
|
||||
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)"
|
||||
if [ -z "$STATUS" ]; then
|
||||
echo "No changes detected."
|
||||
echo "✅ No changes detected. Hashes are already up to date."
|
||||
summarize "no changes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Changes detected:"
|
||||
echo "📝 Changes detected:"
|
||||
echo "$STATUS"
|
||||
echo "Staging files..."
|
||||
echo "🔗 Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
echo "Committing changes..."
|
||||
git commit -m "Update $TITLE"
|
||||
echo "Changes committed"
|
||||
echo "💾 Committing changes..."
|
||||
git commit -m "Update Nix flake.lock and x86_64-linux hash"
|
||||
echo "✅ Changes committed"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
echo "Pulling latest from branch: $BRANCH"
|
||||
git pull --rebase --autostash origin "$BRANCH"
|
||||
echo "Pushing changes to branch: $BRANCH"
|
||||
echo "🌳 Pulling latest from branch: $BRANCH"
|
||||
git pull --rebase origin "$BRANCH"
|
||||
echo "🚀 Pushing changes to branch: $BRANCH"
|
||||
git push origin HEAD:"$BRANCH"
|
||||
echo "Changes pushed successfully"
|
||||
echo "✅ Changes pushed successfully"
|
||||
|
||||
summarize "committed $(git rev-parse --short HEAD)"
|
||||
|
||||
compute-node-modules-hash:
|
||||
needs: update-flake
|
||||
update-macos:
|
||||
needs: update-linux
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- system: x86_64-linux
|
||||
host: blacksmith-4vcpu-ubuntu-2404
|
||||
- system: aarch64-linux
|
||||
host: blacksmith-4vcpu-ubuntu-2404-arm
|
||||
- system: x86_64-darwin
|
||||
host: macos-15-intel
|
||||
- system: aarch64-darwin
|
||||
host: macos-latest
|
||||
runs-on: ${{ matrix.host }}
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
SYSTEM: ${{ matrix.system }}
|
||||
SYSTEM: aarch64-darwin
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -124,105 +118,7 @@ jobs:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
|
||||
- name: Setup Nix
|
||||
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 }}
|
||||
uses: DeterminateSystems/nix-installer-action@v20
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
@@ -234,71 +130,27 @@ jobs:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
git pull --rebase --autostash origin "$BRANCH"
|
||||
git pull origin "$BRANCH"
|
||||
|
||||
- name: Download all hash artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: hash-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge hashes into hashes.json
|
||||
- name: Update node_modules hash for aarch64-darwin
|
||||
run: |
|
||||
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"
|
||||
|
||||
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
|
||||
- name: Commit macOS hash changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
HASH_FILE="nix/hashes.json"
|
||||
echo "Checking for changes..."
|
||||
echo "🔍 Checking for changes in tracked Nix files..."
|
||||
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
echo "### Nix $TITLE"
|
||||
echo "### Nix Hash Update (aarch64-darwin)"
|
||||
echo ""
|
||||
echo "- ref: ${GITHUB_REF_NAME}"
|
||||
echo "- status: ${status}"
|
||||
@@ -309,22 +161,27 @@ jobs:
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
FILES=("$HASH_FILE")
|
||||
FILES=(nix/hashes.json)
|
||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||
if [ -z "$STATUS" ]; then
|
||||
echo "No changes detected."
|
||||
echo "✅ No changes detected. Hash is already up to date."
|
||||
summarize "no changes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Changes detected:"
|
||||
echo "📝 Changes detected:"
|
||||
echo "$STATUS"
|
||||
echo "🔗 Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
git commit -m "Update $TITLE"
|
||||
echo "💾 Committing changes..."
|
||||
git commit -m "Update aarch64-darwin hash"
|
||||
echo "✅ Changes committed"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
git pull --rebase --autostash origin "$BRANCH"
|
||||
echo "🌳 Pulling latest from branch: $BRANCH"
|
||||
git pull --rebase origin "$BRANCH"
|
||||
echo "🚀 Pushing changes to branch: $BRANCH"
|
||||
git push origin HEAD:"$BRANCH"
|
||||
echo "Changes pushed successfully"
|
||||
echo "✅ Changes pushed successfully"
|
||||
|
||||
summarize "committed $(git rev-parse --short HEAD)"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,7 +20,6 @@ opencode.json
|
||||
a.out
|
||||
target
|
||||
.scripts
|
||||
.direnv/
|
||||
|
||||
# Local dev files
|
||||
opencode-dev
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
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
|
||||
320
.opencode/plans/1768330644696-gentle-harbor.md
Normal file
320
.opencode/plans/1768330644696-gentle-harbor.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# Plan: Implement enter_plan and exit_plan Tools
|
||||
|
||||
## Summary
|
||||
|
||||
The plan mode workflow in `prompt.ts` references `exit_plan` tool that doesn't exist. We need to implement two tools:
|
||||
|
||||
1. **`exit_plan`** - Called when the AI finishes planning; uses the Question module to ask the user if they want to switch to build mode (yes/no). **Only available in plan mode.** If user says yes, creates a synthetic user message with the "build" agent to trigger the mode switch in the loop.
|
||||
2. **`enter_plan`** - Called to enter plan mode. **Only available in build mode.** If user says yes, creates a synthetic user message with the "plan" agent.
|
||||
|
||||
## Key Insight: How Mode Switching Works
|
||||
|
||||
Looking at `prompt.ts:455-478`, the session loop determines the current agent from the last user message's `agent` field (line 510: `const agent = await Agent.get(lastUser.agent)`).
|
||||
|
||||
To switch modes, we need to:
|
||||
|
||||
1. Ask the user for confirmation
|
||||
2. If confirmed, create a synthetic user message with the **new agent** specified
|
||||
3. The loop will pick up this new user message and use the new agent
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Action |
|
||||
| ------------------------------------------ | --------------------------------------------------------------- |
|
||||
| `packages/opencode/src/tool/plan.ts` | **CREATE** - New file with both tools |
|
||||
| `packages/opencode/src/tool/exitplan.txt` | **CREATE** - Description for exit_plan tool |
|
||||
| `packages/opencode/src/tool/enterplan.txt` | **CREATE** - Description for enter_plan tool |
|
||||
| `packages/opencode/src/tool/registry.ts` | **MODIFY** - Register the new tools |
|
||||
| `packages/opencode/src/agent/agent.ts` | **MODIFY** - Add permission rules to restrict tool availability |
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Create `packages/opencode/src/tool/plan.ts`
|
||||
|
||||
```typescript
|
||||
import z from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import { Question } from "../question"
|
||||
import { Session } from "../session"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Provider } from "../provider/provider"
|
||||
import EXIT_DESCRIPTION from "./exitplan.txt"
|
||||
import ENTER_DESCRIPTION from "./enterplan.txt"
|
||||
|
||||
export const ExitPlanTool = Tool.define("exit_plan", {
|
||||
description: EXIT_DESCRIPTION,
|
||||
parameters: z.object({}),
|
||||
async execute(_params, ctx) {
|
||||
const answers = await Question.ask({
|
||||
sessionID: ctx.sessionID,
|
||||
questions: [
|
||||
{
|
||||
question: "Planning is complete. Would you like to switch to build mode and start implementing?",
|
||||
header: "Build Mode",
|
||||
options: [
|
||||
{ label: "Yes", description: "Switch to build mode and start implementing the plan" },
|
||||
{ label: "No", description: "Stay in plan mode to continue refining the plan" },
|
||||
],
|
||||
},
|
||||
],
|
||||
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
|
||||
})
|
||||
|
||||
const answer = answers[0]?.[0]
|
||||
const shouldSwitch = answer === "Yes"
|
||||
|
||||
// If user wants to switch, create a synthetic user message with the new agent
|
||||
if (shouldSwitch) {
|
||||
// Get model from the last user message in the session
|
||||
const model = await getLastModel(ctx.sessionID)
|
||||
|
||||
const userMsg: MessageV2.User = {
|
||||
id: Identifier.ascending("message"),
|
||||
sessionID: ctx.sessionID,
|
||||
role: "user",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
agent: "build", // Switch to build agent
|
||||
model,
|
||||
}
|
||||
await Session.updateMessage(userMsg)
|
||||
await Session.updatePart({
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: ctx.sessionID,
|
||||
type: "text",
|
||||
text: "User has approved the plan. Switch to build mode and begin implementing the plan.",
|
||||
synthetic: true,
|
||||
} satisfies MessageV2.TextPart)
|
||||
}
|
||||
|
||||
return {
|
||||
title: shouldSwitch ? "Switching to build mode" : "Staying in plan mode",
|
||||
output: shouldSwitch
|
||||
? "User confirmed to switch to build mode. A new message has been created to switch you to build mode. Begin implementing the plan."
|
||||
: "User chose to stay in plan mode. Continue refining the plan or address any concerns.",
|
||||
metadata: {
|
||||
switchToBuild: shouldSwitch,
|
||||
answer,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const EnterPlanTool = Tool.define("enter_plan", {
|
||||
description: ENTER_DESCRIPTION,
|
||||
parameters: z.object({}),
|
||||
async execute(_params, ctx) {
|
||||
const answers = await Question.ask({
|
||||
sessionID: ctx.sessionID,
|
||||
questions: [
|
||||
{
|
||||
question:
|
||||
"Would you like to switch to plan mode? In plan mode, the AI will only research and create a plan without making changes.",
|
||||
header: "Plan Mode",
|
||||
options: [
|
||||
{ label: "Yes", description: "Switch to plan mode for research and planning" },
|
||||
{ label: "No", description: "Stay in build mode to continue making changes" },
|
||||
],
|
||||
},
|
||||
],
|
||||
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
|
||||
})
|
||||
|
||||
const answer = answers[0]?.[0]
|
||||
const shouldSwitch = answer === "Yes"
|
||||
|
||||
// If user wants to switch, create a synthetic user message with the new agent
|
||||
if (shouldSwitch) {
|
||||
const model = await getLastModel(ctx.sessionID)
|
||||
|
||||
const userMsg: MessageV2.User = {
|
||||
id: Identifier.ascending("message"),
|
||||
sessionID: ctx.sessionID,
|
||||
role: "user",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
agent: "plan", // Switch to plan agent
|
||||
model,
|
||||
}
|
||||
await Session.updateMessage(userMsg)
|
||||
await Session.updatePart({
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: ctx.sessionID,
|
||||
type: "text",
|
||||
text: "User has requested to enter plan mode. Switch to plan mode and begin planning.",
|
||||
synthetic: true,
|
||||
} satisfies MessageV2.TextPart)
|
||||
}
|
||||
|
||||
return {
|
||||
title: shouldSwitch ? "Switching to plan mode" : "Staying in build mode",
|
||||
output: shouldSwitch
|
||||
? "User confirmed to switch to plan mode. A new message has been created to switch you to plan mode. Begin planning."
|
||||
: "User chose to stay in build mode. Continue with the current task.",
|
||||
metadata: {
|
||||
switchToPlan: shouldSwitch,
|
||||
answer,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Helper to get the model from the last user message
|
||||
async function getLastModel(sessionID: string) {
|
||||
for await (const item of MessageV2.stream(sessionID)) {
|
||||
if (item.info.role === "user" && item.info.model) return item.info.model
|
||||
}
|
||||
return Provider.defaultModel()
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create `packages/opencode/src/tool/exitplan.txt`
|
||||
|
||||
```
|
||||
Use this tool when you have completed the planning phase and are ready to exit plan mode.
|
||||
|
||||
This tool will ask the user if they want to switch to build mode to start implementing the plan.
|
||||
|
||||
Call this tool:
|
||||
- After you have written a complete plan to the plan file
|
||||
- After you have clarified any questions with the user
|
||||
- When you are confident the plan is ready for implementation
|
||||
|
||||
Do NOT call this tool:
|
||||
- Before you have created or finalized the plan
|
||||
- If you still have unanswered questions about the implementation
|
||||
- If the user has indicated they want to continue planning
|
||||
```
|
||||
|
||||
### 3. Create `packages/opencode/src/tool/enterplan.txt`
|
||||
|
||||
```
|
||||
Use this tool to suggest entering plan mode when the user's request would benefit from planning before implementation.
|
||||
|
||||
This tool will ask the user if they want to switch to plan mode.
|
||||
|
||||
Call this tool when:
|
||||
- The user's request is complex and would benefit from planning first
|
||||
- You want to research and design before making changes
|
||||
- The task involves multiple files or significant architectural decisions
|
||||
|
||||
Do NOT call this tool:
|
||||
- For simple, straightforward tasks
|
||||
- When the user explicitly wants immediate implementation
|
||||
- When already in plan mode
|
||||
```
|
||||
|
||||
### 4. Modify `packages/opencode/src/tool/registry.ts`
|
||||
|
||||
Add import and register tools:
|
||||
|
||||
```typescript
|
||||
// Add import at top (around line 27)
|
||||
import { ExitPlanTool, EnterPlanTool } from "./plan"
|
||||
|
||||
// Add to the all() function return array (around line 110-112)
|
||||
return [
|
||||
// ... existing tools
|
||||
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
|
||||
...(config.experimental?.batch_tool === true ? [BatchTool] : []),
|
||||
ExitPlanTool,
|
||||
EnterPlanTool,
|
||||
...custom,
|
||||
]
|
||||
```
|
||||
|
||||
### 5. Modify `packages/opencode/src/agent/agent.ts`
|
||||
|
||||
Add permission rules to control which agent can use which tool:
|
||||
|
||||
**In the `defaults` ruleset (around line 47-63):**
|
||||
|
||||
```typescript
|
||||
const defaults = PermissionNext.fromConfig({
|
||||
"*": "allow",
|
||||
doom_loop: "ask",
|
||||
// Add these new defaults - both denied by default
|
||||
exit_plan: "deny",
|
||||
enter_plan: "deny",
|
||||
external_directory: {
|
||||
// ... existing
|
||||
},
|
||||
// ... rest of existing defaults
|
||||
})
|
||||
```
|
||||
|
||||
**In the `build` agent (around line 67-79):**
|
||||
|
||||
```typescript
|
||||
build: {
|
||||
name: "build",
|
||||
options: {},
|
||||
permission: PermissionNext.merge(
|
||||
defaults,
|
||||
PermissionNext.fromConfig({
|
||||
question: "allow",
|
||||
enter_plan: "allow", // Allow build agent to suggest plan mode
|
||||
}),
|
||||
user,
|
||||
),
|
||||
mode: "primary",
|
||||
native: true,
|
||||
},
|
||||
```
|
||||
|
||||
**In the `plan` agent (around line 80-96):**
|
||||
|
||||
```typescript
|
||||
plan: {
|
||||
name: "plan",
|
||||
options: {},
|
||||
permission: PermissionNext.merge(
|
||||
defaults,
|
||||
PermissionNext.fromConfig({
|
||||
question: "allow",
|
||||
exit_plan: "allow", // Allow plan agent to exit plan mode
|
||||
edit: {
|
||||
"*": "deny",
|
||||
".opencode/plans/*.md": "allow",
|
||||
},
|
||||
}),
|
||||
user,
|
||||
),
|
||||
mode: "primary",
|
||||
native: true,
|
||||
},
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
1. **Synthetic user message for mode switching**: When the user confirms a mode switch, a synthetic user message is created with the new agent specified. The loop picks this up on the next iteration and switches to the new agent. This follows the existing pattern in `prompt.ts:455-478`.
|
||||
|
||||
2. **Permission-based tool availability**: Uses the existing permission system to control which tools are available to which agents. `exit_plan` is only available in plan mode, `enter_plan` only in build mode.
|
||||
|
||||
3. **Question-based confirmation**: Both tools use the Question module for consistent UX.
|
||||
|
||||
4. **Model preservation**: The synthetic user message preserves the model from the previous user message.
|
||||
|
||||
## Verification
|
||||
|
||||
1. Run `bun dev` in `packages/opencode`
|
||||
2. Start a session in build mode
|
||||
- Verify `exit_plan` is NOT available (denied by permission)
|
||||
- Verify `enter_plan` IS available
|
||||
3. Call `enter_plan` in build mode
|
||||
- Verify the question prompt appears
|
||||
- Select "Yes" and verify:
|
||||
- A synthetic user message is created with `agent: "plan"`
|
||||
- The next assistant response is from the plan agent
|
||||
- The plan mode system reminder appears
|
||||
4. In plan mode, call `exit_plan`
|
||||
- Verify the question prompt appears
|
||||
- Select "Yes" and verify:
|
||||
- A synthetic user message is created with `agent: "build"`
|
||||
- The next assistant response is from the build agent
|
||||
5. Test "No" responses - verify no mode switch occurs
|
||||
29
SECURITY.md
29
SECURITY.md
@@ -1,32 +1,3 @@
|
||||
# Security
|
||||
|
||||
## Threat Model
|
||||
|
||||
### Overview
|
||||
|
||||
OpenCode is an AI-powered coding assistant that runs locally on your machine. It provides an agent system with access to powerful tools including shell execution, file operations, and web access.
|
||||
|
||||
### No Sandbox
|
||||
|
||||
OpenCode does **not** sandbox the agent. The permission system exists as a UX feature to help users stay aware of what actions the agent is taking - it prompts for confirmation before executing commands, writing files, etc. However, it is not designed to provide security isolation.
|
||||
|
||||
If you need true isolation, run OpenCode inside a Docker container or VM.
|
||||
|
||||
### Server Mode
|
||||
|
||||
Server mode is opt-in only. When enabled, set `OPENCODE_SERVER_PASSWORD` to require HTTP Basic Auth. Without this, the server runs unauthenticated (with a warning). It is the end user's responsibility to secure the server - any functionality it provides is not a vulnerability.
|
||||
|
||||
### Out of Scope
|
||||
|
||||
| Category | Rationale |
|
||||
| ------------------------------- | ----------------------------------------------------------------------- |
|
||||
| **Server access when opted-in** | If you enable server mode, API access is expected behavior |
|
||||
| **Sandbox escapes** | The permission system is not a sandbox (see above) |
|
||||
| **LLM provider data handling** | Data sent to your configured LLM provider is governed by their policies |
|
||||
| **MCP server behavior** | External MCP servers you configure are outside our trust boundary |
|
||||
|
||||
---
|
||||
|
||||
# Reporting Security Issues
|
||||
|
||||
We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
|
||||
|
||||
403
STATS.md
403
STATS.md
@@ -1,205 +1,202 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | -------------------- | -------------------- | -------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
||||
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
|
||||
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
|
||||
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
|
||||
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
|
||||
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
|
||||
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
|
||||
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
|
||||
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
|
||||
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
|
||||
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
||||
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
||||
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
||||
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
||||
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
|
||||
| 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) |
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | -------------------- | ------------------- | -------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
|
||||
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
|
||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
||||
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
|
||||
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
|
||||
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
|
||||
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
|
||||
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
|
||||
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
|
||||
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
|
||||
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
|
||||
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
|
||||
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
||||
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
||||
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
||||
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
||||
|
||||
141
bun.lock
141
bun.lock
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -81,30 +81,25 @@
|
||||
"@opencode-ai/console-mail": "workspace:*",
|
||||
"@opencode-ai/console-resource": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.7",
|
||||
"@smithy/util-utf8": "4.2.0",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"@stripe/stripe-js": "8.6.1",
|
||||
"chart.js": "4.5.1",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
"solid-list": "0.3.0",
|
||||
"solid-stripe": "0.8.1",
|
||||
"vite": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@webgpu/types": "0.1.54",
|
||||
"typescript": "catalog:",
|
||||
"wrangler": "4.50.0",
|
||||
},
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -131,7 +126,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -155,7 +150,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -179,7 +174,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -208,7 +203,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -237,7 +232,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -253,7 +248,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -281,7 +276,6 @@
|
||||
"@ai-sdk/vercel": "1.0.31",
|
||||
"@ai-sdk/xai": "2.0.51",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@gitlab/gitlab-ai-provider": "3.1.1",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.25.2",
|
||||
@@ -293,8 +287,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.74",
|
||||
"@opentui/solid": "0.1.74",
|
||||
"@opentui/core": "0.1.72",
|
||||
"@opentui/solid": "0.1.72",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -357,7 +351,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -377,9 +371,9 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.4",
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
@@ -388,7 +382,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -401,7 +395,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -413,7 +407,7 @@
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"dompurify": "3.3.1",
|
||||
"dompurify": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"katex": "0.16.27",
|
||||
"luxon": "catalog:",
|
||||
@@ -424,7 +418,6 @@
|
||||
"shiki": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"solid-list": "catalog:",
|
||||
"strip-ansi": "7.1.2",
|
||||
"virtua": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -442,7 +435,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -453,7 +446,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -509,7 +502,7 @@
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.5",
|
||||
"@types/bun": "1.3.4",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
@@ -593,10 +586,6 @@
|
||||
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="],
|
||||
|
||||
"@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="],
|
||||
|
||||
"@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="],
|
||||
|
||||
"@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="],
|
||||
@@ -917,17 +906,13 @@
|
||||
|
||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-7AtFrCflq2NzC99bj7YaqbQDCZyaScM1+L4ujllV5syiRTFE239Uhnd/yEkPXa7sUAnNRfN3CWusCkQ2zK/q9g=="],
|
||||
|
||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||
|
||||
"@happy-dom/global-registrator": ["@happy-dom/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.5.2", "", { "dependencies": { "ansi-colors": "4.1.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-88cqrrB2cLXN8nMOHidQTcVOnZsJ5kebEbBefjMCifaUCwTA30ouSSWvTZqrOX4O104zjJyu7M8Gcv/NNYQuaA=="],
|
||||
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="],
|
||||
|
||||
"@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.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=="],
|
||||
"@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=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
|
||||
|
||||
@@ -1219,21 +1204,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@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": ["@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-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.74", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rfmlDLtm/u17CnuhJgCxPeYMvOST+A2MOdVOk46IurtHO849bdYqK6iudKNlFRs1FOrymgSKF9GlWBHAOKeRjg=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.72", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RoU48kOrhLZYDBiXaDu1LXS2bwRdlJlFle8eUQiqJjLRbMIY34J/srBuL0JnAS3qKW4J34NepUQa0l0/S43Q3w=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.74", "", { "os": "darwin", "cpu": "x64" }, "sha512-WAD8orsDV0ZdW/5GwjOOB4FY96772xbkz+rcV7WRzEFUVaqoBaC04IuqYzS9d5s+cjkbT5Cpj47hrVYkkVQKng=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.72", "", { "os": "darwin", "cpu": "x64" }, "sha512-hHUQw8i2LWPToRW1rjAiRqmNf34iJPS9ve9CJDygvFs5JOqUxN5yrfLfKfE+1bQjfFDHnpqW1HUk96iLhkPj8Q=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.74", "", { "os": "linux", "cpu": "arm64" }, "sha512-lgmHzrzLy4e+rgBS+lhtsMLLgIMLbtLNMm6EzVPyYVDlLDGjM7+ulXMem7AtpaRrWrUUl4REiG9BoQUsCFDwYA=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.72", "", { "os": "linux", "cpu": "arm64" }, "sha512-63yml0OQ8tVa0JuDF9lBAWiChX6Q+iDO7lKv7c2n0352n/WyPr3iAgq4uSoH49HXuKeAXY/VwHGjvPzjXD/SDA=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.74", "", { "os": "linux", "cpu": "x64" }, "sha512-8Mn2WbdBQ29xCThuPZezjDhd1N3+fXwKkGvCBOdTI0le6h2A/vCNbfUVjwfr/EGZSRXxCG+Yapol34BAULGpOA=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.72", "", { "os": "linux", "cpu": "x64" }, "sha512-51veiQXNLvzDsFzsEvt71uK7WhiRe2DnvlJSGBSe6aRRHHxjCFYHzYi7t6bitJqtDTUj+EaMPbH81oZ6xy7tyg=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.74", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvYUXz03avnI6ZluyLp00HPmR0UT/IE/6QS97XBsgJlUTtpnbKkBtB5jD1NHwWkElaRj1Qv2QP36ngFoJqbl9g=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.72", "", { "os": "win32", "cpu": "arm64" }, "sha512-1Ep6OcaYTy1RlLOln+LNN7DL1iNyLwLjG2M8aO0pVJKFvxeD5P7rdRzY065E4uhkHeJIHuduUqxvUjD0dyuwbw=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.74", "", { "os": "win32", "cpu": "x64" }, "sha512-3wfWXaAKOIlDQz6ZZIESf2M+YGZ7uFHijjTEM8w/STRlLw8Y6+QyGYi1myHSM4d6RSO+/s2EMDxvjDf899W9vQ=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.72", "", { "os": "win32", "cpu": "x64" }, "sha512-5QUv91UkOINlkEaPky3kaxmJvshcJMBAX7LZtIroduaKBGpWRA1aogNhPZzp+30WkvgOU7aOtUktAZuFXb9WdQ=="],
|
||||
|
||||
"@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=="],
|
||||
"@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=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -1525,7 +1510,7 @@
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="],
|
||||
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.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-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-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=="],
|
||||
|
||||
@@ -1615,8 +1600,6 @@
|
||||
|
||||
"@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="],
|
||||
|
||||
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||
|
||||
"@solid-primitives/active-element": ["@solid-primitives/active-element@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="],
|
||||
|
||||
"@solid-primitives/audio": ["@solid-primitives/audio@1.4.2", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="],
|
||||
@@ -1669,8 +1652,6 @@
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
|
||||
"@stripe/stripe-js": ["@stripe/stripe-js@8.6.1", "", {}, "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
||||
@@ -1777,7 +1758,7 @@
|
||||
|
||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
||||
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
@@ -1907,7 +1888,7 @@
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
||||
|
||||
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
|
||||
"@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
|
||||
|
||||
"@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
|
||||
|
||||
@@ -2079,7 +2060,7 @@
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.4", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -2095,7 +2076,7 @@
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -2337,10 +2318,6 @@
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="],
|
||||
|
||||
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
@@ -2563,10 +2540,6 @@
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="],
|
||||
|
||||
"graphql-request": ["graphql-request@6.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw=="],
|
||||
|
||||
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
|
||||
|
||||
"gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
|
||||
@@ -2795,8 +2768,6 @@
|
||||
|
||||
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
||||
|
||||
"isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
|
||||
|
||||
"iterate-iterator": ["iterate-iterator@1.0.2", "", {}, "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw=="],
|
||||
|
||||
"iterate-value": ["iterate-value@1.0.2", "", { "dependencies": { "es-get-iterator": "^1.0.2", "iterate-iterator": "^1.0.1" } }, "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ=="],
|
||||
@@ -2829,8 +2800,6 @@
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
@@ -3107,8 +3076,6 @@
|
||||
|
||||
"named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
|
||||
|
||||
"nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
@@ -3495,7 +3462,7 @@
|
||||
|
||||
"selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -3551,10 +3518,6 @@
|
||||
|
||||
"smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="],
|
||||
|
||||
"socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
|
||||
|
||||
"socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="],
|
||||
|
||||
"solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="],
|
||||
|
||||
"solid-list": ["solid-list@0.3.0", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="],
|
||||
@@ -3565,8 +3528,6 @@
|
||||
|
||||
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
|
||||
|
||||
"solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="],
|
||||
|
||||
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
|
||||
|
||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
@@ -3721,8 +3682,6 @@
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
||||
|
||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||
|
||||
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
||||
@@ -3915,8 +3874,6 @@
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||
|
||||
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
||||
|
||||
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
@@ -3969,8 +3926,6 @@
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
|
||||
@@ -4069,8 +4024,6 @@
|
||||
|
||||
"@expressive-code/plugin-shiki/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=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||
@@ -4241,10 +4194,6 @@
|
||||
|
||||
"@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
|
||||
|
||||
"@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="],
|
||||
|
||||
"@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
|
||||
|
||||
"@solidjs/start/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"@solidjs/start/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
@@ -4281,8 +4230,6 @@
|
||||
|
||||
"astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
|
||||
|
||||
"astro/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"astro/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
|
||||
|
||||
"astro/unstorage": ["unstorage@1.17.3", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q=="],
|
||||
@@ -4303,10 +4250,6 @@
|
||||
|
||||
"body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||
|
||||
"bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
|
||||
|
||||
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
@@ -4323,10 +4266,6 @@
|
||||
|
||||
"editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="],
|
||||
|
||||
"editorconfig/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"es-get-iterator/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"esbuild-plugin-copy/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
@@ -4351,8 +4290,6 @@
|
||||
|
||||
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"gel/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@@ -4367,8 +4304,6 @@
|
||||
|
||||
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||
|
||||
"jsonwebtoken/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
@@ -4453,8 +4388,6 @@
|
||||
|
||||
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"shiki/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
||||
|
||||
"shiki/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
||||
@@ -4933,8 +4866,6 @@
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1768456270,
|
||||
"narHash": "sha256-NgaL2CCiUR6nsqUIY4yxkzz07iQUlUCany44CFv+OxY=",
|
||||
"lastModified": 1768178648,
|
||||
"narHash": "sha256-kz/F6mhESPvU1diB7tOM3nLcBfQe7GU7GQCymRlTi/s=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f4606b01b39e09065df37905a2133905246db9ed",
|
||||
"rev": "3fbab70c6e69c87ea2b6e48aa6629da2aa6a23b0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
28
flake.nix
28
flake.nix
@@ -7,7 +7,6 @@
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
...
|
||||
}:
|
||||
@@ -108,10 +107,33 @@
|
||||
};
|
||||
in
|
||||
{
|
||||
default = self.packages.${system}.opencode;
|
||||
opencode = opencodePkg;
|
||||
default = opencodePkg;
|
||||
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";
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,10 +91,8 @@ This will walk you through installing the GitHub app, creating the workflow, and
|
||||
uses: anomalyco/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
use_github_token: true
|
||||
```
|
||||
|
||||
3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys.
|
||||
|
||||
@@ -119,11 +119,9 @@ const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS5"),
|
||||
new sst.Secret("ZEN_MODELS6"),
|
||||
new sst.Secret("ZEN_MODELS7"),
|
||||
new sst.Secret("ZEN_MODELS8"),
|
||||
]
|
||||
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
properties: { value: auth.url.apply((url) => url!) },
|
||||
})
|
||||
@@ -179,7 +177,6 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
//VITE_DOCS_URL: web.url.apply((url) => url!),
|
||||
//VITE_API_URL: gateway.url.apply((url) => url!),
|
||||
VITE_AUTH_URL: auth.url.apply((url) => url!),
|
||||
VITE_STRIPE_PUBLISHABLE_KEY: STRIPE_PUBLISHABLE_KEY.value,
|
||||
},
|
||||
transform: {
|
||||
server: {
|
||||
|
||||
2
install
2
install
@@ -369,7 +369,7 @@ case $current_shell in
|
||||
config_files="$HOME/.config/fish/config.fish"
|
||||
;;
|
||||
zsh)
|
||||
config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
|
||||
config_files="$HOME/.zshrc $HOME/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
|
||||
;;
|
||||
bash)
|
||||
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
cargo,
|
||||
rustc,
|
||||
makeBinaryWrapper,
|
||||
copyDesktopItems,
|
||||
makeDesktopItem,
|
||||
nodejs,
|
||||
jq,
|
||||
}:
|
||||
@@ -59,28 +57,12 @@ rustPlatform.buildRustPackage rec {
|
||||
pkg-config
|
||||
bun
|
||||
makeBinaryWrapper
|
||||
copyDesktopItems
|
||||
cargo
|
||||
rustc
|
||||
nodejs
|
||||
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 = [
|
||||
openssl
|
||||
]
|
||||
@@ -139,10 +121,6 @@ rustPlatform.buildRustPackage rec {
|
||||
# It looks for them in the location specified in tauri.conf.json.
|
||||
|
||||
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
|
||||
wrapProgram $out/bin/opencode-desktop \
|
||||
--prefix LD_LIBRARY_PATH : ${
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-4zchRpxzvHnPMcwumgL9yaX0deIXS5IGPp131eYsSvg=",
|
||||
"aarch64-linux": "sha256-3/BSRsl5pI0Iz3qAFZxIkOehFLZ2Ox9UsbdDHYzqlVg=",
|
||||
"aarch64-darwin": "sha256-86d/G1q6xiHSSlm+/irXoKLb/yLQbV348uuSrBV70+Q=",
|
||||
"x86_64-darwin": "sha256-WYaP44PWRGtoG1DIuUJUH4DvuaCuFhlJZ9fPzGsiIfE="
|
||||
"x86_64-linux": "sha256-UCPTTk4b7d2bets7KgCeYBHWAUwUAPUyKm+xDYkSexE=",
|
||||
"aarch64-darwin": "sha256-Y3o6lovahSWoG9un/l1qxu7hCmIlZXm2LxOLKNiPQfQ="
|
||||
}
|
||||
}
|
||||
|
||||
119
nix/scripts/update-hashes.sh
Executable file
119
nix/scripts/update-hashes.sh
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/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
|
||||
@@ -21,7 +21,7 @@
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@types/bun": "1.3.5",
|
||||
"@types/bun": "1.3.4",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"ulid": "3.0.1",
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
<!-- Theme preload script - applies cached theme to avoid FOUC -->
|
||||
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-dvh p-px"></div>
|
||||
<div id="root" class="flex flex-col h-dvh"></div>
|
||||
<script src="/src/entry.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -29,7 +29,7 @@ import { Suspense } from "solid-js"
|
||||
|
||||
const Home = lazy(() => import("@/pages/home"))
|
||||
const Session = lazy(() => import("@/pages/session"))
|
||||
const Loading = () => <div class="size-full" />
|
||||
const Loading = () => <div class="size-full flex items-center justify-center text-text-weak">Loading...</div>
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createMemo, createSignal, onCleanup, Show } from "solid-js"
|
||||
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
||||
import { createMemo } from "solid-js"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useFile } from "@/context/file"
|
||||
|
||||
type EntryType = "command" | "file"
|
||||
|
||||
type Entry = {
|
||||
id: string
|
||||
type: EntryType
|
||||
title: string
|
||||
description?: string
|
||||
keybind?: string
|
||||
category: "Commands" | "Files"
|
||||
option?: CommandOption
|
||||
path?: string
|
||||
}
|
||||
|
||||
export function DialogSelectFile() {
|
||||
const command = useCommand()
|
||||
const layout = useLayout()
|
||||
const file = useFile()
|
||||
const dialog = useDialog()
|
||||
@@ -32,148 +16,35 @@ export function DialogSelectFile() {
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const [grouped, setGrouped] = createSignal(false)
|
||||
const common = ["session.new", "session.previous", "session.next", "terminal.toggle", "review.toggle"]
|
||||
const limit = 5
|
||||
|
||||
const allowed = createMemo(() =>
|
||||
command.options.filter(
|
||||
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
),
|
||||
)
|
||||
|
||||
const commandItem = (option: CommandOption): Entry => ({
|
||||
id: "command:" + option.id,
|
||||
type: "command",
|
||||
title: option.title,
|
||||
description: option.description,
|
||||
keybind: option.keybind,
|
||||
category: "Commands",
|
||||
option,
|
||||
})
|
||||
|
||||
const fileItem = (path: string): Entry => ({
|
||||
id: "file:" + path,
|
||||
type: "file",
|
||||
title: path,
|
||||
category: "Files",
|
||||
path,
|
||||
})
|
||||
|
||||
const list = createMemo(() => allowed().map(commandItem))
|
||||
|
||||
const picks = createMemo(() => {
|
||||
const all = allowed()
|
||||
const order = new Map(common.map((id, index) => [id, index]))
|
||||
const picked = all.filter((option) => order.has(option.id))
|
||||
const base = picked.length ? picked : all.slice(0, limit)
|
||||
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
||||
return sorted.map(commandItem)
|
||||
})
|
||||
|
||||
const recent = createMemo(() => {
|
||||
const all = tabs().all()
|
||||
const active = tabs().active()
|
||||
const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
||||
const seen = new Set<string>()
|
||||
const items: Entry[] = []
|
||||
|
||||
for (const item of order) {
|
||||
const path = file.pathFromTab(item)
|
||||
if (!path) continue
|
||||
if (seen.has(path)) continue
|
||||
seen.add(path)
|
||||
items.push(fileItem(path))
|
||||
}
|
||||
|
||||
return items.slice(0, limit)
|
||||
})
|
||||
|
||||
const items = async (filter: string) => {
|
||||
const query = filter.trim()
|
||||
setGrouped(query.length > 0)
|
||||
if (!query) return [...picks(), ...recent()]
|
||||
const files = await file.searchFiles(query)
|
||||
const entries = files.map(fileItem)
|
||||
return [...list(), ...entries]
|
||||
}
|
||||
|
||||
const handleMove = (item: Entry | undefined) => {
|
||||
state.cleanup?.()
|
||||
if (!item) return
|
||||
if (item.type !== "command") return
|
||||
state.cleanup = item.option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const open = (path: string) => {
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
view().reviewPanel.open()
|
||||
}
|
||||
|
||||
const handleSelect = (item: Entry | undefined) => {
|
||||
if (!item) return
|
||||
state.committed = true
|
||||
state.cleanup = undefined
|
||||
dialog.close()
|
||||
|
||||
if (item.type === "command") {
|
||||
item.option?.onSelect?.("palette")
|
||||
return
|
||||
}
|
||||
|
||||
if (!item.path) return
|
||||
open(item.path)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (state.committed) return
|
||||
state.cleanup?.()
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog class="pt-3 pb-0 !max-h-[480px]">
|
||||
<Dialog title="Select file">
|
||||
<List
|
||||
search={{ placeholder: "Search files and commands", autofocus: true, hideIcon: true, class: "pl-3 pr-2 !mb-0" }}
|
||||
emptyMessage="No results found"
|
||||
items={items}
|
||||
key={(item) => item.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
groupBy={(item) => item.category}
|
||||
onMove={handleMove}
|
||||
onSelect={handleSelect}
|
||||
search={{ placeholder: "Search files", autofocus: true }}
|
||||
emptyMessage="No files found"
|
||||
items={file.searchFiles}
|
||||
key={(x) => x}
|
||||
onSelect={(path) => {
|
||||
if (path) {
|
||||
const value = file.tab(path)
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
view().reviewPanel.open()
|
||||
}
|
||||
dialog.close()
|
||||
}}
|
||||
>
|
||||
{(item) => (
|
||||
<Show
|
||||
when={item.type === "command"}
|
||||
fallback={
|
||||
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(item.path ?? "")}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span>
|
||||
</div>
|
||||
</div>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(i)}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
|
||||
</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>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogManageModels } from "./dialog-manage-models"
|
||||
|
||||
@@ -35,6 +37,12 @@ const ModelList: Component<{
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||
groupBy={(x) => x.provider.name}
|
||||
groupHeader={(group) => (
|
||||
<div class="flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={group.items[0].provider.id as IconName} />
|
||||
<span>{group.category}</span>
|
||||
</div>
|
||||
)}
|
||||
sortGroupsBy={(a, b) => {
|
||||
if (a.category === "Recent" && b.category !== "Recent") return -1
|
||||
if (b.category === "Recent" && a.category !== "Recent") return 1
|
||||
@@ -52,7 +60,8 @@ const ModelList: Component<{
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2 text-13-regular">
|
||||
<div class="w-full flex items-center gap-x-3 pl-1 text-13-regular">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.provider.id as IconName} />
|
||||
<span class="truncate">{i.name}</span>
|
||||
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
|
||||
<Tag>Free</Tag>
|
||||
|
||||
@@ -364,12 +364,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
if (!isFocused()) setStore("popover", null)
|
||||
})
|
||||
|
||||
// Safety: reset composing state on focus change to prevent stuck state
|
||||
// This handles edge cases where compositionend event may not fire
|
||||
createEffect(() => {
|
||||
if (!isFocused()) setComposing(false)
|
||||
})
|
||||
|
||||
type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string }
|
||||
|
||||
const agentList = createMemo(() =>
|
||||
@@ -887,14 +881,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Shift+Enter BEFORE IME check - Shift+Enter is never used for IME input
|
||||
// and should always insert a newline regardless of composition state
|
||||
if (event.key === "Enter" && event.shiftKey) {
|
||||
addPart({ type: "text", content: "\n", start: 0, end: 0 })
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === "Enter" && isImeComposing(event)) {
|
||||
return
|
||||
}
|
||||
@@ -958,7 +944,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
// Note: Shift+Enter is handled earlier, before IME check
|
||||
if (event.key === "Enter" && event.shiftKey) {
|
||||
addPart({ type: "text", content: "\n", start: 0, end: 0 })
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
handleSubmit(event)
|
||||
}
|
||||
|
||||
@@ -1,203 +1,267 @@
|
||||
import { createMemo, createResource, Show } from "solid-js"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useCommand } from "@/context/command"
|
||||
// import { useServer } from "@/context/server"
|
||||
// import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Popover } from "@opencode-ai/ui/popover"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
|
||||
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
|
||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { same } from "@/utils/same"
|
||||
|
||||
export function SessionHeader() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const command = useCommand()
|
||||
// const server = useServer()
|
||||
// const dialog = useDialog()
|
||||
const server = useServer()
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
|
||||
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
const project = createMemo(() => {
|
||||
const directory = projectDirectory()
|
||||
if (!directory) return
|
||||
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
|
||||
})
|
||||
const name = createMemo(() => {
|
||||
const current = project()
|
||||
if (current) return current.name || getFilename(current.worktree)
|
||||
return getFilename(projectDirectory())
|
||||
})
|
||||
const hotkey = createMemo(() => command.keybind("file.open"))
|
||||
|
||||
const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
|
||||
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
||||
const parentSession = createMemo(() => {
|
||||
const current = currentSession()
|
||||
if (!current?.parentID) return undefined
|
||||
return sync.data.session.find((s) => s.id === current.parentID)
|
||||
})
|
||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||
const worktrees = createMemo(() => layout.projects.list().map((p) => p.worktree), [], { equals: same })
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const view = createMemo(() => layout.view(sessionKey()))
|
||||
|
||||
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
||||
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
||||
function navigateToProject(directory: string) {
|
||||
navigate(`/${base64Encode(directory)}`)
|
||||
}
|
||||
|
||||
function navigateToSession(session: Session | undefined) {
|
||||
if (!session) return
|
||||
// Only navigate if we're actually changing to a different session
|
||||
if (session.id === params.id) return
|
||||
navigate(`/${params.dir}/session/${session.id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={centerMount()}>
|
||||
{(mount) => (
|
||||
<Portal mount={mount()}>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="magnifying-glass" size="normal" class="icon-base" />
|
||||
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate h-3.5 flex items-center overflow-visible">
|
||||
Search {name()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Show when={hotkey()}>{(keybind) => <Keybind>{keybind()}</Keybind>}</Show>
|
||||
</button>
|
||||
</Portal>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={rightMount()}>
|
||||
{(mount) => (
|
||||
<Portal mount={mount()}>
|
||||
<div class="flex items-center gap-3">
|
||||
{/* <div class="hidden md:flex items-center gap-1"> */}
|
||||
{/* <Button */}
|
||||
{/* size="small" */}
|
||||
{/* variant="ghost" */}
|
||||
{/* onClick={() => { */}
|
||||
{/* dialog.show(() => <DialogSelectServer />) */}
|
||||
{/* }} */}
|
||||
{/* > */}
|
||||
{/* <div */}
|
||||
{/* classList={{ */}
|
||||
{/* "size-1.5 rounded-full": true, */}
|
||||
{/* "bg-icon-success-base": server.healthy() === true, */}
|
||||
{/* "bg-icon-critical-base": server.healthy() === false, */}
|
||||
{/* "bg-border-weak-base": server.healthy() === undefined, */}
|
||||
{/* }} */}
|
||||
{/* /> */}
|
||||
{/* <Icon name="server" size="small" class="text-icon-weak" /> */}
|
||||
{/* <span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span> */}
|
||||
{/* </Button> */}
|
||||
{/* <SessionLspIndicator /> */}
|
||||
{/* <SessionMcpIndicator /> */}
|
||||
{/* </div> */}
|
||||
<div class="flex items-center gap-1">
|
||||
<Show when={currentSession()?.summary?.files}>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle review"
|
||||
keybind={command.keybind("review.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/review-toggle size-6 p-0"
|
||||
onClick={() => view().reviewPanel.toggle()}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
|
||||
size="small"
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
|
||||
size="small"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
|
||||
size="small"
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle terminal"
|
||||
keybind={command.keybind("terminal.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/terminal-toggle size-6 p-0"
|
||||
onClick={() => view().terminal.toggle()}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<Show when={shareEnabled() && currentSession()}>
|
||||
<Popover
|
||||
title="Share session"
|
||||
trigger={
|
||||
<Tooltip class="shrink-0" value="Share session">
|
||||
<IconButton icon="share" variant="ghost" class="" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{iife(() => {
|
||||
const [url] = createResource(
|
||||
() => currentSession(),
|
||||
async (session) => {
|
||||
if (!session) return
|
||||
let shareURL = session.share?.url
|
||||
if (!shareURL) {
|
||||
shareURL = await globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: projectDirectory() })
|
||||
.then((r) => r.data?.share?.url)
|
||||
.catch((e) => {
|
||||
console.error("Failed to share session", e)
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
return shareURL
|
||||
},
|
||||
{ initialValue: "" },
|
||||
)
|
||||
return (
|
||||
<Show when={url.latest}>
|
||||
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
|
||||
</Show>
|
||||
)
|
||||
})}
|
||||
</Popover>
|
||||
</Show>
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex">
|
||||
<button
|
||||
type="button"
|
||||
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
>
|
||||
<Icon name="menu" size="small" />
|
||||
</button>
|
||||
<div class="px-4 flex items-center justify-between gap-4 w-full">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div class="hidden xl:flex items-center gap-2">
|
||||
<Select
|
||||
options={worktrees()}
|
||||
current={sync.project?.worktree ?? projectDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
<Show
|
||||
when={parentSession()}
|
||||
fallback={
|
||||
<>
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
placeholder="New session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={navigateToSession}
|
||||
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={parentSession()}
|
||||
placeholder="Back to parent session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={(session) => {
|
||||
// Only navigate if selecting a different session than current parent
|
||||
const currentParent = parentSession()
|
||||
if (session && currentParent && session.id !== currentParent.id) {
|
||||
navigateToSession(session)
|
||||
}
|
||||
}}
|
||||
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
<div class="text-text-weaker">/</div>
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
<Tooltip value="Back to parent session">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center gap-1 p-1 rounded hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors flex-shrink-0"
|
||||
onClick={() => navigateToSession(parentSession())}
|
||||
>
|
||||
<Icon name="arrow-left" size="small" class="text-icon-base" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={currentSession() && !parentSession()}>
|
||||
<TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
|
||||
<IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="hidden md:flex items-center gap-1">
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
dialog.show(() => <DialogSelectServer />)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full": true,
|
||||
"bg-icon-success-base": server.healthy() === true,
|
||||
"bg-icon-critical-base": server.healthy() === false,
|
||||
"bg-border-weak-base": server.healthy() === undefined,
|
||||
}}
|
||||
/>
|
||||
<Icon name="server" size="small" class="text-icon-weak" />
|
||||
<span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
|
||||
</Button>
|
||||
<SessionLspIndicator />
|
||||
<SessionMcpIndicator />
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Show when={currentSession()?.summary?.files}>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle review"
|
||||
keybind={command.keybind("review.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/review-toggle size-6 p-0"
|
||||
onClick={() => view().reviewPanel.toggle()}
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
|
||||
size="small"
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
|
||||
size="small"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
|
||||
size="small"
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
class="hidden md:block shrink-0"
|
||||
title="Toggle terminal"
|
||||
keybind={command.keybind("terminal.toggle")}
|
||||
>
|
||||
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={() => view().terminal.toggle()}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<Show when={shareEnabled() && currentSession()}>
|
||||
<Popover
|
||||
title="Share session"
|
||||
trigger={
|
||||
<Tooltip class="shrink-0" value="Share session">
|
||||
<IconButton icon="share" variant="ghost" class="" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{iife(() => {
|
||||
const [url] = createResource(
|
||||
() => currentSession(),
|
||||
async (session) => {
|
||||
if (!session) return
|
||||
let shareURL = session.share?.url
|
||||
if (!shareURL) {
|
||||
shareURL = await globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: projectDirectory() })
|
||||
.then((r) => r.data?.share?.url)
|
||||
.catch((e) => {
|
||||
console.error("Failed to share session", e)
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
return shareURL
|
||||
},
|
||||
{ initialValue: "" },
|
||||
)
|
||||
return (
|
||||
<Show when={url.latest}>
|
||||
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
|
||||
</Show>
|
||||
)
|
||||
})}
|
||||
</Popover>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
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,6 +1,8 @@
|
||||
import { createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createMemo, createSignal, onCleanup, onMount, Show, type Accessor } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
|
||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||
|
||||
@@ -105,27 +107,74 @@ export function formatKeybind(config: string): string {
|
||||
if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
|
||||
|
||||
if (kb.key) {
|
||||
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))
|
||||
const displayKey = kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1)
|
||||
parts.push(displayKey)
|
||||
}
|
||||
|
||||
return IS_MAC ? parts.join("") : parts.join("+")
|
||||
}
|
||||
|
||||
function DialogCommand(props: { options: CommandOption[] }) {
|
||||
const dialog = useDialog()
|
||||
let cleanup: (() => void) | void
|
||||
let committed = false
|
||||
|
||||
const handleMove = (option: CommandOption | undefined) => {
|
||||
cleanup?.()
|
||||
cleanup = option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const handleSelect = (option: CommandOption | undefined) => {
|
||||
if (option) {
|
||||
committed = true
|
||||
cleanup = undefined
|
||||
dialog.close()
|
||||
option.onSelect?.("palette")
|
||||
}
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (!committed) {
|
||||
cleanup?.()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog title="Commands">
|
||||
<List
|
||||
search={{ placeholder: "Search commands", autofocus: true }}
|
||||
emptyMessage="No commands found"
|
||||
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
|
||||
key={(x) => x?.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
groupBy={(x) => x.category ?? ""}
|
||||
onMove={handleMove}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{(option) => (
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{option.title}</span>
|
||||
<Show when={option.description}>
|
||||
<span class="text-14-regular text-text-weak truncate">{option.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={option.keybind}>
|
||||
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(option.keybind!)}</span>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
|
||||
name: "Command",
|
||||
init: () => {
|
||||
const dialog = useDialog()
|
||||
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
||||
const [suspendCount, setSuspendCount] = createSignal(0)
|
||||
const dialog = useDialog()
|
||||
|
||||
const options = createMemo(() => {
|
||||
const seen = new Set<string>()
|
||||
@@ -153,21 +202,14 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
|
||||
const suspended = () => suspendCount() > 0
|
||||
|
||||
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
|
||||
for (const option of options()) {
|
||||
if (option.id === id || option.id === "suggested." + id) {
|
||||
option.onSelect?.(source)
|
||||
return
|
||||
}
|
||||
const showPalette = () => {
|
||||
if (!dialog.active) {
|
||||
dialog.show(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
|
||||
}
|
||||
}
|
||||
|
||||
const showPalette = () => {
|
||||
run("file.open", "palette")
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (suspended() || dialog.active) return
|
||||
if (suspended()) return
|
||||
|
||||
const paletteKeybinds = parseKeybind("mod+shift+p")
|
||||
if (matchKeybind(paletteKeybinds, event)) {
|
||||
@@ -206,7 +248,12 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
})
|
||||
},
|
||||
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
||||
run(id, source)
|
||||
for (const option of options()) {
|
||||
if (option.id === id || option.id === "suggested." + id) {
|
||||
option.onSelect?.(source)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
keybind(id: string) {
|
||||
const option = options().find((x) => x.id === id || x.id === "suggested." + id)
|
||||
|
||||
@@ -16,32 +16,17 @@ import {
|
||||
type LspStatus,
|
||||
type VcsInfo,
|
||||
type PermissionRequest,
|
||||
type QuestionRequest,
|
||||
createOpencodeClient,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { createStore, produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { retry } from "@opencode-ai/util/retry"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { ErrorPage, type InitError } from "../pages/error"
|
||||
import {
|
||||
batch,
|
||||
createContext,
|
||||
createEffect,
|
||||
getOwner,
|
||||
runWithOwner,
|
||||
useContext,
|
||||
onCleanup,
|
||||
onMount,
|
||||
type Accessor,
|
||||
type ParentProps,
|
||||
Switch,
|
||||
Match,
|
||||
} from "solid-js"
|
||||
import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { usePlatform } from "./platform"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
type State = {
|
||||
status: "loading" | "partial" | "complete"
|
||||
@@ -52,7 +37,6 @@ type State = {
|
||||
config: Config
|
||||
path: Path
|
||||
session: Session[]
|
||||
sessionTotal: number
|
||||
session_status: {
|
||||
[sessionID: string]: SessionStatus
|
||||
}
|
||||
@@ -65,9 +49,6 @@ type State = {
|
||||
permission: {
|
||||
[sessionID: string]: PermissionRequest[]
|
||||
}
|
||||
question: {
|
||||
[sessionID: string]: QuestionRequest[]
|
||||
}
|
||||
mcp: {
|
||||
[name: string]: McpStatus
|
||||
}
|
||||
@@ -82,18 +63,9 @@ type State = {
|
||||
}
|
||||
}
|
||||
|
||||
type VcsCache = {
|
||||
store: Store<{ value: VcsInfo | undefined }>
|
||||
setStore: SetStoreFunction<{ value: VcsInfo | undefined }>
|
||||
ready: Accessor<boolean>
|
||||
}
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const platform = usePlatform()
|
||||
const owner = getOwner()
|
||||
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||
const vcsCache = new Map<string, VcsCache>()
|
||||
const [globalStore, setGlobalStore] = createStore<{
|
||||
ready: boolean
|
||||
error?: InitError
|
||||
@@ -109,81 +81,52 @@ function createGlobalSync() {
|
||||
provider_auth: {},
|
||||
})
|
||||
|
||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||
|
||||
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
||||
function child(directory: string) {
|
||||
if (!directory) console.error("No directory provided")
|
||||
if (!children[directory]) {
|
||||
const cache = runWithOwner(owner, () =>
|
||||
persisted(
|
||||
Persist.workspace(directory, "vcs", ["vcs.v1"]),
|
||||
createStore({ value: undefined as VcsInfo | undefined }),
|
||||
),
|
||||
)
|
||||
if (!cache) throw new Error("Failed to create persisted cache")
|
||||
vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })
|
||||
|
||||
const init = () => {
|
||||
children[directory] = createStore<State>({
|
||||
project: "",
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
status: "loading" as const,
|
||||
agent: [],
|
||||
command: [],
|
||||
session: [],
|
||||
sessionTotal: 0,
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
permission: {},
|
||||
question: {},
|
||||
mcp: {},
|
||||
lsp: [],
|
||||
vcs: cache[0].value,
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
})
|
||||
bootstrapInstance(directory)
|
||||
}
|
||||
|
||||
runWithOwner(owner, init)
|
||||
children[directory] = createStore<State>({
|
||||
project: "",
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
status: "loading" as const,
|
||||
agent: [],
|
||||
command: [],
|
||||
session: [],
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
permission: {},
|
||||
mcp: {},
|
||||
lsp: [],
|
||||
vcs: undefined,
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
})
|
||||
bootstrapInstance(directory)
|
||||
}
|
||||
const childStore = children[directory]
|
||||
if (!childStore) throw new Error("Failed to create store")
|
||||
return childStore
|
||||
return children[directory]
|
||||
}
|
||||
|
||||
async function loadSessions(directory: string) {
|
||||
const [store, setStore] = child(directory)
|
||||
const limit = store.limit
|
||||
|
||||
return globalSDK.client.session
|
||||
.list({ directory, roots: true })
|
||||
globalSDK.client.session
|
||||
.list({ directory })
|
||||
.then((x) => {
|
||||
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
|
||||
const nonArchived = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.filter((s) => !s.time?.archived)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory))
|
||||
if (sandboxWorkspace) {
|
||||
setStore("session", reconcile(nonArchived, { key: "id" }))
|
||||
return
|
||||
}
|
||||
|
||||
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
|
||||
// Include up to the limit, plus any updated in the last 4 hours
|
||||
const sessions = nonArchived.filter((s, i) => {
|
||||
if (i < limit) return true
|
||||
if (i < store.limit) return true
|
||||
const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
|
||||
return updated > fourHoursAgo
|
||||
})
|
||||
// Store total session count (used for "load more" pagination)
|
||||
setStore("sessionTotal", nonArchived.length)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -196,8 +139,6 @@ function createGlobalSync() {
|
||||
async function bootstrapInstance(directory: string) {
|
||||
if (!directory) return
|
||||
const [store, setStore] = child(directory)
|
||||
const cache = vcsCache.get(directory)
|
||||
if (!cache) return
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
fetch: platform.fetch,
|
||||
@@ -205,13 +146,6 @@ function createGlobalSync() {
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!cache.ready()) return
|
||||
const cached = cache.store.value
|
||||
if (!cached?.branch) return
|
||||
setStore("vcs", (value) => value ?? cached)
|
||||
})
|
||||
|
||||
const blockingRequests = {
|
||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () =>
|
||||
@@ -241,11 +175,7 @@ function createGlobalSync() {
|
||||
loadSessions(directory),
|
||||
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
|
||||
sdk.vcs.get().then((x) => {
|
||||
const next = x.data ?? store.vcs
|
||||
setStore("vcs", next)
|
||||
if (next?.branch) cache.setStore("value", next)
|
||||
}),
|
||||
sdk.vcs.get().then((x) => setStore("vcs", x.data)),
|
||||
sdk.permission.list().then((x) => {
|
||||
const grouped: Record<string, PermissionRequest[]> = {}
|
||||
for (const perm of x.data ?? []) {
|
||||
@@ -278,38 +208,6 @@ function createGlobalSync() {
|
||||
}
|
||||
})
|
||||
}),
|
||||
sdk.question.list().then((x) => {
|
||||
const grouped: Record<string, QuestionRequest[]> = {}
|
||||
for (const question of x.data ?? []) {
|
||||
if (!question?.id || !question.sessionID) continue
|
||||
const existing = grouped[question.sessionID]
|
||||
if (existing) {
|
||||
existing.push(question)
|
||||
continue
|
||||
}
|
||||
grouped[question.sessionID] = [question]
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(store.question)) {
|
||||
if (grouped[sessionID]) continue
|
||||
setStore("question", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||
setStore(
|
||||
"question",
|
||||
sessionID,
|
||||
reconcile(
|
||||
questions
|
||||
.filter((q) => !!q?.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}),
|
||||
]).then(() => {
|
||||
setStore("status", "complete")
|
||||
})
|
||||
@@ -351,23 +249,6 @@ function createGlobalSync() {
|
||||
bootstrapInstance(directory)
|
||||
break
|
||||
}
|
||||
case "session.created": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setStore("session", result.index, reconcile(event.properties.info))
|
||||
break
|
||||
}
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties.info)
|
||||
}),
|
||||
)
|
||||
if (!event.properties.info.parentID) {
|
||||
setStore("sessionTotal", store.sessionTotal + 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "session.updated": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (event.properties.info.time.archived) {
|
||||
@@ -379,8 +260,6 @@ function createGlobalSync() {
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (event.properties.info.parentID) break
|
||||
setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||
break
|
||||
}
|
||||
if (result.found) {
|
||||
@@ -477,10 +356,7 @@ function createGlobalSync() {
|
||||
break
|
||||
}
|
||||
case "vcs.branch.updated": {
|
||||
const next = { branch: event.properties.branch }
|
||||
setStore("vcs", next)
|
||||
const cache = vcsCache.get(directory)
|
||||
if (cache) cache.setStore("value", next)
|
||||
setStore("vcs", { branch: event.properties.branch })
|
||||
break
|
||||
}
|
||||
case "permission.asked": {
|
||||
@@ -520,44 +396,6 @@ function createGlobalSync() {
|
||||
)
|
||||
break
|
||||
}
|
||||
case "question.asked": {
|
||||
const sessionID = event.properties.sessionID
|
||||
const questions = store.question[sessionID]
|
||||
if (!questions) {
|
||||
setStore("question", sessionID, [event.properties])
|
||||
break
|
||||
}
|
||||
|
||||
const result = Binary.search(questions, event.properties.id, (q) => q.id)
|
||||
if (result.found) {
|
||||
setStore("question", sessionID, result.index, reconcile(event.properties))
|
||||
break
|
||||
}
|
||||
|
||||
setStore(
|
||||
"question",
|
||||
sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "question.replied":
|
||||
case "question.rejected": {
|
||||
const questions = store.question[event.properties.sessionID]
|
||||
if (!questions) break
|
||||
const result = Binary.search(questions, event.properties.requestID, (q) => q.id)
|
||||
if (!result.found) break
|
||||
setStore(
|
||||
"question",
|
||||
event.properties.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "lsp.updated": {
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
|
||||
@@ -47,34 +47,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
const globalSdk = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const server = useServer()
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
|
||||
const migrate = (value: unknown) => {
|
||||
if (!isRecord(value)) return value
|
||||
const sidebar = value.sidebar
|
||||
if (!isRecord(sidebar)) return value
|
||||
if (typeof sidebar.workspaces !== "boolean") return value
|
||||
return {
|
||||
...value,
|
||||
sidebar: {
|
||||
...sidebar,
|
||||
workspaces: {},
|
||||
workspacesDefault: sidebar.workspaces,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const target = Persist.global("layout", ["layout.v6"])
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
{ ...target, migrate },
|
||||
Persist.global("layout", ["layout.v6"]),
|
||||
createStore({
|
||||
sidebar: {
|
||||
opened: false,
|
||||
width: 344,
|
||||
workspaces: {} as Record<string, boolean>,
|
||||
workspacesDefault: false,
|
||||
width: 280,
|
||||
},
|
||||
terminal: {
|
||||
height: 280,
|
||||
@@ -326,16 +304,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
resize(width: number) {
|
||||
setStore("sidebar", "width", width)
|
||||
},
|
||||
workspaces(directory: string) {
|
||||
return createMemo(() => store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false)
|
||||
},
|
||||
setWorkspaces(directory: string, value: boolean) {
|
||||
setStore("sidebar", "workspaces", directory, value)
|
||||
},
|
||||
toggleWorkspaces(directory: string) {
|
||||
const current = store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false
|
||||
setStore("sidebar", "workspaces", directory, !current)
|
||||
},
|
||||
},
|
||||
terminal: {
|
||||
height: createMemo(() => store.terminal.height),
|
||||
|
||||
@@ -5,9 +5,6 @@ export type Platform = {
|
||||
/** Platform discriminator */
|
||||
platform: "web" | "desktop"
|
||||
|
||||
/** Desktop OS (Tauri only) */
|
||||
os?: "macos" | "windows" | "linux"
|
||||
|
||||
/** App version */
|
||||
version?: string
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
createStore({
|
||||
list: [] as string[],
|
||||
projects: {} as Record<string, StoredProject[]>,
|
||||
lastProject: {} as Record<string, string>,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -198,16 +197,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
result.splice(toIndex, 0, item)
|
||||
setStore("projects", key, result)
|
||||
},
|
||||
last() {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
return store.lastProject[key]
|
||||
},
|
||||
touch(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
setStore("lastProject", key, directory)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const sdk = useSDK()
|
||||
const [store, setStore] = globalSync.child(sdk.directory)
|
||||
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
|
||||
const chunk = 400
|
||||
const chunk = 200
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const inflightDiff = new Map<string, Promise<void>>()
|
||||
const inflightTodo = new Map<string, Promise<void>>()
|
||||
|
||||
@@ -5,7 +5,3 @@
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
*[data-tauri-drag-region] {
|
||||
app-region: drag;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { LocalProvider } from "@/context/local"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import type { QuestionAnswer } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const params = useParams()
|
||||
@@ -28,11 +27,6 @@ export default function Layout(props: ParentProps) {
|
||||
response: "once" | "always" | "reject"
|
||||
}) => sdk.client.permission.respond(input)
|
||||
|
||||
const replyToQuestion = (input: { requestID: string; answers: QuestionAnswer[] }) =>
|
||||
sdk.client.question.reply(input)
|
||||
|
||||
const rejectQuestion = (input: { requestID: string }) => sdk.client.question.reject(input)
|
||||
|
||||
const navigateToSession = (sessionID: string) => {
|
||||
navigate(`/${params.dir}/session/${sessionID}`)
|
||||
}
|
||||
@@ -42,8 +36,6 @@ export default function Layout(props: ParentProps) {
|
||||
data={sync.data}
|
||||
directory={directory()}
|
||||
onPermissionRespond={respond}
|
||||
onQuestionReply={replyToQuestion}
|
||||
onQuestionReject={rejectQuestion}
|
||||
onNavigateToSession={navigateToSession}
|
||||
>
|
||||
<LocalProvider>{props.children}</LocalProvider>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
@@ -11,7 +12,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
|
||||
export default function Home() {
|
||||
const sync = useGlobalSync()
|
||||
@@ -24,7 +24,6 @@ export default function Home() {
|
||||
|
||||
function openProject(directory: string) {
|
||||
layout.projects.open(directory)
|
||||
server.projects.touch(directory)
|
||||
navigate(`/${base64Encode(directory)}`)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -419,6 +419,7 @@ export default function Page() {
|
||||
{
|
||||
id: "session.new",
|
||||
title: "New session",
|
||||
description: "Create a new session",
|
||||
category: "Session",
|
||||
keybind: "mod+shift+s",
|
||||
slash: "new",
|
||||
@@ -427,7 +428,7 @@ export default function Page() {
|
||||
{
|
||||
id: "file.open",
|
||||
title: "Open file",
|
||||
description: "Search files and commands",
|
||||
description: "Search and open a file",
|
||||
category: "File",
|
||||
keybind: "mod+p",
|
||||
slash: "open",
|
||||
@@ -436,7 +437,7 @@ export default function Page() {
|
||||
{
|
||||
id: "terminal.toggle",
|
||||
title: "Toggle terminal",
|
||||
description: "",
|
||||
description: "Show or hide the terminal",
|
||||
category: "View",
|
||||
keybind: "ctrl+`",
|
||||
slash: "terminal",
|
||||
@@ -445,7 +446,7 @@ export default function Page() {
|
||||
{
|
||||
id: "review.toggle",
|
||||
title: "Toggle review",
|
||||
description: "",
|
||||
description: "Show or hide the review panel",
|
||||
category: "View",
|
||||
keybind: "mod+shift+r",
|
||||
onSelect: () => view().reviewPanel.toggle(),
|
||||
@@ -884,19 +885,6 @@ export default function Page() {
|
||||
window.history.replaceState(null, "", `#${anchor(id)}`)
|
||||
}
|
||||
|
||||
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
|
||||
const root = scroller
|
||||
if (!root) {
|
||||
el.scrollIntoView({ behavior, block: "start" })
|
||||
return
|
||||
}
|
||||
|
||||
const a = el.getBoundingClientRect()
|
||||
const b = root.getBoundingClientRect()
|
||||
const top = a.top - b.top + root.scrollTop
|
||||
root.scrollTo({ top, behavior })
|
||||
}
|
||||
|
||||
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||
setActiveMessage(message)
|
||||
|
||||
@@ -908,7 +896,7 @@ export default function Page() {
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById(anchor(message.id))
|
||||
if (el) scrollToElement(el, behavior)
|
||||
if (el) el.scrollIntoView({ behavior, block: "start" })
|
||||
})
|
||||
|
||||
updateHash(message.id)
|
||||
@@ -916,7 +904,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const el = document.getElementById(anchor(message.id))
|
||||
if (el) scrollToElement(el, behavior)
|
||||
if (el) el.scrollIntoView({ behavior, block: "start" })
|
||||
updateHash(message.id)
|
||||
}
|
||||
|
||||
@@ -968,7 +956,7 @@ export default function Page() {
|
||||
|
||||
const hashTarget = document.getElementById(hash)
|
||||
if (hashTarget) {
|
||||
scrollToElement(hashTarget, "auto")
|
||||
hashTarget.scrollIntoView({ behavior: "auto", block: "start" })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1091,7 +1079,7 @@ export default function Page() {
|
||||
file.load(path)
|
||||
}}
|
||||
classes={{
|
||||
root: "pb-[calc(var(--prompt-height,8rem)+24px)]",
|
||||
root: "pb-[calc(var(--prompt-height,8rem)+32px)]",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
}}
|
||||
@@ -1237,7 +1225,7 @@ export default function Page() {
|
||||
{/* Prompt input */}
|
||||
<div
|
||||
ref={(el) => (promptDock = el)}
|
||||
class="absolute inset-x-0 bottom-0 pt-12 pb-4 md:pb-6 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none"
|
||||
class="absolute inset-x-0 bottom-0 pt-12 pb-4 md:pb-8 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none"
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"dev": "vite dev --host 0.0.0.0",
|
||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev",
|
||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
||||
"build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||
"start": "vite start"
|
||||
},
|
||||
@@ -20,23 +20,18 @@
|
||||
"@opencode-ai/console-mail": "workspace:*",
|
||||
"@opencode-ai/console-resource": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.7",
|
||||
"@smithy/util-utf8": "4.2.0",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"@stripe/stripe-js": "8.6.1",
|
||||
"chart.js": "4.5.1",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
"solid-list": "0.3.0",
|
||||
"solid-stripe": "0.8.1",
|
||||
"vite": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@webgpu/types": "0.1.54",
|
||||
"typescript": "catalog:",
|
||||
"wrangler": "4.50.0"
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../ui/src/assets/images/social-share-black.png
|
||||
@@ -24,9 +24,6 @@ export function Footer() {
|
||||
<div data-slot="cell">
|
||||
<a href="/docs">Docs</a>
|
||||
</div>
|
||||
<div data-slot="cell">
|
||||
<a href="/changelog">Changelog</a>
|
||||
</div>
|
||||
<div data-slot="cell">
|
||||
<a href="/discord">Discord</a>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
.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%;
|
||||
}
|
||||
@@ -1,820 +0,0 @@
|
||||
import { createSignal, createEffect, onMount, onCleanup, Accessor } from "solid-js"
|
||||
import "./spotlight.css"
|
||||
|
||||
export interface ParticlesConfig {
|
||||
enabled: boolean
|
||||
amount: number
|
||||
size: [number, number]
|
||||
speed: number
|
||||
opacity: number
|
||||
drift: number
|
||||
}
|
||||
|
||||
export interface SpotlightConfig {
|
||||
placement: [number, number]
|
||||
color: string
|
||||
speed: number
|
||||
spread: number
|
||||
length: number
|
||||
width: number
|
||||
pulsating: false | [number, number]
|
||||
distance: number
|
||||
saturation: number
|
||||
noiseAmount: number
|
||||
distortion: number
|
||||
opacity: number
|
||||
particles: ParticlesConfig
|
||||
}
|
||||
|
||||
export const defaultConfig: SpotlightConfig = {
|
||||
placement: [0.5, -0.15],
|
||||
color: "#ffffff",
|
||||
speed: 0.8,
|
||||
spread: 0.5,
|
||||
length: 4.0,
|
||||
width: 0.15,
|
||||
pulsating: [0.95, 1.1],
|
||||
distance: 3.5,
|
||||
saturation: 0.35,
|
||||
noiseAmount: 0.15,
|
||||
distortion: 0.05,
|
||||
opacity: 0.325,
|
||||
particles: {
|
||||
enabled: true,
|
||||
amount: 70,
|
||||
size: [1.25, 1.5],
|
||||
speed: 0.75,
|
||||
opacity: 0.9,
|
||||
drift: 1.5,
|
||||
},
|
||||
}
|
||||
|
||||
export interface SpotlightAnimationState {
|
||||
time: number
|
||||
intensity: number
|
||||
pulseValue: number
|
||||
}
|
||||
|
||||
interface SpotlightProps {
|
||||
config: Accessor<SpotlightConfig>
|
||||
class?: string
|
||||
onAnimationFrame?: (state: SpotlightAnimationState) => void
|
||||
}
|
||||
|
||||
const hexToRgb = (hex: string): [number, number, number] => {
|
||||
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1]
|
||||
}
|
||||
|
||||
const getAnchorAndDir = (
|
||||
placement: [number, number],
|
||||
w: number,
|
||||
h: number,
|
||||
): { anchor: [number, number]; dir: [number, number] } => {
|
||||
const [px, py] = placement
|
||||
const outside = 0.2
|
||||
|
||||
let anchorX = px * w
|
||||
let anchorY = py * h
|
||||
let dirX = 0
|
||||
let dirY = 0
|
||||
|
||||
const centerX = 0.5
|
||||
const centerY = 0.5
|
||||
|
||||
if (py <= 0.25) {
|
||||
anchorY = -outside * h + py * h
|
||||
dirY = 1
|
||||
dirX = (centerX - px) * 0.5
|
||||
} else if (py >= 0.75) {
|
||||
anchorY = (1 + outside) * h - (1 - py) * h
|
||||
dirY = -1
|
||||
dirX = (centerX - px) * 0.5
|
||||
} else if (px <= 0.25) {
|
||||
anchorX = -outside * w + px * w
|
||||
dirX = 1
|
||||
dirY = (centerY - py) * 0.5
|
||||
} else if (px >= 0.75) {
|
||||
anchorX = (1 + outside) * w - (1 - px) * w
|
||||
dirX = -1
|
||||
dirY = (centerY - py) * 0.5
|
||||
} else {
|
||||
dirY = 1
|
||||
}
|
||||
|
||||
const len = Math.sqrt(dirX * dirX + dirY * dirY)
|
||||
if (len > 0) {
|
||||
dirX /= len
|
||||
dirY /= len
|
||||
}
|
||||
|
||||
return { anchor: [anchorX, anchorY], dir: [dirX, dirY] }
|
||||
}
|
||||
|
||||
interface UniformData {
|
||||
iTime: number
|
||||
iResolution: [number, number]
|
||||
lightPos: [number, number]
|
||||
lightDir: [number, number]
|
||||
color: [number, number, number]
|
||||
speed: number
|
||||
lightSpread: number
|
||||
lightLength: number
|
||||
sourceWidth: number
|
||||
pulsating: number
|
||||
pulsatingMin: number
|
||||
pulsatingMax: number
|
||||
fadeDistance: number
|
||||
saturation: number
|
||||
noiseAmount: number
|
||||
distortion: number
|
||||
particlesEnabled: number
|
||||
particleAmount: number
|
||||
particleSizeMin: number
|
||||
particleSizeMax: number
|
||||
particleSpeed: number
|
||||
particleOpacity: number
|
||||
particleDrift: number
|
||||
}
|
||||
|
||||
const WGSL_SHADER = `
|
||||
struct Uniforms {
|
||||
iTime: f32,
|
||||
_pad0: f32,
|
||||
iResolution: vec2<f32>,
|
||||
lightPos: vec2<f32>,
|
||||
lightDir: vec2<f32>,
|
||||
color: vec3<f32>,
|
||||
speed: f32,
|
||||
lightSpread: f32,
|
||||
lightLength: f32,
|
||||
sourceWidth: f32,
|
||||
pulsating: f32,
|
||||
pulsatingMin: f32,
|
||||
pulsatingMax: f32,
|
||||
fadeDistance: f32,
|
||||
saturation: f32,
|
||||
noiseAmount: f32,
|
||||
distortion: f32,
|
||||
particlesEnabled: f32,
|
||||
particleAmount: f32,
|
||||
particleSizeMin: f32,
|
||||
particleSizeMax: f32,
|
||||
particleSpeed: f32,
|
||||
particleOpacity: f32,
|
||||
particleDrift: f32,
|
||||
_pad1: f32,
|
||||
_pad2: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) vUv: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
||||
var positions = array<vec2<f32>, 3>(
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>(3.0, -1.0),
|
||||
vec2<f32>(-1.0, 3.0)
|
||||
);
|
||||
|
||||
var output: VertexOutput;
|
||||
let pos = positions[vertexIndex];
|
||||
output.position = vec4<f32>(pos, 0.0, 1.0);
|
||||
output.vUv = pos * 0.5 + 0.5;
|
||||
return output;
|
||||
}
|
||||
|
||||
fn hash(p: vec2<f32>) -> f32 {
|
||||
let p3 = fract(p.xyx * 0.1031);
|
||||
return fract((p3.x + p3.y) * p3.z + dot(p3, p3.yzx + 33.33));
|
||||
}
|
||||
|
||||
fn hash2(p: vec2<f32>) -> vec2<f32> {
|
||||
let n = sin(dot(p, vec2<f32>(41.0, 289.0)));
|
||||
return fract(vec2<f32>(n * 262144.0, n * 32768.0));
|
||||
}
|
||||
|
||||
fn fastNoise(st: vec2<f32>) -> f32 {
|
||||
return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
fn lightStrengthCombined(lightSource: vec2<f32>, lightRefDirection: vec2<f32>, coord: vec2<f32>) -> f32 {
|
||||
let sourceToCoord = coord - lightSource;
|
||||
let distSq = dot(sourceToCoord, sourceToCoord);
|
||||
let distance = sqrt(distSq);
|
||||
|
||||
let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
|
||||
let maxDistance = max(baseSize * uniforms.lightLength, 0.001);
|
||||
if (distance > maxDistance) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let invDist = 1.0 / max(distance, 0.001);
|
||||
let dirNorm = sourceToCoord * invDist;
|
||||
let cosAngle = dot(dirNorm, lightRefDirection);
|
||||
|
||||
if (cosAngle < 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let side = dot(dirNorm, vec2<f32>(-lightRefDirection.y, lightRefDirection.x));
|
||||
let time = uniforms.iTime;
|
||||
let speed = uniforms.speed;
|
||||
|
||||
let asymNoise = fastNoise(vec2<f32>(side * 6.0 + time * 0.12, distance * 0.004 + cosAngle * 2.0));
|
||||
let asymShift = (asymNoise - 0.5) * uniforms.distortion * 0.6;
|
||||
|
||||
let distortPhase = time * 1.4 + distance * 0.006 + cosAngle * 4.5 + side * 1.7;
|
||||
let distortedAngle = cosAngle + uniforms.distortion * sin(distortPhase) * 0.22 + asymShift;
|
||||
|
||||
let flickerSeed = cosAngle * 9.0 + side * 4.0 + time * speed * 0.35;
|
||||
let flicker = 0.86 + fastNoise(vec2<f32>(flickerSeed, distance * 0.01)) * 0.28;
|
||||
|
||||
let asymSpread = max(uniforms.lightSpread * (0.9 + (asymNoise - 0.5) * 0.25), 0.001);
|
||||
let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / asymSpread);
|
||||
let lengthFalloff = clamp(1.0 - distance / maxDistance, 0.0, 1.0);
|
||||
|
||||
let fadeMaxDist = max(baseSize * uniforms.fadeDistance, 0.001);
|
||||
let fadeFalloff = clamp((fadeMaxDist - distance) / fadeMaxDist, 0.0, 1.0);
|
||||
|
||||
var pulse: f32 = 1.0;
|
||||
if (uniforms.pulsating > 0.5) {
|
||||
let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5;
|
||||
let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5;
|
||||
pulse = pulseCenter + pulseAmplitude * sin(time * speed * 3.0);
|
||||
}
|
||||
|
||||
let timeSpeed = time * speed;
|
||||
let wave = 0.5
|
||||
+ 0.25 * sin(cosAngle * 28.0 + side * 8.0 + timeSpeed * 1.2)
|
||||
+ 0.18 * cos(cosAngle * 22.0 - timeSpeed * 0.95 + side * 6.0)
|
||||
+ 0.12 * sin(cosAngle * 35.0 + timeSpeed * 1.6 + asymNoise * 3.0);
|
||||
let minStrength = 0.14 + asymNoise * 0.06;
|
||||
let baseStrength = max(clamp(wave * (0.85 + asymNoise * 0.3), 0.0, 1.0), minStrength);
|
||||
|
||||
let lightStrength = baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse * flicker;
|
||||
let ambientLight = (0.06 + asymNoise * 0.04) * lengthFalloff * fadeFalloff * spreadFactor;
|
||||
|
||||
return max(lightStrength, ambientLight);
|
||||
}
|
||||
|
||||
fn particle(coord: vec2<f32>, particlePos: vec2<f32>, size: f32) -> f32 {
|
||||
let delta = coord - particlePos;
|
||||
let distSq = dot(delta, delta);
|
||||
let sizeSq = size * size;
|
||||
|
||||
if (distSq > sizeSq * 9.0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let d = sqrt(distSq);
|
||||
let core = smoothstep(size, size * 0.35, d);
|
||||
let glow = smoothstep(size * 3.0, 0.0, d) * 0.55;
|
||||
return core + glow;
|
||||
}
|
||||
|
||||
fn renderParticles(coord: vec2<f32>, lightSource: vec2<f32>, lightDir: vec2<f32>) -> f32 {
|
||||
if (uniforms.particlesEnabled < 0.5 || uniforms.particleAmount < 1.0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
var particleSum: f32 = 0.0;
|
||||
let particleCount = i32(uniforms.particleAmount);
|
||||
let time = uniforms.iTime * uniforms.particleSpeed;
|
||||
let perpDir = vec2<f32>(-lightDir.y, lightDir.x);
|
||||
let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
|
||||
let maxDist = max(baseSize * uniforms.lightLength, 1.0);
|
||||
let spreadScale = uniforms.lightSpread * baseSize * 0.65;
|
||||
let coneHalfWidth = uniforms.lightSpread * baseSize * 0.55;
|
||||
|
||||
for (var i: i32 = 0; i < particleCount; i = i + 1) {
|
||||
let fi = f32(i);
|
||||
let seed = vec2<f32>(fi * 127.1, fi * 311.7);
|
||||
let rnd = hash2(seed);
|
||||
|
||||
let lifeDuration = 2.0 + hash(seed + vec2<f32>(19.0, 73.0)) * 3.0;
|
||||
let lifeOffset = hash(seed + vec2<f32>(91.0, 37.0)) * lifeDuration;
|
||||
let lifeProgress = fract((time + lifeOffset) / lifeDuration);
|
||||
|
||||
let fadeIn = smoothstep(0.0, 0.2, lifeProgress);
|
||||
let fadeOut = 1.0 - smoothstep(0.8, 1.0, lifeProgress);
|
||||
let lifeFade = fadeIn * fadeOut;
|
||||
if (lifeFade < 0.01) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let alongLight = rnd.x * maxDist * 0.8;
|
||||
let perpOffset = (rnd.y - 0.5) * spreadScale;
|
||||
|
||||
let floatPhase = rnd.y * 6.28318 + fi * 0.37;
|
||||
let floatSpeed = 0.35 + rnd.x * 0.9;
|
||||
let drift = vec2<f32>(
|
||||
sin(time * floatSpeed + floatPhase),
|
||||
cos(time * floatSpeed * 0.85 + floatPhase * 1.3)
|
||||
) * uniforms.particleDrift * baseSize * 0.08;
|
||||
|
||||
let wobble = vec2<f32>(
|
||||
sin(time * 1.4 + floatPhase * 2.1),
|
||||
cos(time * 1.1 + floatPhase * 1.6)
|
||||
) * uniforms.particleDrift * baseSize * 0.03;
|
||||
|
||||
let flowOffset = (rnd.x - 0.5) * baseSize * 0.12 + fract(time * 0.06 + rnd.y) * baseSize * 0.1;
|
||||
|
||||
let basePos = lightSource + lightDir * (alongLight + flowOffset) + perpDir * perpOffset + drift + wobble;
|
||||
|
||||
let toParticle = basePos - lightSource;
|
||||
let projLen = dot(toParticle, lightDir);
|
||||
if (projLen < 0.0 || projLen > maxDist) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sideDist = abs(dot(toParticle, perpDir));
|
||||
if (sideDist > coneHalfWidth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let size = mix(uniforms.particleSizeMin, uniforms.particleSizeMax, rnd.x);
|
||||
let twinkle = 0.7 + 0.3 * sin(time * (1.5 + rnd.y * 2.0) + floatPhase);
|
||||
let distFade = 1.0 - smoothstep(maxDist * 0.2, maxDist * 0.95, projLen);
|
||||
if (distFade < 0.01) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let p = particle(coord, basePos, size);
|
||||
if (p > 0.0) {
|
||||
particleSum = particleSum + p * lifeFade * twinkle * distFade * uniforms.particleOpacity;
|
||||
if (particleSum >= 1.0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return min(particleSum, 1.0);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragmentMain(@builtin(position) fragCoord: vec4<f32>, @location(0) vUv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
let coord = vec2<f32>(fragCoord.x, fragCoord.y);
|
||||
|
||||
let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5;
|
||||
let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x;
|
||||
|
||||
let perpDir = vec2<f32>(-uniforms.lightDir.y, uniforms.lightDir.x);
|
||||
let adjustedLightPos = uniforms.lightPos + perpDir * widthOffset;
|
||||
|
||||
let lightValue = lightStrengthCombined(adjustedLightPos, uniforms.lightDir, coord);
|
||||
|
||||
if (lightValue < 0.001) {
|
||||
let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
|
||||
if (particles < 0.001) {
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
let particleBrightness = particles * 1.8;
|
||||
return vec4<f32>(uniforms.color * particleBrightness, particles * 0.9);
|
||||
}
|
||||
|
||||
var fragColor = vec4<f32>(lightValue, lightValue, lightValue, lightValue);
|
||||
|
||||
if (uniforms.noiseAmount > 0.01) {
|
||||
let n = fastNoise(coord * 0.5 + uniforms.iTime * 0.5);
|
||||
let grain = mix(1.0, n, uniforms.noiseAmount * 0.5);
|
||||
fragColor = vec4<f32>(fragColor.rgb * grain, fragColor.a);
|
||||
}
|
||||
|
||||
let brightness = 1.0 - (coord.y / uniforms.iResolution.y);
|
||||
fragColor = vec4<f32>(
|
||||
fragColor.x * (0.15 + brightness * 0.85),
|
||||
fragColor.y * (0.35 + brightness * 0.65),
|
||||
fragColor.z * (0.55 + brightness * 0.45),
|
||||
fragColor.a
|
||||
);
|
||||
|
||||
if (abs(uniforms.saturation - 1.0) > 0.01) {
|
||||
let gray = dot(fragColor.rgb, vec3<f32>(0.299, 0.587, 0.114));
|
||||
fragColor = vec4<f32>(mix(vec3<f32>(gray), fragColor.rgb, uniforms.saturation), fragColor.a);
|
||||
}
|
||||
|
||||
fragColor = vec4<f32>(fragColor.rgb * uniforms.color, fragColor.a);
|
||||
|
||||
let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
|
||||
if (particles > 0.001) {
|
||||
let particleBrightness = particles * 1.8;
|
||||
fragColor = vec4<f32>(fragColor.rgb + uniforms.color * particleBrightness, max(fragColor.a, particles * 0.9));
|
||||
}
|
||||
|
||||
return fragColor;
|
||||
}
|
||||
`
|
||||
|
||||
const UNIFORM_BUFFER_SIZE = 144
|
||||
|
||||
function updateUniformBuffer(buffer: Float32Array, data: UniformData): void {
|
||||
buffer[0] = data.iTime
|
||||
buffer[2] = data.iResolution[0]
|
||||
buffer[3] = data.iResolution[1]
|
||||
buffer[4] = data.lightPos[0]
|
||||
buffer[5] = data.lightPos[1]
|
||||
buffer[6] = data.lightDir[0]
|
||||
buffer[7] = data.lightDir[1]
|
||||
buffer[8] = data.color[0]
|
||||
buffer[9] = data.color[1]
|
||||
buffer[10] = data.color[2]
|
||||
buffer[11] = data.speed
|
||||
buffer[12] = data.lightSpread
|
||||
buffer[13] = data.lightLength
|
||||
buffer[14] = data.sourceWidth
|
||||
buffer[15] = data.pulsating
|
||||
buffer[16] = data.pulsatingMin
|
||||
buffer[17] = data.pulsatingMax
|
||||
buffer[18] = data.fadeDistance
|
||||
buffer[19] = data.saturation
|
||||
buffer[20] = data.noiseAmount
|
||||
buffer[21] = data.distortion
|
||||
buffer[22] = data.particlesEnabled
|
||||
buffer[23] = data.particleAmount
|
||||
buffer[24] = data.particleSizeMin
|
||||
buffer[25] = data.particleSizeMax
|
||||
buffer[26] = data.particleSpeed
|
||||
buffer[27] = data.particleOpacity
|
||||
buffer[28] = data.particleDrift
|
||||
}
|
||||
|
||||
export default function Spotlight(props: SpotlightProps) {
|
||||
let containerRef: HTMLDivElement | undefined
|
||||
let canvasRef: HTMLCanvasElement | null = null
|
||||
let deviceRef: GPUDevice | null = null
|
||||
let contextRef: GPUCanvasContext | null = null
|
||||
let pipelineRef: GPURenderPipeline | null = null
|
||||
let uniformBufferRef: GPUBuffer | null = null
|
||||
let bindGroupRef: GPUBindGroup | null = null
|
||||
let animationIdRef: number | null = null
|
||||
let cleanupFunctionRef: (() => void) | null = null
|
||||
let uniformDataRef: UniformData | null = null
|
||||
let uniformArrayRef: Float32Array | null = null
|
||||
let configRef: SpotlightConfig = props.config()
|
||||
let frameCount = 0
|
||||
|
||||
const [isVisible, setIsVisible] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
configRef = props.config()
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (!containerRef) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
setIsVisible(entry.isIntersecting)
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
)
|
||||
|
||||
observer.observe(containerRef)
|
||||
|
||||
onCleanup(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const visible = isVisible()
|
||||
const config = props.config()
|
||||
if (!visible || !containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
if (cleanupFunctionRef) {
|
||||
cleanupFunctionRef()
|
||||
cleanupFunctionRef = null
|
||||
}
|
||||
|
||||
const initializeWebGPU = async () => {
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!navigator.gpu) {
|
||||
console.warn("WebGPU is not supported in this browser")
|
||||
return
|
||||
}
|
||||
|
||||
const adapter = await navigator.gpu.requestAdapter({
|
||||
powerPreference: "high-performance",
|
||||
})
|
||||
if (!adapter) {
|
||||
console.warn("Failed to get WebGPU adapter")
|
||||
return
|
||||
}
|
||||
|
||||
const device = await adapter.requestDevice()
|
||||
deviceRef = device
|
||||
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.style.width = "100%"
|
||||
canvas.style.height = "100%"
|
||||
canvasRef = canvas
|
||||
|
||||
while (containerRef.firstChild) {
|
||||
containerRef.removeChild(containerRef.firstChild)
|
||||
}
|
||||
containerRef.appendChild(canvas)
|
||||
|
||||
const context = canvas.getContext("webgpu")
|
||||
if (!context) {
|
||||
console.warn("Failed to get WebGPU context")
|
||||
return
|
||||
}
|
||||
contextRef = context
|
||||
|
||||
const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
|
||||
context.configure({
|
||||
device,
|
||||
format: presentationFormat,
|
||||
alphaMode: "premultiplied",
|
||||
})
|
||||
|
||||
const shaderModule = device.createShaderModule({
|
||||
code: WGSL_SHADER,
|
||||
})
|
||||
|
||||
const uniformBuffer = device.createBuffer({
|
||||
size: UNIFORM_BUFFER_SIZE,
|
||||
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||
})
|
||||
uniformBufferRef = uniformBuffer
|
||||
|
||||
const bindGroupLayout = device.createBindGroupLayout({
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
||||
buffer: { type: "uniform" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const bindGroup = device.createBindGroup({
|
||||
layout: bindGroupLayout,
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: { buffer: uniformBuffer },
|
||||
},
|
||||
],
|
||||
})
|
||||
bindGroupRef = bindGroup
|
||||
|
||||
const pipelineLayout = device.createPipelineLayout({
|
||||
bindGroupLayouts: [bindGroupLayout],
|
||||
})
|
||||
|
||||
const pipeline = device.createRenderPipeline({
|
||||
layout: pipelineLayout,
|
||||
vertex: {
|
||||
module: shaderModule,
|
||||
entryPoint: "vertexMain",
|
||||
},
|
||||
fragment: {
|
||||
module: shaderModule,
|
||||
entryPoint: "fragmentMain",
|
||||
targets: [
|
||||
{
|
||||
format: presentationFormat,
|
||||
blend: {
|
||||
color: {
|
||||
srcFactor: "src-alpha",
|
||||
dstFactor: "one-minus-src-alpha",
|
||||
operation: "add",
|
||||
},
|
||||
alpha: {
|
||||
srcFactor: "one",
|
||||
dstFactor: "one-minus-src-alpha",
|
||||
operation: "add",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: "triangle-list",
|
||||
},
|
||||
})
|
||||
pipelineRef = pipeline
|
||||
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const w = wCSS * dpr
|
||||
const h = hCSS * dpr
|
||||
const { anchor, dir } = getAnchorAndDir(config.placement, w, h)
|
||||
|
||||
uniformDataRef = {
|
||||
iTime: 0,
|
||||
iResolution: [w, h],
|
||||
lightPos: anchor,
|
||||
lightDir: dir,
|
||||
color: hexToRgb(config.color),
|
||||
speed: config.speed,
|
||||
lightSpread: config.spread,
|
||||
lightLength: config.length,
|
||||
sourceWidth: config.width,
|
||||
pulsating: config.pulsating !== false ? 1.0 : 0.0,
|
||||
pulsatingMin: config.pulsating !== false ? config.pulsating[0] : 1.0,
|
||||
pulsatingMax: config.pulsating !== false ? config.pulsating[1] : 1.0,
|
||||
fadeDistance: config.distance,
|
||||
saturation: config.saturation,
|
||||
noiseAmount: config.noiseAmount,
|
||||
distortion: config.distortion,
|
||||
particlesEnabled: config.particles.enabled ? 1.0 : 0.0,
|
||||
particleAmount: config.particles.amount,
|
||||
particleSizeMin: config.particles.size[0],
|
||||
particleSizeMax: config.particles.size[1],
|
||||
particleSpeed: config.particles.speed,
|
||||
particleOpacity: config.particles.opacity,
|
||||
particleDrift: config.particles.drift,
|
||||
}
|
||||
|
||||
const updatePlacement = () => {
|
||||
if (!containerRef || !canvasRef || !uniformDataRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const w = Math.floor(wCSS * dpr)
|
||||
const h = Math.floor(hCSS * dpr)
|
||||
|
||||
canvasRef.width = w
|
||||
canvasRef.height = h
|
||||
|
||||
uniformDataRef.iResolution = [w, h]
|
||||
|
||||
const { anchor, dir } = getAnchorAndDir(configRef.placement, w, h)
|
||||
uniformDataRef.lightPos = anchor
|
||||
uniformDataRef.lightDir = dir
|
||||
}
|
||||
|
||||
const loop = (t: number) => {
|
||||
if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeSeconds = t * 0.001
|
||||
uniformDataRef.iTime = timeSeconds
|
||||
frameCount++
|
||||
|
||||
if (props.onAnimationFrame && frameCount % 2 === 0) {
|
||||
const pulsatingMin = configRef.pulsating !== false ? configRef.pulsating[0] : 1.0
|
||||
const pulsatingMax = configRef.pulsating !== false ? configRef.pulsating[1] : 1.0
|
||||
const pulseCenter = (pulsatingMin + pulsatingMax) * 0.5
|
||||
const pulseAmplitude = (pulsatingMax - pulsatingMin) * 0.5
|
||||
const pulseValue =
|
||||
configRef.pulsating !== false
|
||||
? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * configRef.speed * 3.0)
|
||||
: 1.0
|
||||
|
||||
const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * configRef.speed * 1.5)
|
||||
const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * configRef.speed * 1.1)
|
||||
const intensity = Math.max((baseIntensity1 + baseIntensity2) * pulseValue, 0.55)
|
||||
|
||||
props.onAnimationFrame({
|
||||
time: timeSeconds,
|
||||
intensity,
|
||||
pulseValue: Math.max(pulseValue, 0.9),
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
if (!uniformArrayRef) {
|
||||
uniformArrayRef = new Float32Array(36)
|
||||
}
|
||||
updateUniformBuffer(uniformArrayRef, uniformDataRef)
|
||||
deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformArrayRef.buffer)
|
||||
|
||||
const commandEncoder = deviceRef.createCommandEncoder()
|
||||
|
||||
const textureView = contextRef.getCurrentTexture().createView()
|
||||
|
||||
const renderPass = commandEncoder.beginRenderPass({
|
||||
colorAttachments: [
|
||||
{
|
||||
view: textureView,
|
||||
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
||||
loadOp: "clear",
|
||||
storeOp: "store",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
renderPass.setPipeline(pipelineRef)
|
||||
renderPass.setBindGroup(0, bindGroupRef)
|
||||
renderPass.draw(3)
|
||||
renderPass.end()
|
||||
|
||||
deviceRef.queue.submit([commandEncoder.finish()])
|
||||
|
||||
animationIdRef = requestAnimationFrame(loop)
|
||||
} catch (error) {
|
||||
console.warn("WebGPU rendering error:", error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", updatePlacement)
|
||||
updatePlacement()
|
||||
animationIdRef = requestAnimationFrame(loop)
|
||||
|
||||
cleanupFunctionRef = () => {
|
||||
if (animationIdRef) {
|
||||
cancelAnimationFrame(animationIdRef)
|
||||
animationIdRef = null
|
||||
}
|
||||
|
||||
window.removeEventListener("resize", updatePlacement)
|
||||
|
||||
if (uniformBufferRef) {
|
||||
uniformBufferRef.destroy()
|
||||
uniformBufferRef = null
|
||||
}
|
||||
|
||||
if (deviceRef) {
|
||||
deviceRef.destroy()
|
||||
deviceRef = null
|
||||
}
|
||||
|
||||
if (canvasRef && canvasRef.parentNode) {
|
||||
canvasRef.parentNode.removeChild(canvasRef)
|
||||
}
|
||||
|
||||
canvasRef = null
|
||||
contextRef = null
|
||||
pipelineRef = null
|
||||
bindGroupRef = null
|
||||
uniformDataRef = null
|
||||
}
|
||||
}
|
||||
|
||||
initializeWebGPU()
|
||||
|
||||
onCleanup(() => {
|
||||
if (cleanupFunctionRef) {
|
||||
cleanupFunctionRef()
|
||||
cleanupFunctionRef = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!uniformDataRef || !containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const config = props.config()
|
||||
|
||||
uniformDataRef.color = hexToRgb(config.color)
|
||||
uniformDataRef.speed = config.speed
|
||||
uniformDataRef.lightSpread = config.spread
|
||||
uniformDataRef.lightLength = config.length
|
||||
uniformDataRef.sourceWidth = config.width
|
||||
uniformDataRef.pulsating = config.pulsating !== false ? 1.0 : 0.0
|
||||
uniformDataRef.pulsatingMin = config.pulsating !== false ? config.pulsating[0] : 1.0
|
||||
uniformDataRef.pulsatingMax = config.pulsating !== false ? config.pulsating[1] : 1.0
|
||||
uniformDataRef.fadeDistance = config.distance
|
||||
uniformDataRef.saturation = config.saturation
|
||||
uniformDataRef.noiseAmount = config.noiseAmount
|
||||
uniformDataRef.distortion = config.distortion
|
||||
uniformDataRef.particlesEnabled = config.particles.enabled ? 1.0 : 0.0
|
||||
uniformDataRef.particleAmount = config.particles.amount
|
||||
uniformDataRef.particleSizeMin = config.particles.size[0]
|
||||
uniformDataRef.particleSizeMax = config.particles.size[1]
|
||||
uniformDataRef.particleSpeed = config.particles.speed
|
||||
uniformDataRef.particleOpacity = config.particles.opacity
|
||||
uniformDataRef.particleDrift = config.particles.drift
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const { anchor, dir } = getAnchorAndDir(config.placement, wCSS * dpr, hCSS * dpr)
|
||||
uniformDataRef.lightPos = anchor
|
||||
uniformDataRef.lightDir = dir
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={`spotlight-container ${props.class ?? ""}`.trim()}
|
||||
style={{ opacity: props.config().opacity }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -9,8 +9,8 @@ export const config = {
|
||||
github: {
|
||||
repoUrl: "https://github.com/anomalyco/opencode",
|
||||
starsFormatted: {
|
||||
compact: "70K",
|
||||
full: "70,000",
|
||||
compact: "60K",
|
||||
full: "60,000",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ export const config = {
|
||||
// Static stats (used on landing page)
|
||||
stats: {
|
||||
contributors: "500",
|
||||
commits: "7,000",
|
||||
commits: "6,500",
|
||||
monthlyUsers: "650,000",
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -14,14 +14,13 @@ export const github = query(async () => {
|
||||
fetch(`${apiBaseUrl}/releases`, { headers }).then((res) => res.json()),
|
||||
fetch(`${apiBaseUrl}/contributors?per_page=1`, { headers }),
|
||||
])
|
||||
if (!Array.isArray(releases) || releases.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
const [release] = releases
|
||||
const linkHeader = contributors.headers.get("Link")
|
||||
const contributorCount = linkHeader
|
||||
? Number.parseInt(linkHeader.match(/&page=(\d+)>; rel="last"/)?.at(1) ?? "0")
|
||||
: 0
|
||||
const contributorCount = Number.parseInt(
|
||||
contributors.headers
|
||||
.get("Link")!
|
||||
.match(/&page=(\d+)>; rel="last"/)!
|
||||
.at(1)!,
|
||||
)
|
||||
return {
|
||||
stars: meta.stargazers_count,
|
||||
release: {
|
||||
|
||||
@@ -2,9 +2,6 @@ import type { APIEvent } from "@solidjs/start/server"
|
||||
import { AuthClient } from "~/context/auth"
|
||||
|
||||
export async function GET(input: APIEvent) {
|
||||
const url = new URL(input.request.url)
|
||||
const cont = url.searchParams.get("continue") ?? ""
|
||||
const callbackUrl = new URL(`./callback${cont}`, input.request.url)
|
||||
const result = await AuthClient.authorize(callbackUrl.toString(), "code")
|
||||
const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code")
|
||||
return Response.redirect(result.url, 302)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useAuthSession } from "~/context/auth"
|
||||
|
||||
export async function GET(input: APIEvent) {
|
||||
const url = new URL(input.request.url)
|
||||
|
||||
try {
|
||||
const code = url.searchParams.get("code")
|
||||
if (!code) throw new Error("No code found")
|
||||
@@ -28,7 +27,7 @@ export async function GET(input: APIEvent) {
|
||||
current: id,
|
||||
}
|
||||
})
|
||||
return redirect(url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", ""))
|
||||
return redirect("/auth")
|
||||
} catch (e: any) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
@@ -1,828 +0,0 @@
|
||||
::view-transition-group(*) {
|
||||
animation-duration: 250ms;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation-duration: 250ms;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
::view-transition-image-pair(root) {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reveal-terms {
|
||||
from {
|
||||
mask-position: 0% 200%;
|
||||
}
|
||||
to {
|
||||
mask-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hide-terms {
|
||||
from {
|
||||
mask-position: 0% 50%;
|
||||
}
|
||||
to {
|
||||
mask-position: 0% 200%;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(terms-20),
|
||||
::view-transition-old(terms-100),
|
||||
::view-transition-old(terms-200) {
|
||||
mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent);
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 100% 200%;
|
||||
animation: hide-terms 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
|
||||
}
|
||||
|
||||
::view-transition-new(terms-20),
|
||||
::view-transition-new(terms-100),
|
||||
::view-transition-new(terms-200) {
|
||||
mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent);
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 0% 200%;
|
||||
mask-size: 100% 200%;
|
||||
animation: reveal-terms 300ms cubic-bezier(0.25, 0, 0.5, 1) 50ms forwards;
|
||||
}
|
||||
|
||||
::view-transition-old(actions-20),
|
||||
::view-transition-old(actions-100),
|
||||
::view-transition-old(actions-200) {
|
||||
animation: fade-out 80ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
::view-transition-new(actions-20),
|
||||
::view-transition-new(actions-100),
|
||||
::view-transition-new(actions-200) {
|
||||
animation: fade-in-up 300ms cubic-bezier(0.16, 1, 0.3, 1) 300ms forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
::view-transition-group(card-20),
|
||||
::view-transition-group(card-100),
|
||||
::view-transition-group(card-200) {
|
||||
animation-duration: 250ms;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
[data-page="black"] {
|
||||
background: #000;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
font-family: var(--font-mono);
|
||||
color: #fff;
|
||||
|
||||
[data-component="header-logo"] {
|
||||
filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.25)) drop-shadow(0 4px 16px rgba(0, 0, 0, 0.1));
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header-light-rays {
|
||||
position: absolute;
|
||||
inset: 0 0 auto 0;
|
||||
height: 30dvh;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
[data-component="header"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-component="content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
|
||||
[data-slot="hero"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
margin-top: 40px;
|
||||
padding: 0 20px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
margin: 0;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
margin: 0;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="hero-black"] {
|
||||
margin-top: 40px;
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
max-width: 590px;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
filter: drop-shadow(0 0 20px rgba(255, 255, 255, calc(0.1 + var(--hero-black-glow-intensity, 0) * 0.15)))
|
||||
drop-shadow(0 -5px 30px rgba(255, 255, 255, calc(var(--hero-black-glow-intensity, 0) * 0.2)));
|
||||
mask-image: linear-gradient(to bottom, black, transparent);
|
||||
stroke-width: 1.5;
|
||||
|
||||
[data-slot="black-base"] {
|
||||
fill: url(#hero-black-fill-gradient);
|
||||
stroke: url(#hero-black-stroke-gradient);
|
||||
}
|
||||
|
||||
[data-slot="black-glow"] {
|
||||
fill: url(#hero-black-top-glow);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-slot="black-shimmer"] {
|
||||
fill: url(#hero-black-shimmer-gradient);
|
||||
pointer-events: none;
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="cta"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-top: -40px;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
[data-slot="heading"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
[data-slot="subheading"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 15px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
font-size: 18px;
|
||||
line-height: 160%;
|
||||
}
|
||||
}
|
||||
[data-slot="button"] {
|
||||
display: inline-flex;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
|
||||
&:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
[data-slot="back-soon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%; /* 20.8px */
|
||||
}
|
||||
[data-slot="follow-us"] {
|
||||
display: inline-flex;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-slot="pricing"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
padding: 0 20px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="pricing-card"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
background: black;
|
||||
background-clip: padding-box;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
transition: border-color 0.15s ease;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
&:hover:not(:active) {
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
[data-slot="icon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
}
|
||||
|
||||
[data-slot="price"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="period"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: "·";
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="selected-plan"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
z-index: 1;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
margin: 0 20px;
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="selected-card"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
|
||||
[data-slot="icon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
}
|
||||
|
||||
[data-slot="price"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="period"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: "·";
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="terms"] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
|
||||
li {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding-left: 16px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "▪";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="actions"] {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
|
||||
button,
|
||||
a {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-slot="cancel"] {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
transition-property: background-color, border-color;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="continue"] {
|
||||
background: rgb(255, 255, 255);
|
||||
color: rgb(0, 0, 0);
|
||||
transition: background-color 150ms cubic-bezier(0.25, 0, 0.5, 1);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="fine-print"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%; /* 20.8px */
|
||||
font-style: italic;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Subscribe page styles */
|
||||
[data-slot="subscribe-form"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
margin-top: -18px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
padding: 0 20px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[data-slot="form-card"] {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
[data-slot="plan-header"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-slot="title"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
[data-slot="icon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
isolation: isolate;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
[data-slot="price"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="period"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: "·";
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="divider"] {
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.17);
|
||||
}
|
||||
|
||||
[data-slot="section-title"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
[data-slot="checkout-form"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
[data-slot="error"] {
|
||||
color: #ff6b6b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="submit-button"] {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #000;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="charge-notice"] {
|
||||
color: #d4a500;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-slot="loading"] {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 40px 0;
|
||||
|
||||
p {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="fine-print"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
view-transition-name: fine-print;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="workspace-picker"] {
|
||||
[data-slot="workspace-list"] {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
outline: none;
|
||||
overflow-y: auto;
|
||||
max-height: 240px;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="workspace-item"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
cursor: pointer;
|
||||
|
||||
[data-slot="selected-icon"] {
|
||||
visibility: hidden;
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&[data-active="true"] {
|
||||
background: #161616;
|
||||
|
||||
[data-slot="selected-icon"] {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="footer"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
[data-slot="footer-content"] {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
span,
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-slot="github-stars"] {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
[data-slot="anomaly"] {
|
||||
display: none;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
[data-slot="anomaly-alt"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
text-decoration: none;
|
||||
margin-bottom: 24px;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
import { A, createAsync, RouteSectionProps } from "@solidjs/router"
|
||||
import { Title, Meta, Link } from "@solidjs/meta"
|
||||
import { createMemo, createSignal } from "solid-js"
|
||||
import { github } from "~/lib/github"
|
||||
import { config } from "~/config"
|
||||
import Spotlight, { defaultConfig, type SpotlightAnimationState } from "~/component/spotlight"
|
||||
import "./black.css"
|
||||
|
||||
export default function BlackLayout(props: RouteSectionProps) {
|
||||
const githubData = createAsync(() => github())
|
||||
const starCount = createMemo(() =>
|
||||
githubData()?.stars
|
||||
? new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
compactDisplay: "short",
|
||||
}).format(githubData()!.stars!)
|
||||
: config.github.starsFormatted.compact,
|
||||
)
|
||||
|
||||
const [spotlightAnimationState, setSpotlightAnimationState] = createSignal<SpotlightAnimationState>({
|
||||
time: 0,
|
||||
intensity: 0.5,
|
||||
pulseValue: 1,
|
||||
})
|
||||
|
||||
const svgLightingValues = createMemo(() => {
|
||||
const state = spotlightAnimationState()
|
||||
const t = state.time
|
||||
|
||||
const wave1 = Math.sin(t * 1.5) * 0.5 + 0.5
|
||||
const wave2 = Math.sin(t * 2.3 + 1.2) * 0.5 + 0.5
|
||||
const wave3 = Math.sin(t * 0.8 + 2.5) * 0.5 + 0.5
|
||||
|
||||
const shimmerPos = Math.sin(t * 0.7) * 0.5 + 0.5
|
||||
const glowIntensity = Math.max(state.intensity * state.pulseValue * 0.35, 0.15)
|
||||
const fillOpacity = Math.max(0.1 + wave1 * 0.08 * state.pulseValue, 0.12)
|
||||
const strokeBrightness = Math.max(55 + wave2 * 25 * state.pulseValue, 60)
|
||||
|
||||
const shimmerIntensity = Math.max(wave3 * 0.15 * state.pulseValue, 0.08)
|
||||
|
||||
return {
|
||||
glowIntensity,
|
||||
fillOpacity,
|
||||
strokeBrightness,
|
||||
shimmerPos,
|
||||
shimmerIntensity,
|
||||
}
|
||||
})
|
||||
|
||||
const svgLightingStyle = createMemo(() => {
|
||||
const values = svgLightingValues()
|
||||
return {
|
||||
"--hero-black-glow-intensity": values.glowIntensity.toFixed(3),
|
||||
"--hero-black-stroke-brightness": `${values.strokeBrightness.toFixed(0)}%`,
|
||||
} as Record<string, string>
|
||||
})
|
||||
|
||||
const handleAnimationFrame = (state: SpotlightAnimationState) => {
|
||||
setSpotlightAnimationState(state)
|
||||
}
|
||||
|
||||
const spotlightConfig = () => defaultConfig
|
||||
|
||||
return (
|
||||
<div data-page="black">
|
||||
<Title>OpenCode Black | Access all the world's best coding models</Title>
|
||||
<Meta
|
||||
name="description"
|
||||
content="Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans."
|
||||
/>
|
||||
<Link rel="canonical" href={`${config.baseUrl}/black`} />
|
||||
<Meta property="og:type" content="website" />
|
||||
<Meta property="og:url" content={`${config.baseUrl}/black`} />
|
||||
<Meta property="og:title" content="OpenCode Black | Access all the world's best coding models" />
|
||||
<Meta
|
||||
property="og:description"
|
||||
content="Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans."
|
||||
/>
|
||||
<Meta property="og:image" content="/social-share-black.png" />
|
||||
<Meta name="twitter:card" content="summary_large_image" />
|
||||
<Meta name="twitter:title" content="OpenCode Black | Access all the world's best coding models" />
|
||||
<Meta
|
||||
name="twitter:description"
|
||||
content="Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans."
|
||||
/>
|
||||
<Meta name="twitter:image" content="/social-share-black.png" />
|
||||
|
||||
<Spotlight config={spotlightConfig} class="header-spotlight" onAnimationFrame={handleAnimationFrame} />
|
||||
|
||||
<header data-component="header">
|
||||
<A href="/" data-component="header-logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="179" height="32" viewBox="0 0 179 32" fill="none">
|
||||
<title>opencode</title>
|
||||
<g clip-path="url(#clip0_3654_210259)">
|
||||
<mask
|
||||
id="mask0_3654_210259"
|
||||
style="mask-type:luminance"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="179"
|
||||
height="32"
|
||||
>
|
||||
<path d="M178.286 0H0V32H178.286V0Z" fill="white" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_3654_210259)">
|
||||
<path d="M13.7132 22.8577H4.57031V13.7148H13.7132V22.8577Z" fill="#444444" />
|
||||
<path
|
||||
d="M13.7143 9.14174H4.57143V22.856H13.7143V9.14174ZM18.2857 27.4275H0V4.57031H18.2857V27.4275Z"
|
||||
fill="#CDCDCD"
|
||||
/>
|
||||
<path d="M36.5725 22.8577H27.4297V13.7148H36.5725V22.8577Z" fill="#444444" />
|
||||
<path
|
||||
d="M27.4308 22.856H36.5737V9.14174H27.4308V22.856ZM41.1451 27.4275H27.4308V31.9989H22.8594V4.57031H41.1451V27.4275Z"
|
||||
fill="#CDCDCD"
|
||||
/>
|
||||
<path d="M64.0033 18.2852V22.8566H50.2891V18.2852H64.0033Z" fill="#444444" />
|
||||
<path
|
||||
d="M63.9967 18.2846H50.2824V22.856H63.9967V27.4275H45.7109V4.57031H63.9967V18.2846ZM50.2824 13.7132H59.4252V9.14174H50.2824V13.7132Z"
|
||||
fill="#CDCDCD"
|
||||
/>
|
||||
<path d="M82.2835 27.4291H73.1406V13.7148H82.2835V27.4291Z" fill="#444444" />
|
||||
<path
|
||||
d="M82.2846 9.14174H73.1417V27.4275H68.5703V4.57031H82.2846V9.14174ZM86.856 27.4275H82.2846V9.14174H86.856V27.4275Z"
|
||||
fill="#CDCDCD"
|
||||
/>
|
||||
<path d="M109.714 22.8577H96V13.7148H109.714V22.8577Z" fill="#444444" />
|
||||
<path
|
||||
d="M109.715 9.14174H96.0011V22.856H109.715V27.4275H91.4297V4.57031H109.715V9.14174Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M128.002 22.8577H118.859V13.7148H128.002V22.8577Z" fill="#444444" />
|
||||
<path
|
||||
d="M128.003 9.14174H118.86V22.856H128.003V9.14174ZM132.575 27.4275H114.289V4.57031H132.575V27.4275Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M150.854 22.8577H141.711V13.7148H150.854V22.8577Z" fill="#444444" />
|
||||
<path
|
||||
d="M150.855 9.14286H141.712V22.8571H150.855V9.14286ZM155.426 27.4286H137.141V4.57143H150.855V0H155.426V27.4286Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M178.285 18.2852V22.8566H164.57V18.2852H178.285Z" fill="#444444" />
|
||||
<path
|
||||
d="M164.571 9.14174V13.7132H173.714V9.14174H164.571ZM178.286 18.2846H164.571V22.856H178.286V27.4275H160V4.57031H178.286V18.2846Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3654_210259">
|
||||
<rect width="178.286" height="32" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</A>
|
||||
</header>
|
||||
<main data-component="content">
|
||||
<div data-slot="hero">
|
||||
<h1>Access all the world's best coding models</h1>
|
||||
<p>Including Claude, GPT, Gemini and more</p>
|
||||
</div>
|
||||
<div data-slot="hero-black" style={svgLightingStyle()}>
|
||||
<svg width="591" height="90" viewBox="0 0 591 90" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="hero-black-fill-gradient"
|
||||
x1="290.82"
|
||||
y1="1.57422"
|
||||
x2="290.82"
|
||||
y2="87.0326"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-stroke-gradient"
|
||||
x1="290.82"
|
||||
y1="2.03255"
|
||||
x2="290.82"
|
||||
y2="87.0325"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color={`hsl(0 0% ${svgLightingValues().strokeBrightness}%)`} />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-shimmer-gradient"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="591"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset={Math.max(0, svgLightingValues().shimmerPos - 0.12)} stop-color="transparent" />
|
||||
<stop
|
||||
offset={svgLightingValues().shimmerPos}
|
||||
stop-color={`rgba(255, 255, 255, ${svgLightingValues().shimmerIntensity})`}
|
||||
/>
|
||||
<stop offset={Math.min(1, svgLightingValues().shimmerPos + 0.12)} stop-color="transparent" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-top-glow"
|
||||
x1="290.82"
|
||||
y1="0"
|
||||
x2="290.82"
|
||||
y2="45"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color={`rgba(255, 255, 255, ${svgLightingValues().glowIntensity})`} />
|
||||
<stop offset="1" stop-color="transparent" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-shimmer-mask"
|
||||
x1="290.82"
|
||||
y1="0"
|
||||
x2="290.82"
|
||||
y2="50"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color="white" />
|
||||
<stop offset="0.8" stop-color="white" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
|
||||
<mask id="shimmer-top-mask">
|
||||
<rect x="0" y="0" width="591" height="90" fill="url(#hero-black-shimmer-mask)" />
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<path
|
||||
d="M425.56 0.75C429.464 0.750017 432.877 1.27807 435.78 2.35645C438.656 3.42455 441.138 4.86975 443.215 6.69727C445.268 8.50382 446.995 10.5587 448.394 12.8604C449.77 15.0464 450.986 17.2741 452.04 19.5439L452.357 20.2275L451.672 20.542L443.032 24.502L442.311 24.833L442.021 24.0938C441.315 22.2906 440.494 20.6079 439.557 19.0459L439.552 19.0391L439.548 19.0322C438.626 17.419 437.517 16.0443 436.223 14.9023L436.206 14.8867L436.189 14.8701C434.989 13.6697 433.518 12.7239 431.766 12.0381L431.755 12.0342V12.0332C430.111 11.3607 428.053 11.0098 425.56 11.0098C419.142 11.0098 414.433 13.4271 411.308 18.2295C408.212 23.109 406.629 29.6717 406.629 37.9805V51.6602C406.629 59.9731 408.214 66.5377 411.312 71.418C414.438 76.2157 419.145 78.6299 425.56 78.6299C428.054 78.6299 430.111 78.2782 431.756 77.6055L431.766 77.6016L432.413 77.333C433.893 76.6811 435.154 75.8593 436.206 74.873C437.512 73.644 438.625 72.2626 439.548 70.7275C440.489 69.0801 441.314 67.3534 442.021 65.5469L442.311 64.8076L443.032 65.1387L451.672 69.0986L452.348 69.4082L452.044 70.0869C450.99 72.439 449.773 74.7099 448.395 76.8994C446.995 79.1229 445.266 81.1379 443.215 82.9434C441.138 84.7708 438.656 86.2151 435.78 87.2832C432.877 88.3616 429.464 88.8896 425.56 88.8896C415.111 88.8896 407.219 85.0777 402.019 77.4004L402.016 77.3965C396.939 69.7818 394.449 58.891 394.449 44.8203C394.449 30.7495 396.939 19.8589 402.016 12.2441L402.019 12.2393C407.219 4.56202 415.111 0.75 425.56 0.75ZM29.9404 2.19043C37.2789 2.19051 43.125 4.19131 47.3799 8.2793C51.6307 12.3635 53.7305 17.8115 53.7305 24.54C53.7305 29.6953 52.4605 33.8451 49.835 36.8994L49.8359 36.9004C47.7064 39.4558 45.0331 41.367 41.835 42.6445C45.893 43.8751 49.3115 45.9006 52.0703 48.7295C55.2954 51.9546 56.8496 56.6143 56.8496 62.5801C56.8496 66.0251 56.2751 69.2753 55.1211 72.3252C53.9689 75.3702 52.3185 78.014 50.1689 80.249L50.1699 80.25C48.0996 82.4858 45.6172 84.2628 42.7314 85.582L42.7227 85.5859C39.9002 86.8312 36.8362 87.4502 33.54 87.4502H0.75V2.19043H29.9404ZM148.123 2.19043V77.1904H187.843V87.4502H136.543V2.19043H148.123ZM298.121 2.19043L298.283 2.71973L323.963 86.4805L324.261 87.4502H312.006L311.848 86.9131L304.927 63.5703H276.646L269.726 86.9131L269.566 87.4502H257.552L257.85 86.4805L283.529 2.71973L283.691 2.19043H298.121ZM539.782 2.19043V44.9209L549.845 32.2344L549.851 32.2275L549.855 32.2207L574.575 2.46094L574.801 2.19043H588.874L587.849 3.41992L558.795 38.2832L588.749 86.3027L589.464 87.4502H575.934L575.714 87.0938L550.937 46.9316L539.782 60.0947V87.4502H528.202V2.19043H539.782ZM12.3301 77.1904H30.54C35.0749 77.1904 38.5307 76.1729 40.9961 74.2305C43.4059 72.3317 44.6699 69.3811 44.6699 65.2197V60.2998C44.6699 56.2239 43.4093 53.3106 40.9961 51.4092L40.9854 51.4004C38.5207 49.3838 35.0691 48.3301 30.54 48.3301H12.3301V77.1904ZM279.485 53.3096H302.087L290.786 14.4482L279.485 53.3096ZM12.3301 38.5498H28.8604C33 38.5498 36.1378 37.6505 38.3633 35.9443C40.5339 34.2015 41.6698 31.5679 41.6699 27.9004V23.2197C41.6699 19.5455 40.5299 16.9088 38.3516 15.166C36.1272 13.3865 32.9938 12.4502 28.8604 12.4502H12.3301V38.5498Z"
|
||||
fill="url(#hero-black-fill-gradient)"
|
||||
fill-opacity={svgLightingValues().fillOpacity}
|
||||
stroke="url(#hero-black-stroke-gradient)"
|
||||
stroke-width="1.5"
|
||||
data-slot="black-base"
|
||||
/>
|
||||
|
||||
<path
|
||||
d="M425.56 0.75C429.464 0.750017 432.877 1.27807 435.78 2.35645C438.656 3.42455 441.138 4.86975 443.215 6.69727C445.268 8.50382 446.995 10.5587 448.394 12.8604C449.77 15.0464 450.986 17.2741 452.04 19.5439L452.357 20.2275L451.672 20.542L443.032 24.502L442.311 24.833L442.021 24.0938C441.315 22.2906 440.494 20.6079 439.557 19.0459L439.552 19.0391L439.548 19.0322C438.626 17.419 437.517 16.0443 436.223 14.9023L436.206 14.8867L436.189 14.8701C434.989 13.6697 433.518 12.7239 431.766 12.0381L431.755 12.0342V12.0332C430.111 11.3607 428.053 11.0098 425.56 11.0098C419.142 11.0098 414.433 13.4271 411.308 18.2295C408.212 23.109 406.629 29.6717 406.629 37.9805V51.6602C406.629 59.9731 408.214 66.5377 411.312 71.418C414.438 76.2157 419.145 78.6299 425.56 78.6299C428.054 78.6299 430.111 78.2782 431.756 77.6055L431.766 77.6016L432.413 77.333C433.893 76.6811 435.154 75.8593 436.206 74.873C437.512 73.644 438.625 72.2626 439.548 70.7275C440.489 69.0801 441.314 67.3534 442.021 65.5469L442.311 64.8076L443.032 65.1387L451.672 69.0986L452.348 69.4082L452.044 70.0869C450.99 72.439 449.773 74.7099 448.395 76.8994C446.995 79.1229 445.266 81.1379 443.215 82.9434C441.138 84.7708 438.656 86.2151 435.78 87.2832C432.877 88.3616 429.464 88.8896 425.56 88.8896C415.111 88.8896 407.219 85.0777 402.019 77.4004L402.016 77.3965C396.939 69.7818 394.449 58.891 394.449 44.8203C394.449 30.7495 396.939 19.8589 402.016 12.2441L402.019 12.2393C407.219 4.56202 415.111 0.75 425.56 0.75ZM29.9404 2.19043C37.2789 2.19051 43.125 4.19131 47.3799 8.2793C51.6307 12.3635 53.7305 17.8115 53.7305 24.54C53.7305 29.6953 52.4605 33.8451 49.835 36.8994L49.8359 36.9004C47.7064 39.4558 45.0331 41.367 41.835 42.6445C45.893 43.8751 49.3115 45.9006 52.0703 48.7295C55.2954 51.9546 56.8496 56.6143 56.8496 62.5801C56.8496 66.0251 56.2751 69.2753 55.1211 72.3252C53.9689 75.3702 52.3185 78.014 50.1689 80.249L50.1699 80.25C48.0996 82.4858 45.6172 84.2628 42.7314 85.582L42.7227 85.5859C39.9002 86.8312 36.8362 87.4502 33.54 87.4502H0.75V2.19043H29.9404ZM148.123 2.19043V77.1904H187.843V87.4502H136.543V2.19043H148.123ZM298.121 2.19043L298.283 2.71973L323.963 86.4805L324.261 87.4502H312.006L311.848 86.9131L304.927 63.5703H276.646L269.726 86.9131L269.566 87.4502H257.552L257.85 86.4805L283.529 2.71973L283.691 2.19043H298.121ZM539.782 2.19043V44.9209L549.845 32.2344L549.851 32.2275L549.855 32.2207L574.575 2.46094L574.801 2.19043H588.874L587.849 3.41992L558.795 38.2832L588.749 86.3027L589.464 87.4502H575.934L575.714 87.0938L550.937 46.9316L539.782 60.0947V87.4502H528.202V2.19043H539.782ZM12.3301 77.1904H30.54C35.0749 77.1904 38.5307 76.1729 40.9961 74.2305C43.4059 72.3317 44.6699 69.3811 44.6699 65.2197V60.2998C44.6699 56.2239 43.4093 53.3106 40.9961 51.4092L40.9854 51.4004C38.5207 49.3838 35.0691 48.3301 30.54 48.3301H12.3301V77.1904ZM279.485 53.3096H302.087L290.786 14.4482L279.485 53.3096ZM12.3301 38.5498H28.8604C33 38.5498 36.1378 37.6505 38.3633 35.9443C40.5339 34.2015 41.6698 31.5679 41.6699 27.9004V23.2197C41.6699 19.5455 40.5299 16.9088 38.3516 15.166C36.1272 13.3865 32.9938 12.4502 28.8604 12.4502H12.3301V38.5498Z"
|
||||
fill="url(#hero-black-top-glow)"
|
||||
stroke="none"
|
||||
data-slot="black-glow"
|
||||
/>
|
||||
|
||||
<path
|
||||
d="M425.56 0.75C429.464 0.750017 432.877 1.27807 435.78 2.35645C438.656 3.42455 441.138 4.86975 443.215 6.69727C445.268 8.50382 446.995 10.5587 448.394 12.8604C449.77 15.0464 450.986 17.2741 452.04 19.5439L452.357 20.2275L451.672 20.542L443.032 24.502L442.311 24.833L442.021 24.0938C441.315 22.2906 440.494 20.6079 439.557 19.0459L439.552 19.0391L439.548 19.0322C438.626 17.419 437.517 16.0443 436.223 14.9023L436.206 14.8867L436.189 14.8701C434.989 13.6697 433.518 12.7239 431.766 12.0381L431.755 12.0342V12.0332C430.111 11.3607 428.053 11.0098 425.56 11.0098C419.142 11.0098 414.433 13.4271 411.308 18.2295C408.212 23.109 406.629 29.6717 406.629 37.9805V51.6602C406.629 59.9731 408.214 66.5377 411.312 71.418C414.438 76.2157 419.145 78.6299 425.56 78.6299C428.054 78.6299 430.111 78.2782 431.756 77.6055L431.766 77.6016L432.413 77.333C433.893 76.6811 435.154 75.8593 436.206 74.873C437.512 73.644 438.625 72.2626 439.548 70.7275C440.489 69.0801 441.314 67.3534 442.021 65.5469L442.311 64.8076L443.032 65.1387L451.672 69.0986L452.348 69.4082L452.044 70.0869C450.99 72.439 449.773 74.7099 448.395 76.8994C446.995 79.1229 445.266 81.1379 443.215 82.9434C441.138 84.7708 438.656 86.2151 435.78 87.2832C432.877 88.3616 429.464 88.8896 425.56 88.8896C415.111 88.8896 407.219 85.0777 402.019 77.4004L402.016 77.3965C396.939 69.7818 394.449 58.891 394.449 44.8203C394.449 30.7495 396.939 19.8589 402.016 12.2441L402.019 12.2393C407.219 4.56202 415.111 0.75 425.56 0.75ZM29.9404 2.19043C37.2789 2.19051 43.125 4.19131 47.3799 8.2793C51.6307 12.3635 53.7305 17.8115 53.7305 24.54C53.7305 29.6953 52.4605 33.8451 49.835 36.8994L49.8359 36.9004C47.7064 39.4558 45.0331 41.367 41.835 42.6445C45.893 43.8751 49.3115 45.9006 52.0703 48.7295C55.2954 51.9546 56.8496 56.6143 56.8496 62.5801C56.8496 66.0251 56.2751 69.2753 55.1211 72.3252C53.9689 75.3702 52.3185 78.014 50.1689 80.249L50.1699 80.25C48.0996 82.4858 45.6172 84.2628 42.7314 85.582L42.7227 85.5859C39.9002 86.8312 36.8362 87.4502 33.54 87.4502H0.75V2.19043H29.9404ZM148.123 2.19043V77.1904H187.843V87.4502H136.543V2.19043H148.123ZM298.121 2.19043L298.283 2.71973L323.963 86.4805L324.261 87.4502H312.006L311.848 86.9131L304.927 63.5703H276.646L269.726 86.9131L269.566 87.4502H257.552L257.85 86.4805L283.529 2.71973L283.691 2.19043H298.121ZM539.782 2.19043V44.9209L549.845 32.2344L549.851 32.2275L549.855 32.2207L574.575 2.46094L574.801 2.19043H588.874L587.849 3.41992L558.795 38.2832L588.749 86.3027L589.464 87.4502H575.934L575.714 87.0938L550.937 46.9316L539.782 60.0947V87.4502H528.202V2.19043H539.782ZM12.3301 77.1904H30.54C35.0749 77.1904 38.5307 76.1729 40.9961 74.2305C43.4059 72.3317 44.6699 69.3811 44.6699 65.2197V60.2998C44.6699 56.2239 43.4093 53.3106 40.9961 51.4092L40.9854 51.4004C38.5207 49.3838 35.0691 48.3301 30.54 48.3301H12.3301V77.1904ZM279.485 53.3096H302.087L290.786 14.4482L279.485 53.3096ZM12.3301 38.5498H28.8604C33 38.5498 36.1378 37.6505 38.3633 35.9443C40.5339 34.2015 41.6698 31.5679 41.6699 27.9004V23.2197C41.6699 19.5455 40.5299 16.9088 38.3516 15.166C36.1272 13.3865 32.9938 12.4502 28.8604 12.4502H12.3301V38.5498Z"
|
||||
fill="url(#hero-black-shimmer-gradient)"
|
||||
stroke="none"
|
||||
data-slot="black-shimmer"
|
||||
mask="url(#shimmer-top-mask)"
|
||||
style={{ "mix-blend-mode": "overlay" }}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{props.children}
|
||||
</main>
|
||||
<footer data-component="footer">
|
||||
<div data-slot="footer-content">
|
||||
<span data-slot="anomaly">
|
||||
©{new Date().getFullYear()} <a href="https://anoma.ly">Anomaly</a>
|
||||
</span>
|
||||
<a href={config.github.repoUrl} target="_blank">
|
||||
GitHub <span data-slot="github-stars">[{starCount()}]</span>
|
||||
</a>
|
||||
<a href="/docs">Docs</a>
|
||||
<span>
|
||||
<A href="/legal/privacy-policy">Privacy</A>
|
||||
</span>
|
||||
<span>
|
||||
<A href="/legal/terms-of-service">Terms</A>
|
||||
</span>
|
||||
</div>
|
||||
<span data-slot="anomaly-alt">
|
||||
©{new Date().getFullYear()} <a href="https://anoma.ly">Anomaly</a>
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Match, Switch } from "solid-js"
|
||||
|
||||
export const plans = [
|
||||
{ id: "20", multiplier: null },
|
||||
{ id: "100", multiplier: "5x more usage than Black 20" },
|
||||
{ id: "200", multiplier: "20x more usage than Black 20" },
|
||||
] as const
|
||||
|
||||
export type PlanID = (typeof plans)[number]["id"]
|
||||
export type Plan = (typeof plans)[number]
|
||||
|
||||
export function PlanIcon(props: { plan: string }) {
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={props.plan === "20"}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Black 20 plan</title>
|
||||
<rect x="0.5" y="0.5" width="23" height="23" stroke="currentColor" />
|
||||
</svg>
|
||||
</Match>
|
||||
<Match when={props.plan === "100"}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Black 100 plan</title>
|
||||
<rect x="0.5" y="0.5" width="9" height="9" stroke="currentColor" />
|
||||
<rect x="0.5" y="14.5" width="9" height="9" stroke="currentColor" />
|
||||
<rect x="14.5" y="0.5" width="9" height="9" stroke="currentColor" />
|
||||
<rect x="14.5" y="14.5" width="9" height="9" stroke="currentColor" />
|
||||
</svg>
|
||||
</Match>
|
||||
<Match when={props.plan === "200"}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Black 200 plan</title>
|
||||
<rect x="0.5" y="0.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="0.5" y="5.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="0.5" y="10.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="0.5" y="15.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="0.5" y="20.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="5.5" y="0.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="5.5" y="5.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="5.5" y="10.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="5.5" y="15.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="5.5" y="20.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="10.5" y="0.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="10.5" y="5.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="10.5" y="10.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="10.5" y="15.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="10.5" y="20.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="15.5" y="0.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="15.5" y="5.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="15.5" y="10.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="15.5" y="15.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="15.5" y="20.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="20.5" y="0.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="20.5" y="5.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="20.5" y="10.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="20.5" y="15.5" width="3" height="3" stroke="currentColor" />
|
||||
<rect x="20.5" y="20.5" width="3" height="3" stroke="currentColor" />
|
||||
</svg>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
409
packages/console/app/src/routes/black/index.css
Normal file
409
packages/console/app/src/routes/black/index.css
Normal file
@@ -0,0 +1,409 @@
|
||||
[data-page="black"] {
|
||||
background: #000;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
font-family: var(--font-mono);
|
||||
color: #fff;
|
||||
|
||||
[data-component="header-gradient"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 288px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
|
||||
[data-component="header"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 40px;
|
||||
flex-shrink: 0;
|
||||
|
||||
/* [data-component="header-logo"] { */
|
||||
/* } */
|
||||
}
|
||||
|
||||
[data-component="content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
|
||||
[data-slot="hero-black"] {
|
||||
margin-top: 110px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="cta"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-top: -18px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
[data-slot="heading"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%; /* 28.8px */
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
[data-slot="subheading"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 15px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
font-size: 18px;
|
||||
line-height: 160%;
|
||||
}
|
||||
}
|
||||
[data-slot="button"] {
|
||||
display: inline-flex;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
|
||||
&:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
[data-slot="back-soon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%; /* 20.8px */
|
||||
}
|
||||
[data-slot="follow-us"] {
|
||||
display: inline-flex;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-slot="pricing"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
padding: 0 20px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="pricing-card"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
transition: border-color 0.15s ease;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
[data-slot="icon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
}
|
||||
|
||||
[data-slot="price"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="period"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: "·";
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="selected-plan"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
width: fit-content;
|
||||
max-width: calc(100% - 40px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
[data-slot="selected-card"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
border-radius: 4px;
|
||||
width: fit-content;
|
||||
|
||||
[data-slot="icon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
}
|
||||
|
||||
[data-slot="price"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="period"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: "·";
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="terms"] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
text-align: left;
|
||||
|
||||
li {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
padding-left: 16px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
|
||||
&::before {
|
||||
content: "▪";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="actions"] {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
|
||||
button,
|
||||
a {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-slot="cancel"] {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="continue"] {
|
||||
background: rgba(255, 255, 255, 0.17);
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="fine-print"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%; /* 20.8px */
|
||||
font-style: italic;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="footer"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
[data-slot="footer-content"] {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
span,
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-slot="github-stars"] {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
[data-slot="anomaly"] {
|
||||
display: none;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
[data-slot="anomaly-alt"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
text-decoration: none;
|
||||
margin-bottom: 24px;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,451 +0,0 @@
|
||||
import { A, createAsync, query, redirect, useParams } from "@solidjs/router"
|
||||
import { Title } from "@solidjs/meta"
|
||||
import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js"
|
||||
import { type Stripe, type PaymentMethod, loadStripe } from "@stripe/stripe-js"
|
||||
import { Elements, PaymentElement, useStripe, useElements, AddressElement } from "solid-stripe"
|
||||
import { PlanID, plans } from "../common"
|
||||
import { getActor, useAuthSession } from "~/context/auth"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { createList } from "solid-list"
|
||||
import { Modal } from "~/component/modal"
|
||||
import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
|
||||
const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record<PlanID, (typeof plans)[number]>
|
||||
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!)
|
||||
|
||||
const getWorkspaces = query(async (plan: string) => {
|
||||
"use server"
|
||||
const actor = await getActor()
|
||||
if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe/" + plan)
|
||||
return withActor(async () => {
|
||||
return Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
id: WorkspaceTable.id,
|
||||
name: WorkspaceTable.name,
|
||||
slug: WorkspaceTable.slug,
|
||||
billing: {
|
||||
customerID: BillingTable.customerID,
|
||||
paymentMethodID: BillingTable.paymentMethodID,
|
||||
paymentMethodType: BillingTable.paymentMethodType,
|
||||
paymentMethodLast4: BillingTable.paymentMethodLast4,
|
||||
subscriptionID: BillingTable.subscriptionID,
|
||||
timeSubscriptionBooked: BillingTable.timeSubscriptionBooked,
|
||||
},
|
||||
})
|
||||
.from(UserTable)
|
||||
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||
.innerJoin(BillingTable, eq(WorkspaceTable.id, BillingTable.workspaceID))
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.accountID, Actor.account()),
|
||||
isNull(WorkspaceTable.timeDeleted),
|
||||
isNull(UserTable.timeDeleted),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
}, "black.subscribe.workspaces")
|
||||
|
||||
const createSetupIntent = async (input: { plan: string; workspaceID: string }) => {
|
||||
"use server"
|
||||
const { plan, workspaceID } = input
|
||||
|
||||
if (!plan || !["20", "100", "200"].includes(plan)) return { error: "Invalid plan" }
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
|
||||
return withActor(async () => {
|
||||
const session = await useAuthSession()
|
||||
const account = session.data.account?.[session.data.current ?? ""]
|
||||
const email = account?.email
|
||||
|
||||
const customer = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
customerID: BillingTable.customerID,
|
||||
subscriptionID: BillingTable.subscriptionID,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (customer?.subscriptionID) {
|
||||
return { error: "This workspace already has a subscription" }
|
||||
}
|
||||
|
||||
let customerID = customer?.customerID
|
||||
if (!customerID) {
|
||||
const customer = await Billing.stripe().customers.create({
|
||||
email,
|
||||
metadata: {
|
||||
workspaceID,
|
||||
},
|
||||
})
|
||||
customerID = customer.id
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
customerID,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID)),
|
||||
)
|
||||
}
|
||||
|
||||
const intent = await Billing.stripe().setupIntents.create({
|
||||
customer: customerID,
|
||||
payment_method_types: ["card"],
|
||||
metadata: {
|
||||
workspaceID,
|
||||
},
|
||||
})
|
||||
|
||||
return { clientSecret: intent.client_secret ?? undefined }
|
||||
}, workspaceID)
|
||||
}
|
||||
|
||||
const bookSubscription = async (input: {
|
||||
workspaceID: string
|
||||
plan: PlanID
|
||||
paymentMethodID: string
|
||||
paymentMethodType: string
|
||||
paymentMethodLast4?: string
|
||||
}) => {
|
||||
"use server"
|
||||
return withActor(
|
||||
() =>
|
||||
Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
paymentMethodID: input.paymentMethodID,
|
||||
paymentMethodType: input.paymentMethodType,
|
||||
paymentMethodLast4: input.paymentMethodLast4,
|
||||
subscriptionPlan: input.plan,
|
||||
timeSubscriptionBooked: new Date(),
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, input.workspaceID)),
|
||||
),
|
||||
input.workspaceID,
|
||||
)
|
||||
}
|
||||
|
||||
interface SuccessData {
|
||||
plan: string
|
||||
paymentMethodType: string
|
||||
paymentMethodLast4?: string
|
||||
}
|
||||
|
||||
function Failure(props: { message: string }) {
|
||||
return (
|
||||
<div data-slot="failure">
|
||||
<p data-slot="message">Uh oh! {props.message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Success(props: SuccessData) {
|
||||
return (
|
||||
<div data-slot="success">
|
||||
<p data-slot="title">You're on the OpenCode Black waitlist</p>
|
||||
<dl data-slot="details">
|
||||
<div>
|
||||
<dt>Subscription plan</dt>
|
||||
<dd>OpenCode Black {props.plan}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Amount</dt>
|
||||
<dd>${props.plan} per month</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Payment method</dt>
|
||||
<dd>
|
||||
<Show when={props.paymentMethodLast4} fallback={<span>{props.paymentMethodType}</span>}>
|
||||
<span>
|
||||
{props.paymentMethodType} - {props.paymentMethodLast4}
|
||||
</span>
|
||||
</Show>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Date joined</dt>
|
||||
<dd>{new Date().toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p data-slot="charge-notice">Your card will be charged when your subscription is activated</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data: SuccessData) => void }) {
|
||||
const stripe = useStripe()
|
||||
const elements = useElements()
|
||||
const [error, setError] = createSignal<string | undefined>(undefined)
|
||||
const [loading, setLoading] = createSignal(false)
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault()
|
||||
if (!stripe() || !elements()) return
|
||||
|
||||
setLoading(true)
|
||||
setError(undefined)
|
||||
|
||||
const result = await elements()!.submit()
|
||||
if (result.error) {
|
||||
setError(result.error.message ?? "An error occurred")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const { error: confirmError, setupIntent } = await stripe()!.confirmSetup({
|
||||
elements: elements()!,
|
||||
confirmParams: {
|
||||
expand: ["payment_method"],
|
||||
payment_method_data: {
|
||||
allow_redisplay: "always",
|
||||
},
|
||||
},
|
||||
redirect: "if_required",
|
||||
})
|
||||
|
||||
if (confirmError) {
|
||||
setError(confirmError.message ?? "An error occurred")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
console.log(setupIntent)
|
||||
if (setupIntent?.status === "succeeded") {
|
||||
const pm = setupIntent.payment_method as PaymentMethod
|
||||
|
||||
await bookSubscription({
|
||||
workspaceID: props.workspaceID,
|
||||
plan: props.plan,
|
||||
paymentMethodID: pm.id,
|
||||
paymentMethodType: pm.type,
|
||||
paymentMethodLast4: pm.card?.last4,
|
||||
})
|
||||
|
||||
props.onSuccess({
|
||||
plan: props.plan,
|
||||
paymentMethodType: pm.type,
|
||||
paymentMethodLast4: pm.card?.last4,
|
||||
})
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} data-slot="checkout-form">
|
||||
<PaymentElement />
|
||||
<AddressElement options={{ mode: "billing" }} />
|
||||
<Show when={error()}>
|
||||
<p data-slot="error">{error()}</p>
|
||||
</Show>
|
||||
<button type="submit" disabled={loading() || !stripe() || !elements()} data-slot="submit-button">
|
||||
{loading() ? "Processing..." : `Subscribe $${props.plan}`}
|
||||
</button>
|
||||
<p data-slot="charge-notice">You will only be charged when your subscription is activated</p>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default function BlackSubscribe() {
|
||||
const params = useParams()
|
||||
const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
|
||||
const plan = planData.id
|
||||
|
||||
const workspaces = createAsync(() => getWorkspaces(plan))
|
||||
const [selectedWorkspace, setSelectedWorkspace] = createSignal<string | undefined>(undefined)
|
||||
const [success, setSuccess] = createSignal<SuccessData | undefined>(undefined)
|
||||
const [failure, setFailure] = createSignal<string | undefined>(undefined)
|
||||
const [clientSecret, setClientSecret] = createSignal<string | undefined>(undefined)
|
||||
const [stripe, setStripe] = createSignal<Stripe | undefined>(undefined)
|
||||
|
||||
// Resolve stripe promise once
|
||||
createEffect(() => {
|
||||
stripePromise.then((s) => {
|
||||
if (s) setStripe(s)
|
||||
})
|
||||
})
|
||||
|
||||
// Auto-select if only one workspace
|
||||
createEffect(() => {
|
||||
const ws = workspaces()
|
||||
if (ws?.length === 1 && !selectedWorkspace()) {
|
||||
setSelectedWorkspace(ws[0].id)
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch setup intent when workspace is selected (unless workspace already has payment method)
|
||||
createEffect(async () => {
|
||||
const id = selectedWorkspace()
|
||||
if (!id) return
|
||||
|
||||
const ws = workspaces()?.find((w) => w.id === id)
|
||||
if (ws?.billing?.subscriptionID) {
|
||||
setFailure("This workspace already has a subscription")
|
||||
return
|
||||
}
|
||||
if (ws?.billing?.paymentMethodID) {
|
||||
if (!ws?.billing?.timeSubscriptionBooked) {
|
||||
await bookSubscription({
|
||||
workspaceID: id,
|
||||
plan: planData.id,
|
||||
paymentMethodID: ws.billing.paymentMethodID!,
|
||||
paymentMethodType: ws.billing.paymentMethodType!,
|
||||
paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
|
||||
})
|
||||
}
|
||||
setSuccess({
|
||||
plan: planData.id,
|
||||
paymentMethodType: ws.billing.paymentMethodType!,
|
||||
paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const result = await createSetupIntent({ plan, workspaceID: id })
|
||||
if (result.error) {
|
||||
setFailure(result.error)
|
||||
} else if ("clientSecret" in result) {
|
||||
setClientSecret(result.clientSecret)
|
||||
}
|
||||
})
|
||||
|
||||
// Keyboard navigation for workspace picker
|
||||
const { active, setActive, onKeyDown } = createList({
|
||||
items: () => workspaces()?.map((w) => w.id) ?? [],
|
||||
initialActive: null,
|
||||
})
|
||||
|
||||
const handleSelectWorkspace = (id: string) => {
|
||||
setSelectedWorkspace(id)
|
||||
}
|
||||
|
||||
let listRef: HTMLUListElement | undefined
|
||||
|
||||
// Show workspace picker if multiple workspaces and none selected
|
||||
const showWorkspacePicker = () => {
|
||||
const ws = workspaces()
|
||||
return ws && ws.length > 1 && !selectedWorkspace()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Subscribe to OpenCode Black</Title>
|
||||
<section data-slot="subscribe-form">
|
||||
<div data-slot="form-card">
|
||||
<Switch>
|
||||
<Match when={success()}>{(data) => <Success {...data()} />}</Match>
|
||||
<Match when={failure()}>{(data) => <Failure message={data()} />}</Match>
|
||||
<Match when={true}>
|
||||
<>
|
||||
<div data-slot="plan-header">
|
||||
<p data-slot="title">Subscribe to OpenCode Black</p>
|
||||
<p data-slot="price">
|
||||
<span data-slot="amount">${planData.id}</span> <span data-slot="period">per month</span>
|
||||
<Show when={planData.multiplier}>
|
||||
<span data-slot="multiplier">{planData.multiplier}</span>
|
||||
</Show>
|
||||
</p>
|
||||
</div>
|
||||
<div data-slot="divider" />
|
||||
<p data-slot="section-title">Payment method</p>
|
||||
|
||||
<Show
|
||||
when={clientSecret() && selectedWorkspace() && stripe()}
|
||||
fallback={
|
||||
<div data-slot="loading">
|
||||
<p>{selectedWorkspace() ? "Loading payment form..." : "Select a workspace to continue"}</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Elements
|
||||
stripe={stripe()!}
|
||||
options={{
|
||||
clientSecret: clientSecret()!,
|
||||
appearance: {
|
||||
theme: "night",
|
||||
variables: {
|
||||
colorPrimary: "#ffffff",
|
||||
colorBackground: "#1a1a1a",
|
||||
colorText: "#ffffff",
|
||||
colorTextSecondary: "#999999",
|
||||
colorDanger: "#ff6b6b",
|
||||
fontFamily: "JetBrains Mono, monospace",
|
||||
borderRadius: "4px",
|
||||
spacingUnit: "4px",
|
||||
},
|
||||
rules: {
|
||||
".Input": {
|
||||
backgroundColor: "#1a1a1a",
|
||||
border: "1px solid rgba(255, 255, 255, 0.17)",
|
||||
color: "#ffffff",
|
||||
},
|
||||
".Input:focus": {
|
||||
borderColor: "rgba(255, 255, 255, 0.35)",
|
||||
boxShadow: "none",
|
||||
},
|
||||
".Label": {
|
||||
color: "rgba(255, 255, 255, 0.59)",
|
||||
fontSize: "14px",
|
||||
marginBottom: "8px",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IntentForm plan={plan} workspaceID={selectedWorkspace()!} onSuccess={setSuccess} />
|
||||
</Elements>
|
||||
</Show>
|
||||
</>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
{/* Workspace picker modal */}
|
||||
<Modal open={showWorkspacePicker() ?? false} onClose={() => {}} title="Select a workspace for this plan">
|
||||
<div data-slot="workspace-picker">
|
||||
<ul
|
||||
ref={listRef}
|
||||
data-slot="workspace-list"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && active()) {
|
||||
handleSelectWorkspace(active()!)
|
||||
} else {
|
||||
onKeyDown(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<For each={workspaces()}>
|
||||
{(workspace) => (
|
||||
<li
|
||||
data-slot="workspace-item"
|
||||
data-active={active() === workspace.id}
|
||||
onMouseEnter={() => setActive(workspace.id)}
|
||||
onClick={() => handleSelectWorkspace(workspace.id)}
|
||||
>
|
||||
<span data-slot="selected-icon">[*]</span>
|
||||
<span>{workspace.name || workspace.slug}</span>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
<p data-slot="fine-print">
|
||||
Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
|
||||
</p>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,477 +0,0 @@
|
||||
::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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
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,6 +34,7 @@
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-text);
|
||||
padding-bottom: 5rem;
|
||||
overflow-x: hidden;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--color-background: hsl(0, 9%, 7%);
|
||||
|
||||
@@ -441,8 +441,7 @@ export default function Download() {
|
||||
</li>
|
||||
<li>
|
||||
<Faq question="Can I only use OpenCode in the terminal?">
|
||||
Not anymore! OpenCode is now available as an app for your <a href="/download">desktop</a> and{" "}
|
||||
<a href="/docs/cli/#web">web</a>!
|
||||
Not anymore! OpenCode is now available as an app for your desktop.
|
||||
</Faq>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -692,8 +692,7 @@ export default function Home() {
|
||||
</li>
|
||||
<li>
|
||||
<Faq question="Can I only use OpenCode in the terminal?">
|
||||
Not anymore! OpenCode is now available as an app for your <a href="/download">desktop</a> and{" "}
|
||||
<a href="/docs/web">web</a>!
|
||||
Not anymore! OpenCode is now available as an app for your desktop.
|
||||
</Faq>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -183,12 +183,7 @@ export async function POST(input: APIEvent) {
|
||||
.set({
|
||||
customerID,
|
||||
subscriptionID,
|
||||
subscription: {
|
||||
status: "subscribed",
|
||||
coupon: couponID,
|
||||
seats: 1,
|
||||
plan: "200",
|
||||
},
|
||||
subscriptionCouponID: couponID,
|
||||
paymentMethodID: paymentMethod.id,
|
||||
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
|
||||
paymentMethodType: paymentMethod.type,
|
||||
@@ -413,7 +408,7 @@ export async function POST(input: APIEvent) {
|
||||
await Database.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(BillingTable)
|
||||
.set({ subscriptionID: null, subscription: null })
|
||||
.set({ subscriptionID: null, subscriptionCouponID: null })
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
|
||||
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
|
||||
|
||||
@@ -5,58 +5,4 @@
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
[data-slot="usage"] {
|
||||
display: flex;
|
||||
gap: var(--space-6);
|
||||
margin-top: var(--space-4);
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="usage-item"] {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
[data-slot="usage-header"] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
[data-slot="usage-label"] {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
[data-slot="usage-value"] {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
[data-slot="progress"] {
|
||||
height: 8px;
|
||||
background-color: var(--color-bg-surface);
|
||||
border-radius: var(--border-radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-slot="progress-bar"] {
|
||||
height: 100%;
|
||||
background-color: var(--color-accent);
|
||||
border-radius: var(--border-radius-sm);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
[data-slot="reset-time"] {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,10 @@
|
||||
import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router"
|
||||
import { action, useParams, useAction, useSubmission, json } from "@solidjs/router"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Show } from "solid-js"
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { Black } from "@opencode-ai/console-core/black.js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { queryBillingInfo } from "../../common"
|
||||
import styles from "./black-section.module.css"
|
||||
|
||||
const querySubscription = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
const row = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
rollingUsage: SubscriptionTable.rollingUsage,
|
||||
fixedUsage: SubscriptionTable.fixedUsage,
|
||||
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
|
||||
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
|
||||
})
|
||||
.from(SubscriptionTable)
|
||||
.where(and(eq(SubscriptionTable.workspaceID, Actor.workspace()), isNull(SubscriptionTable.timeDeleted)))
|
||||
.then((r) => r[0]),
|
||||
)
|
||||
if (!row) return null
|
||||
|
||||
return {
|
||||
rollingUsage: Black.analyzeRollingUsage({
|
||||
usage: row.rollingUsage ?? 0,
|
||||
timeUpdated: row.timeRollingUpdated ?? new Date(),
|
||||
}),
|
||||
weeklyUsage: Black.analyzeWeeklyUsage({
|
||||
usage: row.fixedUsage ?? 0,
|
||||
timeUpdated: row.timeFixedUpdated ?? new Date(),
|
||||
}),
|
||||
}
|
||||
}, workspaceID)
|
||||
}, "subscription.get")
|
||||
|
||||
function formatResetTime(seconds: number) {
|
||||
const days = Math.floor(seconds / 86400)
|
||||
if (days >= 1) {
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
return `${days} ${days === 1 ? "day" : "days"} ${hours} ${hours === 1 ? "hour" : "hours"}`
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
if (hours >= 1) return `${hours} ${hours === 1 ? "hour" : "hours"} ${minutes} ${minutes === 1 ? "minute" : "minutes"}`
|
||||
if (minutes === 0) return "a few seconds"
|
||||
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`
|
||||
}
|
||||
|
||||
const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
|
||||
"use server"
|
||||
return json(
|
||||
@@ -74,7 +26,6 @@ export function BlackSection() {
|
||||
const params = useParams()
|
||||
const sessionAction = useAction(createSessionUrl)
|
||||
const sessionSubmission = useSubmission(createSessionUrl)
|
||||
const subscription = createAsync(() => querySubscription(params.id!))
|
||||
const [store, setStore] = createStore({
|
||||
sessionRedirecting: false,
|
||||
})
|
||||
@@ -102,32 +53,6 @@ export function BlackSection() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={subscription()}>
|
||||
{(sub) => (
|
||||
<div data-slot="usage">
|
||||
<div data-slot="usage-item">
|
||||
<div data-slot="usage-header">
|
||||
<span data-slot="usage-label">5-hour Usage</span>
|
||||
<span data-slot="usage-value">{sub().rollingUsage.usagePercent}%</span>
|
||||
</div>
|
||||
<div data-slot="progress">
|
||||
<div data-slot="progress-bar" style={{ width: `${sub().rollingUsage.usagePercent}%` }} />
|
||||
</div>
|
||||
<span data-slot="reset-time">Resets in {formatResetTime(sub().rollingUsage.resetInSec)}</span>
|
||||
</div>
|
||||
<div data-slot="usage-item">
|
||||
<div data-slot="usage-header">
|
||||
<span data-slot="usage-label">Weekly Usage</span>
|
||||
<span data-slot="usage-value">{sub().weeklyUsage.usagePercent}%</span>
|
||||
</div>
|
||||
<div data-slot="progress">
|
||||
<div data-slot="progress-bar" style={{ width: `${sub().weeklyUsage.usagePercent}%` }} />
|
||||
</div>
|
||||
<span data-slot="reset-time">Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { action, json, query } from "@solidjs/router"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
import { and, Database, desc, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
@@ -95,22 +96,11 @@ export const queryBillingInfo = query(async (workspaceID: string) => {
|
||||
return withActor(async () => {
|
||||
const billing = await Billing.get()
|
||||
return {
|
||||
customerID: billing.customerID,
|
||||
paymentMethodID: billing.paymentMethodID,
|
||||
paymentMethodType: billing.paymentMethodType,
|
||||
paymentMethodLast4: billing.paymentMethodLast4,
|
||||
balance: billing.balance,
|
||||
reload: billing.reload,
|
||||
...billing,
|
||||
reloadAmount: billing.reloadAmount ?? Billing.RELOAD_AMOUNT,
|
||||
reloadAmountMin: Billing.RELOAD_AMOUNT_MIN,
|
||||
reloadTrigger: billing.reloadTrigger ?? Billing.RELOAD_TRIGGER,
|
||||
reloadTriggerMin: Billing.RELOAD_TRIGGER_MIN,
|
||||
monthlyLimit: billing.monthlyLimit,
|
||||
monthlyUsage: billing.monthlyUsage,
|
||||
timeMonthlyUsageUpdated: billing.timeMonthlyUsageUpdated,
|
||||
reloadError: billing.reloadError,
|
||||
timeReloadError: billing.timeReloadError,
|
||||
subscriptionID: billing.subscriptionID,
|
||||
}
|
||||
}, workspaceID)
|
||||
}, "billing.get")
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
import { Black, BlackData } from "@opencode-ai/console-core/black.js"
|
||||
import { BlackData } from "@opencode-ai/console-core/black.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
|
||||
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
|
||||
@@ -81,13 +81,12 @@ export async function handler(
|
||||
const isTrial = await trialLimiter?.isTrial()
|
||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
|
||||
await rateLimiter?.check()
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider ?? false, sessionId)
|
||||
const stickyProvider = await stickyTracker?.get()
|
||||
const authInfo = await authenticate(modelInfo)
|
||||
|
||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||
const providerInfo = selectProvider(
|
||||
model,
|
||||
zenData,
|
||||
authInfo,
|
||||
modelInfo,
|
||||
@@ -102,7 +101,7 @@ export async function handler(
|
||||
logger.metric({ provider: providerInfo.id })
|
||||
|
||||
const startTimestamp = Date.now()
|
||||
const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)
|
||||
const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream)
|
||||
const reqBody = JSON.stringify(
|
||||
providerInfo.modifyBody({
|
||||
...createBodyConverter(opts.format, providerInfo.format)(body),
|
||||
@@ -136,7 +135,7 @@ export async function handler(
|
||||
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||
res.status !== 404 &&
|
||||
// ie. cannot change codex model providers mid-session
|
||||
modelInfo.stickyProvider !== "strict" &&
|
||||
!modelInfo.stickyProvider &&
|
||||
modelInfo.fallbackProvider &&
|
||||
providerInfo.id !== modelInfo.fallbackProvider
|
||||
) {
|
||||
@@ -195,19 +194,17 @@ export async function handler(
|
||||
// Handle streaming response
|
||||
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
|
||||
const usageParser = providerInfo.createUsageParser()
|
||||
const binaryDecoder = providerInfo.createBinaryStreamDecoder()
|
||||
const stream = new ReadableStream({
|
||||
start(c) {
|
||||
const reader = res.body?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
let buffer = ""
|
||||
let responseLength = 0
|
||||
|
||||
function pump(): Promise<void> {
|
||||
return (
|
||||
reader?.read().then(async ({ done, value: rawValue }) => {
|
||||
reader?.read().then(async ({ done, value }) => {
|
||||
if (done) {
|
||||
logger.metric({
|
||||
response_length: responseLength,
|
||||
@@ -233,10 +230,6 @@ export async function handler(
|
||||
"timestamp.first_byte": now,
|
||||
})
|
||||
}
|
||||
|
||||
const value = binaryDecoder ? binaryDecoder(rawValue) : rawValue
|
||||
if (!value) return
|
||||
|
||||
responseLength += value.length
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
dataDumper?.provideStream(buffer)
|
||||
@@ -338,7 +331,6 @@ export async function handler(
|
||||
}
|
||||
|
||||
function selectProvider(
|
||||
reqModel: string,
|
||||
zenData: ZenData,
|
||||
authInfo: AuthInfo,
|
||||
modelInfo: ModelInfo,
|
||||
@@ -347,7 +339,7 @@ export async function handler(
|
||||
retry: RetryOptions,
|
||||
stickyProvider: string | undefined,
|
||||
) {
|
||||
const modelProvider = (() => {
|
||||
const provider = (() => {
|
||||
if (authInfo?.provider?.credentials) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
|
||||
}
|
||||
@@ -380,19 +372,18 @@ export async function handler(
|
||||
return providers[index || 0]
|
||||
})()
|
||||
|
||||
if (!modelProvider) throw new ModelError("No provider available")
|
||||
if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`)
|
||||
if (!provider) throw new ModelError("No provider available")
|
||||
if (!(provider.id in zenData.providers)) throw new ModelError(`Provider ${provider.id} not supported`)
|
||||
|
||||
return {
|
||||
...modelProvider,
|
||||
...zenData.providers[modelProvider.id],
|
||||
...provider,
|
||||
...zenData.providers[provider.id],
|
||||
...(() => {
|
||||
const format = zenData.providers[modelProvider.id].format
|
||||
const providerModel = modelProvider.model
|
||||
if (format === "anthropic") return anthropicHelper({ reqModel, providerModel })
|
||||
if (format === "google") return googleHelper({ reqModel, providerModel })
|
||||
if (format === "openai") return openaiHelper({ reqModel, providerModel })
|
||||
return oaCompatHelper({ reqModel, providerModel })
|
||||
const format = zenData.providers[provider.id].format
|
||||
if (format === "anthropic") return anthropicHelper
|
||||
if (format === "google") return googleHelper
|
||||
if (format === "openai") return openaiHelper
|
||||
return oaCompatHelper
|
||||
})(),
|
||||
}
|
||||
}
|
||||
@@ -504,28 +495,27 @@ export async function handler(
|
||||
|
||||
// Check weekly limit
|
||||
if (sub.fixedUsage && sub.timeFixedUpdated) {
|
||||
const result = Black.analyzeWeeklyUsage({
|
||||
usage: sub.fixedUsage,
|
||||
timeUpdated: sub.timeFixedUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
const week = getWeekBounds(now)
|
||||
if (sub.timeFixedUpdated >= week.start && sub.fixedUsage >= centsToMicroCents(black.fixedLimit * 100)) {
|
||||
const retryAfter = Math.ceil((week.end.getTime() - now.getTime()) / 1000)
|
||||
throw new SubscriptionError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
|
||||
retryAfter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check rolling limit
|
||||
if (sub.rollingUsage && sub.timeRollingUpdated) {
|
||||
const result = Black.analyzeRollingUsage({
|
||||
usage: sub.rollingUsage,
|
||||
timeUpdated: sub.timeRollingUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
const rollingWindowMs = black.rollingWindow * 3600 * 1000
|
||||
const windowStart = new Date(now.getTime() - rollingWindowMs)
|
||||
if (sub.timeRollingUpdated >= windowStart && sub.rollingUsage >= centsToMicroCents(black.rollingLimit * 100)) {
|
||||
const retryAfter = Math.ceil((sub.timeRollingUpdated.getTime() + rollingWindowMs - now.getTime()) / 1000)
|
||||
throw new SubscriptionError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
|
||||
retryAfter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { EventStreamCodec } from "@smithy/eventstream-codec"
|
||||
import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider"
|
||||
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
|
||||
|
||||
type Usage = {
|
||||
cache_creation?: {
|
||||
@@ -16,169 +14,65 @@ type Usage = {
|
||||
}
|
||||
}
|
||||
|
||||
export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => {
|
||||
const isBedrockModelArn = providerModel.startsWith("arn:aws:bedrock:")
|
||||
const isBedrockModelID = providerModel.startsWith("global.anthropic.")
|
||||
const isBedrock = isBedrockModelArn || isBedrockModelID
|
||||
const isSonnet = reqModel.includes("sonnet")
|
||||
return {
|
||||
format: "anthropic",
|
||||
modifyUrl: (providerApi: string, isStream?: boolean) =>
|
||||
isBedrock
|
||||
? `${providerApi}/model/${isBedrockModelArn ? encodeURIComponent(providerModel) : providerModel}/${isStream ? "invoke-with-response-stream" : "invoke"}`
|
||||
: providerApi + "/messages",
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
if (isBedrock) {
|
||||
headers.set("Authorization", `Bearer ${apiKey}`)
|
||||
} else {
|
||||
headers.set("x-api-key", apiKey)
|
||||
headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
|
||||
if (body.model.startsWith("claude-sonnet-")) {
|
||||
headers.set("anthropic-beta", "context-1m-2025-08-07")
|
||||
}
|
||||
}
|
||||
},
|
||||
modifyBody: (body: Record<string, any>) => ({
|
||||
export const anthropicHelper = {
|
||||
format: "anthropic",
|
||||
modifyUrl: (providerApi: string) => providerApi + "/messages",
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
headers.set("x-api-key", apiKey)
|
||||
headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
|
||||
if (body.model.startsWith("claude-sonnet-")) {
|
||||
headers.set("anthropic-beta", "context-1m-2025-08-07")
|
||||
}
|
||||
},
|
||||
modifyBody: (body: Record<string, any>) => {
|
||||
return {
|
||||
...body,
|
||||
...(isBedrock
|
||||
? {
|
||||
anthropic_version: "bedrock-2023-05-31",
|
||||
anthropic_beta: isSonnet ? "context-1m-2025-08-07" : undefined,
|
||||
model: undefined,
|
||||
stream: undefined,
|
||||
}
|
||||
: {
|
||||
service_tier: "standard_only",
|
||||
}),
|
||||
}),
|
||||
createBinaryStreamDecoder: () => {
|
||||
if (!isBedrock) return undefined
|
||||
service_tier: "standard_only",
|
||||
}
|
||||
},
|
||||
streamSeparator: "\n\n",
|
||||
createUsageParser: () => {
|
||||
let usage: Usage
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
const encoder = new TextEncoder()
|
||||
const codec = new EventStreamCodec(toUtf8, fromUtf8)
|
||||
let buffer = new Uint8Array(0)
|
||||
return (value: Uint8Array) => {
|
||||
const newBuffer = new Uint8Array(buffer.length + value.length)
|
||||
newBuffer.set(buffer)
|
||||
newBuffer.set(value, buffer.length)
|
||||
buffer = newBuffer
|
||||
return {
|
||||
parse: (chunk: string) => {
|
||||
const data = chunk.split("\n")[1]
|
||||
if (!data.startsWith("data: ")) return
|
||||
|
||||
const messages = []
|
||||
while (buffer.length >= 4) {
|
||||
// first 4 bytes are the total length (big-endian)
|
||||
const totalLength = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint32(0, false)
|
||||
|
||||
// wait for more chunks
|
||||
if (buffer.length < totalLength) break
|
||||
|
||||
try {
|
||||
const subView = buffer.subarray(0, totalLength)
|
||||
const decoded = codec.decode(subView)
|
||||
buffer = buffer.slice(totalLength)
|
||||
|
||||
/* Example of Bedrock data
|
||||
```
|
||||
{
|
||||
bytes: 'eyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtb3B1cy00LTUtMjAyNTExMDEiLCJpZCI6Im1zZ19iZHJrXzAxMjVGdHRGb2lkNGlwWmZ4SzZMbktxeCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo0LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjEsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMTk2MywiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MSwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjF9fX0=',
|
||||
p: '...'
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(data.slice(6))
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
Decoded bytes
|
||||
```
|
||||
{
|
||||
type: 'message_start',
|
||||
message: {
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
id: 'msg_bdrk_0125FttFoid4ipZfxK6LnKqx',
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: 4,
|
||||
cache_creation_input_tokens: 1,
|
||||
cache_read_input_tokens: 11963,
|
||||
cache_creation: [Object],
|
||||
output_tokens: 1
|
||||
}
|
||||
}
|
||||
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,
|
||||
},
|
||||
}
|
||||
```
|
||||
*/
|
||||
|
||||
/* Example of Anthropic data
|
||||
```
|
||||
event: message_delta
|
||||
data: {"type":"message_start","message":{"model":"claude-opus-4-5-20251101","id":"msg_01ETvwVWSKULxzPdkQ1xAnk2","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11543,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":11543,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}}}
|
||||
```
|
||||
*/
|
||||
if (decoded.headers[":message-type"]?.value === "event") {
|
||||
const data = decoder.decode(decoded.body, { stream: true })
|
||||
|
||||
const parsedDataResult = JSON.parse(data)
|
||||
delete parsedDataResult.p
|
||||
const 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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
},
|
||||
retrieve: () => usage,
|
||||
}
|
||||
},
|
||||
normalizeUsage: (usage: Usage) => ({
|
||||
inputTokens: usage.input_tokens ?? 0,
|
||||
outputTokens: usage.output_tokens ?? 0,
|
||||
reasoningTokens: undefined,
|
||||
cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
|
||||
cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
|
||||
cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
|
||||
}),
|
||||
} satisfies ProviderHelper
|
||||
|
||||
export function fromAnthropicRequest(body: any): CommonRequest {
|
||||
if (!body || typeof body !== "object") return body
|
||||
|
||||
@@ -26,17 +26,16 @@ type Usage = {
|
||||
thoughtsTokenCount?: number
|
||||
}
|
||||
|
||||
export const googleHelper: ProviderHelper = ({ providerModel }) => ({
|
||||
export const googleHelper = {
|
||||
format: "google",
|
||||
modifyUrl: (providerApi: string, isStream?: boolean) =>
|
||||
`${providerApi}/models/${providerModel}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
|
||||
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) =>
|
||||
`${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
headers.set("x-goog-api-key", apiKey)
|
||||
},
|
||||
modifyBody: (body: Record<string, any>) => {
|
||||
return body
|
||||
},
|
||||
createBinaryStreamDecoder: () => undefined,
|
||||
streamSeparator: "\r\n\r\n",
|
||||
createUsageParser: () => {
|
||||
let usage: Usage
|
||||
@@ -72,4 +71,4 @@ export const googleHelper: ProviderHelper = ({ providerModel }) => ({
|
||||
cacheWrite1hTokens: undefined,
|
||||
}
|
||||
},
|
||||
})
|
||||
} satisfies ProviderHelper
|
||||
|
||||
@@ -21,7 +21,7 @@ type Usage = {
|
||||
}
|
||||
}
|
||||
|
||||
export const oaCompatHelper: ProviderHelper = () => ({
|
||||
export const oaCompatHelper = {
|
||||
format: "oa-compat",
|
||||
modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
@@ -33,7 +33,6 @@ export const oaCompatHelper: ProviderHelper = () => ({
|
||||
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
||||
}
|
||||
},
|
||||
createBinaryStreamDecoder: () => undefined,
|
||||
streamSeparator: "\n\n",
|
||||
createUsageParser: () => {
|
||||
let usage: Usage
|
||||
@@ -69,7 +68,7 @@ export const oaCompatHelper: ProviderHelper = () => ({
|
||||
cacheWrite1hTokens: undefined,
|
||||
}
|
||||
},
|
||||
})
|
||||
} satisfies ProviderHelper
|
||||
|
||||
export function fromOaCompatibleRequest(body: any): CommonRequest {
|
||||
if (!body || typeof body !== "object") return body
|
||||
|
||||
@@ -12,7 +12,7 @@ type Usage = {
|
||||
total_tokens?: number
|
||||
}
|
||||
|
||||
export const openaiHelper: ProviderHelper = () => ({
|
||||
export const openaiHelper = {
|
||||
format: "openai",
|
||||
modifyUrl: (providerApi: string) => providerApi + "/responses",
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
@@ -21,7 +21,6 @@ export const openaiHelper: ProviderHelper = () => ({
|
||||
modifyBody: (body: Record<string, any>) => {
|
||||
return body
|
||||
},
|
||||
createBinaryStreamDecoder: () => undefined,
|
||||
streamSeparator: "\n\n",
|
||||
createUsageParser: () => {
|
||||
let usage: Usage
|
||||
@@ -59,7 +58,7 @@ export const openaiHelper: ProviderHelper = () => ({
|
||||
cacheWrite1hTokens: undefined,
|
||||
}
|
||||
},
|
||||
})
|
||||
} satisfies ProviderHelper
|
||||
|
||||
export function fromOpenaiRequest(body: any): CommonRequest {
|
||||
if (!body || typeof body !== "object") return body
|
||||
|
||||
@@ -33,12 +33,11 @@ export type UsageInfo = {
|
||||
cacheWrite1hTokens?: number
|
||||
}
|
||||
|
||||
export type ProviderHelper = (input: { reqModel: string; providerModel: string }) => {
|
||||
export type ProviderHelper = {
|
||||
format: ZenData.Format
|
||||
modifyUrl: (providerApi: string, isStream?: boolean) => string
|
||||
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
|
||||
modifyBody: (body: Record<string, any>) => Record<string, any>
|
||||
createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined
|
||||
streamSeparator: string
|
||||
createUsageParser: () => {
|
||||
parse: (chunk: string) => void
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
|
||||
export function createStickyTracker(stickyProvider: "strict" | "prefer" | undefined, session: string) {
|
||||
export function createStickyTracker(stickyProvider: boolean, session: string) {
|
||||
if (!stickyProvider) return
|
||||
if (!session) return
|
||||
const key = `sticky:${session}`
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"types": ["vite/client", "@webgpu/types"],
|
||||
"types": ["vite/client"],
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE `billing` ADD `time_subscription_booked` timestamp(3);
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE `billing` ADD `subscription_plan` enum('20','100','200');
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE `billing` ADD `subscription` json;
|
||||
@@ -1 +0,0 @@
|
||||
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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -358,34 +358,6 @@
|
||||
"when": 1767931290031,
|
||||
"tag": "0050_bumpy_mephistopheles",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 51,
|
||||
"version": "5",
|
||||
"when": 1768341152722,
|
||||
"tag": "0051_jazzy_green_goblin",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 52,
|
||||
"version": "5",
|
||||
"when": 1768343920467,
|
||||
"tag": "0052_aromatic_agent_zero",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -32,7 +32,6 @@
|
||||
"promote-models-to-dev": "script/promote-models.ts dev",
|
||||
"promote-models-to-prod": "script/promote-models.ts production",
|
||||
"pull-models-from-dev": "script/pull-models.ts dev",
|
||||
"pull-models-from-prod": "script/pull-models.ts production",
|
||||
"update-black": "script/update-black.ts",
|
||||
"promote-black-to-dev": "script/promote-black.ts dev",
|
||||
"promote-black-to-prod": "script/promote-black.ts production",
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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`)
|
||||
@@ -1,112 +0,0 @@
|
||||
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`)
|
||||
@@ -1,163 +0,0 @@
|
||||
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,6 +1,4 @@
|
||||
import { Billing } from "../src/billing.js"
|
||||
import { Database, eq } from "../src/drizzle/index.js"
|
||||
import { WorkspaceTable } from "../src/schema/workspace.sql.js"
|
||||
|
||||
// get input from command line
|
||||
const workspaceID = process.argv[2]
|
||||
@@ -11,19 +9,6 @@ if (!workspaceID || !dollarAmount) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// check workspace exists
|
||||
const workspace = await Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(WorkspaceTable)
|
||||
.where(eq(WorkspaceTable.id, workspaceID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (!workspace) {
|
||||
console.error("Error: Workspace not found")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const amountInDollars = parseFloat(dollarAmount)
|
||||
if (isNaN(amountInDollars) || amountInDollars <= 0) {
|
||||
console.error("Error: dollarAmount must be a positive number")
|
||||
|
||||
@@ -55,9 +55,8 @@ if (identifier.startsWith("wrk_")) {
|
||||
),
|
||||
)
|
||||
|
||||
for (const user of users) {
|
||||
await printWorkspace(user.workspaceID)
|
||||
}
|
||||
// Get all payments for these workspaces
|
||||
await Promise.all(users.map((u: { workspaceID: string }) => printWorkspace(u.workspaceID)))
|
||||
}
|
||||
|
||||
async function printWorkspace(workspaceID: string) {
|
||||
@@ -114,13 +113,8 @@ async function printWorkspace(workspaceID: string) {
|
||||
.select({
|
||||
balance: BillingTable.balance,
|
||||
customerID: BillingTable.customerID,
|
||||
reload: BillingTable.reload,
|
||||
subscriptionID: BillingTable.subscriptionID,
|
||||
subscription: {
|
||||
plan: BillingTable.subscriptionPlan,
|
||||
booked: BillingTable.timeSubscriptionBooked,
|
||||
enrichment: BillingTable.subscription,
|
||||
},
|
||||
subscriptionCouponID: BillingTable.subscriptionCouponID,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, workspace.id))
|
||||
@@ -129,16 +123,6 @@ async function printWorkspace(workspaceID: string) {
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
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],
|
||||
),
|
||||
)
|
||||
@@ -149,7 +133,6 @@ async function printWorkspace(workspaceID: string) {
|
||||
amount: PaymentTable.amount,
|
||||
paymentID: PaymentTable.paymentID,
|
||||
invoiceID: PaymentTable.invoiceID,
|
||||
customerID: PaymentTable.customerID,
|
||||
timeCreated: PaymentTable.timeCreated,
|
||||
timeRefunded: PaymentTable.timeRefunded,
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ const email = process.argv[3]
|
||||
console.log(`Onboarding workspace ${workspaceID} for email ${email}`)
|
||||
|
||||
if (!workspaceID || !email) {
|
||||
console.error("Usage: bun foo.ts <workspaceID> <email>")
|
||||
console.error("Usage: bun onboard-zen-black.ts <workspaceID> <email>")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ const existingSubscription = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ workspaceID: BillingTable.workspaceID })
|
||||
.from(BillingTable)
|
||||
.where(sql`JSON_EXTRACT(${BillingTable.subscription}, '$.id') = ${subscriptionID}`)
|
||||
.where(eq(BillingTable.subscriptionID, subscriptionID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (existingSubscription) {
|
||||
@@ -128,15 +128,10 @@ await Database.transaction(async (tx) => {
|
||||
.set({
|
||||
customerID,
|
||||
subscriptionID,
|
||||
subscriptionCouponID: couponID,
|
||||
paymentMethodID,
|
||||
paymentMethodLast4,
|
||||
paymentMethodType,
|
||||
subscription: {
|
||||
status: "subscribed",
|
||||
coupon: couponID,
|
||||
seats: 1,
|
||||
plan: "200",
|
||||
},
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
|
||||
@@ -8,25 +8,33 @@ const stage = process.argv[2]
|
||||
if (!stage) throw new Error("Stage is required")
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const PARTS = 8
|
||||
|
||||
// read the secret
|
||||
const ret = await $`bun sst secret list`.cwd(root).text()
|
||||
const lines = ret.split("\n")
|
||||
const values = Array.from({ length: PARTS }, (_, i) => {
|
||||
const value = lines
|
||||
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
|
||||
?.split("=")
|
||||
.slice(1)
|
||||
.join("=")
|
||||
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
|
||||
return value
|
||||
})
|
||||
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!value6) throw new Error("ZEN_MODELS6 not found")
|
||||
if (!value7) throw new Error("ZEN_MODELS7 not found")
|
||||
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(values.join("")))
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
|
||||
|
||||
// update the secret
|
||||
for (let i = 0; i < PARTS; i++) {
|
||||
await $`bun sst secret set ZEN_MODELS${i + 1} --stage ${stage} -- ${values[i]}`
|
||||
}
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${value6} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS7 ${value7} --stage ${stage}`
|
||||
|
||||
@@ -8,25 +8,32 @@ const stage = process.argv[2]
|
||||
if (!stage) throw new Error("Stage is required")
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const PARTS = 8
|
||||
|
||||
// read the secret
|
||||
const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()
|
||||
const lines = ret.split("\n")
|
||||
const values = Array.from({ length: PARTS }, (_, i) => {
|
||||
const value = lines
|
||||
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
|
||||
?.split("=")
|
||||
.slice(1)
|
||||
.join("=")
|
||||
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
|
||||
return value
|
||||
})
|
||||
|
||||
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!value6) throw new Error("ZEN_MODELS6 not found")
|
||||
if (!value7) throw new Error("ZEN_MODELS7 not found")
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(values.join("")))
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
|
||||
|
||||
// update the secret
|
||||
for (let i = 0; i < PARTS; i++) {
|
||||
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${values[i]}`
|
||||
}
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${value2}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${value3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${value6}`
|
||||
await $`bun sst secret set ZEN_MODELS7 ${value7}`
|
||||
|
||||
78
packages/console/core/script/remove-black.ts
Normal file
78
packages/console/core/script/remove-black.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
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,24 +7,34 @@ import { ZenData } from "../src/model"
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const models = await $`bun sst secret list`.cwd(root).text()
|
||||
const PARTS = 8
|
||||
|
||||
// read the line starting with "ZEN_MODELS"
|
||||
const lines = models.split("\n")
|
||||
const oldValues = Array.from({ length: PARTS }, (_, i) => {
|
||||
const value = lines
|
||||
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
|
||||
?.split("=")
|
||||
.slice(1)
|
||||
.join("=")
|
||||
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
|
||||
return value
|
||||
})
|
||||
const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
|
||||
const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const oldValue6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
const oldValue7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
||||
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!oldValue5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!oldValue6) throw new Error("ZEN_MODELS6 not found")
|
||||
if (!oldValue7) throw new Error("ZEN_MODELS7 not found")
|
||||
|
||||
// store the prettified json to a temp file
|
||||
const filename = `models-${Date.now()}.json`
|
||||
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
||||
await tempFile.write(JSON.stringify(JSON.parse(oldValues.join("")), null, 2))
|
||||
await tempFile.write(
|
||||
JSON.stringify(
|
||||
JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5 + oldValue6 + oldValue7),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
console.log("tempFile", tempFile.name)
|
||||
|
||||
// open temp file in vim and read the file on close
|
||||
@@ -33,11 +43,19 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
||||
ZenData.validate(JSON.parse(newValue))
|
||||
|
||||
// update the secret
|
||||
const chunk = Math.ceil(newValue.length / PARTS)
|
||||
const newValues = Array.from({ length: PARTS }, (_, i) =>
|
||||
newValue.slice(chunk * i, i === PARTS - 1 ? undefined : chunk * (i + 1)),
|
||||
)
|
||||
const chunk = Math.ceil(newValue.length / 7)
|
||||
const newValue1 = newValue.slice(0, chunk)
|
||||
const newValue2 = newValue.slice(chunk, chunk * 2)
|
||||
const newValue3 = newValue.slice(chunk * 2, chunk * 3)
|
||||
const newValue4 = newValue.slice(chunk * 3, chunk * 4)
|
||||
const newValue5 = newValue.slice(chunk * 4, chunk * 5)
|
||||
const newValue6 = newValue.slice(chunk * 5, chunk * 6)
|
||||
const newValue7 = newValue.slice(chunk * 6)
|
||||
|
||||
for (let i = 0; i < PARTS; i++) {
|
||||
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${newValues[i]}`
|
||||
}
|
||||
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${newValue5}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${newValue6}`
|
||||
await $`bun sst secret set ZEN_MODELS7 ${newValue7}`
|
||||
|
||||
@@ -25,7 +25,22 @@ export namespace Billing {
|
||||
export const get = async () => {
|
||||
return Database.use(async (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)
|
||||
.where(eq(BillingTable.workspaceID, Actor.workspace()))
|
||||
.then((r) => r[0]),
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { z } from "zod"
|
||||
import { fn } from "./util/fn"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { centsToMicroCents } from "./util/price"
|
||||
import { getWeekBounds } from "./util/date"
|
||||
|
||||
export namespace BlackData {
|
||||
const Schema = z.object({
|
||||
@@ -20,73 +18,3 @@ export namespace BlackData {
|
||||
return Schema.parse(json)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Black {
|
||||
export const analyzeRollingUsage = fn(
|
||||
z.object({
|
||||
usage: z.number().int(),
|
||||
timeUpdated: z.date(),
|
||||
}),
|
||||
({ usage, timeUpdated }) => {
|
||||
const now = new Date()
|
||||
const black = BlackData.get()
|
||||
const rollingWindowMs = black.rollingWindow * 3600 * 1000
|
||||
const rollingLimitInMicroCents = centsToMicroCents(black.rollingLimit * 100)
|
||||
const windowStart = new Date(now.getTime() - rollingWindowMs)
|
||||
if (timeUpdated < windowStart) {
|
||||
return {
|
||||
status: "ok" as const,
|
||||
resetInSec: black.rollingWindow * 3600,
|
||||
usagePercent: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const windowEnd = new Date(timeUpdated.getTime() + rollingWindowMs)
|
||||
if (usage < rollingLimitInMicroCents) {
|
||||
return {
|
||||
status: "ok" as const,
|
||||
resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000),
|
||||
usagePercent: Math.ceil(Math.min(100, (usage / rollingLimitInMicroCents) * 100)),
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: "rate-limited" as const,
|
||||
resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000),
|
||||
usagePercent: 100,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const analyzeWeeklyUsage = fn(
|
||||
z.object({
|
||||
usage: z.number().int(),
|
||||
timeUpdated: z.date(),
|
||||
}),
|
||||
({ usage, timeUpdated }) => {
|
||||
const black = BlackData.get()
|
||||
const now = new Date()
|
||||
const week = getWeekBounds(now)
|
||||
const fixedLimitInMicroCents = centsToMicroCents(black.fixedLimit * 100)
|
||||
if (timeUpdated < week.start) {
|
||||
return {
|
||||
status: "ok" as const,
|
||||
resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
|
||||
usagePercent: 0,
|
||||
}
|
||||
}
|
||||
if (usage < fixedLimitInMicroCents) {
|
||||
return {
|
||||
status: "ok" as const,
|
||||
resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
|
||||
usagePercent: Math.ceil(Math.min(100, (usage / fixedLimitInMicroCents) * 100)),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: "rate-limited" as const,
|
||||
resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
|
||||
usagePercent: 100,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export namespace ZenData {
|
||||
cost200K: ModelCostSchema.optional(),
|
||||
allowAnonymous: z.boolean().optional(),
|
||||
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
|
||||
stickyProvider: z.enum(["strict", "prefer"]).optional(),
|
||||
stickyProvider: z.boolean().optional(),
|
||||
trial: TrialSchema.optional(),
|
||||
rateLimit: z.number().optional(),
|
||||
fallbackProvider: z.string().optional(),
|
||||
@@ -74,8 +74,7 @@ export namespace ZenData {
|
||||
Resource.ZEN_MODELS4.value +
|
||||
Resource.ZEN_MODELS5.value +
|
||||
Resource.ZEN_MODELS6.value +
|
||||
Resource.ZEN_MODELS7.value +
|
||||
Resource.ZEN_MODELS8.value,
|
||||
Resource.ZEN_MODELS7.value,
|
||||
)
|
||||
return ModelsSchema.parse(json)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { bigint, boolean, index, int, json, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
@@ -21,15 +21,8 @@ export const BillingTable = mysqlTable(
|
||||
reloadError: varchar("reload_error", { length: 255 }),
|
||||
timeReloadError: utc("time_reload_error"),
|
||||
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 }),
|
||||
subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const),
|
||||
timeSubscriptionBooked: utc("time_subscription_booked"),
|
||||
subscriptionCouponID: varchar("subscription_coupon_id", { length: 28 }),
|
||||
},
|
||||
(table) => [
|
||||
...workspaceIndexes(table),
|
||||
|
||||
8
packages/console/core/sst-env.d.ts
vendored
8
packages/console/core/sst-env.d.ts
vendored
@@ -78,10 +78,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"STRIPE_PUBLISHABLE_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"STRIPE_SECRET_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -134,10 +130,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS8": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_SESSION_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
8
packages/console/function/sst-env.d.ts
vendored
8
packages/console/function/sst-env.d.ts
vendored
@@ -78,10 +78,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"STRIPE_PUBLISHABLE_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"STRIPE_SECRET_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -134,10 +130,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS8": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_SESSION_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
8
packages/console/resource/sst-env.d.ts
vendored
8
packages/console/resource/sst-env.d.ts
vendored
@@ -78,10 +78,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"STRIPE_PUBLISHABLE_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"STRIPE_SECRET_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -134,10 +130,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS8": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_SESSION_SECRET": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-dvh"></div>
|
||||
<div id="root" class="flex flex-col h-screen"></div>
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.1.25",
|
||||
"version": "1.1.18",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -41,7 +41,6 @@ semver = "1.0.27"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||
uuid = { version = "1.19.0", features = ["v4"] }
|
||||
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
gtk = "0.18.2"
|
||||
webkit2gtk = "=2.0.1"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user