Compare commits

..

7 Commits

Author SHA1 Message Date
Aiden Cline a0fd15e9e4 rm dead code 2026-01-09 17:41:36 -06:00
Aiden Cline 4f2514d9f5 rm comment 2026-01-09 17:40:32 -06:00
Aiden Cline 5d9801f206 rm unnecessary code 2026-01-09 17:39:30 -06:00
Aiden Cline a300cd6465 fix: model list 2026-01-09 17:06:13 -06:00
Aiden Cline 399699469b workin 2026-01-09 16:43:11 -06:00
Aiden Cline cacf9df24c filter out old plugin 2026-01-09 16:35:00 -06:00
Aiden Cline 2844086752 wip: codex 2026-01-09 16:27:44 -06:00
178 changed files with 1293 additions and 5121 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ jobs:
publish-tauri: publish-tauri:
needs: publish needs: publish
continue-on-error: false continue-on-error: true
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
+7 -92
View File
@@ -17,7 +17,7 @@ on:
- "packages/*/package.json" - "packages/*/package.json"
jobs: jobs:
update-linux: update:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: blacksmith-4vcpu-ubuntu-2404
env: env:
@@ -47,14 +47,14 @@ jobs:
nix flake update nix flake update
echo "✅ flake.lock updated successfully" echo "✅ flake.lock updated successfully"
- name: Update node_modules hash for x86_64-linux - name: Update node_modules hash
run: | run: |
set -euo pipefail set -euo pipefail
echo "🔄 Updating node_modules hash for x86_64-linux..." echo "🔄 Updating node_modules hash..."
nix/scripts/update-hashes.sh nix/scripts/update-hashes.sh
echo "✅ node_modules hash for x86_64-linux updated successfully" echo "✅ node_modules hash updated successfully"
- name: Commit Linux hash changes - name: Commit hash changes
env: env:
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
run: | run: |
@@ -65,7 +65,7 @@ jobs:
summarize() { summarize() {
local status="$1" local status="$1"
{ {
echo "### Nix Hash Update (x86_64-linux)" echo "### Nix Hash Update"
echo "" echo ""
echo "- ref: ${GITHUB_REF_NAME}" echo "- ref: ${GITHUB_REF_NAME}"
echo "- status: ${status}" echo "- status: ${status}"
@@ -89,92 +89,7 @@ jobs:
echo "🔗 Staging files..." echo "🔗 Staging files..."
git add "${FILES[@]}" git add "${FILES[@]}"
echo "💾 Committing changes..." echo "💾 Committing changes..."
git commit -m "Update Nix flake.lock and x86_64-linux hash" git commit -m "Update Nix flake.lock and hashes"
echo "✅ Changes committed"
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
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"
summarize "committed $(git rev-parse --short HEAD)"
update-macos:
needs: update-linux
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: macos-latest
env:
SYSTEM: aarch64-darwin
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ github.head_ref || github.ref_name }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
- name: Setup Nix
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: Pull latest changes
env:
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
git pull origin "$BRANCH"
- 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"
- name: Commit macOS hash changes
env:
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
set -euo pipefail
echo "🔍 Checking for changes in tracked Nix files..."
summarize() {
local status="$1"
{
echo "### Nix Hash Update (aarch64-darwin)"
echo ""
echo "- ref: ${GITHUB_REF_NAME}"
echo "- status: ${status}"
} >> "$GITHUB_STEP_SUMMARY"
if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY"
fi
echo "" >> "$GITHUB_STEP_SUMMARY"
}
FILES=(nix/hashes.json)
STATUS="$(git status --short -- "${FILES[@]}" || true)"
if [ -z "$STATUS" ]; then
echo "✅ No changes detected. Hash is already up to date."
summarize "no changes"
exit 0
fi
echo "📝 Changes detected:"
echo "$STATUS"
echo "🔗 Staging files..."
git add "${FILES[@]}"
echo "💾 Committing changes..."
git commit -m "Update aarch64-darwin hash"
echo "✅ Changes committed" echo "✅ Changes committed"
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
-1
View File
@@ -24,4 +24,3 @@ target
# Local dev files # Local dev files
opencode-dev opencode-dev
logs/ logs/
*.bun-build
+2 -2
View File
@@ -45,9 +45,9 @@ Desktop app issues:
#### zen #### zen
**Only** add if the issue mentions "zen" or "opencode zen" or "opencode black". **Only** add if the issue mentions "zen" or "opencode zen". Zen is our gateway for coding models. **Do not** add for other gateways or inference providers.
If the issue doesn't have "zen" or "opencode black" in it then don't add zen label If the issue doesn't have "zen" in it then don't add zen label
#### docs #### docs
+3 -3
View File
@@ -1,4 +1,4 @@
- To test opencode in `packages/opencode`, run `bun dev`. - To test opencode in the `packages/opencode` directory you can run `bun dev`
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`. - To regenerate the javascript SDK, run ./packages/sdk/js/script/build.ts
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. - ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
- The default branch in this repo is `dev`. - the default branch in this repo is `dev`
-11
View File
@@ -1,11 +0,0 @@
# Reporting Security Issues
We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/anomalyco/opencode/security/advisories/new) tab.
The team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
## Escalation
If you do not receive an acknowledgement of your report within 6 business days, you may send an email to security@anoma.ly
-4
View File
@@ -196,7 +196,3 @@
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) | | 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) | | 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) | | 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
| 2026-01-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) |
+9 -69
View File
@@ -1,71 +1,11 @@
## Style Guide ## Style Guide
- Keep things in one function unless composable or reusable - Try to keep things in one function unless composable or reusable
- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context - AVOID unnecessary destructuring of variables. instead of doing `const { a, b }
- Avoid `try`/`catch` where possible = obj` just reference it as obj.a and obj.b. this preserves context
- Avoid using the `any` type - AVOID `try`/`catch` where possible
- Prefer single word variable names where possible - AVOID `else` statements
- Use Bun APIs when possible, like `Bun.file()` - AVOID using `any` type
- AVOID `let` statements
# Avoid let statements - PREFER single word variable names where possible
- Use as many bun apis as possible like Bun.file()
We don't like `let` statements, especially combined with if/else statements.
Prefer `const`.
Good:
```ts
const foo = condition ? 1 : 2
```
Bad:
```ts
let foo
if (condition) foo = 1
else foo = 2
```
# Avoid else statements
Prefer early returns or using an `iife` to avoid else statements.
Good:
```ts
function foo() {
if (condition) return 1
return 2
}
```
Bad:
```ts
function foo() {
if (condition) return 1
else return 2
}
```
# Prefer single word naming
Try your best to find a single word name for your variables, functions, etc.
Only use multiple words if you cannot.
Good:
```ts
const foo = 1
const bar = 2
const baz = 3
```
Bad:
```ts
const fooBar = 1
const barBaz = 2
const bazFoo = 3
```
+80 -97
View File
@@ -22,7 +22,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -70,7 +70,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -87,7 +87,6 @@
"chart.js": "4.5.1", "chart.js": "4.5.1",
"nitro": "3.0.1-alpha.1", "nitro": "3.0.1-alpha.1",
"solid-js": "catalog:", "solid-js": "catalog:",
"solid-list": "0.3.0",
"vite": "catalog:", "vite": "catalog:",
"zod": "catalog:", "zod": "catalog:",
}, },
@@ -99,7 +98,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -126,7 +125,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "2.0.0", "@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2", "@ai-sdk/openai": "2.0.2",
@@ -150,7 +149,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -174,7 +173,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@@ -203,7 +202,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
@@ -232,7 +231,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -248,7 +247,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.1.17", "version": "1.1.8",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -256,25 +255,25 @@
"@actions/core": "1.11.1", "@actions/core": "1.11.1",
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.5.1", "@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/amazon-bedrock": "3.0.73", "@ai-sdk/amazon-bedrock": "3.0.57",
"@ai-sdk/anthropic": "2.0.57", "@ai-sdk/anthropic": "2.0.56",
"@ai-sdk/azure": "2.0.91", "@ai-sdk/azure": "2.0.82",
"@ai-sdk/cerebras": "1.0.34", "@ai-sdk/cerebras": "1.0.33",
"@ai-sdk/cohere": "2.0.22", "@ai-sdk/cohere": "2.0.21",
"@ai-sdk/deepinfra": "1.0.31", "@ai-sdk/deepinfra": "1.0.30",
"@ai-sdk/gateway": "2.0.25", "@ai-sdk/gateway": "2.0.23",
"@ai-sdk/google": "2.0.52", "@ai-sdk/google": "2.0.49",
"@ai-sdk/google-vertex": "3.0.97", "@ai-sdk/google-vertex": "3.0.81",
"@ai-sdk/groq": "2.0.34", "@ai-sdk/groq": "2.0.33",
"@ai-sdk/mistral": "2.0.27", "@ai-sdk/mistral": "2.0.26",
"@ai-sdk/openai": "2.0.89", "@ai-sdk/openai": "2.0.71",
"@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/openai-compatible": "1.0.29",
"@ai-sdk/perplexity": "2.0.23", "@ai-sdk/perplexity": "2.0.22",
"@ai-sdk/provider": "2.0.1", "@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.20", "@ai-sdk/provider-utils": "3.0.19",
"@ai-sdk/togetherai": "1.0.31", "@ai-sdk/togetherai": "1.0.30",
"@ai-sdk/vercel": "1.0.31", "@ai-sdk/vercel": "1.0.31",
"@ai-sdk/xai": "2.0.51", "@ai-sdk/xai": "2.0.42",
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@hono/standard-validator": "0.1.5", "@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:", "@hono/zod-validator": "catalog:",
@@ -351,7 +350,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"zod": "catalog:", "zod": "catalog:",
@@ -371,7 +370,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.1.17", "version": "1.1.8",
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "0.88.1", "@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
@@ -382,7 +381,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -395,7 +394,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -407,7 +406,6 @@
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
"dompurify": "catalog:",
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
"katex": "0.16.27", "katex": "0.16.27",
"luxon": "catalog:", "luxon": "catalog:",
@@ -435,7 +433,7 @@
}, },
"packages/util": { "packages/util": {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"zod": "catalog:", "zod": "catalog:",
}, },
@@ -446,7 +444,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -506,9 +504,8 @@
"@types/luxon": "3.7.1", "@types/luxon": "3.7.1",
"@types/node": "22.13.9", "@types/node": "22.13.9",
"@typescript/native-preview": "7.0.0-dev.20251207.1", "@typescript/native-preview": "7.0.0-dev.20251207.1",
"ai": "5.0.119", "ai": "5.0.97",
"diff": "8.0.2", "diff": "8.0.2",
"dompurify": "3.3.1",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
"hono": "4.10.7", "hono": "4.10.7",
"hono-openapi": "1.1.2", "hono-openapi": "1.1.2",
@@ -544,43 +541,43 @@
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.5.1", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-9bq2TgjhLBSUSC5jE04MEe+Hqw8YePzKghhYZ9QcjOyonY3q2oJfX6GoSO83hURpEnsqEPIrex6VZN3+61fBJg=="], "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.5.1", "", { "dependencies": { "zod": "^3.0.0" } }, "sha512-9bq2TgjhLBSUSC5jE04MEe+Hqw8YePzKghhYZ9QcjOyonY3q2oJfX6GoSO83hURpEnsqEPIrex6VZN3+61fBJg=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.73", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EAAGJ/dfbAZaqIhK3w52hq6cftSLZwXdC6uHKh8Cls1T0N4MxS6ykDf54UyFO3bZWkQxR+Mdw1B3qireGOxtJQ=="], "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.57", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.45", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mOUSLe+RgZzx0rtL1p9QXmSd/08z1EkBR+vQ1ydpd1t5P0Nx2kB8afiukEgM8nuDvmO9eYQlp7VTy1n5ffPs2g=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
"@ai-sdk/azure": ["@ai-sdk/azure@2.0.91", "", { "dependencies": { "@ai-sdk/openai": "2.0.89", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9tznVSs6LGQNKKxb8pKd7CkBV9yk+a/ENpFicHCj2CmBUKefxzwJ9JbUqrlK3VF6dGZw3LXq0dWxt7/Yekaj1w=="], "@ai-sdk/azure": ["@ai-sdk/azure@2.0.82", "", { "dependencies": { "@ai-sdk/openai": "2.0.80", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Bpab51ETBB4adZC1xGMYsryL/CB8j1sA+t5aDqhRv3t3WRLTxhaBDcFKtQTIuxiEQTFosz9Q2xQqdfBvQm5jHw=="],
"@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.34", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XOK0dJsAGoPYi/lfR4KFBi8xhvJ46oCpAxUD6FmJAuJ4eh0qlj5zDt+myvzM8gvN7S6K7zHD+mdWlOPKGQT8Vg=="], "@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2gSSS/7kunIwMdC4td5oWsUAzoLw84ccGpz6wQbxVnrb1iWnrEnKa5tRBduaP6IXpzLWsu8wME3+dQhZy+gT7w=="],
"@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="], "@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZjaZFvJlc5XOPi3QwTLEFZbHIgTJc6YGvxz+8zIMGVZi/hdynR8/f/C1A9x6mhzmBtAqi/dZ2h11oouAQH5z4g=="],
"@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-87qFcYNvDF/89hB//MQjYTb3tlsAfmgeZrZ34RESeBTZpSgs0EzYOMqPMwFTHUNp4wteoifikDJbaS/9Da8cfw=="], "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.30", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XK8oRZFApzo6xnS5C+FhWUUkB2itA5Nfon3pU9dJVM0goViq8GwdleZTBRqhu4DE4KJURo5DGWpJr2hfV54cEg=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.25", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rq+FX55ne7lMiqai7NcvvDZj4HLsr+hg77WayqmySqc6zhw3tIOLxd4Ty6OpwNj0C0bVMi3iCl2zvJIEirh9XA=="], "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qmX7afPRszUqG5hryHF3UN8ITPIRSGmDW6VYCmByzjoUkgm3MekzSx2hMV1wr0P+llDeuXb378SjqUfpvWJulg=="],
"@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="], "@ai-sdk/google": ["@ai-sdk/google@2.0.49", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-efwKk4mOV0SpumUaQskeYABk37FJPmEYwoDJQEjyLRmGSjtHRe9P5Cwof5ffLvaFav2IaJpBGEz98pyTs7oNWA=="],
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.97", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/google": "2.0.52", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-s4tI7Z15i6FlbtCvS4SBRal8wRfkOXJzKxlS6cU4mJW/QfUfoVy4b22836NVNJwDvkG/HkDSfzwm/X8mn46MhA=="], "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.81", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.50", "@ai-sdk/google": "2.0.44", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yrl5Ug0Mqwo9ya45oxczgy2RWgpEA/XQQCSFYP+3NZMQ4yA3Iim1vkOjVCsGaZZ8rjVk395abi1ZMZV0/6rqVA=="],
"@ai-sdk/groq": ["@ai-sdk/groq@2.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wfCYkVgmVjxNA32T57KbLabVnv9aFUflJ4urJ7eWgTwbnmGQHElCTu+rJ3ydxkXSqxOkXPwMOttDm7XNrvPjmg=="], "@ai-sdk/groq": ["@ai-sdk/groq@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FWGl7xNr88NBveao3y9EcVWYUt9ABPrwLFY7pIutSNgaTf32vgvyhREobaMrLU4Scr5G/2tlNqOPZ5wkYMaZig=="],
"@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gaptHgaXjMw3+eA0Q4FABcsj5nQNP6EpFaGUR+Pj5WJy7Kn6mApl975/x57224MfeJIShNpt8wFKK3tvh5ewKg=="], "@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.26", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jxDB++4WI1wEx5ONNBI+VbkmYJOYIuS8UQY13/83UGRaiW7oB/WHiH4ETe6KzbKpQPB3XruwTJQjUMsMfKyTXA=="],
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="], "@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="],
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="], "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="],
"@ai-sdk/perplexity": ["@ai-sdk/perplexity@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-aiaRvnc6mhQZKhTTSXPCjPH8Iqr5D/PfCN1hgVP/3RGTBbJtsd9HemIBSABeSdAKbsMH/PwJxgnqH75HEamcBA=="], "@ai-sdk/perplexity": ["@ai-sdk/perplexity@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zwzcnk08R2J3mZcQPn4Ifl4wYGrvANR7jsBB0hCTUSbb+Rx3ybpikSWiGuXQXxdiRc1I5MWXgj70m+bZaLPvHw=="],
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RlYubjStoZQxna4Ng91Vvo8YskvL7lW9zj68IwZfCnaDBSAp1u6Nhc5BR4ZtKnY6PA3XEtu4bATIQl7yiiQ+Lw=="], "@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.30", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9bxQbIXnWSN4bNismrza3NvIo+ui/Y3pj3UN6e9vCszCWFCN45RgISi4oDe10RqmzaJ/X8cfO/Tem+K8MT3wGQ=="],
"@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ggvwAMt/KsbqcdR6ILQrjwrRONLV/8aG6rOLbjcOGvV0Ai+WdZRRKQj5nOeQ06PvwVQtKdkp7S4IinpXIhCiHg=="], "@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ggvwAMt/KsbqcdR6ILQrjwrRONLV/8aG6rOLbjcOGvV0Ai+WdZRRKQj5nOeQ06PvwVQtKdkp7S4IinpXIhCiHg=="],
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.51", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AI3le03qiegkZvn9hpnpDwez49lOvQLj4QUBT8H41SMbrdTYOxn3ktTwrsSu90cNDdzKGMvoH0u2GHju1EdnCg=="], "@ai-sdk/xai": ["@ai-sdk/xai@2.0.42", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wlwO4yRoZ/d+ca29vN8SDzxus7POdnL7GBTyRdSrt6icUF0hooLesauC8qRUC4aLxtqvMEc1YHtJOU7ZnLWbTQ=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@@ -1830,8 +1827,6 @@
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/tsscmp": ["@types/tsscmp@1.0.2", "", {}, "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g=="], "@types/tsscmp": ["@types/tsscmp@1.0.2", "", {}, "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g=="],
"@types/tunnel": ["@types/tunnel@0.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="], "@types/tunnel": ["@types/tunnel@0.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="],
@@ -1908,7 +1903,7 @@
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ai": ["ai@5.0.119", "", { "dependencies": { "@ai-sdk/gateway": "2.0.25", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HUOwhc17fl2SZTJGZyA/99aNu706qKfXaUBCy9vgZiXBwrxg2eTzn2BCz7kmYDsfx6Fg2ACBy2icm41bsDXCTw=="], "ai": ["ai@5.0.97", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
@@ -2210,8 +2205,6 @@
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
@@ -2284,8 +2277,6 @@
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="],
@@ -2428,8 +2419,6 @@
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], "file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -2460,8 +2449,6 @@
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
@@ -2484,9 +2471,9 @@
"fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
"gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], "gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], "gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
"gel": ["gel@2.2.0", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ=="], "gel": ["gel@2.2.0", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ=="],
@@ -2532,9 +2519,9 @@
"globby": ["globby@11.0.4", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.1.1", "ignore": "^5.1.4", "merge2": "^1.3.0", "slash": "^3.0.0" } }, "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg=="], "globby": ["globby@11.0.4", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.1.1", "ignore": "^5.1.4", "merge2": "^1.3.0", "slash": "^3.0.0" } }, "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg=="],
"google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], "google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], "google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
@@ -2542,7 +2529,7 @@
"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=="], "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=="], "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
"h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="], "h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="],
@@ -3430,8 +3417,6 @@
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
"rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="],
"rou3": ["rou3@0.7.10", "", {}, "sha512-aoFj6f7MJZ5muJ+Of79nrhs9N3oLGqi2VEMe94Zbkjb6Wupha46EuoYgpWSOZlXww3bbd8ojgXTAA2mzimX5Ww=="], "rou3": ["rou3@0.7.10", "", {}, "sha512-aoFj6f7MJZ5muJ+Of79nrhs9N3oLGqi2VEMe94Zbkjb6Wupha46EuoYgpWSOZlXww3bbd8ojgXTAA2mzimX5Ww=="],
@@ -3924,33 +3909,35 @@
"@agentclientprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@agentclientprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="], "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.45", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ipv62vavDCmrV/oE/lXehL9FzwQuZOnnlhPEftWizx464Wb6lvnBTJx8uhmEYruFSzOWTI95Z33ncZ4tA8E6RQ=="],
"@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/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
"@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.80", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tNHuraF11db+8xJEDBoU9E3vMcpnHFKRhnLQ3DQX2LnEzfPB9DksZ8rE+yVuDN1WRW9cm2OWAhgHFgVKs7ICuw=="],
"@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], "@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/google-vertex/@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/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
"@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
"@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
"@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
"@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], "@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], "@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], "@ai-sdk/vercel/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
"@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], "@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
@@ -4218,6 +4205,10 @@
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W+cB1sOWvPcz9qiIsNtD+HxUrBUva2vWv2K1EFukuImX+HA0uZx3EyyOjhYQ9gtf/teqEG80M6OvJ7xx/VLV2A=="],
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -4282,13 +4273,13 @@
"express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], "express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], "gaxios/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
@@ -4330,11 +4321,11 @@
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"opencode/@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=="], "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], "opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
@@ -4372,8 +4363,6 @@
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], "readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
"safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
@@ -4936,6 +4925,8 @@
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
@@ -4952,12 +4943,6 @@
"readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -5140,8 +5125,6 @@
"pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1768178648, "lastModified": 1767364772,
"narHash": "sha256-kz/F6mhESPvU1diB7tOM3nLcBfQe7GU7GQCymRlTi/s=", "narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3fbab70c6e69c87ea2b6e48aa6629da2aa6a23b0", "rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github" "type": "github"
}, },
"original": { "original": {
+3 -23
View File
@@ -27,28 +27,11 @@
"aarch64-darwin" = "bun-darwin-arm64"; "aarch64-darwin" = "bun-darwin-arm64";
"x86_64-darwin" = "bun-darwin-x64"; "x86_64-darwin" = "bun-darwin-x64";
}; };
defaultNodeModules = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
# Parse "bun-{os}-{cpu}" to {os, cpu}
parseBunTarget =
target:
let
parts = lib.splitString "-" target;
in
{
os = builtins.elemAt parts 1;
cpu = builtins.elemAt parts 2;
};
hashesFile = "${./nix}/hashes.json"; hashesFile = "${./nix}/hashes.json";
hashesData = hashesData =
if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { }; if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { };
# Lookup hash: supports per-system ({system: hash}) or legacy single hash nodeModulesHash = hashesData.nodeModules or defaultNodeModules;
nodeModulesHashFor =
system:
if builtins.isAttrs hashesData.nodeModules then
hashesData.nodeModules.${system}
else
hashesData.nodeModules;
modelsDev = forEachSystem ( modelsDev = forEachSystem (
system: system:
let let
@@ -80,11 +63,8 @@
system: system:
let let
pkgs = pkgsFor system; pkgs = pkgsFor system;
bunPlatform = parseBunTarget bunTarget.${system};
mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
hash = nodeModulesHashFor system; hash = nodeModulesHash;
bunCpu = bunPlatform.cpu;
bunOs = bunPlatform.os;
}; };
mkOpencode = pkgs.callPackage ./nix/opencode.nix { }; mkOpencode = pkgs.callPackage ./nix/opencode.nix { };
mkDesktop = pkgs.callPackage ./nix/desktop.nix { }; mkDesktop = pkgs.callPackage ./nix/desktop.nix { };
-1
View File
@@ -85,7 +85,6 @@ This will walk you through installing the GitHub app, creating the workflow, and
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
fetch-depth: 1 fetch-depth: 1
persist-credentials: false
- name: Run opencode - name: Run opencode
uses: anomalyco/opencode/github@latest uses: anomalyco/opencode/github@latest
-1
View File
@@ -163,7 +163,6 @@ new sst.cloudflare.x.SolidStart("Console", {
AWS_SES_ACCESS_KEY_ID, AWS_SES_ACCESS_KEY_ID,
AWS_SES_SECRET_ACCESS_KEY, AWS_SES_SECRET_ACCESS_KEY,
ZEN_BLACK, ZEN_BLACK,
new sst.Secret("ZEN_SESSION_SECRET"),
...ZEN_MODELS, ...ZEN_MODELS,
...($dev ...($dev
? [ ? [
+1 -4
View File
@@ -1,6 +1,3 @@
{ {
"nodeModules": { "nodeModules": "sha256-+QM5BDFxzrm1HY5ealjCm7jIO1t/rpW1q4GGLViPMmA="
"x86_64-linux": "sha256-UCPTTk4b7d2bets7KgCeYBHWAUwUAPUyKm+xDYkSexE=",
"aarch64-darwin": "sha256-Y3o6lovahSWoG9un/l1qxu7hCmIlZXm2LxOLKNiPQfQ="
}
} }
+2 -4
View File
@@ -5,8 +5,6 @@
bun, bun,
cacert, cacert,
curl, curl,
bunCpu,
bunOs,
}: }:
args: args:
stdenvNoCC.mkDerivation { stdenvNoCC.mkDerivation {
@@ -31,8 +29,8 @@ stdenvNoCC.mkDerivation {
export HOME=$(mktemp -d) export HOME=$(mktemp -d)
export BUN_INSTALL_CACHE_DIR=$(mktemp -d) export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
bun install \ bun install \
--cpu="${bunCpu}" \ --cpu="*" \
--os="${bunOs}" \ --os="*" \
--frozen-lockfile \ --frozen-lockfile \
--ignore-scripts \ --ignore-scripts \
--no-progress \ --no-progress \
+3 -10
View File
@@ -10,7 +10,7 @@ HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE}
if [ ! -f "$HASH_FILE" ]; then if [ ! -f "$HASH_FILE" ]; then
cat >"$HASH_FILE" <<EOF cat >"$HASH_FILE" <<EOF
{ {
"nodeModules": {} "nodeModules": "$DUMMY"
} }
EOF EOF
fi fi
@@ -33,16 +33,9 @@ trap cleanup EXIT
write_node_modules_hash() { write_node_modules_hash() {
local value="$1" local value="$1"
local system="${2:-$SYSTEM}"
local temp local temp
temp=$(mktemp) temp=$(mktemp)
jq --arg value "$value" '.nodeModules = $value' "$HASH_FILE" >"$temp"
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" mv "$temp" "$HASH_FILE"
} }
@@ -111,7 +104,7 @@ fi
write_node_modules_hash "$CORRECT_HASH" write_node_modules_hash "$CORRECT_HASH"
jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null jq -e --arg hash "$CORRECT_HASH" '.nodeModules == $hash' "$HASH_FILE" >/dev/null
echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH" echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH"
+1 -2
View File
@@ -36,8 +36,7 @@
"@solid-primitives/storage": "4.3.3", "@solid-primitives/storage": "4.3.3",
"@tailwindcss/vite": "4.1.11", "@tailwindcss/vite": "4.1.11",
"diff": "8.0.2", "diff": "8.0.2",
"dompurify": "3.3.1", "ai": "5.0.97",
"ai": "5.0.119",
"hono": "4.10.7", "hono": "4.10.7",
"hono-openapi": "1.1.2", "hono-openapi": "1.1.2",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.1.17", "version": "1.1.8",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
+12 -12
View File
@@ -33,10 +33,19 @@ const Loading = () => <div class="size-full flex items-center justify-center tex
declare global { declare global {
interface Window { interface Window {
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string } __OPENCODE__?: { updaterEnabled?: boolean; port?: number; serverReady?: boolean }
} }
} }
const defaultServerUrl = iife(() => {
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}`
if (import.meta.env.DEV)
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
return window.location.origin
})
export function AppBaseProviders(props: ParentProps) { export function AppBaseProviders(props: ParentProps) {
return ( return (
<MetaProvider> <MetaProvider>
@@ -65,18 +74,9 @@ function ServerKey(props: ParentProps) {
) )
} }
export function AppInterface(props: { defaultUrl?: string }) { export function AppInterface() {
const defaultServerUrl = () => {
if (props.defaultUrl) return props.defaultUrl
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
if (import.meta.env.DEV)
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
return window.location.origin
}
return ( return (
<ServerProvider defaultUrl={defaultServerUrl()}> <ServerProvider defaultUrl={defaultServerUrl}>
<ServerKey> <ServerKey>
<GlobalSDKProvider> <GlobalSDKProvider>
<GlobalSyncProvider> <GlobalSyncProvider>
@@ -1,99 +0,0 @@
import { Component, createMemo } from "solid-js"
import { useNavigate, useParams } from "@solidjs/router"
import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { usePrompt } from "@/context/prompt"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { extractPromptFromParts } from "@/utils/prompt"
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
import { base64Encode } from "@opencode-ai/util/encode"
interface ForkableMessage {
id: string
text: string
time: string
}
function formatTime(date: Date): string {
return date.toLocaleTimeString(undefined, { timeStyle: "short" })
}
export const DialogFork: Component = () => {
const params = useParams()
const navigate = useNavigate()
const sync = useSync()
const sdk = useSDK()
const prompt = usePrompt()
const dialog = useDialog()
const messages = createMemo((): ForkableMessage[] => {
const sessionID = params.id
if (!sessionID) return []
const msgs = sync.data.message[sessionID] ?? []
const result: ForkableMessage[] = []
for (const message of msgs) {
if (message.role !== "user") continue
const parts = sync.data.part[message.id] ?? []
const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored)
if (!textPart) continue
result.push({
id: message.id,
text: textPart.text.replace(/\n/g, " ").slice(0, 200),
time: formatTime(new Date(message.time.created)),
})
}
return result.reverse()
})
const handleSelect = (item: ForkableMessage | undefined) => {
if (!item) return
const sessionID = params.id
if (!sessionID) return
const parts = sync.data.part[item.id] ?? []
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
dialog.close()
sdk.client.session.fork({ sessionID, messageID: item.id }).then((forked) => {
if (!forked.data) return
navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`)
requestAnimationFrame(() => {
prompt.set(restored)
})
})
}
return (
<Dialog title="Fork from message">
<List
class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
search={{ placeholder: "Search", autofocus: true }}
emptyMessage="No messages to fork from"
key={(x) => x.id}
items={messages}
filterKeys={["text"]}
onSelect={handleSelect}
>
{(item) => (
<div class="w-full flex items-center gap-2">
<span class="truncate flex-1 min-w-0 text-left" style={{ "font-weight": "400" }}>
{item.text}
</span>
<span class="text-text-weak shrink-0" style={{ "font-weight": "400" }}>
{item.time}
</span>
</div>
)}
</List>
</Dialog>
)
}
@@ -7,8 +7,6 @@ import { Button } from "@opencode-ai/ui/button"
import { Tag } from "@opencode-ai/ui/tag" import { Tag } from "@opencode-ai/ui/tag"
import { Dialog } from "@opencode-ai/ui/dialog" import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list" 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 { DialogSelectProvider } from "./dialog-select-provider"
import { DialogManageModels } from "./dialog-manage-models" import { DialogManageModels } from "./dialog-manage-models"
@@ -37,12 +35,6 @@ const ModelList: Component<{
filterKeys={["provider.name", "name", "id"]} filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)} sortBy={(a, b) => a.name.localeCompare(b.name)}
groupBy={(x) => x.provider.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) => { sortGroupsBy={(a, b) => {
if (a.category === "Recent" && b.category !== "Recent") return -1 if (a.category === "Recent" && b.category !== "Recent") return -1
if (b.category === "Recent" && a.category !== "Recent") return 1 if (b.category === "Recent" && a.category !== "Recent") return 1
@@ -60,8 +52,7 @@ const ModelList: Component<{
}} }}
> >
{(i) => ( {(i) => (
<div class="w-full flex items-center gap-x-3 pl-1 text-13-regular"> <div class="w-full flex items-center gap-x-2 text-13-regular">
<ProviderIcon data-slot="list-item-extra-icon" id={i.provider.id as IconName} />
<span class="truncate">{i.name}</span> <span class="truncate">{i.name}</span>
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}> <Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
<Tag>Free</Tag> <Tag>Free</Tag>
@@ -1,11 +1,10 @@
import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js" import { createEffect, createMemo, onCleanup } from "solid-js"
import { createStore, reconcile } from "solid-js/store" import { createStore, reconcile } from "solid-js/store"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog" import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list" import { List } from "@opencode-ai/ui/list"
import { TextField } from "@opencode-ai/ui/text-field" import { TextField } from "@opencode-ai/ui/text-field"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server" import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
@@ -36,8 +35,6 @@ export function DialogSelectServer() {
error: "", error: "",
status: {} as Record<string, ServerStatus | undefined>, status: {} as Record<string, ServerStatus | undefined>,
}) })
const [defaultUrl, defaultUrlActions] = createResource(() => platform.getDefaultServerUrl?.())
const isDesktop = platform.platform === "desktop"
const items = createMemo(() => { const items = createMemo(() => {
const current = server.url const current = server.url
@@ -117,10 +114,6 @@ export function DialogSelectServer() {
select(value, true) select(value, true)
} }
async function handleRemove(url: string) {
server.remove(url)
}
return ( return (
<Dialog title="Servers" description="Switch which OpenCode server this app connects to."> <Dialog title="Servers" description="Switch which OpenCode server this app connects to.">
<div class="flex flex-col gap-4 pb-4"> <div class="flex flex-col gap-4 pb-4">
@@ -135,7 +128,6 @@ export function DialogSelectServer() {
}} }}
> >
{(i) => ( {(i) => (
<div class="flex items-center gap-2 min-w-0 flex-1 group/item">
<div <div
class="flex items-center gap-2 min-w-0 flex-1" class="flex items-center gap-2 min-w-0 flex-1"
classList={{ "opacity-50": store.status[i]?.healthy === false }} classList={{ "opacity-50": store.status[i]?.healthy === false }}
@@ -151,18 +143,6 @@ export function DialogSelectServer() {
<span class="truncate">{serverDisplayName(i)}</span> <span class="truncate">{serverDisplayName(i)}</span>
<span class="text-text-weak">{store.status[i]?.version}</span> <span class="text-text-weak">{store.status[i]?.version}</span>
</div> </div>
<Show when={current() !== i && server.list.includes(i)}>
<IconButton
icon="circle-x"
variant="ghost"
class="bg-transparent transition-opacity shrink-0 hover:scale-110"
onClick={(e) => {
e.stopPropagation()
handleRemove(i)
}}
/>
</Show>
</div>
)} )}
</List> </List>
@@ -193,53 +173,6 @@ export function DialogSelectServer() {
</div> </div>
</form> </form>
</div> </div>
<Show when={isDesktop}>
<div class="mt-6 px-3 flex flex-col gap-1.5">
<div class="px-3">
<h3 class="text-14-regular text-text-weak">Default server</h3>
<p class="text-12-regular text-text-weak mt-1">
Connect to this server on app launch instead of starting a local server. Requires restart.
</p>
</div>
<div class="flex items-center gap-2 px-3 py-2">
<Show
when={defaultUrl()}
fallback={
<Show
when={server.url}
fallback={<span class="text-14-regular text-text-weak">No server selected</span>}
>
<Button
variant="secondary"
size="small"
onClick={async () => {
await platform.setDefaultServerUrl?.(server.url)
defaultUrlActions.refetch(server.url)
}}
>
Set current server as default
</Button>
</Show>
}
>
<div class="flex items-center gap-2 flex-1 min-w-0">
<span class="truncate text-14-regular">{serverDisplayName(defaultUrl()!)}</span>
</div>
<Button
variant="ghost"
size="small"
onClick={async () => {
await platform.setDefaultServerUrl?.(null)
defaultUrlActions.refetch()
}}
>
Clear
</Button>
</Show>
</div>
</div>
</Show>
</div> </div>
</Dialog> </Dialog>
) )
+3 -20
View File
@@ -33,14 +33,11 @@ import { useSync } from "@/context/sync"
import { FileIcon } from "@opencode-ai/ui/file-icon" import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import type { IconName } from "@opencode-ai/ui/icons/provider"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
import { Select } from "@opencode-ai/ui/select" import { Select } from "@opencode-ai/ui/select"
import { getDirectory, getFilename } from "@opencode-ai/util/path" import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { ModelSelectorPopover } from "@/components/dialog-select-model" import { ModelSelectorPopover } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers" import { useProviders } from "@/hooks/use-providers"
@@ -389,7 +386,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const { const {
flat: atFlat, flat: atFlat,
active: atActive, active: atActive,
setActive: setAtActive,
onInput: atOnInput, onInput: atOnInput,
onKeyDown: atOnKeyDown, onKeyDown: atOnKeyDown,
} = useFilteredList<AtOption>({ } = useFilteredList<AtOption>({
@@ -456,7 +452,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const { const {
flat: slashFlat, flat: slashFlat,
active: slashActive, active: slashActive,
setActive: setSlashActive,
onInput: slashOnInput, onInput: slashOnInput,
onKeyDown: slashOnKeyDown, onKeyDown: slashOnKeyDown,
refetch: slashRefetch, refetch: slashRefetch,
@@ -1304,7 +1299,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-80 min-h-10 class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-80 min-h-10
overflow-auto no-scrollbar flex flex-col p-2 rounded-md overflow-auto no-scrollbar flex flex-col p-2 rounded-md
border border-border-base bg-surface-raised-stronger-non-alpha shadow-md" border border-border-base bg-surface-raised-stronger-non-alpha shadow-md"
onMouseDown={(e) => e.preventDefault()}
> >
<Switch> <Switch>
<Match when={store.popover === "at"}> <Match when={store.popover === "at"}>
@@ -1320,7 +1314,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
"bg-surface-raised-base-hover": atActive() === atKey(item), "bg-surface-raised-base-hover": atActive() === atKey(item),
}} }}
onClick={() => handleAtSelect(item)} onClick={() => handleAtSelect(item)}
onMouseEnter={() => setAtActive(atKey(item))}
> >
<Show <Show
when={item.type === "agent"} when={item.type === "agent"}
@@ -1367,7 +1360,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
"bg-surface-raised-base-hover": slashActive() === cmd.id, "bg-surface-raised-base-hover": slashActive() === cmd.id,
}} }}
onClick={() => handleSlashSelect(cmd)} onClick={() => handleSlashSelect(cmd)}
onMouseEnter={() => setSlashActive(cmd.id)}
> >
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-2 min-w-0">
<span class="text-14-regular text-text-strong whitespace-nowrap">/{cmd.trigger}</span> <span class="text-14-regular text-text-strong whitespace-nowrap">/{cmd.trigger}</span>
@@ -1487,10 +1479,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<img <img
src={attachment.dataUrl} src={attachment.dataUrl}
alt={attachment.filename} alt={attachment.filename}
class="size-16 rounded-md object-cover border border-border-base hover:border-border-strong-base transition-colors" class="size-16 rounded-md object-cover border border-border-base"
onClick={() =>
dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
}
/> />
</Show> </Show>
<button <button
@@ -1562,9 +1551,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
fallback={ fallback={
<TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}> <TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
<Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}> <Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
</Show>
{local.model.current()?.name ?? "Select model"} {local.model.current()?.name ?? "Select model"}
<Icon name="chevron-down" size="small" /> <Icon name="chevron-down" size="small" />
</Button> </Button>
@@ -1574,9 +1560,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<ModelSelectorPopover> <ModelSelectorPopover>
<TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}> <TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
<Button as="div" variant="ghost"> <Button as="div" variant="ghost">
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
</Show>
{local.model.current()?.name ?? "Select model"} {local.model.current()?.name ?? "Select model"}
<Icon name="chevron-down" size="small" /> <Icon name="chevron-down" size="small" />
</Button> </Button>
@@ -1591,10 +1574,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
> >
<Button <Button
variant="ghost" variant="ghost"
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular" class="text-text-base _hidden group-hover/prompt-input:inline-block"
onClick={() => local.model.variant.cycle()} onClick={() => local.model.variant.cycle()}
> >
{local.model.variant.current() ?? "Default"} <span class="capitalize text-12-regular">{local.model.variant.current() ?? "Default"}</span>
</Button> </Button>
</TooltipKeybind> </TooltipKeybind>
</Show> </Show>
+3 -6
View File
@@ -100,12 +100,9 @@ export const Terminal = (props: TerminalProps) => {
const mod = await import("ghostty-web") const mod = await import("ghostty-web")
ghostty = await mod.Ghostty.load() ghostty = await mod.Ghostty.load()
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`) const socket = new WebSocket(
if (window.__OPENCODE__?.serverPassword) { sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`,
url.username = "opencode" )
url.password = window.__OPENCODE__?.serverPassword
}
const socket = new WebSocket(url)
ws = socket ws = socket
const t = new mod.Terminal({ const t = new mod.Terminal({
+1 -2
View File
@@ -9,13 +9,11 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
name: "GlobalSDK", name: "GlobalSDK",
init: () => { init: () => {
const server = useServer() const server = useServer()
const platform = usePlatform()
const abort = new AbortController() const abort = new AbortController()
const eventSdk = createOpencodeClient({ const eventSdk = createOpencodeClient({
baseUrl: server.url, baseUrl: server.url,
signal: abort.signal, signal: abort.signal,
fetch: platform.fetch,
}) })
const emitter = createGlobalEmitter<{ const emitter = createGlobalEmitter<{
[key: string]: Event [key: string]: Event
@@ -95,6 +93,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
stop() stop()
}) })
const platform = usePlatform()
const sdk = createOpencodeClient({ const sdk = createOpencodeClient({
baseUrl: server.url, baseUrl: server.url,
fetch: platform.fetch, fetch: platform.fetch,
-4
View File
@@ -26,7 +26,6 @@ import { ErrorPage, type InitError } from "../pages/error"
import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js" import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/util/path" import { getFilename } from "@opencode-ai/util/path"
import { usePlatform } from "./platform"
type State = { type State = {
status: "loading" | "partial" | "complete" status: "loading" | "partial" | "complete"
@@ -65,7 +64,6 @@ type State = {
function createGlobalSync() { function createGlobalSync() {
const globalSDK = useGlobalSDK() const globalSDK = useGlobalSDK()
const platform = usePlatform()
const [globalStore, setGlobalStore] = createStore<{ const [globalStore, setGlobalStore] = createStore<{
ready: boolean ready: boolean
error?: InitError error?: InitError
@@ -141,7 +139,6 @@ function createGlobalSync() {
const [store, setStore] = child(directory) const [store, setStore] = child(directory)
const sdk = createOpencodeClient({ const sdk = createOpencodeClient({
baseUrl: globalSDK.url, baseUrl: globalSDK.url,
fetch: platform.fetch,
directory, directory,
throwOnError: true, throwOnError: true,
}) })
@@ -399,7 +396,6 @@ function createGlobalSync() {
case "lsp.updated": { case "lsp.updated": {
const sdk = createOpencodeClient({ const sdk = createOpencodeClient({
baseUrl: globalSDK.url, baseUrl: globalSDK.url,
fetch: platform.fetch,
directory, directory,
throwOnError: true, throwOnError: true,
}) })
-6
View File
@@ -37,12 +37,6 @@ export type Platform = {
/** Fetch override */ /** Fetch override */
fetch?: typeof fetch fetch?: typeof fetch
/** Get the configured default server URL (desktop only) */
getDefaultServerUrl?(): Promise<string | null>
/** Set the default server URL to use on app startup (desktop only) */
setDefaultServerUrl?(url: string | null): Promise<void>
} }
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({ export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
+4 -1
View File
@@ -16,7 +16,10 @@ export function normalizeServerUrl(input: string) {
export function serverDisplayName(url: string) { export function serverDisplayName(url: string) {
if (!url) return "" if (!url) return ""
return url.replace(/^https?:\/\//, "").replace(/\/+$/, "") return url
.replace(/^https?:\/\//, "")
.replace(/\/+$/, "")
.split("/")[0]
} }
function projectsKey(url: string) { function projectsKey(url: string) {
+1 -15
View File
@@ -8,7 +8,6 @@ import { Persist, persisted } from "@/utils/persist"
export type LocalPTY = { export type LocalPTY = {
id: string id: string
title: string title: string
titleNumber: number
rows?: number rows?: number
cols?: number cols?: number
buffer?: string buffer?: string
@@ -43,20 +42,8 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
all: createMemo(() => Object.values(store.all)), all: createMemo(() => Object.values(store.all)),
active: createMemo(() => store.active), active: createMemo(() => store.active),
new() { new() {
const existingTitleNumbers = new Set(
store.all.map((pty) => {
const match = pty.titleNumber
return match
}),
)
let nextNumber = 1
while (existingTitleNumbers.has(nextNumber)) {
nextNumber++
}
sdk.client.pty sdk.client.pty
.create({ title: `Terminal ${nextNumber}` }) .create({ title: `Terminal ${store.all.length + 1}` })
.then((pty) => { .then((pty) => {
const id = pty.data?.id const id = pty.data?.id
if (!id) return if (!id) return
@@ -65,7 +52,6 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
{ {
id, id,
title: pty.data?.title ?? "Terminal", title: pty.data?.title ?? "Terminal",
titleNumber: nextNumber,
}, },
]) ])
setStore("active", id) setStore("active", id)
-10
View File
@@ -31,7 +31,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectFile } from "@/components/dialog-select-file" import { DialogSelectFile } from "@/components/dialog-select-file"
import { DialogSelectModel } from "@/components/dialog-select-model" import { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectMcp } from "@/components/dialog-select-mcp" import { DialogSelectMcp } from "@/components/dialog-select-mcp"
import { DialogFork } from "@/components/dialog-fork"
import { useCommand } from "@/context/command" import { useCommand } from "@/context/command"
import { useNavigate, useParams } from "@solidjs/router" import { useNavigate, useParams } from "@solidjs/router"
import { UserMessage } from "@opencode-ai/sdk/v2" import { UserMessage } from "@opencode-ai/sdk/v2"
@@ -646,15 +645,6 @@ export default function Page() {
}) })
}, },
}, },
{
id: "session.fork",
title: "Fork from message",
description: "Create a new session from a previous message",
category: "Session",
slash: "fork",
disabled: !params.id || visibleUserMessages().length === 0,
onSelect: () => dialog.show(() => <DialogFork />),
},
]) ])
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
+1 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.1.17", "version": "1.1.8",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@@ -26,7 +26,6 @@
"chart.js": "4.5.1", "chart.js": "4.5.1",
"nitro": "3.0.1-alpha.1", "nitro": "3.0.1-alpha.1",
"solid-js": "catalog:", "solid-js": "catalog:",
"solid-list": "0.3.0",
"vite": "catalog:", "vite": "catalog:",
"zod": "catalog:" "zod": "catalog:"
}, },
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

+2 -2
View File
@@ -9,8 +9,8 @@ export const config = {
github: { github: {
repoUrl: "https://github.com/anomalyco/opencode", repoUrl: "https://github.com/anomalyco/opencode",
starsFormatted: { starsFormatted: {
compact: "60K", compact: "50K",
full: "60,000", full: "50,000",
}, },
}, },
@@ -0,0 +1,24 @@
import { useSession } from "@solidjs/start/http"
export interface AuthSession {
account?: Record<
string,
{
id: string
email: string
}
>
current?: string
}
export function useAuthSession() {
return useSession<AuthSession>({
password: "0".repeat(32),
name: "auth",
maxAge: 60 * 60 * 24 * 365,
cookie: {
secure: false,
httpOnly: true,
},
})
}
+1 -26
View File
@@ -5,38 +5,13 @@ import { redirect } from "@solidjs/router"
import { Actor } from "@opencode-ai/console-core/actor.js" import { Actor } from "@opencode-ai/console-core/actor.js"
import { createClient } from "@openauthjs/openauth/client" import { createClient } from "@openauthjs/openauth/client"
import { useAuthSession } from "./auth.session"
export const AuthClient = createClient({ export const AuthClient = createClient({
clientID: "app", clientID: "app",
issuer: import.meta.env.VITE_AUTH_URL, issuer: import.meta.env.VITE_AUTH_URL,
}) })
import { useSession } from "@solidjs/start/http"
import { Resource } from "@opencode-ai/console-resource"
export interface AuthSession {
account?: Record<
string,
{
id: string
email: string
}
>
current?: string
}
export function useAuthSession() {
return useSession<AuthSession>({
password: Resource.ZEN_SESSION_SECRET.value,
name: "auth",
maxAge: 60 * 60 * 24 * 365,
cookie: {
secure: false,
httpOnly: true,
},
})
}
export const getActor = async (workspace?: string): Promise<Actor.Info> => { export const getActor = async (workspace?: string): Promise<Actor.Info> => {
"use server" "use server"
const evt = getRequestEvent() const evt = getRequestEvent()
@@ -1,7 +1,7 @@
import { redirect } from "@solidjs/router" import { redirect } from "@solidjs/router"
import type { APIEvent } from "@solidjs/start/server" import type { APIEvent } from "@solidjs/start/server"
import { AuthClient } from "~/context/auth" import { AuthClient } from "~/context/auth"
import { useAuthSession } from "~/context/auth" import { useAuthSession } from "~/context/auth.session"
export async function GET(input: APIEvent) { export async function GET(input: APIEvent) {
const url = new URL(input.request.url) const url = new URL(input.request.url)
@@ -1,6 +1,6 @@
import { redirect } from "@solidjs/router" import { redirect } from "@solidjs/router"
import { APIEvent } from "@solidjs/start" import { APIEvent } from "@solidjs/start"
import { useAuthSession } from "~/context/auth" import { useAuthSession } from "~/context/auth.session"
export async function GET(event: APIEvent) { export async function GET(event: APIEvent) {
const auth = await useAuthSession() const auth = await useAuthSession()
@@ -1,5 +1,5 @@
import { APIEvent } from "@solidjs/start" import { APIEvent } from "@solidjs/start"
import { useAuthSession } from "~/context/auth" import { useAuthSession } from "~/context/auth.session"
export async function GET(input: APIEvent) { export async function GET(input: APIEvent) {
const session = await useAuthSession() const session = await useAuthSession()
@@ -1,409 +0,0 @@
[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,214 +0,0 @@
[data-page="black"] {
background: #000;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: stretch;
font-family: var(--font-mono);
color: #fff;
[data-component="header-gradient"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 288px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%);
}
[data-component="header"] {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 40px;
flex-shrink: 0;
/* [data-component="header-logo"] { */
/* } */
}
[data-component="content"] {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
flex-grow: 1;
[data-slot="hero-black"] {
margin-top: 110px;
@media (min-width: 768px) {
margin-top: 150px;
}
}
[data-slot="select-workspace"] {
display: flex;
margin-top: -24px;
width: 100%;
max-width: 480px;
height: 305px;
padding: 32px 20px 0 20px;
flex-direction: column;
align-items: flex-start;
gap: 24px;
border: 1px solid #303030;
background: #0a0a0a;
box-shadow:
0 100px 80px 0 rgba(0, 0, 0, 0.04),
0 41.778px 33.422px 0 rgba(0, 0, 0, 0.05),
0 22.336px 17.869px 0 rgba(0, 0, 0, 0.06),
0 12.522px 10.017px 0 rgba(0, 0, 0, 0.08),
0 6.65px 5.32px 0 rgba(0, 0, 0, 0.09),
0 2.767px 2.214px 0 rgba(0, 0, 0, 0.13);
[data-slot="select-workspace-title"] {
flex-shrink: 0;
align-self: stretch;
color: rgba(255, 255, 255, 0.59);
text-align: center;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 160%; /* 25.6px */
}
[data-slot="workspaces"] {
width: 100%;
padding: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
outline: none;
overflow-y: auto;
flex: 1;
min-height: 0;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
[data-slot="workspace"] {
width: 100%;
display: flex;
padding: 8px 12px;
align-items: center;
gap: 8px;
align-self: stretch;
cursor: pointer;
[data-slot="selected-icon"] {
visibility: hidden;
color: rgba(255, 255, 255, 0.39);
font-family: "IBM Plex Mono";
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 160%; /* 25.6px */
}
a {
color: rgba(255, 255, 255, 0.92);
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 160%; /* 25.6px */
text-decoration: none;
}
}
[data-slot="workspace"]:hover,
[data-slot="workspace"][data-active="true"] {
background: #161616;
[data-slot="selected-icon"] {
visibility: visible;
}
}
}
}
}
[data-component="footer"] {
display: flex;
flex-direction: column;
width: 100%;
justify-content: center;
align-items: center;
gap: 24px;
flex-shrink: 0;
@media (min-width: 768px) {
height: 120px;
}
[data-slot="footer-content"] {
display: flex;
gap: 24px;
align-items: center;
justify-content: center;
@media (min-width: 768px) {
gap: 40px;
}
span,
a {
color: rgba(255, 255, 255, 0.39);
font-family: "JetBrains Mono Nerd Font";
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: normal;
text-decoration: none;
}
[data-slot="github-stars"] {
color: rgba(255, 255, 255, 0.25);
font-family: "JetBrains Mono Nerd Font";
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
[data-slot="anomaly"] {
display: none;
@media (min-width: 768px) {
display: block;
}
}
}
[data-slot="anomaly-alt"] {
color: rgba(255, 255, 255, 0.39);
font-family: "JetBrains Mono Nerd Font";
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: normal;
text-decoration: none;
margin-bottom: 24px;
a {
color: rgba(255, 255, 255, 0.39);
font-family: "JetBrains Mono Nerd Font";
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: normal;
text-decoration: none;
}
@media (min-width: 768px) {
display: none;
}
}
}
}
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@ export async function GET({ params: { platform } }: APIEvent) {
const resp = await fetch(`https://github.com/anomalyco/opencode/releases/latest/download/${assetName}`, { const resp = await fetch(`https://github.com/anomalyco/opencode/releases/latest/download/${assetName}`, {
cf: { cf: {
// in case gh releases has rate limits // in case gh releases has rate limits
cacheTtl: 60 * 5, cacheTtl: 60 * 60 * 24,
cacheEverything: true, cacheEverything: true,
}, },
} as any) } as any)
@@ -244,8 +244,7 @@ export default function Download() {
Download Download
</a> </a>
</div> </div>
{/* Disabled temporarily as it doesn't work */} <div data-component="download-row">
{/*<div data-component="download-row">
<div data-component="download-info"> <div data-component="download-info">
<span data-slot="icon"> <span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -260,7 +259,7 @@ export default function Download() {
<a href={getDownloadHref("linux-x64-appimage")} data-component="action-button"> <a href={getDownloadHref("linux-x64-appimage")} data-component="action-button">
Download Download
</a> </a>
</div>*/} </div>
</div> </div>
</section> </section>
+1 -1
View File
@@ -522,7 +522,7 @@ body {
[data-slot="content"] { [data-slot="content"] {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1ch; gap: 4px;
} }
[data-slot="text"] { [data-slot="text"] {
@@ -195,12 +195,6 @@ export default function Home() {
<strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account <strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account
</div> </div>
</li> </li>
<li>
<span>[*]</span>
<div>
<strong>ChatGPT Plus/Pro</strong> Log in with OpenAI to use your ChatGPT Plus or Pro account
</div>
</li>
<li> <li>
<span>[*]</span> <span>[*]</span>
<div> <div>
-3
View File
@@ -91,9 +91,6 @@ export default function Home() {
<li> <li>
<strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account <strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account
</li> </li>
<li>
<strong>ChatGPT Plus/Pro</strong> Log in with OpenAI to use your ChatGPT Plus or Pro account
</li>
<li> <li>
<strong>Use any model</strong> Supports 75+ LLM providers through{" "} <strong>Use any model</strong> Supports 75+ LLM providers through{" "}
<a href="https://models.dev">Models.dev</a>, including local models <a href="https://models.dev">Models.dev</a>, including local models
@@ -1,6 +1,6 @@
import { action } from "@solidjs/router" import { action } from "@solidjs/router"
import { getRequestEvent } from "solid-js/web" import { getRequestEvent } from "solid-js/web"
import { useAuthSession } from "~/context/auth" import { useAuthSession } from "~/context/auth.session"
import { Dropdown } from "~/component/dropdown" import { Dropdown } from "~/component/dropdown"
import "./user-menu.css" import "./user-menu.css"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.1.17", "version": "1.1.8",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+2 -17
View File
@@ -38,21 +38,10 @@ if (identifier.startsWith("wrk_")) {
workspaceID: UserTable.workspaceID, workspaceID: UserTable.workspaceID,
workspaceName: WorkspaceTable.name, workspaceName: WorkspaceTable.name,
role: UserTable.role, role: UserTable.role,
subscribed: SubscriptionTable.timeCreated,
}) })
.from(UserTable) .from(UserTable)
.rightJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID)) .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
.leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id)) .where(eq(UserTable.accountID, accountID)),
.where(eq(UserTable.accountID, accountID))
.then((rows) =>
rows.map((row) => ({
userID: row.userID,
workspaceID: row.workspaceID,
workspaceName: row.workspaceName,
role: row.role,
subscribed: formatDate(row.subscribed),
})),
),
) )
// Get all payments for these workspaces // Get all payments for these workspaces
@@ -113,8 +102,6 @@ async function printWorkspace(workspaceID: string) {
.select({ .select({
balance: BillingTable.balance, balance: BillingTable.balance,
customerID: BillingTable.customerID, customerID: BillingTable.customerID,
subscriptionID: BillingTable.subscriptionID,
subscriptionCouponID: BillingTable.subscriptionCouponID,
}) })
.from(BillingTable) .from(BillingTable)
.where(eq(BillingTable.workspaceID, workspace.id)) .where(eq(BillingTable.workspaceID, workspace.id))
@@ -151,7 +138,6 @@ async function printWorkspace(workspaceID: string) {
), ),
) )
/*
await printTable("Usage", (tx) => await printTable("Usage", (tx) =>
tx tx
.select({ .select({
@@ -177,7 +163,6 @@ async function printWorkspace(workspaceID: string) {
})), })),
), ),
) )
*/
} }
function formatMicroCents(value: number | null | undefined) { function formatMicroCents(value: number | null | undefined) {
@@ -1,78 +0,0 @@
import { Billing } from "../src/billing.js"
import { and, Database, eq } from "../src/drizzle/index.js"
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
const workspaceID = process.argv[2]
if (!workspaceID) {
console.error("Usage: bun remove-black.ts <workspaceID>")
process.exit(1)
}
console.log(`Removing subscription from workspace ${workspaceID}`)
// Look up the workspace billing
const billing = await Database.use((tx) =>
tx
.select({
customerID: BillingTable.customerID,
subscriptionID: BillingTable.subscriptionID,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, workspaceID))
.then((rows) => rows[0]),
)
if (!billing) {
console.error(`Error: No billing record found for workspace ${workspaceID}`)
process.exit(1)
}
if (!billing.subscriptionID) {
console.error(`Error: Workspace ${workspaceID} does not have a subscription`)
process.exit(1)
}
console.log(` Customer ID: ${billing.customerID}`)
console.log(` Subscription ID: ${billing.subscriptionID}`)
// Clear workspaceID from Stripe customer metadata
if (billing.customerID) {
//await Billing.stripe().customers.update(billing.customerID, {
// metadata: {
// workspaceID: "",
// },
//})
//console.log(`Cleared workspaceID from Stripe customer metadata`)
}
await Database.transaction(async (tx) => {
// Clear subscription-related fields from billing table
await tx
.update(BillingTable)
.set({
// customerID: null,
subscriptionID: null,
subscriptionCouponID: null,
// paymentMethodID: null,
// paymentMethodLast4: null,
// paymentMethodType: null,
})
.where(eq(BillingTable.workspaceID, workspaceID))
// Delete from subscription table
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
// Delete from payments table
await tx
.delete(PaymentTable)
.where(
and(
eq(PaymentTable.workspaceID, workspaceID),
eq(PaymentTable.enrichment, { type: "subscription" }),
eq(PaymentTable.amount, 20000000000),
),
)
})
console.log(`Successfully removed subscription from workspace ${workspaceID}`)
-4
View File
@@ -130,10 +130,6 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
} }
} }
// cloudflare // cloudflare
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.1.17", "version": "1.1.8",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
-4
View File
@@ -130,10 +130,6 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
} }
} }
// cloudflare // cloudflare
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.1.17", "version": "1.1.8",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
-4
View File
@@ -130,10 +130,6 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
} }
} }
// cloudflare // cloudflare
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.1.17", "version": "1.1.8",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+2 -2
View File
@@ -1,12 +1,12 @@
import { $ } from "bun" import { $ } from "bun"
import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils" import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils"
const RUST_TARGET = Bun.env.TAURI_ENV_TARGET_TRIPLE const RUST_TARGET = Bun.env.TAURI_ENV_TARGET_TRIPLE
const sidecarConfig = getCurrentSidecar(RUST_TARGET) const sidecarConfig = getCurrentSidecar(RUST_TARGET)
const binaryPath = windowsify(`../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode`) const binaryPath = `../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode${process.platform === "win32" ? ".exe" : ""}`
await $`cd ../opencode && bun run build --single` await $`cd ../opencode && bun run build --single`
+4 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { $ } from "bun" import { $ } from "bun"
import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils" import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils"
const sidecarConfig = getCurrentSidecar() const sidecarConfig = getCurrentSidecar()
@@ -10,4 +10,6 @@ const dir = "src-tauri/target/opencode-binaries"
await $`mkdir -p ${dir}` await $`mkdir -p ${dir}`
await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir) await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir)
await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`)) await copyBinaryToSidecarFolder(
`${dir}/${sidecarConfig.ocBinary}/bin/opencode${process.platform === "win32" ? ".exe" : ""}`,
)
+1 -6
View File
@@ -41,13 +41,8 @@ export function getCurrentSidecar(target = RUST_TARGET) {
export async function copyBinaryToSidecarFolder(source: string, target = RUST_TARGET) { export async function copyBinaryToSidecarFolder(source: string, target = RUST_TARGET) {
await $`mkdir -p src-tauri/sidecars` await $`mkdir -p src-tauri/sidecars`
const dest = windowsify(`src-tauri/sidecars/opencode-cli-${target}`) const dest = `src-tauri/sidecars/opencode-cli-${target}${process.platform === "win32" ? ".exe" : ""}`
await $`cp ${source} ${dest}` await $`cp ${source} ${dest}`
console.log(`Copied ${source} to ${dest}`) console.log(`Copied ${source} to ${dest}`)
} }
export function windowsify(path: string) {
if (path.endsWith(".exe")) return path
return `${path}${process.platform === "win32" ? ".exe" : ""}`
}
+3 -6
View File
@@ -2795,7 +2795,6 @@ dependencies = [
"futures", "futures",
"gtk", "gtk",
"listeners", "listeners",
"reqwest",
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
@@ -2814,9 +2813,7 @@ dependencies = [
"tauri-plugin-updater", "tauri-plugin-updater",
"tauri-plugin-window-state", "tauri-plugin-window-state",
"tokio", "tokio",
"uuid",
"webkit2gtk", "webkit2gtk",
"windows",
] ]
[[package]] [[package]]
@@ -5366,13 +5363,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.19.0" version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
"js-sys", "js-sys",
"serde_core", "serde",
"wasm-bindgen", "wasm-bindgen",
] ]
+1 -11
View File
@@ -3,7 +3,7 @@ name = "opencode-desktop"
version = "0.0.0" version = "0.0.0"
description = "The open source AI coding agent" description = "The open source AI coding agent"
authors = ["Anomaly Innovations"] authors = ["Anomaly Innovations"]
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -38,17 +38,7 @@ listeners = "0.3"
tauri-plugin-os = "2" tauri-plugin-os = "2"
futures = "0.3.31" futures = "0.3.31"
semver = "1.0.27" 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] [target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18.2" gtk = "0.18.2"
webkit2gtk = "=2.0.1" webkit2gtk = "=2.0.1"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61", features = [
"Win32_Foundation",
"Win32_System_JobObjects",
"Win32_System_Threading",
"Win32_Security"
] }
+1 -56
View File
@@ -1,30 +1,8 @@
use tauri::{path::BaseDirectory, AppHandle, Manager}; use tauri::Manager;
use tauri_plugin_shell::{process::Command, ShellExt};
const CLI_INSTALL_DIR: &str = ".opencode/bin"; const CLI_INSTALL_DIR: &str = ".opencode/bin";
const CLI_BINARY_NAME: &str = "opencode"; const CLI_BINARY_NAME: &str = "opencode";
#[derive(serde::Deserialize)]
pub struct ServerConfig {
pub hostname: Option<String>,
pub port: Option<u32>,
}
#[derive(serde::Deserialize)]
pub struct Config {
pub server: Option<ServerConfig>,
}
pub async fn get_config(app: &AppHandle) -> Option<Config> {
create_command(app, "debug config")
.output()
.await
.inspect_err(|e| eprintln!("Failed to read OC config: {e}"))
.ok()
.and_then(|out| String::from_utf8(out.stdout.to_vec()).ok())
.and_then(|s| serde_json::from_str::<Config>(&s).ok())
}
fn get_cli_install_path() -> Option<std::path::PathBuf> { fn get_cli_install_path() -> Option<std::path::PathBuf> {
std::env::var("HOME").ok().map(|home| { std::env::var("HOME").ok().map(|home| {
std::path::PathBuf::from(home) std::path::PathBuf::from(home)
@@ -139,36 +117,3 @@ pub fn sync_cli(app: tauri::AppHandle) -> Result<(), String> {
Ok(()) Ok(())
} }
fn get_user_shell() -> String {
std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
}
pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command {
let state_dir = app
.path()
.resolve("", BaseDirectory::AppLocalData)
.expect("Failed to resolve app local data dir");
#[cfg(target_os = "windows")]
return app
.shell()
.sidecar("opencode-cli")
.unwrap()
.args(args.split_whitespace())
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
.env("OPENCODE_CLIENT", "desktop")
.env("XDG_STATE_HOME", &state_dir);
#[cfg(not(target_os = "windows"))]
return {
let sidecar = get_sidecar_path(app);
let shell = get_user_shell();
app.shell()
.command(&shell)
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
.env("OPENCODE_CLIENT", "desktop")
.env("XDG_STATE_HOME", &state_dir)
.args(["-il", "-c", &format!("\"{}\" {}", sidecar.display(), args)])
};
}
@@ -1,145 +0,0 @@
//! Windows Job Object for reliable child process cleanup.
//!
//! This module provides a wrapper around Windows Job Objects with the
//! `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` flag set. When the job object handle
//! is closed (including when the parent process exits or crashes), Windows
//! automatically terminates all processes assigned to the job.
//!
//! This is more reliable than manual cleanup because it works even if:
//! - The parent process crashes
//! - The parent is killed via Task Manager
//! - The RunEvent::Exit handler fails to run
use std::io::{Error, Result};
#[cfg(windows)]
use std::sync::Mutex;
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
SetInformationJobObject,
};
use windows::Win32::System::Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE};
/// A Windows Job Object configured to kill all assigned processes when closed.
///
/// When this struct is dropped or when the owning process exits (even abnormally),
/// Windows will automatically terminate all processes that have been assigned to it.
pub struct JobObject(HANDLE);
// SAFETY: HANDLE is just a pointer-sized value, and Windows job objects
// can be safely accessed from multiple threads.
unsafe impl Send for JobObject {}
unsafe impl Sync for JobObject {}
impl JobObject {
/// Creates a new anonymous job object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` set.
///
/// When the last handle to this job is closed (including on process exit),
/// Windows will terminate all processes assigned to the job.
pub fn new() -> Result<Self> {
unsafe {
// Create an anonymous job object
let job = CreateJobObjectW(None, None).map_err(|e| Error::other(e.message()))?;
// Configure the job to kill all processes when the handle is closed
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
&info as *const _ as *const std::ffi::c_void,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
.map_err(|e| Error::other(e.message()))?;
Ok(Self(job))
}
}
/// Assigns a process to this job object by its process ID.
///
/// Once assigned, the process will be terminated when this job object is dropped
/// or when the owning process exits.
///
/// # Arguments
/// * `pid` - The process ID of the process to assign
pub fn assign_pid(&self, pid: u32) -> Result<()> {
unsafe {
// Open a handle to the process with the minimum required permissions
// PROCESS_SET_QUOTA and PROCESS_TERMINATE are required by AssignProcessToJobObject
let process = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid)
.map_err(|e| Error::other(e.message()))?;
// Assign the process to the job
let result = AssignProcessToJobObject(self.0, process);
// Close our handle to the process - the job object maintains its own reference
let _ = CloseHandle(process);
result.map_err(|e| Error::other(e.message()))
}
}
}
impl Drop for JobObject {
fn drop(&mut self) {
unsafe {
// When this handle is closed and it's the last handle to the job,
// Windows will terminate all processes in the job due to KILL_ON_JOB_CLOSE
let _ = CloseHandle(self.0);
}
}
}
/// Holds the Windows Job Object that ensures child processes are killed when the app exits.
/// On Windows, when the job object handle is closed (including on crash), all assigned
/// processes are automatically terminated by the OS.
#[cfg(windows)]
pub struct JobObjectState {
job: Mutex<Option<JobObject>>,
error: Mutex<Option<String>>,
}
#[cfg(windows)]
impl JobObjectState {
pub fn new() -> Self {
match JobObject::new() {
Ok(job) => Self {
job: Mutex::new(Some(job)),
error: Mutex::new(None),
},
Err(e) => {
eprintln!("Failed to create job object: {e}");
Self {
job: Mutex::new(None),
error: Mutex::new(Some(format!("Failed to create job object: {e}"))),
}
}
}
}
pub fn assign_pid(&self, pid: u32) {
if let Some(job) = self.job.lock().unwrap().as_ref() {
if let Err(e) = job.assign_pid(pid) {
eprintln!("Failed to assign process {pid} to job object: {e}");
*self.error.lock().unwrap() =
Some(format!("Failed to assign process to job object: {e}"));
} else {
println!("Assigned process {pid} to job object for automatic cleanup");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_job_object_creation() {
let job = JobObject::new();
assert!(job.is_ok(), "Failed to create job object: {:?}", job.err());
}
}
+89 -216
View File
@@ -1,46 +1,34 @@
mod cli; mod cli;
#[cfg(windows)]
mod job_object;
mod window_customizer; mod window_customizer;
use cli::{install_cli, sync_cli}; use cli::{get_sidecar_path, install_cli, sync_cli};
use futures::FutureExt; use futures::FutureExt;
use futures::future;
#[cfg(windows)]
use job_object::*;
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
net::TcpListener, net::{SocketAddr, TcpListener},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, WebviewWindow}; use tauri::{
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; path::BaseDirectory, AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl,
WebviewWindow,
};
use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_store::StoreExt; use tauri_plugin_shell::ShellExt;
use tokio::sync::oneshot; use tokio::net::TcpSocket;
use crate::window_customizer::PinchZoomDisablePlugin; use crate::window_customizer::PinchZoomDisablePlugin;
const SETTINGS_STORE: &str = "opencode.settings.dat";
const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl";
#[derive(Clone, serde::Serialize)]
struct ServerReadyData {
url: String,
password: Option<String>,
}
#[derive(Clone)] #[derive(Clone)]
struct ServerState { struct ServerState {
child: Arc<Mutex<Option<CommandChild>>>, child: Arc<Mutex<Option<CommandChild>>>,
status: future::Shared<oneshot::Receiver<Result<ServerReadyData, String>>>, status: futures::future::Shared<tokio::sync::oneshot::Receiver<Result<(), String>>>,
} }
impl ServerState { impl ServerState {
pub fn new( pub fn new(
child: Option<CommandChild>, child: Option<CommandChild>,
status: oneshot::Receiver<Result<ServerReadyData, String>>, status: tokio::sync::oneshot::Receiver<Result<(), String>>,
) -> Self { ) -> Self {
Self { Self {
child: Arc::new(Mutex::new(child)), child: Arc::new(Mutex::new(child)),
@@ -92,7 +80,7 @@ async fn get_logs(app: AppHandle) -> Result<String, String> {
} }
#[tauri::command] #[tauri::command]
async fn ensure_server_ready(state: State<'_, ServerState>) -> Result<ServerReadyData, String> { async fn ensure_server_started(state: State<'_, ServerState>) -> Result<(), String> {
state state
.status .status
.clone() .clone()
@@ -100,41 +88,6 @@ async fn ensure_server_ready(state: State<'_, ServerState>) -> Result<ServerRead
.map_err(|_| "Failed to get server status".to_string())? .map_err(|_| "Failed to get server status".to_string())?
} }
#[tauri::command]
fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> {
let store = app
.store(SETTINGS_STORE)
.map_err(|e| format!("Failed to open settings store: {}", e))?;
let value = store.get(DEFAULT_SERVER_URL_KEY);
match value {
Some(v) => Ok(v.as_str().map(String::from)),
None => Ok(None),
}
}
#[tauri::command]
async fn set_default_server_url(app: AppHandle, url: Option<String>) -> Result<(), String> {
let store = app
.store(SETTINGS_STORE)
.map_err(|e| format!("Failed to open settings store: {}", e))?;
match url {
Some(u) => {
store.set(DEFAULT_SERVER_URL_KEY, serde_json::Value::String(u));
}
None => {
store.delete(DEFAULT_SERVER_URL_KEY);
}
}
store
.save()
.map_err(|e| format!("Failed to save settings: {}", e))?;
Ok(())
}
fn get_sidecar_port() -> u32 { fn get_sidecar_port() -> u32 {
option_env!("OPENCODE_PORT") option_env!("OPENCODE_PORT")
.map(|s| s.to_string()) .map(|s| s.to_string())
@@ -149,17 +102,49 @@ fn get_sidecar_port() -> u32 {
}) as u32 }) as u32
} }
fn spawn_sidecar(app: &AppHandle, port: u32, password: &str) -> CommandChild { fn get_user_shell() -> String {
std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
}
fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild {
let log_state = app.state::<LogState>(); let log_state = app.state::<LogState>();
let log_state_clone = log_state.inner().clone(); let log_state_clone = log_state.inner().clone();
println!("spawning sidecar on port {port}"); let state_dir = app
.path()
.resolve("", BaseDirectory::AppLocalData)
.expect("Failed to resolve app local data dir");
let (mut rx, child) = cli::create_command(app, format!("serve --port {port}").as_str()) #[cfg(target_os = "windows")]
.env("OPENCODE_SERVER_PASSWORD", password) let (mut rx, child) = app
.shell()
.sidecar("opencode-cli")
.unwrap()
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
.env("OPENCODE_CLIENT", "desktop")
.env("XDG_STATE_HOME", &state_dir)
.args(["serve", &format!("--port={port}")])
.spawn() .spawn()
.expect("Failed to spawn opencode"); .expect("Failed to spawn opencode");
#[cfg(not(target_os = "windows"))]
let (mut rx, child) = {
let sidecar = get_sidecar_path(app);
let shell = get_user_shell();
app.shell()
.command(&shell)
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
.env("OPENCODE_CLIENT", "desktop")
.env("XDG_STATE_HOME", &state_dir)
.args([
"-il",
"-c",
&format!("\"{}\" serve --port={}", sidecar.display(), port),
])
.spawn()
.expect("Failed to spawn opencode")
};
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
match event { match event {
@@ -197,37 +182,21 @@ fn spawn_sidecar(app: &AppHandle, port: u32, password: &str) -> CommandChild {
child child
} }
async fn check_server_health(url: &str, password: Option<&str>) -> bool { async fn is_server_running(port: u32) -> bool {
let health_url = format!("{}/global/health", url.trim_end_matches('/')); TcpSocket::new_v4()
let client = reqwest::Client::builder() .unwrap()
.timeout(Duration::from_secs(3)) .connect(SocketAddr::new(
.build(); "127.0.0.1".parse().expect("Failed to parse IP"),
port as u16,
let Ok(client) = client else { ))
return false;
};
let mut req = client.get(&health_url);
if let Some(password) = password {
req = req.basic_auth("opencode", Some(password));
}
req.send()
.await .await
.map(|r| r.status().is_success()) .is_ok()
.unwrap_or(false)
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some(); let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
#[cfg(target_os = "macos")]
let _ = std::process::Command::new("killall")
.arg("opencode-cli")
.output();
let mut builder = tauri::Builder::default() let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
// Focus existing window when another instance is launched // Focus existing window when another instance is launched
@@ -250,9 +219,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
kill_sidecar, kill_sidecar,
install_cli, install_cli,
ensure_server_ready, ensure_server_started
get_default_server_url,
set_default_server_url
]) ])
.setup(move |app| { .setup(move |app| {
let app = app.handle().clone(); let app = app.handle().clone();
@@ -260,15 +227,15 @@ pub fn run() {
// Initialize log state // Initialize log state
app.manage(LogState(Arc::new(Mutex::new(VecDeque::new())))); app.manage(LogState(Arc::new(Mutex::new(VecDeque::new()))));
#[cfg(windows)] // Get port and create window immediately for faster perceived startup
app.manage(JobObjectState::new()); let port = get_sidecar_port();
let primary_monitor = app.primary_monitor().ok().flatten(); let primary_monitor = app.primary_monitor().ok().flatten();
let size = primary_monitor let size = primary_monitor
.map(|m| m.size().to_logical(m.scale_factor())) .map(|m| m.size().to_logical(m.scale_factor()))
.unwrap_or(LogicalSize::new(1920, 1080)); .unwrap_or(LogicalSize::new(1920, 1080));
#[allow(unused_mut)] // Create window immediately with serverReady = false
let mut window_builder = let mut window_builder =
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into())) WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
.title("OpenCode") .title("OpenCode")
@@ -280,6 +247,7 @@ pub fn run() {
r#" r#"
window.__OPENCODE__ ??= {{}}; window.__OPENCODE__ ??= {{}};
window.__OPENCODE__.updaterEnabled = {updater_enabled}; window.__OPENCODE__.updaterEnabled = {updater_enabled};
window.__OPENCODE__.port = {port};
"# "#
)); ));
@@ -290,43 +258,50 @@ pub fn run() {
.hidden_title(true); .hidden_title(true);
} }
window_builder.build().expect("Failed to create window"); let window = window_builder.build().expect("Failed to create window");
let (tx, rx) = oneshot::channel(); let (tx, rx) = tokio::sync::oneshot::channel();
app.manage(ServerState::new(None, rx)); app.manage(ServerState::new(None, rx));
{ {
let app = app.clone(); let app = app.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let mut custom_url = None; let should_spawn_sidecar = !is_server_running(port).await;
if let Some(url) = get_default_server_url(app.clone()).ok().flatten() { let (child, res) = if should_spawn_sidecar {
println!("Using desktop-specific custom URL: {url}"); let child = spawn_sidecar(&app, port);
custom_url = Some(url);
let timestamp = Instant::now();
let res = loop {
if timestamp.elapsed() > Duration::from_secs(7) {
break Err(format!(
"Failed to spawn OpenCode Server. Logs:\n{}",
get_logs(app.clone()).await.unwrap()
));
} }
if custom_url.is_none() tokio::time::sleep(Duration::from_millis(10)).await;
&& let Some(cli_config) = cli::get_config(&app).await
&& let Some(url) = get_server_url_from_config(&cli_config)
{
println!("Using custom server URL from config: {url}");
custom_url = Some(url);
}
let res = match setup_server_connection(&app, custom_url).await { if is_server_running(port).await {
Ok((child, url)) => { // give the server a little bit more time to warm up
#[cfg(windows)] tokio::time::sleep(Duration::from_millis(10)).await;
if let Some(child) = &child {
let job_state = app.state::<JobObjectState>(); break Ok(());
job_state.assign_pid(child.pid());
} }
};
println!("Server ready after {:?}", timestamp.elapsed());
(Some(child), res)
} else {
(None, Ok(()))
};
app.state::<ServerState>().set_child(child); app.state::<ServerState>().set_child(child);
Ok(url) if res.is_ok() {
let _ = window.eval("window.__OPENCODE__.serverReady = true;");
} }
Err(e) => Err(e),
};
let _ = tx.send(res); let _ = tx.send(res);
}); });
@@ -359,105 +334,3 @@ pub fn run() {
} }
}); });
} }
fn get_server_url_from_config(config: &cli::Config) -> Option<String> {
let server = config.server.as_ref()?;
let port = server.port?;
println!("server.port found in OC config: {port}");
let hostname = server.hostname.as_ref();
Some(format!(
"http://{}:{}",
hostname.map(|v| v.as_str()).unwrap_or("127.0.0.1"),
port
))
}
async fn setup_server_connection(
app: &AppHandle,
custom_url: Option<String>,
) -> Result<(Option<CommandChild>, ServerReadyData), String> {
if let Some(url) = custom_url {
loop {
if check_server_health(&url, None).await {
println!("Connected to custom server: {}", url);
return Ok((
None,
ServerReadyData {
url: url.clone(),
password: None,
},
));
}
const RETRY: &str = "Retry";
let res = app.dialog()
.message(format!("Could not connect to configured server:\n{}\n\nWould you like to retry or start a local server instead?", url))
.title("Connection Failed")
.buttons(MessageDialogButtons::OkCancelCustom(RETRY.to_string(), "Start Local".to_string()))
.blocking_show_with_result();
match res {
MessageDialogResult::Custom(name) if name == RETRY => {
continue;
}
_ => {
break;
}
}
}
}
let local_port = get_sidecar_port();
let local_url = format!("http://127.0.0.1:{local_port}");
if !check_server_health(&local_url, None).await {
let password = uuid::Uuid::new_v4().to_string();
match spawn_local_server(app, local_port, &password).await {
Ok(child) => Ok((
Some(child),
ServerReadyData {
url: local_url,
password: Some(password),
},
)),
Err(err) => Err(err),
}
} else {
Ok((
None,
ServerReadyData {
url: local_url,
password: None,
},
))
}
}
async fn spawn_local_server(
app: &AppHandle,
port: u32,
password: &str,
) -> Result<CommandChild, String> {
let child = spawn_sidecar(app, port, password);
let url = format!("http://127.0.0.1:{port}");
let timestamp = Instant::now();
loop {
if timestamp.elapsed() > Duration::from_secs(30) {
break Err(format!(
"Failed to spawn OpenCode Server. Logs:\n{}",
get_logs(app.clone()).await.unwrap()
));
}
tokio::time::sleep(Duration::from_millis(10)).await;
if check_server_health(&url, Some(password)).await {
println!("Server ready after {:?}", timestamp.elapsed());
break Ok(child);
}
}
}
+1 -1
View File
@@ -26,7 +26,7 @@
"icons/dev/icon.ico" "icons/dev/icon.ico"
], ],
"active": true, "active": true,
"targets": ["deb", "rpm", "dmg", "nsis", "app"], "targets": ["deb", "rpm", "dmg", "nsis", "app", "appimage"],
"externalBin": ["sidecars/opencode-cli"], "externalBin": ["sidecars/opencode-cli"],
"macOS": { "macOS": {
"entitlements": "./entitlements.plist" "entitlements": "./entitlements.plist"
+16 -48
View File
@@ -13,11 +13,12 @@ import { AsyncStorage } from "@solid-primitives/storage"
import { fetch as tauriFetch } from "@tauri-apps/plugin-http" import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
import { Store } from "@tauri-apps/plugin-store" import { Store } from "@tauri-apps/plugin-store"
import { Logo } from "@opencode-ai/ui/logo" import { Logo } from "@opencode-ai/ui/logo"
import { createSignal, Show, Accessor, JSX, createResource } from "solid-js" import { Suspense, createResource, ParentProps } from "solid-js"
import { UPDATER_ENABLED } from "./updater" import { UPDATER_ENABLED } from "./updater"
import { createMenu } from "./menu" import { createMenu } from "./menu"
import pkg from "../package.json" import pkg from "../package.json"
import { Show } from "solid-js"
const root = document.getElementById("root") const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) { if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -28,7 +29,7 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
let update: Update | null = null let update: Update | null = null
const createPlatform = (password: Accessor<string | null>): Platform => ({ const platform: Platform = {
platform: "desktop", platform: "desktop",
version: pkg.version, version: pkg.version,
@@ -255,36 +256,9 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
}, },
// @ts-expect-error // @ts-expect-error
fetch: (input, init) => { fetch: tauriFetch,
const pw = password()
const addHeader = (headers: Headers, password: string) => {
headers.append("Authorization", `Basic ${btoa(`opencode:${password}`)}`)
} }
if (input instanceof Request) {
if (pw) addHeader(input.headers, pw)
return tauriFetch(input)
} else {
const headers = new Headers(init?.headers)
if (pw) addHeader(headers, pw)
return tauriFetch(input, {
...(init as any),
headers: headers,
})
}
},
getDefaultServerUrl: async () => {
const result = await invoke<string | null>("get_default_server_url").catch(() => null)
return result
},
setDefaultServerUrl: async (url: string | null) => {
await invoke("set_default_server_url", { url })
},
})
createMenu() createMenu()
// Stops mousewheel events from reaching Tauri's pinch-to-zoom handler // Stops mousewheel events from reaching Tauri's pinch-to-zoom handler
@@ -293,47 +267,41 @@ root?.addEventListener("mousewheel", (e) => {
}) })
render(() => { render(() => {
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
const platform = createPlatform(() => serverPassword())
return ( return (
<PlatformProvider value={platform}> <PlatformProvider value={platform}>
<AppBaseProviders>
{ostype() === "macos" && ( {ostype() === "macos" && (
<div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region /> <div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
)} )}
<AppBaseProviders>
<ServerGate> <ServerGate>
{(data) => { <AppInterface />
setServerPassword(data().password)
window.__OPENCODE__ ??= {}
window.__OPENCODE__.serverPassword = data().password ?? undefined
return <AppInterface defaultUrl={data().url} />
}}
</ServerGate> </ServerGate>
</AppBaseProviders> </AppBaseProviders>
</PlatformProvider> </PlatformProvider>
) )
}, root!) }, root!)
type ServerReadyData = { url: string; password: string | null }
// Gate component that waits for the server to be ready // Gate component that waits for the server to be ready
function ServerGate(props: { children: (data: Accessor<ServerReadyData>) => JSX.Element }) { function ServerGate(props: ParentProps) {
const [serverData] = createResource<ServerReadyData>(() => invoke("ensure_server_ready")) const [status] = createResource(async () => {
if (window.__OPENCODE__?.serverReady) return
return await invoke("ensure_server_started")
})
return ( return (
// Not using suspense as not all components are compatible with it (undefined refs) // Not using suspense as not all components are compatible with it (undefined refs)
<Show <Show
when={serverData.state !== "pending" && serverData()} when={status.state !== "pending"}
fallback={ fallback={
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base"> <div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
<Logo class="w-xl opacity-12 animate-pulse" /> <Logo class="w-xl opacity-12 animate-pulse" />
<div class="mt-8 text-14-regular text-text-weak">Initializing...</div> <div class="mt-8 text-14-regular text-text-weak">Starting server...</div>
</div> </div>
} }
> >
{(data) => props.children(data)} {/* Trigger error boundary without rendering the returned value */}
{(status(), null)}
{props.children}
</Show> </Show>
) )
} }
+3 -2
View File
@@ -57,10 +57,11 @@ Example: `mintlify`
Hex color codes for your global theme Hex color codes for your global theme
<Expandable title="Colors"> <Expandable title="Colors">
<ResponseField name="primary" type="string" required> <ResponseField name="primary" type="string" required>
The primary color. Used most often for highlighted content, section headers, accents, in light mode The primary color. Used for most often for highlighted content, section headers, accents, in light mode
</ResponseField> </ResponseField>
<ResponseField name="light" type="string"> <ResponseField name="light" type="string">
The primary color for dark mode. Used most often for highlighted content, section headers, accents, in dark mode The primary color for dark mode. Used for most often for highlighted content, section headers, accents, in dark
mode
</ResponseField> </ResponseField>
<ResponseField name="dark" type="string"> <ResponseField name="dark" type="string">
The primary color for important buttons The primary color for important buttons
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.1.17", "version": "1.1.8",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@@ -9,8 +9,7 @@
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"build:cloudflare": "OPENCODE_DEPLOYMENT_TARGET=cloudflare vite build", "build:cloudflare": "OPENCODE_DEPLOYMENT_TARGET=cloudflare vite build",
"start": "vite start", "start": "vite start"
"shell-prod": "sst shell --target Teams --stage production"
}, },
"dependencies": { "dependencies": {
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
-15
View File
@@ -1,15 +0,0 @@
import { Storage } from "../src/core/storage"
// read share id from args
const args = process.argv.slice(2)
if (args.length !== 1) {
console.error("Usage: bun script/scrap.ts <shareID>")
process.exit(1)
}
const shareID = args[0]
await Storage.remove(["share", shareID])
const list = await Storage.list({ prefix: ["share_data", shareID] })
for (const item of list) {
await Storage.remove(item)
}
-4
View File
@@ -130,10 +130,6 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
} }
} }
// cloudflare // cloudflare
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode" id = "opencode"
name = "OpenCode" name = "OpenCode"
description = "The open source coding agent." description = "The open source coding agent."
version = "1.1.17" version = "1.1.8"
schema_version = 1 schema_version = 1
authors = ["Anomaly"] authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode" repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg" icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64] [agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.17/opencode-darwin-arm64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-darwin-arm64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64] [agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.17/opencode-darwin-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-darwin-x64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64] [agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.17/opencode-linux-arm64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-linux-arm64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64] [agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.17/opencode-linux-x64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-linux-x64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64] [agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.17/opencode-windows-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.8/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.1.17", "version": "1.1.8",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
-4
View File
@@ -130,10 +130,6 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
} }
} }
// cloudflare // cloudflare
+19 -19
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.1.17", "version": "1.1.8",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@@ -50,25 +50,25 @@
"@actions/core": "1.11.1", "@actions/core": "1.11.1",
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.5.1", "@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/amazon-bedrock": "3.0.73", "@ai-sdk/amazon-bedrock": "3.0.57",
"@ai-sdk/anthropic": "2.0.57", "@ai-sdk/anthropic": "2.0.56",
"@ai-sdk/azure": "2.0.91", "@ai-sdk/azure": "2.0.82",
"@ai-sdk/cerebras": "1.0.34", "@ai-sdk/cerebras": "1.0.33",
"@ai-sdk/cohere": "2.0.22", "@ai-sdk/cohere": "2.0.21",
"@ai-sdk/deepinfra": "1.0.31", "@ai-sdk/deepinfra": "1.0.30",
"@ai-sdk/gateway": "2.0.25", "@ai-sdk/gateway": "2.0.23",
"@ai-sdk/google": "2.0.52", "@ai-sdk/google": "2.0.49",
"@ai-sdk/google-vertex": "3.0.97", "@ai-sdk/google-vertex": "3.0.81",
"@ai-sdk/groq": "2.0.34", "@ai-sdk/groq": "2.0.33",
"@ai-sdk/mistral": "2.0.27", "@ai-sdk/mistral": "2.0.26",
"@ai-sdk/openai": "2.0.89", "@ai-sdk/openai": "2.0.71",
"@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/openai-compatible": "1.0.29",
"@ai-sdk/perplexity": "2.0.23", "@ai-sdk/perplexity": "2.0.22",
"@ai-sdk/provider": "2.0.1", "@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.20", "@ai-sdk/provider-utils": "3.0.19",
"@ai-sdk/togetherai": "1.0.31", "@ai-sdk/togetherai": "1.0.30",
"@ai-sdk/vercel": "1.0.31", "@ai-sdk/vercel": "1.0.31",
"@ai-sdk/xai": "2.0.51", "@ai-sdk/xai": "2.0.42",
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@hono/standard-validator": "0.1.5", "@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:", "@hono/zod-validator": "catalog:",
+2 -5
View File
@@ -106,11 +106,8 @@ async function main() {
return return
} }
// On non-Windows platforms, just verify the binary package exists const { binaryPath, binaryName } = findBinary()
// Don't replace the wrapper script - it handles binary execution symlinkBinary(binaryPath, binaryName)
const { binaryPath } = findBinary()
console.log(`Platform binary verified at: ${binaryPath}`)
console.log("Wrapper script will handle binary execution")
} catch (error) { } catch (error) {
console.error("Failed to setup opencode binary:", error.message) console.error("Failed to setup opencode binary:", error.message)
process.exit(1) process.exit(1)
-26
View File
@@ -30,7 +30,6 @@ import { Todo } from "@/session/todo"
import { z } from "zod" import { z } from "zod"
import { LoadAPIKeyError } from "ai" import { LoadAPIKeyError } from "ai"
import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2" import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
import { applyPatch } from "diff"
export namespace ACP { export namespace ACP {
const log = Log.create({ service: "acp-agent" }) const log = Log.create({ service: "acp-agent" })
@@ -106,22 +105,6 @@ export namespace ACP {
}) })
return return
} }
if (res.outcome.optionId !== "reject" && permission.permission == "edit") {
const metadata = permission.metadata || {}
const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
const content = await Bun.file(filepath).text()
const newContent = getNewContent(content, diff)
if (newContent) {
this.connection.writeTextFile({
sessionId: sessionId,
path: filepath,
content: newContent,
})
}
}
await this.config.sdk.permission.reply({ await this.config.sdk.permission.reply({
requestID: permission.id, requestID: permission.id,
reply: res.outcome.optionId as "once" | "always" | "reject", reply: res.outcome.optionId as "once" | "always" | "reject",
@@ -1112,13 +1095,4 @@ export namespace ACP {
} }
} }
} }
function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined {
const result = applyPatch(fileOriginal, unifiedDiff)
if (result === false) {
log.error("Failed to apply unified diff (context mismatch)")
return undefined
}
return result
}
} }
+6 -10
View File
@@ -50,14 +50,13 @@ export namespace Agent {
external_directory: { external_directory: {
"*": "ask", "*": "ask",
[Truncate.DIR]: "allow", [Truncate.DIR]: "allow",
[Truncate.GLOB]: "allow",
}, },
question: "deny", question: "deny",
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files // mirrors github.com/github/gitignore Node.gitignore pattern for .env files
read: { read: {
"*": "allow", "*": "allow",
"*.env": "ask", "*.env": "deny",
"*.env.*": "ask", "*.env.*": "deny",
"*.env.example": "allow", "*.env.example": "allow",
}, },
}) })
@@ -125,7 +124,6 @@ export namespace Agent {
read: "allow", read: "allow",
external_directory: { external_directory: {
[Truncate.DIR]: "allow", [Truncate.DIR]: "allow",
[Truncate.GLOB]: "allow",
}, },
}), }),
user, user,
@@ -215,16 +213,14 @@ export namespace Agent {
// Ensure Truncate.DIR is allowed unless explicitly configured // Ensure Truncate.DIR is allowed unless explicitly configured
for (const name in result) { for (const name in result) {
const agent = result[name] const agent = result[name]
const explicit = agent.permission.some((r) => { const explicit = agent.permission.some(
if (r.permission !== "external_directory") return false (r) => r.permission === "external_directory" && r.pattern === Truncate.DIR && r.action === "deny",
if (r.action !== "deny") return false )
return r.pattern === Truncate.DIR || r.pattern === Truncate.GLOB
})
if (explicit) continue if (explicit) continue
result[name].permission = PermissionNext.merge( result[name].permission = PermissionNext.merge(
result[name].permission, result[name].permission,
PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow" } }), PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow" } }),
) )
} }
-1
View File
@@ -12,7 +12,6 @@ export namespace Auth {
refresh: z.string(), refresh: z.string(),
access: z.string(), access: z.string(),
expires: z.number(), expires: z.number(),
accountId: z.string().optional(),
enterpriseUrl: z.string().optional(), enterpriseUrl: z.string().optional(),
}) })
.meta({ ref: "OAuth" }) .meta({ ref: "OAuth" })
-1
View File
@@ -294,7 +294,6 @@ export const AuthLoginCommand = cmd({
hint: { hint: {
opencode: "recommended", opencode: "recommended",
anthropic: "Claude Max or API key", anthropic: "Claude Max or API key",
openai: "ChatGPT Plus/Pro or API key",
}[x.id], }[x.id],
})), })),
), ),
+2 -139
View File
@@ -1,14 +1,6 @@
import { EOL } from "os" import { EOL } from "os"
import { basename } from "path" import { basename } from "path"
import { Agent } from "../../../agent/agent" import { Agent } from "../../../agent/agent"
import { Provider } from "../../../provider/provider"
import { Session } from "../../../session"
import type { MessageV2 } from "../../../session/message-v2"
import { Identifier } from "../../../id/id"
import { ToolRegistry } from "../../../tool/registry"
import { Instance } from "../../../project/instance"
import { PermissionNext } from "../../../permission/next"
import { iife } from "../../../util/iife"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
@@ -16,19 +8,10 @@ export const AgentCommand = cmd({
command: "agent <name>", command: "agent <name>",
describe: "show agent configuration details", describe: "show agent configuration details",
builder: (yargs) => builder: (yargs) =>
yargs yargs.positional("name", {
.positional("name", {
type: "string", type: "string",
demandOption: true, demandOption: true,
description: "Agent name", description: "Agent name",
})
.option("tool", {
type: "string",
description: "Tool id to execute",
})
.option("params", {
type: "string",
description: "Tool params as JSON or a JS object literal",
}), }),
async handler(args) { async handler(args) {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
@@ -40,127 +23,7 @@ export const AgentCommand = cmd({
) )
process.exit(1) process.exit(1)
} }
const availableTools = await getAvailableTools(agent) process.stdout.write(JSON.stringify(agent, null, 2) + EOL)
const resolvedTools = await resolveTools(agent, availableTools)
const toolID = args.tool as string | undefined
if (toolID) {
const tool = availableTools.find((item) => item.id === toolID)
if (!tool) {
process.stderr.write(`Tool ${toolID} not found for agent ${agentName}` + EOL)
process.exit(1)
}
if (resolvedTools[toolID] === false) {
process.stderr.write(`Tool ${toolID} is disabled for agent ${agentName}` + EOL)
process.exit(1)
}
const params = parseToolParams(args.params as string | undefined)
const ctx = await createToolContext(agent)
const result = await tool.execute(params, ctx)
process.stdout.write(JSON.stringify({ tool: toolID, input: params, result }, null, 2) + EOL)
return
}
const output = {
...agent,
tools: resolvedTools,
}
process.stdout.write(JSON.stringify(output, null, 2) + EOL)
}) })
}, },
}) })
async function getAvailableTools(agent: Agent.Info) {
const providerID = agent.model?.providerID ?? (await Provider.defaultModel()).providerID
return ToolRegistry.tools(providerID, agent)
}
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
const disabled = PermissionNext.disabled(
availableTools.map((tool) => tool.id),
agent.permission,
)
const resolved: Record<string, boolean> = {}
for (const tool of availableTools) {
resolved[tool.id] = !disabled.has(tool.id)
}
return resolved
}
function parseToolParams(input?: string) {
if (!input) return {}
const trimmed = input.trim()
if (trimmed.length === 0) return {}
const parsed = iife(() => {
try {
return JSON.parse(trimmed)
} catch (jsonError) {
try {
return new Function(`return (${trimmed})`)()
} catch (evalError) {
throw new Error(
`Failed to parse --params. Use JSON or a JS object literal. JSON error: ${jsonError}. Eval error: ${evalError}.`,
)
}
}
})
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Tool params must be an object.")
}
return parsed as Record<string, unknown>
}
async function createToolContext(agent: Agent.Info) {
const session = await Session.create({ title: `Debug tool run (${agent.name})` })
const messageID = Identifier.ascending("message")
const model = agent.model ?? (await Provider.defaultModel())
const now = Date.now()
const message: MessageV2.Assistant = {
id: messageID,
sessionID: session.id,
role: "assistant",
time: {
created: now,
},
parentID: messageID,
modelID: model.modelID,
providerID: model.providerID,
mode: "debug",
agent: agent.name,
path: {
cwd: Instance.directory,
root: Instance.worktree,
},
cost: 0,
tokens: {
input: 0,
output: 0,
reasoning: 0,
cache: {
read: 0,
write: 0,
},
},
}
await Session.updateMessage(message)
const ruleset = PermissionNext.merge(agent.permission, session.permission ?? [])
return {
sessionID: session.id,
messageID,
callID: Identifier.ascending("part"),
agent: agent.name,
abort: new AbortController().signal,
metadata: () => {},
async ask(req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) {
for (const pattern of req.patterns) {
const rule = PermissionNext.evaluate(req.permission, pattern, ruleset)
if (rule.action === "deny") {
throw new PermissionNext.DeniedError(ruleset)
}
}
},
}
}
+1 -1
View File
@@ -78,7 +78,7 @@ const FileTreeCommand = cmd({
}), }),
async handler(args) { async handler(args) {
const files = await Ripgrep.tree({ cwd: args.dir, limit: 200 }) const files = await Ripgrep.tree({ cwd: args.dir, limit: 200 })
console.log(JSON.stringify(files, null, 2)) console.log(files)
}, },
}) })
+4 -6
View File
@@ -394,8 +394,6 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v6
with:
persist-credentials: false
- name: Run opencode - name: Run opencode
uses: anomalyco/opencode/github@latest${envStr} uses: anomalyco/opencode/github@latest${envStr}
@@ -626,7 +624,7 @@ export const GithubRunCommand = cmd({
} }
} catch (e: any) { } catch (e: any) {
exitCode = 1 exitCode = 1
console.error(e instanceof Error ? e.message : String(e)) console.error(e)
let msg = e let msg = e
if (e instanceof $.ShellError) { if (e instanceof $.ShellError) {
msg = e.stderr.toString() msg = e.stderr.toString()
@@ -917,7 +915,7 @@ export const GithubRunCommand = cmd({
// result should always be assistant just satisfying type checker // result should always be assistant just satisfying type checker
if (result.info.role === "assistant" && result.info.error) { if (result.info.role === "assistant" && result.info.error) {
console.error("Agent error:", result.info.error) console.error(result.info)
throw new Error( throw new Error(
`${result.info.error.name}: ${"message" in result.info.error ? result.info.error.message : ""}`, `${result.info.error.name}: ${"message" in result.info.error ? result.info.error.message : ""}`,
) )
@@ -946,7 +944,7 @@ export const GithubRunCommand = cmd({
}) })
if (summary.info.role === "assistant" && summary.info.error) { if (summary.info.role === "assistant" && summary.info.error) {
console.error("Summary agent error:", summary.info.error) console.error(summary.info)
throw new Error( throw new Error(
`${summary.info.error.name}: ${"message" in summary.info.error ? summary.info.error.message : ""}`, `${summary.info.error.name}: ${"message" in summary.info.error ? summary.info.error.message : ""}`,
) )
@@ -964,7 +962,7 @@ export const GithubRunCommand = cmd({
try { try {
return await core.getIDToken("opencode-github-action") return await core.getIDToken("opencode-github-action")
} catch (error) { } catch (error) {
console.error("Failed to get OIDC token:", error instanceof Error ? error.message : error) console.error("Failed to get OIDC token:", error)
throw new Error( throw new Error(
"Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions.", "Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions.",
) )
+32 -103
View File
@@ -1,6 +1,7 @@
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { UI } from "../ui" import { UI } from "../ui"
@@ -12,7 +13,6 @@ import { Instance } from "../../project/instance"
import { Installation } from "../../installation" import { Installation } from "../../installation"
import path from "path" import path from "path"
import { Global } from "../../global" import { Global } from "../../global"
import { modify, applyEdits } from "jsonc-parser"
function getAuthStatusIcon(status: MCP.AuthStatus): string { function getAuthStatusIcon(status: MCP.AuthStatus): string {
switch (status) { switch (status) {
@@ -366,83 +366,13 @@ export const McpLogoutCommand = cmd({
}, },
}) })
async function resolveConfigPath(baseDir: string, global = false) {
// Check for existing config files (prefer .jsonc over .json, check .opencode/ subdirectory too)
const candidates = [path.join(baseDir, "opencode.json"), path.join(baseDir, "opencode.jsonc")]
if (!global) {
candidates.push(path.join(baseDir, ".opencode", "opencode.json"), path.join(baseDir, ".opencode", "opencode.jsonc"))
}
for (const candidate of candidates) {
if (await Bun.file(candidate).exists()) {
return candidate
}
}
// Default to opencode.json if none exist
return candidates[0]
}
async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) {
const file = Bun.file(configPath)
let text = "{}"
if (await file.exists()) {
text = await file.text()
}
// Use jsonc-parser to modify while preserving comments
const edits = modify(text, ["mcp", name], mcpConfig, {
formattingOptions: { tabSize: 2, insertSpaces: true },
})
const result = applyEdits(text, edits)
await Bun.write(configPath, result)
return configPath
}
export const McpAddCommand = cmd({ export const McpAddCommand = cmd({
command: "add", command: "add",
describe: "add an MCP server", describe: "add an MCP server",
async handler() { async handler() {
await Instance.provide({
directory: process.cwd(),
async fn() {
UI.empty() UI.empty()
prompts.intro("Add MCP server") prompts.intro("Add MCP server")
const project = Instance.project
// Resolve config paths eagerly for hints
const [projectConfigPath, globalConfigPath] = await Promise.all([
resolveConfigPath(Instance.worktree),
resolveConfigPath(Global.Path.config, true),
])
// Determine scope
let configPath = globalConfigPath
if (project.vcs === "git") {
const scopeResult = await prompts.select({
message: "Location",
options: [
{
label: "Current project",
value: projectConfigPath,
hint: projectConfigPath,
},
{
label: "Global",
value: globalConfigPath,
hint: globalConfigPath,
},
],
})
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
configPath = scopeResult
}
const name = await prompts.text({ const name = await prompts.text({
message: "Enter MCP server name", message: "Enter MCP server name",
validate: (x) => (x && x.length > 0 ? undefined : "Required"), validate: (x) => (x && x.length > 0 ? undefined : "Required"),
@@ -474,13 +404,7 @@ export const McpAddCommand = cmd({
}) })
if (prompts.isCancel(command)) throw new UI.CancelledError() if (prompts.isCancel(command)) throw new UI.CancelledError()
const mcpConfig: Config.Mcp = { prompts.log.info(`Local MCP server "${name}" configured with command: ${command}`)
type: "local",
command: command.split(" "),
}
await addMcpToConfig(name, mcpConfig, configPath)
prompts.log.success(`MCP server "${name}" added to ${configPath}`)
prompts.outro("MCP server added successfully") prompts.outro("MCP server added successfully")
return return
} }
@@ -504,8 +428,6 @@ export const McpAddCommand = cmd({
}) })
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError() if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
let mcpConfig: Config.Mcp
if (useOAuth) { if (useOAuth) {
const hasClientId = await prompts.confirm({ const hasClientId = await prompts.confirm({
message: "Do you have a pre-registered client ID?", message: "Do you have a pre-registered client ID?",
@@ -535,37 +457,44 @@ export const McpAddCommand = cmd({
clientSecret = secret clientSecret = secret
} }
mcpConfig = { prompts.log.info(`Remote MCP server "${name}" configured with OAuth (client ID: ${clientId})`)
type: "remote", prompts.log.info("Add this to your opencode.json:")
url, prompts.log.info(`
oauth: { "mcp": {
clientId, "${name}": {
...(clientSecret && { clientSecret }), "type": "remote",
}, "url": "${url}",
"oauth": {
"clientId": "${clientId}"${clientSecret ? `,\n "clientSecret": "${clientSecret}"` : ""}
}
}
}`)
} else {
prompts.log.info(`Remote MCP server "${name}" configured with OAuth (dynamic registration)`)
prompts.log.info("Add this to your opencode.json:")
prompts.log.info(`
"mcp": {
"${name}": {
"type": "remote",
"url": "${url}",
"oauth": {}
}
}`)
} }
} else { } else {
mcpConfig = { const client = new Client({
type: "remote", name: "opencode",
url, version: "1.0.0",
oauth: {}, })
const transport = new StreamableHTTPClientTransport(new URL(url))
await client.connect(transport)
prompts.log.info(`Remote MCP server "${name}" configured with URL: ${url}`)
} }
} }
} else {
mcpConfig = {
type: "remote",
url,
}
}
await addMcpToConfig(name, mcpConfig, configPath)
prompts.log.success(`MCP server "${name}" added to ${configPath}`)
}
prompts.outro("MCP server added successfully") prompts.outro("MCP server added successfully")
}, },
}) })
},
})
export const McpDebugCommand = cmd({ export const McpDebugCommand = cmd({
command: "debug <name>", command: "debug <name>",
+5 -5
View File
@@ -339,15 +339,13 @@ export const RunCommand = cmd({
} }
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { const server = Server.listen({ port: args.port ?? 0, hostname: "127.0.0.1" })
const request = new Request(input, init) const sdk = createOpencodeClient({ baseUrl: `http://${server.hostname}:${server.port}` })
return Server.App().fetch(request)
}) as typeof globalThis.fetch
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
if (args.command) { if (args.command) {
const exists = await Command.get(args.command) const exists = await Command.get(args.command)
if (!exists) { if (!exists) {
server.stop()
UI.error(`Command "${args.command}" not found`) UI.error(`Command "${args.command}" not found`)
process.exit(1) process.exit(1)
} }
@@ -372,6 +370,7 @@ export const RunCommand = cmd({
})() })()
if (!sessionID) { if (!sessionID) {
server.stop()
UI.error("Session not found") UI.error("Session not found")
process.exit(1) process.exit(1)
} }
@@ -390,6 +389,7 @@ export const RunCommand = cmd({
} }
await execute(sdk, sessionID) await execute(sdk, sessionID)
server.stop()
}) })
}, },
}) })
-4
View File
@@ -1,16 +1,12 @@
import { Server } from "../../server/server" import { Server } from "../../server/server"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network" import { withNetworkOptions, resolveNetworkOptions } from "../network"
import { Flag } from "../../flag/flag"
export const ServeCommand = cmd({ export const ServeCommand = cmd({
command: "serve", command: "serve",
builder: (yargs) => withNetworkOptions(yargs), builder: (yargs) => withNetworkOptions(yargs),
describe: "starts a headless opencode server", describe: "starts a headless opencode server",
handler: async (args) => { handler: async (args) => {
if (!Flag.OPENCODE_SERVER_PASSWORD) {
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
}
const opts = await resolveNetworkOptions(args) const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts) const server = Server.listen(opts)
console.log(`opencode server listening on http://${server.hostname}:${server.port}`) console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
+11 -16
View File
@@ -97,16 +97,7 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
}) })
} }
import type { EventSource } from "./context/sdk" export function tui(input: { url: string; args: Args; directory?: string; onExit?: () => Promise<void> }) {
export function tui(input: {
url: string
args: Args
directory?: string
fetch?: typeof fetch
events?: EventSource
onExit?: () => Promise<void>
}) {
// promise to prevent immediate exit // promise to prevent immediate exit
return new Promise<void>(async (resolve) => { return new Promise<void>(async (resolve) => {
const mode = await getTerminalBackgroundColor() const mode = await getTerminalBackgroundColor()
@@ -126,12 +117,7 @@ export function tui(input: {
<KVProvider> <KVProvider>
<ToastProvider> <ToastProvider>
<RouteProvider> <RouteProvider>
<SDKProvider <SDKProvider url={input.url} directory={input.directory}>
url={input.url}
directory={input.directory}
fetch={input.fetch}
events={input.events}
>
<SyncProvider> <SyncProvider>
<ThemeProvider mode={mode}> <ThemeProvider mode={mode}>
<LocalProvider> <LocalProvider>
@@ -606,6 +592,15 @@ function App() {
}) })
}) })
sdk.event.on(Installation.Event.Updated.type, (evt) => {
toast.show({
variant: "success",
title: "Update Complete",
message: `OpenCode updated to v${evt.properties.version}`,
duration: 5000,
})
})
sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => { sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => {
toast.show({ toast.show({
variant: "info", variant: "info",
@@ -36,7 +36,6 @@ export function createDialogProviderOptions() {
description: { description: {
opencode: "(Recommended)", opencode: "(Recommended)",
anthropic: "(Claude Max or API key)", anthropic: "(Claude Max or API key)",
openai: "(ChatGPT Plus/Pro or API key)",
}[provider.id], }[provider.id],
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
async onSelect() { async onSelect() {
@@ -112,7 +111,8 @@ function AutoMethod(props: AutoMethodProps) {
useKeyboard((evt) => { useKeyboard((evt) => {
if (evt.name === "c" && !evt.ctrl && !evt.meta) { if (evt.name === "c" && !evt.ctrl && !evt.meta) {
const code = props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4}/)?.[0] ?? props.authorization.url const code =
props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4}/)?.[0] ?? props.authorization.instructions
Clipboard.copy(code) Clipboard.copy(code)
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) .then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
.catch(toast.error) .catch(toast.error)
@@ -225,7 +225,7 @@ function ApiMethod(props: ApiMethodProps) {
} }
onConfirm={async (value) => { onConfirm={async (value) => {
if (!value) return if (!value) return
await sdk.client.auth.set({ sdk.client.auth.set({
providerID: props.providerID, providerID: props.providerID,
auth: { auth: {
type: "api", type: "api",
@@ -0,0 +1,85 @@
import { createMemo, createSignal, For } from "solid-js"
import { useTheme } from "@tui/context/theme"
import { useKeybind } from "@tui/context/keybind"
import { TIPS } from "./tips"
import { EmptyBorder } from "./border"
type TipPart = { text: string; highlight: boolean }
function parseTip(tip: string): TipPart[] {
const parts: TipPart[] = []
const regex = /\{highlight\}(.*?)\{\/highlight\}/g
let lastIndex = 0
let match
while ((match = regex.exec(tip)) !== null) {
if (match.index > lastIndex) {
parts.push({ text: tip.slice(lastIndex, match.index), highlight: false })
}
parts.push({ text: match[1], highlight: true })
lastIndex = regex.lastIndex
}
if (lastIndex < tip.length) {
parts.push({ text: tip.slice(lastIndex), highlight: false })
}
return parts
}
const [tipIndex, setTipIndex] = createSignal(Math.floor(Math.random() * TIPS.length))
export function randomizeTip() {
setTipIndex(Math.floor(Math.random() * TIPS.length))
}
const BOX_WIDTH = 42
const TITLE = " 🅘 Did you know? "
export function DidYouKnow() {
const { theme } = useTheme()
const keybind = useKeybind()
const tipParts = createMemo(() => parseTip(TIPS[tipIndex()]))
const dashes = createMemo(() => {
// ╭─ + title + ─...─ + ╮ = BOX_WIDTH
// 1 + 1 + title.length + dashes + 1 = BOX_WIDTH
return Math.max(0, BOX_WIDTH - 2 - TITLE.length - 1)
})
return (
<box position="absolute" bottom={3} right={2} width={BOX_WIDTH}>
<text>
<span style={{ fg: theme.border }}></span>
<span style={{ fg: theme.text }}>{TITLE}</span>
<span style={{ fg: theme.border }}>{"─".repeat(dashes())}</span>
</text>
<box
border={["left", "right", "bottom"]}
borderColor={theme.border}
customBorderChars={{
...EmptyBorder,
bottomLeft: "╰",
bottomRight: "╯",
horizontal: "─",
vertical: "│",
}}
>
<box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
<text>
<For each={tipParts()}>
{(part) => <span style={{ fg: part.highlight ? theme.text : theme.textMuted }}>{part.text}</span>}
</For>
</text>
</box>
</box>
<box flexDirection="row" justifyContent="flex-end">
<text>
<span style={{ fg: theme.text }}>{keybind.print("tips_toggle")}</span>
<span style={{ fg: theme.textMuted }}> hide tips</span>
</text>
</box>
</box>
)
}
@@ -1,7 +1,7 @@
import type { BoxRenderable, TextareaRenderable, KeyEvent, ScrollBoxRenderable } from "@opentui/core" import type { BoxRenderable, TextareaRenderable, KeyEvent, ScrollBoxRenderable } from "@opentui/core"
import fuzzysort from "fuzzysort" import fuzzysort from "fuzzysort"
import { firstBy } from "remeda" import { firstBy } from "remeda"
import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js" import { createMemo, createResource, createEffect, onMount, onCleanup, For, Show, createSignal } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
@@ -601,31 +601,8 @@ export function Autocomplete(props: {
(store.visible === "/" && value.match(/^\S+\s+\S+\s*$/)) (store.visible === "/" && value.match(/^\S+\s+\S+\s*$/))
) { ) {
hide() hide()
}
return return
} }
// Check if autocomplete should reopen (e.g., after backspace deleted a space)
const offset = props.input().cursorOffset
if (offset === 0) return
// Check for "/" at position 0 - reopen slash commands
if (value.startsWith("/") && !value.slice(0, offset).match(/\s/)) {
show("/")
setStore("index", 0)
return
}
// Check for "@" trigger - find the nearest "@" before cursor with no whitespace between
const text = value.slice(0, offset)
const idx = text.lastIndexOf("@")
if (idx === -1) return
const between = text.slice(idx)
const before = idx === 0 ? undefined : value[idx - 1]
if ((before === undefined || /\s/.test(before)) && !between.match(/\s/)) {
show("@")
setStore("index", idx)
} }
}, },
onKeyDown(e: KeyEvent) { onKeyDown(e: KeyEvent) {
@@ -709,7 +686,7 @@ export function Autocomplete(props: {
height={height()} height={height()}
scrollbarOptions={{ visible: false }} scrollbarOptions={{ visible: false }}
> >
<Index <For
each={options()} each={options()}
fallback={ fallback={
<box paddingLeft={1} paddingRight={1}> <box paddingLeft={1} paddingRight={1}>
@@ -721,22 +698,20 @@ export function Autocomplete(props: {
<box <box
paddingLeft={1} paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={index === store.selected ? theme.primary : undefined} backgroundColor={index() === store.selected ? theme.primary : undefined}
flexDirection="row" flexDirection="row"
onMouseOver={() => moveTo(index)}
onMouseUp={() => select()}
> >
<text fg={index === store.selected ? selectedForeground(theme) : theme.text} flexShrink={0}> <text fg={index() === store.selected ? selectedForeground(theme) : theme.text} flexShrink={0}>
{option().display} {option.display}
</text> </text>
<Show when={option().description}> <Show when={option.description}>
<text fg={index === store.selected ? selectedForeground(theme) : theme.textMuted} wrapMode="none"> <text fg={index() === store.selected ? selectedForeground(theme) : theme.textMuted} wrapMode="none">
{option().description} {option.description}
</text> </text>
</Show> </Show>
</box> </box>
)} )}
</Index> </For>
</scrollbox> </scrollbox>
</box> </box>
) )
@@ -89,7 +89,7 @@ export function Prompt(props: PromptProps) {
const fileStyleId = syntax().getStyleId("extmark.file")! const fileStyleId = syntax().getStyleId("extmark.file")!
const agentStyleId = syntax().getStyleId("extmark.agent")! const agentStyleId = syntax().getStyleId("extmark.agent")!
const pasteStyleId = syntax().getStyleId("extmark.paste")! const pasteStyleId = syntax().getStyleId("extmark.paste")!
let promptPartTypeId = 0 let promptPartTypeId: number
sdk.event.on(TuiEvent.PromptAppend.type, (evt) => { sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
input.insertText(evt.properties.text) input.insertText(evt.properties.text)
@@ -310,44 +310,15 @@ export function Prompt(props: PromptProps) {
] ]
}) })
const ref: PromptRef = {
get focused() {
return input.focused
},
get current() {
return store.prompt
},
focus() {
input.focus()
},
blur() {
input.blur()
},
set(prompt) {
input.setText(prompt.input)
setStore("prompt", prompt)
restoreExtmarksFromParts(prompt.parts)
input.gotoBufferEnd()
},
reset() {
input.clear()
input.extmarks.clear()
setStore("prompt", {
input: "",
parts: [],
})
setStore("extmarkToPartIndex", new Map())
},
submit() {
submit()
},
}
createEffect(() => { createEffect(() => {
if (props.visible !== false) input?.focus() if (props.visible !== false) input?.focus()
if (props.visible === false) input?.blur() if (props.visible === false) input?.blur()
}) })
onMount(() => {
promptPartTypeId = input.extmarks.registerType("prompt-part")
})
function restoreExtmarksFromParts(parts: PromptInfo["parts"]) { function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {
input.extmarks.clear() input.extmarks.clear()
setStore("extmarkToPartIndex", new Map()) setStore("extmarkToPartIndex", new Map())
@@ -481,6 +452,39 @@ export function Prompt(props: PromptProps) {
}, },
]) ])
props.ref?.({
get focused() {
return input.focused
},
get current() {
return store.prompt
},
focus() {
input.focus()
},
blur() {
input.blur()
},
set(prompt) {
input.setText(prompt.input)
setStore("prompt", prompt)
restoreExtmarksFromParts(prompt.parts)
input.gotoBufferEnd()
},
reset() {
input.clear()
input.extmarks.clear()
setStore("prompt", {
input: "",
parts: [],
})
setStore("extmarkToPartIndex", new Map())
},
submit() {
submit()
},
})
async function submit() { async function submit() {
if (props.disabled) return if (props.disabled) return
if (autocomplete?.visible) return if (autocomplete?.visible) return
@@ -563,8 +567,7 @@ export function Prompt(props: PromptProps) {
})), })),
}) })
} else { } else {
sdk.client.session sdk.client.session.prompt({
.prompt({
sessionID, sessionID,
...selectedModel, ...selectedModel,
messageID, messageID,
@@ -583,7 +586,6 @@ export function Prompt(props: PromptProps) {
})), })),
], ],
}) })
.catch(() => {})
} }
history.append({ history.append({
...store.prompt, ...store.prompt,
@@ -914,15 +916,12 @@ export function Prompt(props: PromptProps) {
// Force layout update and render for the pasted content // Force layout update and render for the pasted content
setTimeout(() => { setTimeout(() => {
input.getLayoutNode().markDirty() input.getLayoutNode().markDirty()
input.gotoBufferEnd()
renderer.requestRender() renderer.requestRender()
}, 0) }, 0)
}} }}
ref={(r: TextareaRenderable) => { ref={(r: TextareaRenderable) => {
input = r input = r
if (promptPartTypeId === 0) {
promptPartTypeId = input.extmarks.registerType("prompt-part")
}
props.ref?.(ref)
setTimeout(() => { setTimeout(() => {
input.cursorColor = theme.text input.cursorColor = theme.text
}, 0) }, 0)
@@ -1064,10 +1063,7 @@ export function Prompt(props: PromptProps) {
<Switch> <Switch>
<Match when={store.mode === "normal"}> <Match when={store.mode === "normal"}>
<text fg={theme.text}> <text fg={theme.text}>
{keybind.print("variant_cycle")} <span style={{ fg: theme.textMuted }}>variants</span> {keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>switch agent</span>
</text>
<text fg={theme.text}>
{keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>agents</span>
</text> </text>
<text fg={theme.text}> <text fg={theme.text}>
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span> {keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
@@ -1,150 +1,103 @@
import { createMemo, createSignal, For } from "solid-js" export const TIPS = [
import { useTheme } from "@tui/context/theme" "Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files to your prompt.",
"Start a message with {highlight}!{/highlight} to run shell commands directly (e.g., {highlight}!ls -la{/highlight}).",
type TipPart = { text: string; highlight: boolean } "Press {highlight}Tab{/highlight} to cycle between Build (full access) and Plan (read-only) agents.",
"Use {highlight}/undo{/highlight} to revert the last message and any file changes made by OpenCode.",
function parse(tip: string): TipPart[] { "Use {highlight}/redo{/highlight} to restore previously undone messages and file changes.",
const parts: TipPart[] = [] "Run {highlight}/share{/highlight} to create a public link to your conversation at opencode.ai.",
const regex = /\{highlight\}(.*?)\{\/highlight\}/g "Drag and drop images into the terminal to add them as context for your prompts.",
const found = Array.from(tip.matchAll(regex)) "Press {highlight}Ctrl+V{/highlight} to paste images from your clipboard directly into the prompt.",
const state = found.reduce( "Press {highlight}Ctrl+X E{/highlight} or {highlight}/editor{/highlight} to compose messages in your external editor.",
(acc, match) => { "Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase structure.",
const start = match.index ?? 0 "Run {highlight}/models{/highlight} or {highlight}Ctrl+X M{/highlight} to see and switch between available AI models.",
if (start > acc.index) { "Use {highlight}/theme{/highlight} or {highlight}Ctrl+X T{/highlight} to preview and switch between 50+ built-in themes.",
acc.parts.push({ text: tip.slice(acc.index, start), highlight: false }) "Press {highlight}Ctrl+X N{/highlight} or {highlight}/new{/highlight} to start a fresh conversation session.",
} "Use {highlight}/sessions{/highlight} or {highlight}Ctrl+X L{/highlight} to list and continue previous conversations.",
acc.parts.push({ text: match[1], highlight: true }) "Run {highlight}/compact{/highlight} to summarize long sessions when approaching context limits.",
acc.index = start + match[0].length "Press {highlight}Ctrl+X X{/highlight} or {highlight}/export{/highlight} to save the conversation as Markdown.",
return acc "Press {highlight}Ctrl+X Y{/highlight} to copy the assistant's last message to clipboard.",
}, "Press {highlight}Ctrl+P{/highlight} to see all available actions and commands.",
{ parts, index: 0 }, "Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers.",
) "The default leader key is {highlight}Ctrl+X{/highlight}; combine with other keys for quick actions.",
"Press {highlight}F2{/highlight} to quickly switch between recently used models.",
if (state.index < tip.length) { "Press {highlight}Ctrl+X B{/highlight} to show/hide the sidebar panel.",
parts.push({ text: tip.slice(state.index), highlight: false }) "Use {highlight}PageUp{/highlight}/{highlight}PageDown{/highlight} to navigate through conversation history.",
} "Press {highlight}Ctrl+G{/highlight} or {highlight}Home{/highlight} to jump to the beginning of the conversation.",
"Press {highlight}Ctrl+Alt+G{/highlight} or {highlight}End{/highlight} to jump to the most recent message.",
return parts "Press {highlight}Shift+Enter{/highlight} or {highlight}Ctrl+J{/highlight} to add newlines in your prompt.",
} "Press {highlight}Ctrl+C{/highlight} when typing to clear the input field.",
"Press {highlight}Escape{/highlight} to stop the AI mid-response.",
export function Tips() { "Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes.",
const theme = useTheme().theme "Use {highlight}@<agent-name>{/highlight} in prompts to invoke specialized subagents.",
const parts = parse(TIPS[Math.floor(Math.random() * TIPS.length)]) "Press {highlight}Ctrl+X Right/Left{/highlight} to cycle through parent and child sessions.",
"Create {highlight}opencode.json{/highlight} in project root for project-specific settings.",
return ( "Place settings in {highlight}~/.config/opencode/opencode.json{/highlight} for global config.",
<box flexDirection="row" maxWidth="100%"> "Add {highlight}$schema{/highlight} to your config for autocomplete in your editor.",
<text flexShrink={0} style={{ fg: theme.warning }}> "Configure {highlight}model{/highlight} in config to set your default model.",
Tip{" "} "Override any keybind in config via the {highlight}keybinds{/highlight} section.",
</text> "Set any keybind to {highlight}none{/highlight} to disable it completely.",
<text flexShrink={1}> "Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section.",
<For each={parts}> "OpenCode auto-handles OAuth for remote MCP servers requiring auth.",
{(part) => <span style={{ fg: part.highlight ? theme.text : theme.textMuted }}>{part.text}</span>} "Add {highlight}.md{/highlight} files to {highlight}.opencode/command/{/highlight} to define reusable custom prompts.",
</For> "Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input.",
</text> "Use backticks in commands to inject shell output (e.g., {highlight}`git status`{/highlight}).",
</box> "Add {highlight}.md{/highlight} files to {highlight}.opencode/agent/{/highlight} for specialized AI personas.",
) "Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools.",
} 'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions.',
'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands.',
const TIPS = [ 'Configure {highlight}"git push": "ask"{/highlight} to require approval before pushing.',
"Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files", "OpenCode auto-formats files using prettier, gofmt, ruff, and more.",
"Start a message with {highlight}!{/highlight} to run shell commands directly (e.g., {highlight}!ls -la{/highlight})", 'Set {highlight}"formatter": false{/highlight} in config to disable all auto-formatting.',
"Press {highlight}Tab{/highlight} to cycle between Build and Plan agents", "Define custom formatter commands with file extensions in config.",
"Use {highlight}/undo{/highlight} to revert the last message and file changes", "OpenCode uses LSP servers for intelligent code analysis.",
"Use {highlight}/redo{/highlight} to restore previously undone messages and file changes", "Create {highlight}.ts{/highlight} files in {highlight}.opencode/tool/{/highlight} to define new LLM tools.",
"Run {highlight}/share{/highlight} to create a public link to your conversation at opencode.ai", "Tool definitions can invoke scripts written in Python, Go, etc.",
"Drag and drop images into the terminal to add them as context", "Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugin/{/highlight} for event hooks.",
"Press {highlight}Ctrl+V{/highlight} to paste images from your clipboard into the prompt", "Use plugins to send OS notifications when sessions complete.",
"Press {highlight}Ctrl+X E{/highlight} or {highlight}/editor{/highlight} to compose messages in your external editor", "Create a plugin to prevent OpenCode from reading sensitive files.",
"Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase", "Use {highlight}opencode run{/highlight} for non-interactive scripting.",
"Run {highlight}/models{/highlight} or {highlight}Ctrl+X M{/highlight} to see and switch between available AI models", "Use {highlight}opencode run --continue{/highlight} to resume the last session.",
"Use {highlight}/theme{/highlight} or {highlight}Ctrl+X T{/highlight} to switch between 50+ built-in themes", "Use {highlight}opencode run -f file.ts{/highlight} to attach files via CLI.",
"Press {highlight}Ctrl+X N{/highlight} or {highlight}/new{/highlight} to start a fresh conversation session", "Use {highlight}--format json{/highlight} for machine-readable output in scripts.",
"Use {highlight}/sessions{/highlight} or {highlight}Ctrl+X L{/highlight} to list and continue previous conversations", "Run {highlight}opencode serve{/highlight} for headless API access to OpenCode.",
"Run {highlight}/compact{/highlight} to summarize long sessions near context limits", "Use {highlight}opencode run --attach{/highlight} to connect to a running server for faster runs.",
"Press {highlight}Ctrl+X X{/highlight} or {highlight}/export{/highlight} to save the conversation as Markdown", "Run {highlight}opencode upgrade{/highlight} to update to the latest version.",
"Press {highlight}Ctrl+X Y{/highlight} to copy the assistant's last message to clipboard", "Run {highlight}opencode auth list{/highlight} to see all configured providers.",
"Press {highlight}Ctrl+P{/highlight} to see all available actions and commands", "Run {highlight}opencode agent create{/highlight} for guided agent creation.",
"Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers", "Use {highlight}/opencode{/highlight} in GitHub issues/PRs to trigger AI actions.",
"The leader key is {highlight}Ctrl+X{/highlight}; combine with other keys for quick actions", "Run {highlight}opencode github install{/highlight} to set up the GitHub workflow.",
"Press {highlight}F2{/highlight} to quickly switch between recently used models", "Comment {highlight}/opencode fix this{/highlight} on issues to auto-create PRs.",
"Press {highlight}Ctrl+X B{/highlight} to show/hide the sidebar panel", "Comment {highlight}/oc{/highlight} on PR code lines for targeted code reviews.",
"Use {highlight}PageUp{/highlight}/{highlight}PageDown{/highlight} to navigate through conversation history", 'Use {highlight}"theme": "system"{/highlight} to match your terminal\'s colors.',
"Press {highlight}Ctrl+G{/highlight} or {highlight}Home{/highlight} to jump to the beginning of the conversation", "Create JSON theme files in {highlight}.opencode/themes/{/highlight} directory.",
"Press {highlight}Ctrl+Alt+G{/highlight} or {highlight}End{/highlight} to jump to the most recent message", "Themes support dark/light variants for both modes.",
"Press {highlight}Shift+Enter{/highlight} or {highlight}Ctrl+J{/highlight} to add newlines in your prompt", "Reference ANSI colors 0-255 in custom themes.",
"Press {highlight}Ctrl+C{/highlight} when typing to clear the input field", "Use {highlight}{env:VAR_NAME}{/highlight} syntax to reference environment variables in config.",
"Press {highlight}Escape{/highlight} to stop the AI mid-response", "Use {highlight}{file:path}{/highlight} to include file contents in config values.",
"Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes", "Use {highlight}instructions{/highlight} in config to load additional rules files.",
"Use {highlight}@agent-name{/highlight} in prompts to invoke specialized subagents", "Set agent {highlight}temperature{/highlight} from 0.0 (focused) to 1.0 (creative).",
"Press {highlight}Ctrl+X Right/Left{/highlight} to cycle through parent and child sessions", "Configure {highlight}maxSteps{/highlight} to limit agentic iterations per request.",
"Create {highlight}opencode.json{/highlight} in project root for project-specific settings", 'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools.',
"Place settings in {highlight}~/.config/opencode/opencode.json{/highlight} for global config", 'Use {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server.',
"Add {highlight}$schema{/highlight} to your config for autocomplete in your editor", "Override global tool settings per agent configuration.",
"Configure {highlight}model{/highlight} in config to set your default model", 'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions.',
"Override any keybind in config via the {highlight}keybinds{/highlight} section", 'Set {highlight}"share": "disabled"{/highlight} to prevent any session sharing.',
"Set any keybind to {highlight}none{/highlight} to disable it completely", "Run {highlight}/unshare{/highlight} to remove a session from public access.",
"Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section", "Permission {highlight}doom_loop{/highlight} prevents infinite tool call loops.",
"OpenCode auto-handles OAuth for remote MCP servers requiring auth", "Permission {highlight}external_directory{/highlight} protects files outside project.",
"Add {highlight}.md{/highlight} files to {highlight}.opencode/command/{/highlight} to define reusable custom prompts", "Run {highlight}opencode debug config{/highlight} to troubleshoot configuration.",
"Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input", "Use {highlight}--print-logs{/highlight} flag to see detailed logs in stderr.",
"Use backticks in commands to inject shell output (e.g., {highlight}`git status`{/highlight})", "Press {highlight}Ctrl+X G{/highlight} or {highlight}/timeline{/highlight} to jump to specific messages.",
"Add {highlight}.md{/highlight} files to {highlight}.opencode/agent/{/highlight} for specialized AI personas", "Press {highlight}Ctrl+X H{/highlight} to toggle code block visibility in messages.",
"Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools", "Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info.",
'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions', "Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling.",
'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands', "Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight}).",
'Configure {highlight}"git push": "ask"{/highlight} to require approval before pushing', "Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} for containerized use.",
"OpenCode auto-formats files using prettier, gofmt, ruff, and more", "Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models.",
'Set {highlight}"formatter": false{/highlight} in config to disable all auto-formatting', "Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing.",
"Define custom formatter commands with file extensions in config", "Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs.",
"OpenCode uses LSP servers for intelligent code analysis", "Run {highlight}/help{/highlight} or {highlight}Ctrl+X H{/highlight} to show the help dialog.",
"Create {highlight}.ts{/highlight} files in {highlight}.opencode/tool/{/highlight} to define new LLM tools", "Use {highlight}/details{/highlight} to toggle tool execution details visibility.",
"Tool definitions can invoke scripts written in Python, Go, etc", "Use {highlight}/rename{/highlight} to rename the current session.",
"Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugin/{/highlight} for event hooks", "Press {highlight}Ctrl+Z{/highlight} to suspend the terminal and return to your shell.",
"Use plugins to send OS notifications when sessions complete",
"Create a plugin to prevent OpenCode from reading sensitive files",
"Use {highlight}opencode run{/highlight} for non-interactive scripting",
"Use {highlight}opencode run --continue{/highlight} to resume the last session",
"Use {highlight}opencode run -f file.ts{/highlight} to attach files via CLI",
"Use {highlight}--format json{/highlight} for machine-readable output in scripts",
"Run {highlight}opencode serve{/highlight} for headless API access to OpenCode",
"Use {highlight}opencode run --attach{/highlight} to connect to a running server",
"Run {highlight}opencode upgrade{/highlight} to update to the latest version",
"Run {highlight}opencode auth list{/highlight} to see all configured providers",
"Run {highlight}opencode agent create{/highlight} for guided agent creation",
"Use {highlight}/opencode{/highlight} in GitHub issues/PRs to trigger AI actions",
"Run {highlight}opencode github install{/highlight} to set up the GitHub workflow",
"Comment {highlight}/opencode fix this{/highlight} on issues to auto-create PRs",
"Comment {highlight}/oc{/highlight} on PR code lines for targeted code reviews",
'Use {highlight}"theme": "system"{/highlight} to match your terminal\'s colors',
"Create JSON theme files in {highlight}.opencode/themes/{/highlight} directory",
"Themes support dark/light variants for both modes",
"Reference ANSI colors 0-255 in custom themes",
"Use {highlight}{env:VAR_NAME}{/highlight} syntax to reference environment variables in config",
"Use {highlight}{file:path}{/highlight} to include file contents in config values",
"Use {highlight}instructions{/highlight} in config to load additional rules files",
"Set agent {highlight}temperature{/highlight} from 0.0 (focused) to 1.0 (creative)",
"Configure {highlight}maxSteps{/highlight} to limit agentic iterations per request",
'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools',
'Set {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server',
"Override global tool settings per agent configuration",
'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions',
'Set {highlight}"share": "disabled"{/highlight} to prevent any session sharing',
"Run {highlight}/unshare{/highlight} to remove a session from public access",
"Permission {highlight}doom_loop{/highlight} prevents infinite tool call loops",
"Permission {highlight}external_directory{/highlight} protects files outside project",
"Run {highlight}opencode debug config{/highlight} to troubleshoot configuration",
"Use {highlight}--print-logs{/highlight} flag to see detailed logs in stderr",
"Press {highlight}Ctrl+X G{/highlight} or {highlight}/timeline{/highlight} to jump to specific messages",
"Press {highlight}Ctrl+X H{/highlight} to toggle code block visibility in messages",
"Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info",
"Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling",
"Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight})",
"Run {highlight}docker run -it --rm ghcr.io/sst/opencode{/highlight} for containerized use",
"Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models",
"Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing",
"Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs",
"Run {highlight}/help{/highlight} or {highlight}Ctrl+X H{/highlight} to show the help dialog",
"Use {highlight}/details{/highlight} to toggle tool execution details visibility",
"Use {highlight}/rename{/highlight} to rename the current session",
"Press {highlight}Ctrl+Z{/highlight} to suspend the terminal and return to your shell",
] ]
@@ -8,13 +8,13 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
name: "KV", name: "KV",
init: () => { init: () => {
const [ready, setReady] = createSignal(false) const [ready, setReady] = createSignal(false)
const [store, setStore] = createStore<Record<string, any>>() const [kvStore, setKvStore] = createStore<Record<string, any>>()
const file = Bun.file(path.join(Global.Path.state, "kv.json")) const file = Bun.file(path.join(Global.Path.state, "kv.json"))
file file
.json() .json()
.then((x) => { .then((x) => {
setStore(x) setKvStore(x)
}) })
.catch(() => {}) .catch(() => {})
.finally(() => { .finally(() => {
@@ -25,11 +25,8 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
get ready() { get ready() {
return ready() return ready()
}, },
get store() {
return store
},
signal<T>(name: string, defaultValue: T) { signal<T>(name: string, defaultValue: T) {
if (store[name] === undefined) setStore(name, defaultValue) if (kvStore[name] === undefined) setKvStore(name, defaultValue)
return [ return [
function () { function () {
return result.get(name) return result.get(name)
@@ -40,11 +37,11 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
] as const ] as const
}, },
get(key: string, defaultValue?: any) { get(key: string, defaultValue?: any) {
return store[key] ?? defaultValue return kvStore[key] ?? defaultValue
}, },
set(key: string, value: any) { set(key: string, value: any) {
setStore(key, value) setKvStore(key, value)
Bun.write(file, JSON.stringify(store, null, 2)) Bun.write(file, JSON.stringify(kvStore, null, 2))
}, },
} }
return result return result
@@ -3,25 +3,29 @@ import { createSimpleContext } from "./helper"
import { createGlobalEmitter } from "@solid-primitives/event-bus" import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { batch, onCleanup, onMount } from "solid-js" import { batch, onCleanup, onMount } from "solid-js"
export type EventSource = {
on: (handler: (event: Event) => void) => () => void
}
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
name: "SDK", name: "SDK",
init: (props: { url: string; directory?: string; fetch?: typeof fetch; events?: EventSource }) => { init: (props: { url: string; directory?: string }) => {
const abort = new AbortController() const abort = new AbortController()
const sdk = createOpencodeClient({ const sdk = createOpencodeClient({
baseUrl: props.url, baseUrl: props.url,
signal: abort.signal, signal: abort.signal,
directory: props.directory, directory: props.directory,
fetch: props.fetch,
}) })
const emitter = createGlobalEmitter<{ const emitter = createGlobalEmitter<{
[key in Event["type"]]: Extract<Event, { type: key }> [key in Event["type"]]: Extract<Event, { type: key }>
}>() }>()
onMount(async () => {
while (true) {
if (abort.signal.aborted) break
const events = await sdk.event.subscribe(
{},
{
signal: abort.signal,
},
)
let queue: Event[] = [] let queue: Event[] = []
let timer: Timer | undefined let timer: Timer | undefined
let last = 0 let last = 0
@@ -40,42 +44,20 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
}) })
} }
const handleEvent = (event: Event) => { for await (const event of events.stream) {
queue.push(event) queue.push(event)
const elapsed = Date.now() - last const elapsed = Date.now() - last
if (timer) return if (timer) continue
// If we just flushed recently (within 16ms), batch this with future events // If we just flushed recently (within 16ms), batch this with future events
// Otherwise, process immediately to avoid latency // Otherwise, process immediately to avoid latency
if (elapsed < 16) { if (elapsed < 16) {
timer = setTimeout(flush, 16) timer = setTimeout(flush, 16)
return continue
} }
flush() flush()
} }
onMount(async () => {
// If an event source is provided, use it instead of SSE
if (props.events) {
const unsub = props.events.on(handleEvent)
onCleanup(unsub)
return
}
// Fall back to SSE
while (true) {
if (abort.signal.aborted) break
const events = await sdk.event.subscribe(
{},
{
signal: abort.signal,
},
)
for await (const event of events.stream) {
handleEvent(event)
}
// Flush any remaining events // Flush any remaining events
if (timer) clearTimeout(timer) if (timer) clearTimeout(timer)
if (queue.length > 0) { if (queue.length > 0) {
@@ -86,7 +68,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
onCleanup(() => { onCleanup(() => {
abort.abort() abort.abort()
if (timer) clearTimeout(timer)
}) })
return { client: sdk, event: emitter, url: props.url } return { client: sdk, event: emitter, url: props.url }
@@ -1,9 +1,8 @@
import { Prompt, type PromptRef } from "@tui/component/prompt" import { Prompt, type PromptRef } from "@tui/component/prompt"
import { createMemo, Match, onMount, Show, Switch } from "solid-js" import { createMemo, Match, onMount, Show, Switch } from "solid-js"
import { useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { useKeybind } from "@tui/context/keybind"
import { Logo } from "../component/logo" import { Logo } from "../component/logo"
import { Tips } from "../component/tips" import { DidYouKnow, randomizeTip } from "../component/did-you-know"
import { Locale } from "@/util/locale" import { Locale } from "@/util/locale"
import { useSync } from "../context/sync" import { useSync } from "../context/sync"
import { Toast } from "../ui/toast" import { Toast } from "../ui/toast"
@@ -37,6 +36,7 @@ export function Home() {
const isFirstTimeUser = createMemo(() => sync.data.session.length === 0) const isFirstTimeUser = createMemo(() => sync.data.session.length === 0)
const tipsHidden = createMemo(() => kv.get("tips_hidden", false)) const tipsHidden = createMemo(() => kv.get("tips_hidden", false))
const showTips = createMemo(() => { const showTips = createMemo(() => {
return false
// Don't show tips for first-time users // Don't show tips for first-time users
if (isFirstTimeUser()) return false if (isFirstTimeUser()) return false
return !tipsHidden() return !tipsHidden()
@@ -77,6 +77,7 @@ export function Home() {
let prompt: PromptRef let prompt: PromptRef
const args = useArgs() const args = useArgs()
onMount(() => { onMount(() => {
randomizeTip()
if (once) return if (once) return
if (route.initialPrompt) { if (route.initialPrompt) {
prompt.set(route.initialPrompt) prompt.set(route.initialPrompt)
@@ -89,12 +90,9 @@ export function Home() {
}) })
const directory = useDirectory() const directory = useDirectory()
const keybind = useKeybind()
return ( return (
<> <>
<box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}> <box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}>
<box height={3} />
<Logo /> <Logo />
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1}> <box width="100%" maxWidth={75} zIndex={1000} paddingTop={1}>
<Prompt <Prompt
@@ -105,13 +103,13 @@ export function Home() {
hint={Hint} hint={Hint}
/> />
</box> </box>
<box height={3} width="100%" maxWidth={75} alignItems="center" paddingTop={2}>
<Show when={showTips()}>
<Tips />
</Show>
</box>
<Toast /> <Toast />
</box> </box>
<Show when={!isFirstTimeUser()}>
<Show when={showTips()}>
<DidYouKnow />
</Show>
</Show>
<box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexDirection="row" flexShrink={0} gap={2}> <box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexDirection="row" flexShrink={0} gap={2}>
<text fg={theme.textMuted}>{directory()}</text> <text fg={theme.textMuted}>{directory()}</text>
<box gap={1} flexDirection="row" flexShrink={0}> <box gap={1} flexDirection="row" flexShrink={0}>
@@ -1,11 +1,10 @@
import { type Accessor, createMemo, createSignal, Match, Show, Switch } from "solid-js" import { type Accessor, createMemo, Match, Show, Switch } from "solid-js"
import { useRouteData } from "@tui/context/route" import { useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { pipe, sumBy } from "remeda" import { pipe, sumBy } from "remeda"
import { useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border" import { SplitBorder } from "@tui/component/border"
import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2" import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
import { useCommandDialog } from "@tui/component/dialog-command"
import { useKeybind } from "../../context/keybind" import { useKeybind } from "../../context/keybind"
const Title = (props: { session: Accessor<Session> }) => { const Title = (props: { session: Accessor<Session> }) => {
@@ -60,8 +59,6 @@ export function Header() {
const { theme } = useTheme() const { theme } = useTheme()
const keybind = useKeybind() const keybind = useKeybind()
const command = useCommandDialog()
const [hover, setHover] = createSignal<"parent" | "prev" | "next" | null>(null)
return ( return (
<box flexShrink={0}> <box flexShrink={0}>
@@ -82,36 +79,15 @@ export function Header() {
<text fg={theme.text}> <text fg={theme.text}>
<b>Subagent session</b> <b>Subagent session</b>
</text> </text>
<box
onMouseOver={() => setHover("parent")}
onMouseOut={() => setHover(null)}
onMouseUp={() => command.trigger("session.parent")}
backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel}
>
<text fg={theme.text}> <text fg={theme.text}>
Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span> Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span>
</text> </text>
</box>
<box
onMouseOver={() => setHover("prev")}
onMouseOut={() => setHover(null)}
onMouseUp={() => command.trigger("session.child.previous")}
backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel}
>
<text fg={theme.text}> <text fg={theme.text}>
Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span> Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
</text> </text>
</box>
<box
onMouseOver={() => setHover("next")}
onMouseOut={() => setHover(null)}
onMouseUp={() => command.trigger("session.child.next")}
backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel}
>
<text fg={theme.text}> <text fg={theme.text}>
Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span> Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span>
</text> </text>
</box>
<box flexGrow={1} flexShrink={1} /> <box flexGrow={1} flexShrink={1} />
<ContextInfo context={context} cost={cost} /> <ContextInfo context={context} cost={cost} />
</box> </box>
@@ -1,6 +1,6 @@
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createMemo, For, Match, Show, Switch } from "solid-js" import { createMemo, For, Match, Show, Switch } from "solid-js"
import { Portal, useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
import type { TextareaRenderable } from "@opentui/core" import type { TextareaRenderable } from "@opentui/core"
import { useKeybind } from "../../context/keybind" import { useKeybind } from "../../context/keybind"
import { useTheme, selectedForeground } from "../../context/theme" import { useTheme, selectedForeground } from "../../context/theme"
@@ -11,7 +11,6 @@ import { useSync } from "../../context/sync"
import { useTextareaKeybindings } from "../../component/textarea-keybindings" import { useTextareaKeybindings } from "../../component/textarea-keybindings"
import path from "path" import path from "path"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import { Keybind } from "@/util/keybind"
import { Locale } from "@/util/locale" import { Locale } from "@/util/locale"
type PermissionStage = "permission" | "always" | "reject" type PermissionStage = "permission" | "always" | "reject"
@@ -33,9 +32,7 @@ function filetype(input?: string) {
} }
function EditBody(props: { request: PermissionRequest }) { function EditBody(props: { request: PermissionRequest }) {
const themeState = useTheme() const { theme, syntax } = useTheme()
const theme = themeState.theme
const syntax = themeState.syntax
const sync = useSync() const sync = useSync()
const dimensions = useTerminalDimensions() const dimensions = useTerminalDimensions()
@@ -57,7 +54,7 @@ function EditBody(props: { request: PermissionRequest }) {
<text fg={theme.textMuted}>Edit {normalizePath(filepath())}</text> <text fg={theme.textMuted}>Edit {normalizePath(filepath())}</text>
</box> </box>
<Show when={diff()}> <Show when={diff()}>
<scrollbox height="100%"> <box maxHeight={Math.floor(dimensions().height / 4)} overflow="scroll">
<diff <diff
diff={diff()} diff={diff()}
view={view()} view={view()}
@@ -77,7 +74,7 @@ function EditBody(props: { request: PermissionRequest }) {
addedLineNumberBg={theme.diffAddedLineNumberBg} addedLineNumberBg={theme.diffAddedLineNumberBg}
removedLineNumberBg={theme.diffRemovedLineNumberBg} removedLineNumberBg={theme.diffRemovedLineNumberBg}
/> />
</scrollbox> </box>
</Show> </Show>
</box> </box>
) )
@@ -175,14 +172,10 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
message: message || undefined, message: message || undefined,
}) })
}} }}
onCancel={() => { onCancel={() => setStore("stage", "permission")}
setStore("stage", "permission")
}}
/> />
</Match> </Match>
<Match when={store.stage === "permission"}> <Match when={store.stage === "permission"}>
{(() => {
const body = (
<Prompt <Prompt
title="Permission required" title="Permission required"
body={ body={
@@ -238,7 +231,6 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
} }
options={{ once: "Allow once", always: "Allow always", reject: "Reject" }} options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
escapeKey="reject" escapeKey="reject"
fullscreen
onSelect={(option) => { onSelect={(option) => {
if (option === "always") { if (option === "always") {
setStore("stage", "always") setStore("stage", "always")
@@ -260,10 +252,6 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
}) })
}} }}
/> />
)
return body
})()}
</Match> </Match>
</Switch> </Switch>
) )
@@ -339,18 +327,14 @@ function Prompt<const T extends Record<string, string>>(props: {
body: JSX.Element body: JSX.Element
options: T options: T
escapeKey?: keyof T escapeKey?: keyof T
fullscreen?: boolean
onSelect: (option: keyof T) => void onSelect: (option: keyof T) => void
}) { }) {
const { theme } = useTheme() const { theme } = useTheme()
const keybind = useKeybind() const keybind = useKeybind()
const dimensions = useTerminalDimensions()
const keys = Object.keys(props.options) as (keyof T)[] const keys = Object.keys(props.options) as (keyof T)[]
const [store, setStore] = createStore({ const [store, setStore] = createStore({
selected: keys[0], selected: keys[0],
expanded: false,
}) })
const diffKey = Keybind.parse("ctrl+f")[0]
useKeyboard((evt) => { useKeyboard((evt) => {
if (evt.name === "left" || evt.name == "h") { if (evt.name === "left" || evt.name == "h") {
@@ -376,36 +360,17 @@ function Prompt<const T extends Record<string, string>>(props: {
evt.preventDefault() evt.preventDefault()
props.onSelect(props.escapeKey) props.onSelect(props.escapeKey)
} }
if (props.fullscreen && diffKey && Keybind.match(diffKey, keybind.parse(evt))) {
evt.preventDefault()
evt.stopPropagation()
setStore("expanded", (v) => !v)
}
}) })
const hint = createMemo(() => (store.expanded ? "minimize" : "fullscreen")) return (
const renderer = useRenderer()
const content = () => (
<box <box
backgroundColor={theme.backgroundPanel} backgroundColor={theme.backgroundPanel}
border={["left"]} border={["left"]}
borderColor={theme.warning} borderColor={theme.warning}
customBorderChars={SplitBorder.customBorderChars} customBorderChars={SplitBorder.customBorderChars}
{...(store.expanded
? { top: dimensions().height * -1 + 1, bottom: 1, left: 2, right: 2, position: "absolute" }
: {
top: 0,
maxHeight: 15,
bottom: 0,
left: 0,
right: 0,
position: "relative",
})}
> >
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1} flexGrow={1}> <box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}> <box flexDirection="row" gap={1} paddingLeft={1}>
<text fg={theme.warning}>{"△"}</text> <text fg={theme.warning}>{"△"}</text>
<text fg={theme.text}>{props.title}</text> <text fg={theme.text}>{props.title}</text>
</box> </box>
@@ -438,11 +403,6 @@ function Prompt<const T extends Record<string, string>>(props: {
</For> </For>
</box> </box>
<box flexDirection="row" gap={2}> <box flexDirection="row" gap={2}>
<Show when={props.fullscreen}>
<text fg={theme.text}>
{"ctrl+f"} <span style={{ fg: theme.textMuted }}>{hint()}</span>
</text>
</Show>
<text fg={theme.text}> <text fg={theme.text}>
{"⇆"} <span style={{ fg: theme.textMuted }}>select</span> {"⇆"} <span style={{ fg: theme.textMuted }}>select</span>
</text> </text>
@@ -453,10 +413,4 @@ function Prompt<const T extends Record<string, string>>(props: {
</box> </box>
</box> </box>
) )
return (
<Show when={!store.expanded} fallback={<Portal>{content()}</Portal>}>
{content()}
</Show>
)
} }
@@ -86,44 +86,9 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
setStore("answers", answers) setStore("answers", answers)
} }
function moveTo(index: number) {
setStore("selected", index)
}
function selectTab(index: number) {
setStore("tab", index)
setStore("selected", 0)
}
function selectOption() {
if (other()) {
if (!multi()) {
setStore("editing", true)
return
}
const value = input()
if (value && customPicked()) {
toggle(value)
return
}
setStore("editing", true)
return
}
const opt = options()[store.selected]
if (!opt) return
if (multi()) {
toggle(opt.label)
return
}
pick(opt.label)
}
const dialog = useDialog() const dialog = useDialog()
useKeyboard((evt) => { useKeyboard((evt) => {
// Skip processing if a dialog (e.g., command palette) is open
if (dialog.stack.length > 0) return
// When editing "Other" textarea // When editing "Other" textarea
if (store.editing && !confirm()) { if (store.editing && !confirm()) {
if (evt.name === "escape") { if (evt.name === "escape") {
@@ -184,12 +149,16 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
if (evt.name === "left" || evt.name === "h") { if (evt.name === "left" || evt.name === "h") {
evt.preventDefault() evt.preventDefault()
selectTab((store.tab - 1 + tabs()) % tabs()) const next = (store.tab - 1 + tabs()) % tabs()
setStore("tab", next)
setStore("selected", 0)
} }
if (evt.name === "right" || evt.name === "l") { if (evt.name === "right" || evt.name === "l") {
evt.preventDefault() evt.preventDefault()
selectTab((store.tab + 1) % tabs()) const next = (store.tab + 1) % tabs()
setStore("tab", next)
setStore("selected", 0)
} }
if (confirm()) { if (confirm()) {
@@ -207,17 +176,36 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
if (evt.name === "up" || evt.name === "k") { if (evt.name === "up" || evt.name === "k") {
evt.preventDefault() evt.preventDefault()
moveTo((store.selected - 1 + total) % total) setStore("selected", (store.selected - 1 + total) % total)
} }
if (evt.name === "down" || evt.name === "j") { if (evt.name === "down" || evt.name === "j") {
evt.preventDefault() evt.preventDefault()
moveTo((store.selected + 1) % total) setStore("selected", (store.selected + 1) % total)
} }
if (evt.name === "return") { if (evt.name === "return") {
evt.preventDefault() evt.preventDefault()
selectOption() if (other()) {
if (!multi()) {
setStore("editing", true)
return
}
const value = input()
if (value && customPicked()) {
toggle(value)
return
}
setStore("editing", true)
return
}
const opt = opts[store.selected]
if (!opt) return
if (multi()) {
toggle(opt.label)
return
}
pick(opt.label)
} }
if (evt.name === "escape" || keybind.match("app_exit", evt)) { if (evt.name === "escape" || keybind.match("app_exit", evt)) {
@@ -248,7 +236,6 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
paddingLeft={1} paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={isActive() ? theme.accent : theme.backgroundElement} backgroundColor={isActive() ? theme.accent : theme.backgroundElement}
onMouseUp={() => selectTab(index())}
> >
<text fg={isActive() ? theme.selectedListItemText : isAnswered() ? theme.text : theme.textMuted}> <text fg={isActive() ? theme.selectedListItemText : isAnswered() ? theme.text : theme.textMuted}>
{q.header} {q.header}
@@ -257,12 +244,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
) )
}} }}
</For> </For>
<box <box paddingLeft={1} paddingRight={1} backgroundColor={confirm() ? theme.accent : theme.backgroundElement}>
paddingLeft={1}
paddingRight={1}
backgroundColor={confirm() ? theme.accent : theme.backgroundElement}
onMouseUp={() => selectTab(questions().length)}
>
<text fg={confirm() ? theme.selectedListItemText : theme.textMuted}>Confirm</text> <text fg={confirm() ? theme.selectedListItemText : theme.textMuted}>Confirm</text>
</box> </box>
</box> </box>
@@ -282,7 +264,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
const active = () => i() === store.selected const active = () => i() === store.selected
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
return ( return (
<box onMouseOver={() => moveTo(i())} onMouseUp={() => selectOption()}> <box>
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
<box backgroundColor={active() ? theme.backgroundElement : undefined}> <box backgroundColor={active() ? theme.backgroundElement : undefined}>
<text fg={active() ? theme.secondary : picked() ? theme.success : theme.text}> <text fg={active() ? theme.secondary : picked() ? theme.success : theme.text}>
@@ -298,7 +280,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
) )
}} }}
</For> </For>
<box onMouseOver={() => moveTo(options().length)} onMouseUp={() => selectOption()}> <box>
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
<box backgroundColor={other() ? theme.backgroundElement : undefined}> <box backgroundColor={other() ? theme.backgroundElement : undefined}>
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}> <text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
@@ -0,0 +1,48 @@
import { cmd } from "@/cli/cmd/cmd"
import { Instance } from "@/project/instance"
import path from "path"
import { Server } from "@/server/server"
import { upgrade } from "@/cli/upgrade"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
export const TuiSpawnCommand = cmd({
command: "spawn [project]",
builder: (yargs) =>
withNetworkOptions(yargs).positional("project", {
type: "string",
describe: "path to start opencode in",
}),
handler: async (args) => {
upgrade()
const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts)
const bin = process.execPath
const cmd = []
let cwd = process.cwd()
if (bin.endsWith("bun")) {
cmd.push(
process.execPath,
"run",
"--conditions",
"browser",
new URL("../../../index.ts", import.meta.url).pathname,
)
cwd = new URL("../../../../", import.meta.url).pathname
} else cmd.push(process.execPath)
cmd.push("attach", server.url.toString(), "--dir", args.project ? path.resolve(args.project) : process.cwd())
const proc = Bun.spawn({
cmd,
cwd,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
env: {
...process.env,
BUN_OPTIONS: "",
},
})
await proc.exited
await Instance.disposeAll()
await server.stop(true)
},
})
+3 -57
View File
@@ -7,39 +7,11 @@ import { UI } from "@/cli/ui"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import type { Event } from "@opencode-ai/sdk/v2"
import type { EventSource } from "./context/sdk"
declare global { declare global {
const OPENCODE_WORKER_PATH: string const OPENCODE_WORKER_PATH: string
} }
type RpcClient = ReturnType<typeof Rpc.client<typeof rpc>>
function createWorkerFetch(client: RpcClient): typeof fetch {
const fn = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const request = new Request(input, init)
const body = request.body ? await request.text() : undefined
const result = await client.call("fetch", {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
body,
})
return new Response(result.body, {
status: result.status,
headers: result.headers,
})
}
return fn as typeof fetch
}
function createEventSource(client: RpcClient): EventSource {
return {
on: (handler) => client.on<Event>("event", handler),
}
}
export const TuiThreadCommand = cmd({ export const TuiThreadCommand = cmd({
command: "$0 [project]", command: "$0 [project]",
describe: "start opencode tui", describe: "start opencode tui",
@@ -108,42 +80,16 @@ export const TuiThreadCommand = cmd({
process.on("SIGUSR2", async () => { process.on("SIGUSR2", async () => {
await client.call("reload", undefined) await client.call("reload", undefined)
}) })
const opts = await resolveNetworkOptions(args)
const server = await client.call("server", opts)
const prompt = await iife(async () => { const prompt = await iife(async () => {
const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
if (!args.prompt) return piped if (!args.prompt) return piped
return piped ? piped + "\n" + args.prompt : args.prompt return piped ? piped + "\n" + args.prompt : args.prompt
}) })
// Check if server should be started (port or hostname explicitly set in CLI or config)
const networkOpts = await resolveNetworkOptions(args)
const shouldStartServer =
process.argv.includes("--port") ||
process.argv.includes("--hostname") ||
process.argv.includes("--mdns") ||
networkOpts.mdns ||
networkOpts.port !== 0 ||
networkOpts.hostname !== "127.0.0.1"
let url: string
let customFetch: typeof fetch | undefined
let events: EventSource | undefined
if (shouldStartServer) {
// Start HTTP server for external access
const server = await client.call("server", networkOpts)
url = server.url
} else {
// Use direct RPC communication (no HTTP)
url = "http://opencode.internal"
customFetch = createWorkerFetch(client)
events = createEventSource(client)
}
const tuiPromise = tui({ const tuiPromise = tui({
url, url: server.url,
fetch: customFetch,
events,
args: { args: {
continue: args.continue, continue: args.continue,
sessionID: args.session, sessionID: args.session,

Some files were not shown because too many files have changed in this diff Show More