Compare commits
122
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
585378cba0 | ||
|
|
8cd8393339 | ||
|
|
b184b2fb73 | ||
|
|
c88c2da9be | ||
|
|
9b04081ae0 | ||
|
|
7d2d87fa2c | ||
|
|
787f37b382 | ||
|
|
8fa1af851c | ||
|
|
73bc3e704e | ||
|
|
8d2feed30e | ||
|
|
2d8d4e5dee | ||
|
|
b3784588ae | ||
|
|
104d52bc38 | ||
|
|
dff1fe2d28 | ||
|
|
72ab4260ee | ||
|
|
9e9b4a0555 | ||
|
|
e53192889c | ||
|
|
23bbfb3d15 | ||
|
|
37da005a01 | ||
|
|
8b708242f1 | ||
|
|
339d2dcb98 | ||
|
|
bbc8678164 | ||
|
|
a1d54475fe | ||
|
|
55c601d13a | ||
|
|
9115fac4c4 | ||
|
|
cfcb2c1fd8 | ||
|
|
221fc62135 | ||
|
|
faaef45384 | ||
|
|
2d18d80ac3 | ||
|
|
e0e07c5d48 | ||
|
|
281f9e6236 | ||
|
|
f88903a901 | ||
|
|
ad425a6a6a | ||
|
|
e635d37027 | ||
|
|
97081484d5 | ||
|
|
e451504496 | ||
|
|
53211c5d37 | ||
|
|
98b6817e20 | ||
|
|
f54d5377a4 | ||
|
|
a576fdb5e4 | ||
|
|
ae53f876f1 | ||
|
|
a7beba5aa9 | ||
|
|
e9ef72c20f | ||
|
|
fa1ac7bc95 | ||
|
|
c82ab649e2 | ||
|
|
abc7eed92b | ||
|
|
1670d220da | ||
|
|
ddc4e34731 | ||
|
|
af99d83709 | ||
|
|
ed0c0d90be | ||
|
|
e1dd9c4ccb | ||
|
|
4657fa823f | ||
|
|
1d589c7ac7 | ||
|
|
6b5a0fb261 | ||
|
|
6d93a7bf55 | ||
|
|
4ca7ab6be8 | ||
|
|
713d996b9f | ||
|
|
aa16610021 | ||
|
|
d98568fe7e | ||
|
|
1da3550c4d | ||
|
|
0c48e6a116 | ||
|
|
ef266b2c74 | ||
|
|
0a1cdc7a58 | ||
|
|
2dec956a17 | ||
|
|
ef8388f0ee | ||
|
|
e5c5b5e872 | ||
|
|
a1c9a1b8c5 | ||
|
|
76b012139a | ||
|
|
02e5a19242 | ||
|
|
af967648cb | ||
|
|
504a668a26 | ||
|
|
5efb1c7b2d | ||
|
|
fd973d242e | ||
|
|
c3d8672753 | ||
|
|
fe8ef041f6 | ||
|
|
c841de947e | ||
|
|
825dfd48b1 | ||
|
|
923d114ffa | ||
|
|
b157fd10a7 | ||
|
|
67ebe68160 | ||
|
|
7b63c14154 | ||
|
|
cdc11cde2e | ||
|
|
9721223b7e | ||
|
|
35a626e711 | ||
|
|
bb7b0ff221 | ||
|
|
68b4038196 | ||
|
|
3109214900 | ||
|
|
86ccc3409b | ||
|
|
a89089c88f | ||
|
|
e617c5d689 | ||
|
|
31983ca5ff | ||
|
|
59e3b7409f | ||
|
|
b7ce46f7a1 | ||
|
|
82b8d8fa5d | ||
|
|
77c837eb1a | ||
|
|
db77cc9845 | ||
|
|
68043edae6 | ||
|
|
337681dbbf | ||
|
|
66afc034d1 | ||
|
|
11ab8de59f | ||
|
|
5f074edc3a | ||
|
|
56b5cdf883 | ||
|
|
fb0e1e4d8d | ||
|
|
b745b1593f | ||
|
|
7376c3f8e7 | ||
|
|
831e9bce51 | ||
|
|
5de73abd82 | ||
|
|
3adbbc1b23 | ||
|
|
c6c29b3dcf | ||
|
|
a687d7c15f | ||
|
|
0c6da69f39 | ||
|
|
c4930eb6b2 | ||
|
|
a24549fce7 | ||
|
|
c0f9b13630 | ||
|
|
98fd53fd5f | ||
|
|
5b02a3029e | ||
|
|
94e851c2a2 | ||
|
|
1658a3ff59 | ||
|
|
9c8bc64138 | ||
|
|
80f704ebbf | ||
|
|
4dae6d1fcf | ||
|
|
5d2cab39da |
@@ -5,6 +5,9 @@ on:
|
||||
- cron: "0 */12 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
LOOKBACK_HOURS: 4
|
||||
|
||||
jobs:
|
||||
update-docs:
|
||||
if: github.repository == 'sst/opencode'
|
||||
@@ -25,9 +28,9 @@ jobs:
|
||||
- name: Get recent commits
|
||||
id: commits
|
||||
run: |
|
||||
COMMITS=$(git log --since="4 hours ago" --pretty=format:"- %h %s" 2>/dev/null || echo "")
|
||||
COMMITS=$(git log --since="${{ env.LOOKBACK_HOURS }} hours ago" --pretty=format:"- %h %s" 2>/dev/null || echo "")
|
||||
if [ -z "$COMMITS" ]; then
|
||||
echo "No commits in the last 4 hours"
|
||||
echo "No commits in the last ${{ env.LOOKBACK_HOURS }} hours"
|
||||
echo "has_commits=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_commits=true" >> $GITHUB_OUTPUT
|
||||
@@ -47,7 +50,7 @@ jobs:
|
||||
model: opencode/gpt-5.2
|
||||
agent: docs
|
||||
prompt: |
|
||||
Review the following commits from the last 4 hours and identify any new features that may need documentation.
|
||||
Review the following commits from the last ${{ env.LOOKBACK_HOURS }} hours and identify any new features that may need documentation.
|
||||
|
||||
<recent_commits>
|
||||
${{ steps.commits.outputs.list }}
|
||||
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
- cron: "30 1 * * *" # Daily at 1:30 AM
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DAYS_BEFORE_STALE: 90
|
||||
DAYS_BEFORE_CLOSE: 7
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -13,17 +17,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
days-before-stale: 90
|
||||
days-before-close: 7
|
||||
days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
|
||||
days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
|
||||
stale-issue-label: "stale"
|
||||
close-issue-message: |
|
||||
[automated] Closing due to 90+ days of inactivity.
|
||||
[automated] Closing due to ${{ env.DAYS_BEFORE_STALE }}+ days of inactivity.
|
||||
|
||||
Feel free to reopen if you still need this!
|
||||
stale-issue-message: |
|
||||
[automated] This issue has had no activity for 90 days.
|
||||
[automated] This issue has had no activity for ${{ env.DAYS_BEFORE_STALE }} days.
|
||||
|
||||
It will be closed in 7 days if there's no new activity.
|
||||
It will be closed in ${{ env.DAYS_BEFORE_CLOSE }} days if there's no new activity.
|
||||
remove-stale-when-updated: true
|
||||
exempt-issue-labels: "pinned,security,feature-request,on-hold"
|
||||
start-date: "2025-12-27"
|
||||
|
||||
@@ -32,3 +32,4 @@ jobs:
|
||||
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
|
||||
env:
|
||||
ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }}
|
||||
ZED_PR_PAT: ${{ secrets.ZED_PR_PAT }}
|
||||
|
||||
@@ -20,3 +20,7 @@ opencode.json
|
||||
a.out
|
||||
target
|
||||
.scripts
|
||||
|
||||
# Local dev files
|
||||
opencode-dev
|
||||
logs/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: ALWAYS use this when writing docs
|
||||
color: "#38A3EE"
|
||||
---
|
||||
|
||||
You are an expert technical documentation writer
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
mode: primary
|
||||
hidden: true
|
||||
model: opencode/claude-haiku-4-5
|
||||
color: "#44BA81"
|
||||
tools:
|
||||
"*": false
|
||||
"github-triage": true
|
||||
|
||||
@@ -34,6 +34,36 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
|
||||
bun dev
|
||||
```
|
||||
|
||||
### Running against a different directory
|
||||
|
||||
By default, `bun dev` runs OpenCode in the `packages/opencode` directory. To run it against a different directory or repository:
|
||||
|
||||
```bash
|
||||
bun dev <directory>
|
||||
```
|
||||
|
||||
To run OpenCode in the root of the opencode repo itself:
|
||||
|
||||
```bash
|
||||
bun dev .
|
||||
```
|
||||
|
||||
### Building a "localcode"
|
||||
|
||||
To compile a standalone executable:
|
||||
|
||||
```bash
|
||||
./packages/opencode/script/build.ts --single
|
||||
```
|
||||
|
||||
Then run it with:
|
||||
|
||||
```bash
|
||||
./packages/opencode/dist/opencode-<platform>/bin/opencode
|
||||
```
|
||||
|
||||
Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`).
|
||||
|
||||
- Core pieces:
|
||||
- `packages/opencode`: OpenCode core business logic & server.
|
||||
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
|
||||
|
||||
@@ -184,3 +184,5 @@
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -98,7 +98,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -125,7 +125,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -149,7 +149,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -173,13 +173,14 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-http": "~2",
|
||||
"@tauri-apps/plugin-notification": "~2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-process": "~2",
|
||||
@@ -200,7 +201,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -229,7 +230,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -245,7 +246,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -254,22 +255,21 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.5.1",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.57",
|
||||
"@ai-sdk/anthropic": "2.0.50",
|
||||
"@ai-sdk/azure": "2.0.73",
|
||||
"@ai-sdk/anthropic": "2.0.56",
|
||||
"@ai-sdk/azure": "2.0.82",
|
||||
"@ai-sdk/cerebras": "1.0.33",
|
||||
"@ai-sdk/cohere": "2.0.21",
|
||||
"@ai-sdk/deepinfra": "1.0.30",
|
||||
"@ai-sdk/gateway": "2.0.23",
|
||||
"@ai-sdk/google": "2.0.44",
|
||||
"@ai-sdk/google": "2.0.49",
|
||||
"@ai-sdk/google-vertex": "3.0.81",
|
||||
"@ai-sdk/groq": "2.0.33",
|
||||
"@ai-sdk/mcp": "0.0.8",
|
||||
"@ai-sdk/mistral": "2.0.26",
|
||||
"@ai-sdk/openai": "2.0.71",
|
||||
"@ai-sdk/openai-compatible": "1.0.27",
|
||||
"@ai-sdk/openai-compatible": "1.0.29",
|
||||
"@ai-sdk/perplexity": "2.0.22",
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.18",
|
||||
"@ai-sdk/provider-utils": "3.0.19",
|
||||
"@ai-sdk/togetherai": "1.0.30",
|
||||
"@ai-sdk/xai": "2.0.42",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
@@ -284,8 +284,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.63",
|
||||
"@opentui/solid": "0.1.63",
|
||||
"@opentui/core": "0.1.67",
|
||||
"@opentui/solid": "0.1.67",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -347,7 +347,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -367,7 +367,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -378,7 +378,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -391,7 +391,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -403,8 +403,10 @@
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"katex": "0.16.27",
|
||||
"luxon": "catalog:",
|
||||
"marked": "catalog:",
|
||||
"marked-katex-extension": "5.1.6",
|
||||
"marked-shiki": "catalog:",
|
||||
"remeda": "catalog:",
|
||||
"shiki": "catalog:",
|
||||
@@ -416,6 +418,7 @@
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/katex": "0.16.7",
|
||||
"@types/luxon": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
@@ -426,7 +429,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -437,7 +440,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -538,7 +541,7 @@
|
||||
|
||||
"@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.73", "", { "dependencies": { "@ai-sdk/openai": "2.0.71", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LpAg3Ak/V3WOemBu35Qbx9jfQfApsHNXX9p3bXVsnRu3XXi1QQUt5gMOCIb4znPonz+XnHenIDZMBwdsb1TfRQ=="],
|
||||
"@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.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=="],
|
||||
|
||||
@@ -548,14 +551,12 @@
|
||||
|
||||
"@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.44", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-c5dck36FjqiVoeeMJQLTEmUheoURcGTU/nBT6iJu8/nZiKFT/y8pD85KMDRB7RerRYaaQOtslR2d6/5PditiRw=="],
|
||||
"@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.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.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/mcp": ["@ai-sdk/mcp@0.0.8", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9y9GuGcZ9/+pMIHfpOCJgZVp+AZMv6TkjX2NVT17SQZvTF2N8LXuCXyoUPyi1PxIxzxl0n463LxxaB2O6olC+Q=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -566,7 +567,7 @@
|
||||
|
||||
"@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.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/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.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=="],
|
||||
|
||||
@@ -1192,21 +1193,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.63", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.63", "@opentui/core-darwin-x64": "0.1.63", "@opentui/core-linux-arm64": "0.1.63", "@opentui/core-linux-x64": "0.1.63", "@opentui/core-win32-arm64": "0.1.63", "@opentui/core-win32-x64": "0.1.63", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-m4xZQTNCnHXWUWCnGvacJ3Gts1H2aMwP5V/puAG77SDb51jm4W/QOyqAAdgeSakkb9II+8FfUpApX7sfwRXPUg=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.67", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.67", "@opentui/core-darwin-x64": "0.1.67", "@opentui/core-linux-arm64": "0.1.67", "@opentui/core-linux-x64": "0.1.67", "@opentui/core-win32-arm64": "0.1.67", "@opentui/core-win32-x64": "0.1.67", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-zmfyA10QUbzT6ohacPoHmGiYzuJrDSCfQWRWrKtao0BrHj9bii73qWy3V/eR4ibVueoRREwxJs5GlBOSvK6IoA=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.63", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jKCThZGiiublKkP/hMtDtl1MLCw5NU0hMNJdEYvz1WLT9bzliWf6Kb7MIDAmk32XlbQW8/RHdp+hGyGDXK62OQ=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.67", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LtOcTlFD+kO7neItmkiF77H8cnjTYzBOZe8JQGwRSt9aaCke3UzMvLxmQnj4BP/kPC3hi9V6NRnFdptz0sJZIQ=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.63", "", { "os": "darwin", "cpu": "x64" }, "sha512-rfNxynHzJpxN9i+SAMnn1NToEc8rYj64BsOxY78JNsm4Gg1Js1uyMaawwh2WbdGknFy4cDXS9QwkUMdMcfnjiw=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.67", "", { "os": "darwin", "cpu": "x64" }, "sha512-9i+awVWgpEVqZhFLaLq8usNGyCiyT5QxMLy6eH7JmRic79S34u23HfxiniGRtdYh3aqpm9SbLzo60v0nRIUkCA=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.63", "", { "os": "linux", "cpu": "arm64" }, "sha512-wG9d6mHWWKZGrzxYS4c+BrcEGXBv/MYBUPSyjP/lD0CxT+X3h6CYhI317JkRyMNfh3vI9CpAKGFTOFvrTTHimQ=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.67", "", { "os": "linux", "cpu": "arm64" }, "sha512-WLjnTM3Ig//SRo0FUZYZJ5TITVbR6dKDVg6axU2D+sMoUzJMBP/Xo04q/TvZ3wP764Yca9l7oVMKWDxHlygyjQ=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.63", "", { "os": "linux", "cpu": "x64" }, "sha512-TKSzFv4BgWW3RB/iZmq5qxTR4/tRaXo8IZNnVR+LFzShbPOqhUi466AByy9SUmCxD8uYjmMDFYfKtkCy0AnAwA=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.67", "", { "os": "linux", "cpu": "x64" }, "sha512-5UbZ/TqWi/DAmHIZL4NvhdpgTwglszRiddkRiQ8cT0IbnE4lutd4XxWUWcLKwsNT1YJv32TtcGWkuthluLiriQ=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.63", "", { "os": "win32", "cpu": "arm64" }, "sha512-CBWPyPognERP0Mq4eC1q01Ado2C2WU+BLTgMdhyt+E2P4w8rPhJ2kCt2MNxO66vQUiynspmZkgjQr0II/VjxWA=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.67", "", { "os": "win32", "cpu": "arm64" }, "sha512-KNam5rObhN8/U9+GVVuvtAlGXp3MfdMHnw4W2P6YH7xp8HTsLvABUT91SJEyJ/ktVe9e1itLDG2fDHSoA5NbUg=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.63", "", { "os": "win32", "cpu": "x64" }, "sha512-qEp6h//FrT+TQiiHm87wZWUwqTPTqIy1ZD+8R+VCUK+usoQiOAD2SqrYnM7W8JkCMGn5/TKm/GaKLyx/qlK4VA=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.67", "", { "os": "win32", "cpu": "x64" }, "sha512-740lkOw42zLNh9YfahXjCwV2DS/amH2uMDh3tCADDCLckrMhemIhqArXDiMlalDxDqYspoaZCpBsFVsG9dMS6A=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.63", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.63", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-Gccln4qRucAoaoQEZ4NPAHvGmVYzU/8aKCLG8EPgwCKTcpUzlqYt4357cDHq4cnCNOcXOC06hTz/0pK9r0dqXA=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.67", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.67", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-dVNq0+PJIdNb63D0T7vcbyVF/ZvLCihGvivTU50zDOzd0Sk5prbrIfpG8+DjMErFubXfdZQvdy/PqFdtw0rjtQ=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -1706,6 +1707,8 @@
|
||||
|
||||
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="],
|
||||
|
||||
"@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="],
|
||||
|
||||
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
|
||||
|
||||
"@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
|
||||
@@ -1774,6 +1777,8 @@
|
||||
|
||||
"@types/jsonwebtoken": ["@types/jsonwebtoken@8.5.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg=="],
|
||||
|
||||
"@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
|
||||
|
||||
"@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
@@ -2786,6 +2791,8 @@
|
||||
|
||||
"jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="],
|
||||
|
||||
"katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="],
|
||||
|
||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
@@ -2876,6 +2883,8 @@
|
||||
|
||||
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
|
||||
"marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="],
|
||||
|
||||
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
@@ -3892,44 +3901,24 @@
|
||||
|
||||
"@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.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=="],
|
||||
|
||||
"@ai-sdk/azure/@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/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.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/cerebras/@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/cohere/@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/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/deepinfra/@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/gateway/@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/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/groq/@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/mcp/@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/mistral/@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/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-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/perplexity/@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/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/togetherai/@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/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=="],
|
||||
|
||||
"@ai-sdk/xai/@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=="],
|
||||
|
||||
"@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/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
|
||||
@@ -4284,6 +4273,8 @@
|
||||
|
||||
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
@@ -4308,11 +4299,11 @@
|
||||
|
||||
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
|
||||
"opencode/@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=="],
|
||||
"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.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.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bpYruxVLhrTbVH6CCq48zMJNeHu6FmHtEedl9FXckEgcIEAi036idFhJlcRwC1jNCwlacbzb8dPD7OAH1EKJaQ=="],
|
||||
"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=="],
|
||||
|
||||
@@ -4916,8 +4907,6 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"opencode/@ai-sdk/openai-compatible/@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/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766840161,
|
||||
"narHash": "sha256-Ss/LHpJJsng8vz1Pe33RSGIWUOcqM1fjrehjUkdrWio=",
|
||||
"lastModified": 1767026758,
|
||||
"narHash": "sha256-7fsac/f7nh/VaKJ/qm3I338+wAJa/3J57cOGpXi0Sbg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3edc4a30ed3903fdf6f90c837f961fa6b49582d1",
|
||||
"rev": "346dd96ad74dc4457a9db9de4f4f57dab2e5731d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
lib = nixpkgs.lib;
|
||||
inherit (nixpkgs) lib;
|
||||
forEachSystem = lib.genAttrs systems;
|
||||
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||
packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
|
||||
@@ -70,12 +70,12 @@
|
||||
in
|
||||
{
|
||||
default = mkPackage {
|
||||
version = packageJson.version;
|
||||
inherit (packageJson) version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
target = bunTarget.${system};
|
||||
modelsDev = "${modelsDev.${system}}/dist/_api.json";
|
||||
mkNodeModules = mkNodeModules;
|
||||
inherit mkNodeModules;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-lloUZt5mLyNWAcbQrJB4wGUKvKu24WFEhOLfZD5/FMg="
|
||||
"nodeModules": "sha256-2Wbnxy9SPcZkO03Sis3uiypPXa87jc5TzKbo6PvMlxY="
|
||||
}
|
||||
|
||||
+18
-10
@@ -1,18 +1,26 @@
|
||||
{ hash, lib, stdenvNoCC, bun, cacert, curl }:
|
||||
{
|
||||
hash,
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
bun,
|
||||
cacert,
|
||||
curl,
|
||||
}:
|
||||
args:
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "opencode-node_modules";
|
||||
version = args.version;
|
||||
src = args.src;
|
||||
inherit (args) version src;
|
||||
|
||||
impureEnvVars =
|
||||
lib.fetchers.proxyImpureEnvVars
|
||||
++ [
|
||||
"GIT_PROXY_COMMAND"
|
||||
"SOCKS_SERVER"
|
||||
];
|
||||
impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
|
||||
"GIT_PROXY_COMMAND"
|
||||
"SOCKS_SERVER"
|
||||
];
|
||||
|
||||
nativeBuildInputs = [ bun cacert curl ];
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
cacert
|
||||
curl
|
||||
];
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
|
||||
+10
-7
@@ -1,7 +1,13 @@
|
||||
{ lib, stdenvNoCC, bun, ripgrep, makeBinaryWrapper }:
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
bun,
|
||||
ripgrep,
|
||||
makeBinaryWrapper,
|
||||
}:
|
||||
args:
|
||||
let
|
||||
scripts = args.scripts;
|
||||
inherit (args) scripts;
|
||||
mkModules =
|
||||
attrs:
|
||||
args.mkNodeModules (
|
||||
@@ -14,13 +20,10 @@ let
|
||||
in
|
||||
stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
pname = "opencode";
|
||||
version = args.version;
|
||||
|
||||
src = args.src;
|
||||
inherit (args) version src;
|
||||
|
||||
node_modules = mkModules {
|
||||
version = finalAttrs.version;
|
||||
src = finalAttrs.src;
|
||||
inherit (finalAttrs) version src;
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
|
||||
@@ -31,9 +31,13 @@ for (const [name, wasmPath] of byName) {
|
||||
next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
|
||||
|
||||
// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
|
||||
const nixStorePrefix = process.env.NIX_STORE || "/nix/store"
|
||||
next = next.replace(/(\.\/)+/g, "./")
|
||||
next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2")
|
||||
next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||
next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||
next = next.replace(
|
||||
new RegExp(`(\\.\\.\\/)+\\/{1,2}(${nixStorePrefix.replace(/^\//, "").replace(/\//g, "\\/")}[^"']+)`, "g"),
|
||||
"/$2",
|
||||
)
|
||||
next = next.replace(new RegExp(`(["'])\\/{2,}(\\/${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
|
||||
next = next.replace(new RegExp(`(["'])\\/\\/(${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
|
||||
|
||||
if (next !== content) fs.writeFileSync(file, next)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
+47
-33
@@ -1,5 +1,5 @@
|
||||
import "@/index.css"
|
||||
import { ErrorBoundary, Show } from "solid-js"
|
||||
import { ErrorBoundary, Show, type ParentProps } from "solid-js"
|
||||
import { Router, Route, Navigate } from "@solidjs/router"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { Font } from "@opencode-ai/ui/font"
|
||||
@@ -12,6 +12,7 @@ import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||
import { GlobalSyncProvider } from "@/context/global-sync"
|
||||
import { LayoutProvider } from "@/context/layout"
|
||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||
import { ServerProvider, useServer } from "@/context/server"
|
||||
import { TerminalProvider } from "@/context/terminal"
|
||||
import { PromptProvider } from "@/context/prompt"
|
||||
import { NotificationProvider } from "@/context/notification"
|
||||
@@ -30,7 +31,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const url = iife(() => {
|
||||
const defaultServerUrl = iife(() => {
|
||||
const param = new URLSearchParams(document.location.search).get("url")
|
||||
if (param) return param
|
||||
|
||||
@@ -42,6 +43,15 @@ const url = iife(() => {
|
||||
return window.location.origin
|
||||
})
|
||||
|
||||
function ServerKey(props: ParentProps) {
|
||||
const server = useServer()
|
||||
return (
|
||||
<Show when={server.url} keyed>
|
||||
{props.children}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<MetaProvider>
|
||||
@@ -52,38 +62,42 @@ export function App() {
|
||||
<MarkedProvider>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>
|
||||
<GlobalSDKProvider url={url}>
|
||||
<GlobalSyncProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<Router
|
||||
root={(props) => (
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
)}
|
||||
>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id || true} keyed>
|
||||
<TerminalProvider>
|
||||
<PromptProvider>
|
||||
<Session />
|
||||
</PromptProvider>
|
||||
</TerminalProvider>
|
||||
</Show>
|
||||
<ServerProvider defaultUrl={defaultServerUrl}>
|
||||
<ServerKey>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<Router
|
||||
root={(props) => (
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id ?? "new"} keyed>
|
||||
<TerminalProvider>
|
||||
<PromptProvider>
|
||||
<Session />
|
||||
</PromptProvider>
|
||||
</TerminalProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</ServerKey>
|
||||
</ServerProvider>
|
||||
</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</MarkedProvider>
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
|
||||
function getFilename(input: string) {
|
||||
const parts = input.split("/")
|
||||
return parts[parts.length - 1] || input
|
||||
}
|
||||
|
||||
export function DialogEditProject(props: { project: LocalProject }) {
|
||||
const dialog = useDialog()
|
||||
const globalSDK = useGlobalSDK()
|
||||
|
||||
const folderName = createMemo(() => getFilename(props.project.worktree))
|
||||
const defaultName = createMemo(() => props.project.name || folderName())
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
name: defaultName(),
|
||||
color: props.project.icon?.color || "pink",
|
||||
iconUrl: props.project.icon?.url || "",
|
||||
saving: false,
|
||||
})
|
||||
|
||||
const [dragOver, setDragOver] = createSignal(false)
|
||||
|
||||
function handleFileSelect(file: File) {
|
||||
if (!file.type.startsWith("image/")) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => setStore("iconUrl", e.target?.result as string)
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
setDragOver(false)
|
||||
const file = e.dataTransfer?.files[0]
|
||||
if (file) handleFileSelect(file)
|
||||
}
|
||||
|
||||
function handleDragOver(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
setDragOver(true)
|
||||
}
|
||||
|
||||
function handleDragLeave() {
|
||||
setDragOver(false)
|
||||
}
|
||||
|
||||
function handleInputChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (file) handleFileSelect(file)
|
||||
}
|
||||
|
||||
function clearIcon() {
|
||||
setStore("iconUrl", "")
|
||||
}
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
if (!props.project.id) return
|
||||
|
||||
setStore("saving", true)
|
||||
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||
await globalSDK.client.project.update({
|
||||
projectID: props.project.id,
|
||||
name,
|
||||
icon: { color: store.color, url: store.iconUrl },
|
||||
})
|
||||
setStore("saving", false)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title="Edit project">
|
||||
<form onSubmit={handleSubmit} class="flex flex-col gap-6 px-2.5 pb-3">
|
||||
<div class="flex flex-col gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label="Name"
|
||||
placeholder={folderName()}
|
||||
value={store.name}
|
||||
onChange={(v) => setStore("name", v)}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-12-medium text-text-weak">Icon</label>
|
||||
<div class="flex gap-3 items-start">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="size-16 rounded-lg overflow-hidden border border-dashed transition-colors cursor-pointer"
|
||||
classList={{
|
||||
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
|
||||
"border-border-base hover:border-border-strong": !dragOver(),
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => document.getElementById("icon-upload")?.click()}
|
||||
>
|
||||
<Show
|
||||
when={store.iconUrl}
|
||||
fallback={
|
||||
<div class="size-full flex items-center justify-center">
|
||||
<Avatar
|
||||
fallback={store.name || defaultName()}
|
||||
{...getAvatarColors(store.color)}
|
||||
class="size-full"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={store.iconUrl}>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-base border border-border-base flex items-center justify-center hover:bg-surface-raised-base-hover"
|
||||
onClick={clearIcon}
|
||||
>
|
||||
<Icon name="close" class="size-3 text-icon-base" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
|
||||
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak">
|
||||
<span>Click or drag an image</span>
|
||||
<span>Recommended: 128x128px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={!store.iconUrl}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-12-medium text-text-weak">Color</label>
|
||||
<div class="flex gap-2">
|
||||
<For each={AVATAR_COLOR_KEYS}>
|
||||
{(color) => (
|
||||
<button
|
||||
type="button"
|
||||
class="relative size-8 rounded-md transition-all"
|
||||
classList={{
|
||||
"ring-2 ring-offset-2 ring-offset-surface-base ring-text-interactive-base":
|
||||
store.color === color,
|
||||
}}
|
||||
style={{ background: getAvatarColors(color).background }}
|
||||
onClick={() => setStore("color", color)}
|
||||
>
|
||||
<Avatar fallback={store.name || defaultName()} {...getAvatarColors(color)} class="size-full" />
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
|
||||
{store.saving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { createMemo } from "solid-js"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
|
||||
interface DialogSelectDirectoryProps {
|
||||
title?: string
|
||||
multiple?: boolean
|
||||
onSelect: (result: string | string[] | null) => void
|
||||
}
|
||||
|
||||
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
const sync = useGlobalSync()
|
||||
const sdk = useGlobalSDK()
|
||||
const dialog = useDialog()
|
||||
|
||||
const home = createMemo(() => sync.data.path.home)
|
||||
const root = createMemo(() => sync.data.path.home || sync.data.path.directory)
|
||||
|
||||
function join(base: string | undefined, rel: string) {
|
||||
const b = (base ?? "").replace(/[\\/]+$/, "")
|
||||
const r = rel.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")
|
||||
if (!b) return r
|
||||
if (!r) return b
|
||||
return b + "/" + r
|
||||
}
|
||||
|
||||
function display(rel: string) {
|
||||
const full = join(root(), rel)
|
||||
const h = home()
|
||||
if (!h) return full
|
||||
if (full === h) return "~"
|
||||
if (full.startsWith(h + "/") || full.startsWith(h + "\\")) {
|
||||
return "~" + full.slice(h.length)
|
||||
}
|
||||
return full
|
||||
}
|
||||
|
||||
function normalizeQuery(query: string) {
|
||||
const h = home()
|
||||
|
||||
if (!query) return query
|
||||
if (query.startsWith("~/")) return query.slice(2)
|
||||
|
||||
if (h) {
|
||||
const lc = query.toLowerCase()
|
||||
const hc = h.toLowerCase()
|
||||
if (lc === hc || lc.startsWith(hc + "/") || lc.startsWith(hc + "\\")) {
|
||||
return query.slice(h.length).replace(/^[\\/]+/, "")
|
||||
}
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
async function fetchDirs(query: string) {
|
||||
const directory = root()
|
||||
if (!directory) return [] as string[]
|
||||
|
||||
const results = await sdk.client.find
|
||||
.files({ directory, query, type: "directory", limit: 50 })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => [])
|
||||
|
||||
return results.map((x) => x.replace(/[\\/]+$/, ""))
|
||||
}
|
||||
|
||||
const directories = async (filter: string) => {
|
||||
const query = normalizeQuery(filter.trim())
|
||||
return fetchDirs(query)
|
||||
}
|
||||
|
||||
function resolve(rel: string) {
|
||||
const absolute = join(root(), rel)
|
||||
props.onSelect(props.multiple ? [absolute] : absolute)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={props.title ?? "Open project"}>
|
||||
<List
|
||||
search={{ placeholder: "Search folders", autofocus: true }}
|
||||
emptyMessage="No folders found"
|
||||
items={directories}
|
||||
key={(x) => x}
|
||||
onSelect={(path) => {
|
||||
if (!path) return
|
||||
resolve(path)
|
||||
}}
|
||||
>
|
||||
{(rel) => {
|
||||
const path = display(rel)
|
||||
return (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<FileIcon node={{ path: rel, type: "directory" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular min-w-0">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(path)}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(path)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
|
||||
type ServerStatus = { healthy: boolean; version?: string }
|
||||
|
||||
async function checkHealth(url: string, fetch?: typeof globalThis.fetch): Promise<ServerStatus> {
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: url,
|
||||
fetch,
|
||||
signal: AbortSignal.timeout(3000),
|
||||
})
|
||||
return sdk.global
|
||||
.health()
|
||||
.then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version }))
|
||||
.catch(() => ({ healthy: false }))
|
||||
}
|
||||
|
||||
export function DialogSelectServer() {
|
||||
const navigate = useNavigate()
|
||||
const dialog = useDialog()
|
||||
const server = useServer()
|
||||
const platform = usePlatform()
|
||||
const [store, setStore] = createStore({
|
||||
url: "",
|
||||
adding: false,
|
||||
error: "",
|
||||
status: {} as Record<string, ServerStatus | undefined>,
|
||||
})
|
||||
|
||||
const items = createMemo(() => {
|
||||
const current = server.url
|
||||
const list = server.list
|
||||
if (!current) return list
|
||||
if (!list.includes(current)) return [current, ...list]
|
||||
return [current, ...list.filter((x) => x !== current)]
|
||||
})
|
||||
|
||||
const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0])
|
||||
|
||||
const sortedItems = createMemo(() => {
|
||||
const list = items()
|
||||
if (!list.length) return list
|
||||
const active = current()
|
||||
const order = new Map(list.map((url, index) => [url, index] as const))
|
||||
const rank = (value?: ServerStatus) => {
|
||||
if (value?.healthy === true) return 0
|
||||
if (value?.healthy === false) return 2
|
||||
return 1
|
||||
}
|
||||
return list.slice().sort((a, b) => {
|
||||
if (a === active) return -1
|
||||
if (b === active) return 1
|
||||
const diff = rank(store.status[a]) - rank(store.status[b])
|
||||
if (diff !== 0) return diff
|
||||
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
||||
})
|
||||
})
|
||||
|
||||
async function refreshHealth() {
|
||||
const results: Record<string, ServerStatus> = {}
|
||||
await Promise.all(
|
||||
items().map(async (url) => {
|
||||
results[url] = await checkHealth(url, platform.fetch)
|
||||
}),
|
||||
)
|
||||
setStore("status", reconcile(results))
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
items()
|
||||
refreshHealth()
|
||||
const interval = setInterval(refreshHealth, 10_000)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
|
||||
function select(value: string, persist?: boolean) {
|
||||
if (!persist && store.status[value]?.healthy === false) return
|
||||
dialog.close()
|
||||
if (persist) {
|
||||
server.add(value)
|
||||
navigate("/")
|
||||
return
|
||||
}
|
||||
server.setActive(value)
|
||||
navigate("/")
|
||||
}
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
const value = normalizeServerUrl(store.url)
|
||||
if (!value) return
|
||||
|
||||
setStore("adding", true)
|
||||
setStore("error", "")
|
||||
|
||||
const result = await checkHealth(value, platform.fetch)
|
||||
setStore("adding", false)
|
||||
|
||||
if (!result.healthy) {
|
||||
setStore("error", "Could not connect to server")
|
||||
return
|
||||
}
|
||||
|
||||
setStore("url", "")
|
||||
select(value, true)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title="Servers" description="Switch which OpenCode server this app connects to.">
|
||||
<div class="flex flex-col gap-4 pb-4">
|
||||
<List
|
||||
search={{ placeholder: "Search servers", autofocus: true }}
|
||||
emptyMessage="No servers yet"
|
||||
items={sortedItems}
|
||||
key={(x) => x}
|
||||
current={current()}
|
||||
onSelect={(x) => {
|
||||
if (x) select(x)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div
|
||||
class="flex items-center gap-2 min-w-0 flex-1"
|
||||
classList={{ "opacity-50": store.status[i]?.healthy === false }}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full shrink-0": true,
|
||||
"bg-icon-success-base": store.status[i]?.healthy === true,
|
||||
"bg-icon-critical-base": store.status[i]?.healthy === false,
|
||||
"bg-border-weak-base": store.status[i] === undefined,
|
||||
}}
|
||||
/>
|
||||
<span class="truncate">{serverDisplayName(i)}</span>
|
||||
<span class="text-text-weak">{store.status[i]?.version}</span>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<div class="mt-6 px-3 flex flex-col gap-1.5">
|
||||
<div class="px-3">
|
||||
<h3 class="text-14-regular text-text-weak">Add a server</h3>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="flex-1 min-w-0 h-auto">
|
||||
<TextField
|
||||
type="text"
|
||||
label="Server URL"
|
||||
hideLabel
|
||||
placeholder="http://localhost:4096"
|
||||
value={store.url}
|
||||
onChange={(v) => {
|
||||
setStore("url", v)
|
||||
setStore("error", "")
|
||||
}}
|
||||
validationState={store.error ? "invalid" : "valid"}
|
||||
error={store.error}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" variant="secondary" icon="plus-small" size="large" disabled={store.adding}>
|
||||
{store.adding ? "Checking..." : "Add"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -134,6 +134,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
imageAttachments: ImageAttachmentPart[]
|
||||
mode: "normal" | "shell"
|
||||
applyingHistory: boolean
|
||||
killBuffer: string
|
||||
}>({
|
||||
popover: null,
|
||||
historyIndex: -1,
|
||||
@@ -143,6 +144,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
imageAttachments: [],
|
||||
mode: "normal",
|
||||
applyingHistory: false,
|
||||
killBuffer: "",
|
||||
})
|
||||
|
||||
const MAX_HISTORY = 100
|
||||
@@ -648,6 +650,77 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
setStore("popover", null)
|
||||
}
|
||||
|
||||
const setSelectionOffsets = (start: number, end: number) => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection) return false
|
||||
|
||||
const length = promptLength(prompt.current())
|
||||
const a = Math.max(0, Math.min(start, length))
|
||||
const b = Math.max(0, Math.min(end, length))
|
||||
const rangeStart = Math.min(a, b)
|
||||
const rangeEnd = Math.max(a, b)
|
||||
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(editorRef)
|
||||
|
||||
const setEdge = (edge: "start" | "end", offset: number) => {
|
||||
let remaining = offset
|
||||
const nodes = Array.from(editorRef.childNodes)
|
||||
|
||||
for (const node of nodes) {
|
||||
const length = getNodeLength(node)
|
||||
const isText = node.nodeType === Node.TEXT_NODE
|
||||
const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file"
|
||||
const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
|
||||
|
||||
if (isText && remaining <= length) {
|
||||
if (edge === "start") range.setStart(node, remaining)
|
||||
if (edge === "end") range.setEnd(node, remaining)
|
||||
return
|
||||
}
|
||||
|
||||
if ((isFile || isBreak) && remaining <= length) {
|
||||
if (edge === "start" && remaining === 0) range.setStartBefore(node)
|
||||
if (edge === "start" && remaining > 0) range.setStartAfter(node)
|
||||
if (edge === "end" && remaining === 0) range.setEndBefore(node)
|
||||
if (edge === "end" && remaining > 0) range.setEndAfter(node)
|
||||
return
|
||||
}
|
||||
|
||||
remaining -= length
|
||||
}
|
||||
|
||||
const last = editorRef.lastChild
|
||||
if (!last) {
|
||||
if (edge === "start") range.setStart(editorRef, 0)
|
||||
if (edge === "end") range.setEnd(editorRef, 0)
|
||||
return
|
||||
}
|
||||
if (edge === "start") range.setStartAfter(last)
|
||||
if (edge === "end") range.setEndAfter(last)
|
||||
}
|
||||
|
||||
setEdge("start", rangeStart)
|
||||
setEdge("end", rangeEnd)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
return true
|
||||
}
|
||||
|
||||
const replaceOffsets = (start: number, end: number, content: string) => {
|
||||
if (!setSelectionOffsets(start, end)) return false
|
||||
addPart({ type: "text", content, start: 0, end: 0 })
|
||||
return true
|
||||
}
|
||||
|
||||
const killText = (start: number, end: number) => {
|
||||
if (start === end) return
|
||||
const current = prompt.current()
|
||||
if (!current.every((part) => part.type === "text")) return
|
||||
const text = current.map((part) => part.content).join("")
|
||||
setStore("killBuffer", text.slice(start, end))
|
||||
}
|
||||
|
||||
const abort = () =>
|
||||
sdk.client.session
|
||||
.abort({
|
||||
@@ -768,6 +841,164 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
|
||||
const alt = event.altKey && !event.metaKey && !event.ctrlKey && !event.shiftKey
|
||||
|
||||
if (ctrl && event.code === "KeyG") {
|
||||
if (store.popover) {
|
||||
setStore("popover", null)
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (working()) {
|
||||
abort()
|
||||
event.preventDefault()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (ctrl || alt) {
|
||||
const { collapsed, cursorPosition, textLength } = getCaretState()
|
||||
if (collapsed) {
|
||||
const current = prompt.current()
|
||||
const text = current.map((part) => ("content" in part ? part.content : "")).join("")
|
||||
|
||||
if (ctrl) {
|
||||
if (event.code === "KeyA") {
|
||||
const pos = text.lastIndexOf("\n", cursorPosition - 1) + 1
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyE") {
|
||||
const next = text.indexOf("\n", cursorPosition)
|
||||
const pos = next === -1 ? textLength : next
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyB") {
|
||||
const pos = Math.max(0, cursorPosition - 1)
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyF") {
|
||||
const pos = Math.min(textLength, cursorPosition + 1)
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyD") {
|
||||
if (store.mode === "shell" && cursorPosition === 0 && textLength === 0) {
|
||||
setStore("mode", "normal")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (cursorPosition >= textLength) return
|
||||
replaceOffsets(cursorPosition, cursorPosition + 1, "")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyK") {
|
||||
const next = text.indexOf("\n", cursorPosition)
|
||||
const lineEnd = next === -1 ? textLength : next
|
||||
const end = lineEnd === cursorPosition && lineEnd < textLength ? lineEnd + 1 : lineEnd
|
||||
if (end === cursorPosition) return
|
||||
killText(cursorPosition, end)
|
||||
replaceOffsets(cursorPosition, end, "")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyU") {
|
||||
const start = text.lastIndexOf("\n", cursorPosition - 1) + 1
|
||||
if (start === cursorPosition) return
|
||||
killText(start, cursorPosition)
|
||||
replaceOffsets(start, cursorPosition, "")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyW") {
|
||||
let start = cursorPosition
|
||||
while (start > 0 && /\s/.test(text[start - 1])) start -= 1
|
||||
while (start > 0 && !/\s/.test(text[start - 1])) start -= 1
|
||||
if (start === cursorPosition) return
|
||||
killText(start, cursorPosition)
|
||||
replaceOffsets(start, cursorPosition, "")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyY") {
|
||||
if (!store.killBuffer) return
|
||||
addPart({ type: "text", content: store.killBuffer, start: 0, end: 0 })
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyT") {
|
||||
if (!current.every((part) => part.type === "text")) return
|
||||
if (textLength < 2) return
|
||||
if (cursorPosition === 0) return
|
||||
|
||||
const atEnd = cursorPosition === textLength
|
||||
const first = atEnd ? cursorPosition - 2 : cursorPosition - 1
|
||||
const second = atEnd ? cursorPosition - 1 : cursorPosition
|
||||
|
||||
if (text[first] === "\n" || text[second] === "\n") return
|
||||
|
||||
replaceOffsets(first, second + 1, `${text[second]}${text[first]}`)
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (alt) {
|
||||
if (event.code === "KeyB") {
|
||||
let pos = cursorPosition
|
||||
while (pos > 0 && /\s/.test(text[pos - 1])) pos -= 1
|
||||
while (pos > 0 && !/\s/.test(text[pos - 1])) pos -= 1
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyF") {
|
||||
let pos = cursorPosition
|
||||
while (pos < textLength && /\s/.test(text[pos])) pos += 1
|
||||
while (pos < textLength && !/\s/.test(text[pos])) pos += 1
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyD") {
|
||||
let end = cursorPosition
|
||||
while (end < textLength && /\s/.test(text[end])) end += 1
|
||||
while (end < textLength && !/\s/.test(text[end])) end += 1
|
||||
if (end === cursorPosition) return
|
||||
killText(cursorPosition, end)
|
||||
replaceOffsets(cursorPosition, end, "")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey) return
|
||||
const { collapsed } = getCaretState()
|
||||
@@ -884,11 +1115,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
setStore("imageAttachments", [])
|
||||
setStore("mode", "normal")
|
||||
|
||||
const model = {
|
||||
modelID: local.model.current()!.id,
|
||||
providerID: local.model.current()!.provider.id,
|
||||
const currentModel = local.model.current()
|
||||
const currentAgent = local.agent.current()
|
||||
if (!currentModel || !currentAgent) {
|
||||
console.warn("No agent or model available for prompt submission")
|
||||
return
|
||||
}
|
||||
const agent = local.agent.current()!.name
|
||||
const model = {
|
||||
modelID: currentModel.id,
|
||||
providerID: currentModel.provider.id,
|
||||
}
|
||||
const agent = currentAgent.name
|
||||
|
||||
if (isShellMode) {
|
||||
sdk.client.session
|
||||
@@ -1090,6 +1327,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
classList={{
|
||||
"select-text": true,
|
||||
"w-full px-5 py-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
||||
"[&_[data-type=file]]:text-icon-info-active": true,
|
||||
"font-mono!": store.mode === "shell",
|
||||
@@ -1128,7 +1366,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
>
|
||||
<Select
|
||||
options={local.agent.list().map((agent) => agent.name)}
|
||||
current={local.agent.current().name}
|
||||
current={local.agent.current()?.name ?? ""}
|
||||
onSelect={local.agent.set}
|
||||
class="capitalize"
|
||||
variant="ghost"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { AssistantMessage } from "@opencode-ai/sdk/v2"
|
||||
import { AssistantMessage } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
export function SessionContextUsage() {
|
||||
const sync = useSync()
|
||||
@@ -35,19 +35,13 @@ export function SessionContextUsage() {
|
||||
{(ctx) => (
|
||||
<Tooltip
|
||||
value={
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex gap-3">
|
||||
<span class="opacity-70 text-right flex-1">Tokens</span>
|
||||
<span class="text-left flex-1">{ctx().tokens}</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<span class="opacity-70 text-right flex-1">Usage</span>
|
||||
<span class="text-left flex-1">{ctx().percentage ?? 0}%</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<span class="opacity-70 text-right flex-1">Cost</span>
|
||||
<span class="text-left flex-1">{cost()}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
|
||||
<span class="opacity-70 text-right">Tokens</span>
|
||||
<span class="text-left">{ctx().tokens}</span>
|
||||
<span class="opacity-70 text-right">Usage</span>
|
||||
<span class="text-left">{ctx().percentage ?? 0}%</span>
|
||||
<span class="opacity-70 text-right">Cost</span>
|
||||
<span class="text-left">{cost()}</span>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
|
||||
@@ -24,12 +23,11 @@ export function SessionLspIndicator() {
|
||||
<Show when={lspStats().total > 0}>
|
||||
<Tooltip placement="top" value={tooltipContent()}>
|
||||
<div class="flex items-center gap-1 px-2 cursor-default select-none">
|
||||
<Icon
|
||||
name="code"
|
||||
size="small"
|
||||
<div
|
||||
classList={{
|
||||
"text-icon-critical-base": lspStats().hasError,
|
||||
"text-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
|
||||
"size-1.5 rounded-full": true,
|
||||
"bg-icon-critical-base": lspStats().hasError,
|
||||
"bg-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
|
||||
}}
|
||||
/>
|
||||
<span class="text-12-regular text-text-weak">{lspStats().connected} LSP</span>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
||||
@@ -21,12 +20,11 @@ export function SessionMcpIndicator() {
|
||||
return (
|
||||
<Show when={mcpStats().total > 0}>
|
||||
<Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}>
|
||||
<Icon
|
||||
name="mcp"
|
||||
size="small"
|
||||
<div
|
||||
classList={{
|
||||
"text-icon-critical-base": mcpStats().failed,
|
||||
"text-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0,
|
||||
"size-1.5 rounded-full": true,
|
||||
"bg-icon-critical-base": mcpStats().failed,
|
||||
"bg-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0,
|
||||
}}
|
||||
/>
|
||||
<span class="text-12-regular text-text-weak">{mcpStats().enabled} MCP</span>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { createMemo, Show, type ParentProps } from "solid-js"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
|
||||
export function StatusBar(props: ParentProps) {
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
const server = useServer()
|
||||
const sync = useSync()
|
||||
const globalSync = useGlobalSync()
|
||||
|
||||
@@ -19,9 +23,26 @@ export function StatusBar(props: ParentProps) {
|
||||
return (
|
||||
<div class="h-8 w-full shrink-0 flex items-center justify-between px-2 border-t border-border-weak-base bg-background-base">
|
||||
<div class="flex items-center gap-3">
|
||||
<Show when={platform.version}>
|
||||
<span class="text-12-regular text-text-weak">v{platform.version}</span>
|
||||
</Show>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
dialog.show(() => <DialogSelectServer />)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full": true,
|
||||
"bg-icon-success-base": server.healthy() === true,
|
||||
"bg-icon-critical-base": server.healthy() === false,
|
||||
"bg-border-weak-base": server.healthy() === undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<span class="text-12-regular text-text-weak">{server.name}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Show when={directoryDisplay()}>
|
||||
<span class="text-12-regular text-text-weak">{directoryDisplay()}</span>
|
||||
</Show>
|
||||
|
||||
@@ -233,6 +233,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
style={{ "background-color": terminalColors().background }}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
"select-text": true,
|
||||
"size-full px-6 py-3 font-mono": true,
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
|
||||
@@ -1,34 +1,41 @@
|
||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { onCleanup } from "solid-js"
|
||||
import { usePlatform } from "./platform"
|
||||
import { useServer } from "./server"
|
||||
|
||||
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
|
||||
name: "GlobalSDK",
|
||||
init: (props: { url: string }) => {
|
||||
init: () => {
|
||||
const server = useServer()
|
||||
const abort = new AbortController()
|
||||
|
||||
const eventSdk = createOpencodeClient({
|
||||
baseUrl: props.url,
|
||||
// signal: AbortSignal.timeout(1000 * 60 * 10),
|
||||
baseUrl: server.url,
|
||||
signal: abort.signal,
|
||||
})
|
||||
const emitter = createGlobalEmitter<{
|
||||
[key: string]: Event
|
||||
}>()
|
||||
|
||||
eventSdk.global.event().then(async (events) => {
|
||||
void (async () => {
|
||||
const events = await eventSdk.global.event()
|
||||
for await (const event of events.stream) {
|
||||
// console.log("event", event)
|
||||
emitter.emit(event.directory ?? "global", event.payload)
|
||||
}
|
||||
})
|
||||
})().catch(() => undefined)
|
||||
|
||||
onCleanup(() => abort.abort())
|
||||
|
||||
const platform = usePlatform()
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: props.url,
|
||||
baseUrl: server.url,
|
||||
signal: AbortSignal.timeout(1000 * 60 * 10),
|
||||
fetch: platform.fetch,
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
return { url: props.url, client: sdk, event: emitter }
|
||||
return { url: server.url, client: sdk, event: emitter }
|
||||
},
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Binary } from "@opencode-ai/util/binary"
|
||||
import { retry } from "@opencode-ai/util/retry"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { ErrorPage, type InitError } from "../pages/error"
|
||||
import { createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
|
||||
import { batch, createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
|
||||
@@ -71,21 +71,19 @@ function createGlobalSync() {
|
||||
project: Project[]
|
||||
provider: ProviderListResponse
|
||||
provider_auth: ProviderAuthResponse
|
||||
children: Record<string, State>
|
||||
}>({
|
||||
ready: false,
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
project: [],
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
provider_auth: {},
|
||||
children: {},
|
||||
})
|
||||
|
||||
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
||||
function child(directory: string) {
|
||||
if (!directory) console.error("No directory provided")
|
||||
if (!children[directory]) {
|
||||
setGlobalStore("children", directory, {
|
||||
children[directory] = createStore<State>({
|
||||
project: "",
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
@@ -105,7 +103,6 @@ function createGlobalSync() {
|
||||
message: {},
|
||||
part: {},
|
||||
})
|
||||
children[directory] = createStore(globalStore.children[directory])
|
||||
bootstrapInstance(directory)
|
||||
}
|
||||
return children[directory]
|
||||
@@ -127,7 +124,7 @@ function createGlobalSync() {
|
||||
const updated = new Date(s.time.updated).getTime()
|
||||
return updated > fourHoursAgo
|
||||
})
|
||||
setStore("session", sessions)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load sessions", err)
|
||||
@@ -138,7 +135,7 @@ function createGlobalSync() {
|
||||
|
||||
async function bootstrapInstance(directory: string) {
|
||||
if (!directory) return
|
||||
const [, setStore] = child(directory)
|
||||
const [store, setStore] = child(directory)
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
directory,
|
||||
@@ -170,12 +167,32 @@ function createGlobalSync() {
|
||||
vcs: () => sdk.vcs.get().then((x) => setStore("vcs", x.data)),
|
||||
permission: () =>
|
||||
sdk.permission.list().then((x) => {
|
||||
const grouped: Record<string, typeof x.data> = {}
|
||||
const grouped: Record<string, Permission[]> = {}
|
||||
for (const perm of x.data ?? []) {
|
||||
grouped[perm.sessionID] = grouped[perm.sessionID] ?? []
|
||||
grouped[perm.sessionID]!.push(perm)
|
||||
const existing = grouped[perm.sessionID]
|
||||
if (existing) {
|
||||
existing.push(perm)
|
||||
continue
|
||||
}
|
||||
grouped[perm.sessionID] = [perm]
|
||||
}
|
||||
setStore("permission", grouped)
|
||||
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(store.permission)) {
|
||||
if (grouped[sessionID]) continue
|
||||
setStore("permission", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||
setStore(
|
||||
"permission",
|
||||
sessionID,
|
||||
reconcile(
|
||||
permissions.slice().sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
|
||||
@@ -246,7 +263,7 @@ function createGlobalSync() {
|
||||
setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
|
||||
break
|
||||
case "todo.updated":
|
||||
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos))
|
||||
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos, { key: "id" }))
|
||||
break
|
||||
case "session.status": {
|
||||
setStore("session_status", event.properties.sessionID, reconcile(event.properties.status))
|
||||
@@ -328,23 +345,26 @@ function createGlobalSync() {
|
||||
break
|
||||
}
|
||||
case "permission.updated": {
|
||||
const permissions = store.permission[event.properties.sessionID]
|
||||
const sessionID = event.properties.sessionID
|
||||
const permissions = store.permission[sessionID]
|
||||
if (!permissions) {
|
||||
setStore("permission", event.properties.sessionID, [event.properties])
|
||||
} else {
|
||||
const result = Binary.search(permissions, event.properties.id, (p) => p.id)
|
||||
setStore(
|
||||
"permission",
|
||||
event.properties.sessionID,
|
||||
produce((draft) => {
|
||||
if (result.found) {
|
||||
draft[result.index] = event.properties
|
||||
return
|
||||
}
|
||||
draft.push(event.properties)
|
||||
}),
|
||||
)
|
||||
setStore("permission", sessionID, [event.properties])
|
||||
break
|
||||
}
|
||||
|
||||
const result = Binary.search(permissions, event.properties.id, (p) => p.id)
|
||||
if (result.found) {
|
||||
setStore("permission", sessionID, result.index, reconcile(event.properties))
|
||||
break
|
||||
}
|
||||
|
||||
setStore(
|
||||
"permission",
|
||||
sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "permission.replied": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { batch, createMemo, onMount } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { useServer } from "./server"
|
||||
import { Project } from "@opencode-ai/sdk/v2"
|
||||
import { persisted } from "@/utils/persist"
|
||||
|
||||
@@ -34,10 +35,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
init: () => {
|
||||
const globalSdk = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const server = useServer()
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
"layout.v3",
|
||||
"layout.v4",
|
||||
createStore({
|
||||
projects: [] as { worktree: string; expanded: boolean }[],
|
||||
sidebar: {
|
||||
opened: false,
|
||||
width: 280,
|
||||
@@ -70,6 +71,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
{
|
||||
...project,
|
||||
...(metadata ?? {}),
|
||||
icon: { url: metadata?.icon?.url, color: metadata?.icon?.color },
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -85,12 +87,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
return project
|
||||
}
|
||||
|
||||
const enriched = createMemo(() => store.projects.flatMap(enrich))
|
||||
const enriched = createMemo(() => server.projects.list().flatMap(enrich))
|
||||
const list = createMemo(() => enriched().flatMap(colorize))
|
||||
|
||||
onMount(() => {
|
||||
Promise.all(
|
||||
store.projects.map((project) => {
|
||||
server.projects.list().map((project) => {
|
||||
return globalSync.project.loadSessions(project.worktree)
|
||||
}),
|
||||
)
|
||||
@@ -101,32 +103,23 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
projects: {
|
||||
list,
|
||||
open(directory: string) {
|
||||
if (store.projects.find((x) => x.worktree === directory)) {
|
||||
if (server.projects.list().find((x) => x.worktree === directory)) {
|
||||
return
|
||||
}
|
||||
globalSync.project.loadSessions(directory)
|
||||
setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
|
||||
server.projects.open(directory)
|
||||
},
|
||||
close(directory: string) {
|
||||
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
|
||||
server.projects.close(directory)
|
||||
},
|
||||
expand(directory: string) {
|
||||
const index = store.projects.findIndex((x) => x.worktree === directory)
|
||||
if (index !== -1) setStore("projects", index, "expanded", true)
|
||||
server.projects.expand(directory)
|
||||
},
|
||||
collapse(directory: string) {
|
||||
const index = store.projects.findIndex((x) => x.worktree === directory)
|
||||
if (index !== -1) setStore("projects", index, "expanded", false)
|
||||
server.projects.collapse(directory)
|
||||
},
|
||||
move(directory: string, toIndex: number) {
|
||||
setStore("projects", (projects) => {
|
||||
const fromIndex = projects.findIndex((x) => x.worktree === directory)
|
||||
if (fromIndex === -1 || fromIndex === toIndex) return projects
|
||||
const result = [...projects]
|
||||
const [item] = result.splice(fromIndex, 1)
|
||||
result.splice(toIndex, 0, item)
|
||||
return result
|
||||
})
|
||||
server.projects.move(directory, toIndex)
|
||||
},
|
||||
},
|
||||
sidebar: {
|
||||
|
||||
@@ -65,23 +65,40 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const agent = (() => {
|
||||
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
|
||||
const [store, setStore] = createStore<{
|
||||
current: string
|
||||
current?: string
|
||||
}>({
|
||||
current: list()[0].name,
|
||||
current: list()[0]?.name,
|
||||
})
|
||||
return {
|
||||
list,
|
||||
current() {
|
||||
return list().find((x) => x.name === store.current)!
|
||||
const available = list()
|
||||
if (available.length === 0) return undefined
|
||||
return available.find((x) => x.name === store.current) ?? available[0]
|
||||
},
|
||||
set(name: string | undefined) {
|
||||
setStore("current", name ?? list()[0].name)
|
||||
const available = list()
|
||||
if (available.length === 0) {
|
||||
setStore("current", undefined)
|
||||
return
|
||||
}
|
||||
if (name && available.some((x) => x.name === name)) {
|
||||
setStore("current", name)
|
||||
return
|
||||
}
|
||||
setStore("current", available[0].name)
|
||||
},
|
||||
move(direction: 1 | -1) {
|
||||
let next = list().findIndex((x) => x.name === store.current) + direction
|
||||
if (next < 0) next = list().length - 1
|
||||
if (next >= list().length) next = 0
|
||||
const value = list()[next]
|
||||
const available = list()
|
||||
if (available.length === 0) {
|
||||
setStore("current", undefined)
|
||||
return
|
||||
}
|
||||
let next = available.findIndex((x) => x.name === store.current) + direction
|
||||
if (next < 0) next = available.length - 1
|
||||
if (next >= available.length) next = 0
|
||||
const value = available[next]
|
||||
if (!value) return
|
||||
setStore("current", value.name)
|
||||
if (value.model)
|
||||
model.set({
|
||||
@@ -182,11 +199,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const current = createMemo(() => {
|
||||
const a = agent.current()
|
||||
if (!a) return undefined
|
||||
const key = getFirstValidModel(
|
||||
() => ephemeral.model[a.name],
|
||||
() => a.model,
|
||||
fallbackModel,
|
||||
)!
|
||||
)
|
||||
if (!key) return undefined
|
||||
return find(key)
|
||||
})
|
||||
|
||||
@@ -232,7 +251,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
cycle,
|
||||
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
|
||||
batch(() => {
|
||||
setEphemeral("model", agent.current().name, model ?? fallbackModel())
|
||||
const currentAgent = agent.current()
|
||||
if (currentAgent) setEphemeral("model", currentAgent.name, model ?? fallbackModel())
|
||||
if (model) updateVisibility(model, "show")
|
||||
if (options?.recent && model) {
|
||||
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
|
||||
|
||||
@@ -2,7 +2,9 @@ import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||
import { makeAudioPlayer } from "@solid-primitives/audio"
|
||||
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
|
||||
@@ -43,6 +45,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const platform = usePlatform()
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
"notification.v1",
|
||||
@@ -64,8 +67,8 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
const sessionID = event.properties.sessionID
|
||||
const [syncStore] = globalSync.child(directory)
|
||||
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
||||
const isChild = match.found && syncStore.session[match.index].parentID
|
||||
if (isChild) break
|
||||
const session = match.found ? syncStore.session[match.index] : undefined
|
||||
if (session?.parentID) break
|
||||
try {
|
||||
idlePlayer?.play()
|
||||
} catch {}
|
||||
@@ -74,25 +77,29 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
type: "turn-complete",
|
||||
session: sessionID,
|
||||
})
|
||||
const href = `/${base64Encode(directory)}/session/${sessionID}`
|
||||
void platform.notify("Response ready", session?.title ?? sessionID, href)
|
||||
break
|
||||
}
|
||||
case "session.error": {
|
||||
const sessionID = event.properties.sessionID
|
||||
if (sessionID) {
|
||||
const [syncStore] = globalSync.child(directory)
|
||||
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
||||
const isChild = match.found && syncStore.session[match.index].parentID
|
||||
if (isChild) break
|
||||
}
|
||||
const [syncStore] = globalSync.child(directory)
|
||||
const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined
|
||||
const session = sessionID && match?.found ? syncStore.session[match.index] : undefined
|
||||
if (session?.parentID) break
|
||||
try {
|
||||
errorPlayer?.play()
|
||||
} catch {}
|
||||
const error = "error" in event.properties ? event.properties.error : undefined
|
||||
setStore("list", store.list.length, {
|
||||
...base,
|
||||
type: "error",
|
||||
session: sessionID ?? "global",
|
||||
error: "error" in event.properties ? event.properties.error : undefined,
|
||||
error,
|
||||
})
|
||||
const description = session?.title ?? (typeof error === "string" ? error : "An error occurred")
|
||||
const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}`
|
||||
void platform.notify("Session error", description, href)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { createEffect, createRoot, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import type { Permission } from "@opencode-ai/sdk/v2/client"
|
||||
import { persisted } from "@/utils/persist"
|
||||
|
||||
type PermissionsBySession = {
|
||||
[sessionID: string]: Permission[]
|
||||
}
|
||||
|
||||
type PermissionRespondFn = (input: {
|
||||
sessionID: string
|
||||
permissionID: string
|
||||
response: "once" | "always" | "reject"
|
||||
}) => void
|
||||
|
||||
const AUTO_ACCEPT_TYPES = new Set(["edit", "write"])
|
||||
|
||||
function shouldAutoAccept(perm: Permission) {
|
||||
return AUTO_ACCEPT_TYPES.has(perm.type)
|
||||
}
|
||||
|
||||
export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
|
||||
name: "Permission",
|
||||
init: (props: { permissions: PermissionsBySession; onRespond: PermissionRespondFn }) => {
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
"permission.v1",
|
||||
createStore({
|
||||
autoAcceptEdits: {} as Record<string, boolean>,
|
||||
}),
|
||||
)
|
||||
|
||||
const responded = new Set<string>()
|
||||
const watches = new Map<string, () => void>()
|
||||
|
||||
function respond(perm: Permission) {
|
||||
if (responded.has(perm.id)) return
|
||||
responded.add(perm.id)
|
||||
props.onRespond({
|
||||
sessionID: perm.sessionID,
|
||||
permissionID: perm.id,
|
||||
response: "once",
|
||||
})
|
||||
}
|
||||
|
||||
function watch(sessionID: string) {
|
||||
if (watches.has(sessionID)) return
|
||||
|
||||
const dispose = createRoot((dispose) => {
|
||||
createEffect(() => {
|
||||
if (!store.autoAcceptEdits[sessionID]) return
|
||||
|
||||
const permissions = props.permissions[sessionID] ?? []
|
||||
permissions.length
|
||||
|
||||
for (const perm of permissions) {
|
||||
if (!shouldAutoAccept(perm)) continue
|
||||
respond(perm)
|
||||
}
|
||||
})
|
||||
|
||||
return dispose
|
||||
})
|
||||
|
||||
watches.set(sessionID, dispose)
|
||||
}
|
||||
|
||||
function unwatch(sessionID: string) {
|
||||
const dispose = watches.get(sessionID)
|
||||
if (!dispose) return
|
||||
dispose()
|
||||
watches.delete(sessionID)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready()) return
|
||||
|
||||
for (const sessionID in store.autoAcceptEdits) {
|
||||
if (!store.autoAcceptEdits[sessionID]) continue
|
||||
watch(sessionID)
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
for (const dispose of watches.values()) dispose()
|
||||
watches.clear()
|
||||
})
|
||||
|
||||
function enable(sessionID: string) {
|
||||
setStore("autoAcceptEdits", sessionID, true)
|
||||
watch(sessionID)
|
||||
|
||||
const permissions = props.permissions[sessionID] ?? []
|
||||
for (const perm of permissions) {
|
||||
if (!shouldAutoAccept(perm)) continue
|
||||
respond(perm)
|
||||
}
|
||||
}
|
||||
|
||||
function disable(sessionID: string) {
|
||||
setStore("autoAcceptEdits", sessionID, false)
|
||||
unwatch(sessionID)
|
||||
}
|
||||
|
||||
return {
|
||||
get permissions() {
|
||||
return props.permissions
|
||||
},
|
||||
respond: props.onRespond,
|
||||
isAutoAccepting(sessionID: string) {
|
||||
return store.autoAcceptEdits[sessionID] ?? false
|
||||
},
|
||||
toggleAutoAccept(sessionID: string) {
|
||||
if (store.autoAcceptEdits[sessionID]) {
|
||||
disable(sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
enable(sessionID)
|
||||
},
|
||||
enableAutoAccept(sessionID: string) {
|
||||
if (store.autoAcceptEdits[sessionID]) return
|
||||
enable(sessionID)
|
||||
},
|
||||
disableAutoAccept(sessionID: string) {
|
||||
disable(sessionID)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -14,7 +14,10 @@ export type Platform = {
|
||||
/** Restart the app */
|
||||
restart(): Promise<void>
|
||||
|
||||
/** Open native directory picker dialog (Tauri only) */
|
||||
/** Send a system notification (optional deep link) */
|
||||
notify(title: string, description?: string, href?: string): Promise<void>
|
||||
|
||||
/** Open directory picker dialog (native on Tauri, server-backed on web) */
|
||||
openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise<string | string[] | null>
|
||||
|
||||
/** Open native file picker dialog (Tauri only) */
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { batch, createEffect, createMemo, createResource, createSignal, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { persisted } from "@/utils/persist"
|
||||
|
||||
type StoredProject = { worktree: string; expanded: boolean }
|
||||
|
||||
export function normalizeServerUrl(input: string) {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) return
|
||||
const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`
|
||||
const cleaned = withProtocol.replace(/\/+$/, "")
|
||||
return cleaned.replace(/^(https?:\/\/[^/]+).*/, "$1")
|
||||
}
|
||||
|
||||
export function serverDisplayName(url: string) {
|
||||
if (!url) return ""
|
||||
return url
|
||||
.replace(/^https?:\/\//, "")
|
||||
.replace(/\/+$/, "")
|
||||
.split("/")[0]
|
||||
}
|
||||
|
||||
function projectsKey(url: string) {
|
||||
if (!url) return ""
|
||||
const host = url.replace(/^https?:\/\//, "").split(":")[0]
|
||||
if (host === "localhost" || host === "127.0.0.1") return "local"
|
||||
return url
|
||||
}
|
||||
|
||||
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
|
||||
name: "Server",
|
||||
init: (props: { defaultUrl: string }) => {
|
||||
const platform = usePlatform()
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
"server.v3",
|
||||
createStore({
|
||||
list: [] as string[],
|
||||
projects: {} as Record<string, StoredProject[]>,
|
||||
}),
|
||||
)
|
||||
|
||||
const [active, setActiveRaw] = createSignal("")
|
||||
|
||||
function setActive(input: string) {
|
||||
const url = normalizeServerUrl(input)
|
||||
if (!url) return
|
||||
setActiveRaw(url)
|
||||
}
|
||||
|
||||
function add(input: string) {
|
||||
const url = normalizeServerUrl(input)
|
||||
if (!url) return
|
||||
|
||||
const fallback = normalizeServerUrl(props.defaultUrl)
|
||||
if (fallback && url === fallback) {
|
||||
setActiveRaw(url)
|
||||
return
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
if (!store.list.includes(url)) {
|
||||
setStore("list", store.list.length, url)
|
||||
}
|
||||
setActiveRaw(url)
|
||||
})
|
||||
}
|
||||
|
||||
function remove(input: string) {
|
||||
const url = normalizeServerUrl(input)
|
||||
if (!url) return
|
||||
|
||||
const list = store.list.filter((x) => x !== url)
|
||||
const next = active() === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : active()
|
||||
|
||||
batch(() => {
|
||||
setStore("list", list)
|
||||
setActiveRaw(next)
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready()) return
|
||||
if (active()) return
|
||||
const url = normalizeServerUrl(props.defaultUrl)
|
||||
if (!url) return
|
||||
setActiveRaw(url)
|
||||
})
|
||||
|
||||
const isReady = createMemo(() => ready() && !!active())
|
||||
|
||||
const [healthy, { refetch }] = createResource(
|
||||
() => active() || undefined,
|
||||
async (url) => {
|
||||
if (!url) return
|
||||
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: url,
|
||||
fetch: platform.fetch,
|
||||
signal: AbortSignal.timeout(2000),
|
||||
})
|
||||
return sdk.global
|
||||
.health()
|
||||
.then((x) => x.data?.healthy === true)
|
||||
.catch(() => false)
|
||||
},
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (!active()) return
|
||||
const interval = setInterval(() => refetch(), 10_000)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
|
||||
const origin = createMemo(() => projectsKey(active()))
|
||||
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
||||
|
||||
return {
|
||||
ready: isReady,
|
||||
healthy,
|
||||
get url() {
|
||||
return active()
|
||||
},
|
||||
get name() {
|
||||
return serverDisplayName(active())
|
||||
},
|
||||
get list() {
|
||||
return store.list
|
||||
},
|
||||
setActive,
|
||||
add,
|
||||
remove,
|
||||
projects: {
|
||||
list: projectsList,
|
||||
open(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const current = store.projects[key] ?? []
|
||||
if (current.find((x) => x.worktree === directory)) return
|
||||
setStore("projects", key, [{ worktree: directory, expanded: true }, ...current])
|
||||
},
|
||||
close(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const current = store.projects[key] ?? []
|
||||
setStore(
|
||||
"projects",
|
||||
key,
|
||||
current.filter((x) => x.worktree !== directory),
|
||||
)
|
||||
},
|
||||
expand(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const current = store.projects[key] ?? []
|
||||
const index = current.findIndex((x) => x.worktree === directory)
|
||||
if (index !== -1) setStore("projects", key, index, "expanded", true)
|
||||
},
|
||||
collapse(directory: string) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const current = store.projects[key] ?? []
|
||||
const index = current.findIndex((x) => x.worktree === directory)
|
||||
if (index !== -1) setStore("projects", key, index, "expanded", false)
|
||||
},
|
||||
move(directory: string, toIndex: number) {
|
||||
const key = origin()
|
||||
if (!key) return
|
||||
const current = store.projects[key] ?? []
|
||||
const fromIndex = current.findIndex((x) => x.worktree === directory)
|
||||
if (fromIndex === -1 || fromIndex === toIndex) return
|
||||
const result = [...current]
|
||||
const [item] = result.splice(fromIndex, 1)
|
||||
result.splice(toIndex, 0, item)
|
||||
setStore("projects", key, result)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { produce } from "solid-js/store"
|
||||
import { createMemo } from "solid-js"
|
||||
import { batch, createMemo } from "solid-js"
|
||||
import { produce, reconcile } from "solid-js/store"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { retry } from "@opencode-ai/util/retry"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
@@ -56,7 +56,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
||||
messages.splice(result.index, 0, message)
|
||||
}
|
||||
draft.part[input.messageID] = input.parts.slice()
|
||||
draft.part[input.messageID] = input.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
|
||||
}),
|
||||
)
|
||||
},
|
||||
@@ -67,22 +67,46 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
retry(() => sdk.client.session.todo({ sessionID })),
|
||||
retry(() => sdk.client.session.diff({ sessionID })),
|
||||
])
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft.session, sessionID, (s) => s.id)
|
||||
if (match.found) draft.session[match.index] = session.data!
|
||||
if (!match.found) draft.session.splice(match.index, 0, session.data!)
|
||||
draft.todo[sessionID] = todo.data ?? []
|
||||
draft.message[sessionID] = messages
|
||||
.data!.map((x) => x.info)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
for (const message of messages.data!) {
|
||||
draft.part[message.info.id] = message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
|
||||
}
|
||||
draft.session_diff[sessionID] = diff.data ?? []
|
||||
}),
|
||||
)
|
||||
|
||||
batch(() => {
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft, sessionID, (s) => s.id)
|
||||
if (match.found) {
|
||||
draft[match.index] = session.data!
|
||||
return
|
||||
}
|
||||
draft.splice(match.index, 0, session.data!)
|
||||
}),
|
||||
)
|
||||
|
||||
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
|
||||
setStore(
|
||||
"message",
|
||||
sessionID,
|
||||
reconcile(
|
||||
(messages.data ?? [])
|
||||
.map((x) => x.info)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
|
||||
for (const message of messages.data ?? []) {
|
||||
setStore(
|
||||
"part",
|
||||
message.info.id,
|
||||
reconcile(
|
||||
message.parts.slice().sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
|
||||
})
|
||||
},
|
||||
fetch: async (count = 10) => {
|
||||
setStore("limit", (x) => x + count)
|
||||
@@ -91,7 +115,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.slice(0, store.limit)
|
||||
setStore("session", sessions)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
})
|
||||
},
|
||||
more: createMemo(() => store.session.length >= store.limit),
|
||||
|
||||
@@ -20,6 +20,36 @@ const platform: Platform = {
|
||||
restart: async () => {
|
||||
window.location.reload()
|
||||
},
|
||||
notify: async (title, description, href) => {
|
||||
if (!("Notification" in window)) return
|
||||
|
||||
const permission =
|
||||
Notification.permission === "default"
|
||||
? await Notification.requestPermission().catch(() => "denied")
|
||||
: Notification.permission
|
||||
|
||||
if (permission !== "granted") return
|
||||
|
||||
const inView = document.visibilityState === "visible" && document.hasFocus()
|
||||
if (inView) return
|
||||
|
||||
await Promise.resolve()
|
||||
.then(() => {
|
||||
const notification = new Notification(title, {
|
||||
body: description ?? "",
|
||||
icon: "https://opencode.ai/favicon-96x96.png",
|
||||
})
|
||||
notification.onclick = () => {
|
||||
window.focus()
|
||||
if (href) {
|
||||
window.history.pushState(null, "", href)
|
||||
window.dispatchEvent(new PopStateEvent("popstate"))
|
||||
}
|
||||
notification.close()
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
},
|
||||
}
|
||||
|
||||
render(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useParams } from "@solidjs/router"
|
||||
import { SDKProvider, useSDK } from "@/context/sdk"
|
||||
import { SyncProvider, useSync } from "@/context/sync"
|
||||
import { LocalProvider } from "@/context/local"
|
||||
import { PermissionProvider } from "@/context/permission"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
@@ -19,16 +20,18 @@ export default function Layout(props: ParentProps) {
|
||||
{iife(() => {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const respond = (input: {
|
||||
sessionID: string
|
||||
permissionID: string
|
||||
response: "once" | "always" | "reject"
|
||||
}) => sdk.client.permission.respond(input)
|
||||
|
||||
return (
|
||||
<DataProvider
|
||||
data={sync.data}
|
||||
directory={directory()}
|
||||
onPermissionRespond={(input) => {
|
||||
sdk.client.permission.respond(input)
|
||||
}}
|
||||
>
|
||||
<LocalProvider>{props.children}</LocalProvider>
|
||||
</DataProvider>
|
||||
<PermissionProvider permissions={sync.data.permission} onRespond={respond}>
|
||||
<DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
|
||||
<LocalProvider>{props.children}</LocalProvider>
|
||||
</DataProvider>
|
||||
</PermissionProvider>
|
||||
)
|
||||
})}
|
||||
</SyncProvider>
|
||||
|
||||
@@ -20,11 +20,51 @@ function isInitError(error: unknown): error is InitError {
|
||||
)
|
||||
}
|
||||
|
||||
function safeJson(value: unknown): string {
|
||||
const seen = new WeakSet<object>()
|
||||
const json = JSON.stringify(
|
||||
value,
|
||||
(_key, val) => {
|
||||
if (typeof val === "bigint") return val.toString()
|
||||
if (typeof val === "object" && val) {
|
||||
if (seen.has(val)) return "[Circular]"
|
||||
seen.add(val)
|
||||
}
|
||||
return val
|
||||
},
|
||||
2,
|
||||
)
|
||||
return json ?? String(value)
|
||||
}
|
||||
|
||||
function formatInitError(error: InitError): string {
|
||||
const data = error.data
|
||||
switch (error.name) {
|
||||
case "MCPFailed":
|
||||
return `MCP server "${data.name}" failed. Note, opencode does not support MCP authentication yet.`
|
||||
case "ProviderAuthError": {
|
||||
const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
|
||||
const message = typeof data.message === "string" ? data.message : safeJson(data.message)
|
||||
return `Provider authentication failed (${providerID}): ${message}`
|
||||
}
|
||||
case "APIError": {
|
||||
const message = typeof data.message === "string" ? data.message : "API error"
|
||||
const lines: string[] = [message]
|
||||
|
||||
if (typeof data.statusCode === "number") {
|
||||
lines.push(`Status: ${data.statusCode}`)
|
||||
}
|
||||
|
||||
if (typeof data.isRetryable === "boolean") {
|
||||
lines.push(`Retryable: ${data.isRetryable}`)
|
||||
}
|
||||
|
||||
if (typeof data.responseBody === "string" && data.responseBody) {
|
||||
lines.push(`Response body:\n${data.responseBody}`)
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
case "ProviderModelNotFoundError": {
|
||||
const { providerID, modelID, suggestions } = data as {
|
||||
providerID: string
|
||||
@@ -37,10 +77,14 @@ function formatInitError(error: InitError): string {
|
||||
`Check your config (opencode.json) provider/model names`,
|
||||
].join("\n")
|
||||
}
|
||||
case "ProviderInitError":
|
||||
return `Failed to initialize provider "${data.providerID}". Check credentials and configuration.`
|
||||
case "ConfigJsonError":
|
||||
return `Config file at ${data.path} is not valid JSON(C)` + (data.message ? `: ${data.message}` : "")
|
||||
case "ProviderInitError": {
|
||||
const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
|
||||
return `Failed to initialize provider "${providerID}". Check credentials and configuration.`
|
||||
}
|
||||
case "ConfigJsonError": {
|
||||
const message = typeof data.message === "string" ? data.message : ""
|
||||
return `Config file at ${data.path} is not valid JSON(C)` + (message ? `: ${message}` : "")
|
||||
}
|
||||
case "ConfigDirectoryTypoError":
|
||||
return `Directory "${data.dir}" in ${data.path} is not valid. Rename the directory to "${data.suggestion}" or remove it. This is a common typo.`
|
||||
case "ConfigFrontmatterError":
|
||||
@@ -51,14 +95,14 @@ function formatInitError(error: InitError): string {
|
||||
(issue: { message: string; path: string[] }) => "↳ " + issue.message + " " + issue.path.join("."),
|
||||
)
|
||||
: []
|
||||
return [`Config file at ${data.path} is invalid` + (data.message ? `: ${data.message}` : ""), ...issues].join(
|
||||
"\n",
|
||||
)
|
||||
const message = typeof data.message === "string" ? data.message : ""
|
||||
return [`Config file at ${data.path} is invalid` + (message ? `: ${message}` : ""), ...issues].join("\n")
|
||||
}
|
||||
case "UnknownError":
|
||||
return String(data.message)
|
||||
return typeof data.message === "string" ? data.message : safeJson(data)
|
||||
default:
|
||||
return data.message ? String(data.message) : JSON.stringify(data, null, 2)
|
||||
if (typeof data.message === "string") return data.message
|
||||
return safeJson(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +113,7 @@ function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): st
|
||||
const message = formatInitError(error)
|
||||
if (depth > 0 && parentMessage === message) return ""
|
||||
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
||||
return indent + message
|
||||
return indent + `${error.name}\n${message}`
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
@@ -77,15 +121,34 @@ function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): st
|
||||
const parts: string[] = []
|
||||
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
||||
|
||||
if (!isDuplicate) {
|
||||
// Stack already includes error name and message, so prefer it
|
||||
parts.push(indent + (error.stack ?? `${error.name}: ${error.message}`))
|
||||
} else if (error.stack) {
|
||||
// Duplicate message - only show the stack trace lines (skip message)
|
||||
const trace = error.stack.split("\n").slice(1).join("\n").trim()
|
||||
if (trace) {
|
||||
parts.push(trace)
|
||||
const header = `${error.name}${error.message ? `: ${error.message}` : ""}`
|
||||
const stack = error.stack?.trim()
|
||||
|
||||
if (stack) {
|
||||
const startsWithHeader = stack.startsWith(header)
|
||||
|
||||
if (isDuplicate && startsWithHeader) {
|
||||
const trace = stack.split("\n").slice(1).join("\n").trim()
|
||||
if (trace) {
|
||||
parts.push(indent + trace)
|
||||
}
|
||||
}
|
||||
|
||||
if (isDuplicate && !startsWithHeader) {
|
||||
parts.push(indent + stack)
|
||||
}
|
||||
|
||||
if (!isDuplicate && startsWithHeader) {
|
||||
parts.push(indent + stack)
|
||||
}
|
||||
|
||||
if (!isDuplicate && !startsWithHeader) {
|
||||
parts.push(indent + `${header}\n${stack}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!stack && !isDuplicate) {
|
||||
parts.push(indent + header)
|
||||
}
|
||||
|
||||
if (error.cause) {
|
||||
@@ -105,7 +168,7 @@ function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): st
|
||||
}
|
||||
|
||||
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
||||
return indent + JSON.stringify(error, null, 2)
|
||||
return indent + safeJson(error)
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
@@ -131,7 +194,7 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
||||
readOnly
|
||||
copyable
|
||||
multiline
|
||||
class="max-h-96 w-full font-mono text-xs no-scrollbar whitespace-pre"
|
||||
class="max-h-96 w-full font-mono text-xs no-scrollbar"
|
||||
label="Error Details"
|
||||
hideLabel
|
||||
/>
|
||||
|
||||
@@ -8,12 +8,18 @@ import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { DateTime } from "luxon"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { useServer } from "@/context/server"
|
||||
|
||||
export default function Home() {
|
||||
const sync = useGlobalSync()
|
||||
const layout = useLayout()
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
const navigate = useNavigate()
|
||||
const server = useServer()
|
||||
const homedir = createMemo(() => sync.data.path.home)
|
||||
|
||||
function openProject(directory: string) {
|
||||
@@ -22,32 +28,57 @@ export default function Home() {
|
||||
}
|
||||
|
||||
async function chooseProject() {
|
||||
const result = await platform.openDirectoryPickerDialog?.({
|
||||
title: "Open project",
|
||||
multiple: true,
|
||||
})
|
||||
if (Array.isArray(result)) {
|
||||
for (const directory of result) {
|
||||
openProject(directory)
|
||||
function resolve(result: string | string[] | null) {
|
||||
if (Array.isArray(result)) {
|
||||
for (const directory of result) {
|
||||
openProject(directory)
|
||||
}
|
||||
} else if (result) {
|
||||
openProject(result)
|
||||
}
|
||||
} else if (result) {
|
||||
openProject(result)
|
||||
}
|
||||
|
||||
if (platform.openDirectoryPickerDialog) {
|
||||
const result = await platform.openDirectoryPickerDialog?.({
|
||||
title: "Open project",
|
||||
multiple: true,
|
||||
})
|
||||
resolve(result)
|
||||
} else {
|
||||
dialog.show(
|
||||
() => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
|
||||
() => resolve(null),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="mx-auto mt-55">
|
||||
<Logo class="w-xl opacity-12" />
|
||||
<Button
|
||||
size="large"
|
||||
variant="ghost"
|
||||
class="mt-4 mx-auto text-14-regular text-text-weak"
|
||||
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"size-2 rounded-full": true,
|
||||
"bg-icon-success-base": server.healthy() === true,
|
||||
"bg-icon-critical-base": server.healthy() === false,
|
||||
"bg-border-weak-base": server.healthy() === undefined,
|
||||
}}
|
||||
/>
|
||||
{server.name}
|
||||
</Button>
|
||||
<Switch>
|
||||
<Match when={sync.data.project.length > 0}>
|
||||
<div class="mt-20 w-full flex flex-col gap-4">
|
||||
<div class="flex gap-2 items-center justify-between pl-3">
|
||||
<div class="text-14-medium text-text-strong">Recent projects</div>
|
||||
<Show when={platform.openDirectoryPickerDialog}>
|
||||
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
|
||||
Open project
|
||||
</Button>
|
||||
</Show>
|
||||
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
|
||||
Open project
|
||||
</Button>
|
||||
</div>
|
||||
<ul class="flex flex-col gap-2">
|
||||
<For
|
||||
@@ -80,11 +111,9 @@ export default function Home() {
|
||||
<div class="text-12-regular text-text-weak">Get started by opening a local project</div>
|
||||
</div>
|
||||
<div />
|
||||
<Show when={platform.openDirectoryPickerDialog}>
|
||||
<Button class="px-3" onClick={chooseProject}>
|
||||
Open project
|
||||
</Button>
|
||||
</Show>
|
||||
<Button class="px-3" onClick={chooseProject}>
|
||||
Open project
|
||||
</Button>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
@@ -49,8 +49,11 @@ import { Header } from "@/components/header"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
import { DialogSelectProvider } from "@/components/dialog-select-provider"
|
||||
import { DialogEditProject } from "@/components/dialog-edit-project"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const [store, setStore] = createStore({
|
||||
@@ -160,27 +163,33 @@ export default function Layout(props: ParentProps) {
|
||||
if (e.details?.type !== "permission.updated") return
|
||||
const directory = e.name
|
||||
const permission = e.details.properties
|
||||
const currentDir = params.dir ? base64Decode(params.dir) : undefined
|
||||
const currentSession = params.id
|
||||
const [store] = globalSync.child(directory)
|
||||
const session = store.session.find((s) => s.id === permission.sessionID)
|
||||
const sessionTitle = session?.title ?? "New session"
|
||||
const projectName = getFilename(directory)
|
||||
const description = `${sessionTitle} in ${projectName} needs permission`
|
||||
const href = `/${base64Encode(directory)}/session/${permission.sessionID}`
|
||||
void platform.notify("Permission required", description, href)
|
||||
|
||||
if (directory === currentDir && permission.sessionID === currentSession) return
|
||||
if (directory === currentDir && session?.parentID === currentSession) return
|
||||
|
||||
const sessionKey = `${directory}:${permission.sessionID}`
|
||||
if (seenSessions.has(sessionKey)) return
|
||||
seenSessions.add(sessionKey)
|
||||
const currentDir = params.dir ? base64Decode(params.dir) : undefined
|
||||
const currentSession = params.id
|
||||
if (directory === currentDir && permission.sessionID === currentSession) return
|
||||
const [store] = globalSync.child(directory)
|
||||
const session = store.session.find((s) => s.id === permission.sessionID)
|
||||
if (directory === currentDir && session?.parentID === currentSession) return
|
||||
const sessionTitle = session?.title ?? "New session"
|
||||
const projectName = getFilename(directory)
|
||||
|
||||
const toastId = showToast({
|
||||
persistent: true,
|
||||
icon: "checklist",
|
||||
title: "Permission required",
|
||||
description: `${sessionTitle} in ${projectName} needs permission`,
|
||||
description,
|
||||
actions: [
|
||||
{
|
||||
label: "Go to session",
|
||||
onClick: () => {
|
||||
navigate(`/${base64Encode(directory)}/session/${permission.sessionID}`)
|
||||
navigate(href)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -331,23 +340,25 @@ export default function Layout(props: ParentProps) {
|
||||
keybind: "mod+b",
|
||||
onSelect: () => layout.sidebar.toggle(),
|
||||
},
|
||||
...(platform.openDirectoryPickerDialog
|
||||
? [
|
||||
{
|
||||
id: "project.open",
|
||||
title: "Open project",
|
||||
category: "Project",
|
||||
keybind: "mod+o",
|
||||
onSelect: () => chooseProject(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "project.open",
|
||||
title: "Open project",
|
||||
category: "Project",
|
||||
keybind: "mod+o",
|
||||
onSelect: () => chooseProject(),
|
||||
},
|
||||
{
|
||||
id: "provider.connect",
|
||||
title: "Connect provider",
|
||||
category: "Provider",
|
||||
onSelect: () => connectProvider(),
|
||||
},
|
||||
{
|
||||
id: "server.switch",
|
||||
title: "Switch server",
|
||||
category: "Server",
|
||||
onSelect: () => openServer(),
|
||||
},
|
||||
{
|
||||
id: "session.previous",
|
||||
title: "Previous session",
|
||||
@@ -423,6 +434,10 @@ export default function Layout(props: ParentProps) {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}
|
||||
|
||||
function openServer() {
|
||||
dialog.show(() => <DialogSelectServer />)
|
||||
}
|
||||
|
||||
function navigateToProject(directory: string | undefined) {
|
||||
if (!directory) return
|
||||
const lastSession = store.lastSession[directory]
|
||||
@@ -450,17 +465,28 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
async function chooseProject() {
|
||||
const result = await platform.openDirectoryPickerDialog?.({
|
||||
title: "Open project",
|
||||
multiple: true,
|
||||
})
|
||||
if (Array.isArray(result)) {
|
||||
for (const directory of result) {
|
||||
openProject(directory, false)
|
||||
function resolve(result: string | string[] | null) {
|
||||
if (Array.isArray(result)) {
|
||||
for (const directory of result) {
|
||||
openProject(directory, false)
|
||||
}
|
||||
navigateToProject(result[0])
|
||||
} else if (result) {
|
||||
openProject(result)
|
||||
}
|
||||
navigateToProject(result[0])
|
||||
} else if (result) {
|
||||
openProject(result)
|
||||
}
|
||||
|
||||
if (platform.openDirectoryPickerDialog) {
|
||||
const result = await platform.openDirectoryPickerDialog?.({
|
||||
title: "Open project",
|
||||
multiple: true,
|
||||
})
|
||||
resolve(result)
|
||||
} else {
|
||||
dialog.show(
|
||||
() => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
|
||||
() => resolve(null),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,7 +548,7 @@ export default function Layout(props: ParentProps) {
|
||||
const notification = useNotification()
|
||||
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
|
||||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||
const name = createMemo(() => getFilename(props.project.worktree))
|
||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
|
||||
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
@@ -558,7 +584,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
const ProjectVisual = (props: { project: LocalProject; class?: string }): JSX.Element => {
|
||||
const name = createMemo(() => getFilename(props.project.worktree))
|
||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||
const current = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
return (
|
||||
<Switch>
|
||||
@@ -701,7 +727,7 @@ export default function Layout(props: ParentProps) {
|
||||
const sortable = createSortable(props.project.worktree)
|
||||
const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
|
||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||
const name = createMemo(() => getFilename(props.project.worktree))
|
||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||
const [store, setProjectStore] = globalSync.child(props.project.worktree)
|
||||
const sessions = createMemo(() => store.session.toSorted(sortSessions))
|
||||
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
|
||||
@@ -747,6 +773,11 @@ export default function Layout(props: ParentProps) {
|
||||
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => dialog.show(() => <DialogEditProject project={props.project} />)}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>Edit project</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => closeProject(props.project.worktree)}>
|
||||
<DropdownMenu.ItemLabel>Close project</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
@@ -910,7 +941,7 @@ export default function Layout(props: ParentProps) {
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
||||
<Switch>
|
||||
<Match when={!providers.paid().length && expanded()}>
|
||||
<Match when={providers.all().length > 0 && !providers.paid().length && expanded()}>
|
||||
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
|
||||
<div class="p-3 flex flex-col gap-2">
|
||||
<div class="text-12-medium text-text-strong">Getting started</div>
|
||||
@@ -929,7 +960,7 @@ export default function Layout(props: ParentProps) {
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Match when={providers.all().length > 0}>
|
||||
<Tooltip placement="right" value="Connect provider" inactive={expanded()}>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
@@ -943,30 +974,28 @@ export default function Layout(props: ParentProps) {
|
||||
</Tooltip>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={platform.openDirectoryPickerDialog}>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Open project</span>
|
||||
<Show when={!sidebarProps.mobile}>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
inactive={expanded()}
|
||||
<Tooltip
|
||||
placement="right"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Open project</span>
|
||||
<Show when={!sidebarProps.mobile}>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
inactive={expanded()}
|
||||
>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="folder-add-left"
|
||||
onClick={chooseProject}
|
||||
>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="folder-add-left"
|
||||
onClick={chooseProject}
|
||||
>
|
||||
<Show when={expanded()}>Open project</Show>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Show when={expanded()}>Open project</Show>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip placement="right" value="Share feedback" inactive={expanded()}>
|
||||
<Button
|
||||
as={"a"}
|
||||
@@ -986,7 +1015,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="relative flex-1 min-h-0 flex flex-col">
|
||||
<div class="relative flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||
<Header
|
||||
navigateToProject={navigateToProject}
|
||||
navigateToSession={navigateToSession}
|
||||
|
||||
@@ -60,6 +60,14 @@ import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||
import { StatusBar } from "@/components/status-bar"
|
||||
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
|
||||
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
|
||||
function same<T>(a: readonly T[], b: readonly T[]) {
|
||||
if (a === b) return true
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((x, i) => x === b[i])
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const layout = useLayout()
|
||||
@@ -74,22 +82,40 @@ export default function Page() {
|
||||
const sdk = useSDK()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const permission = usePermission()
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
const userMessages = createMemo(() =>
|
||||
messages()
|
||||
.filter((m) => m.role === "user")
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
const emptyUserMessages: UserMessage[] = []
|
||||
const userMessages = createMemo(
|
||||
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
||||
emptyUserMessages,
|
||||
{ equals: same },
|
||||
)
|
||||
const visibleUserMessages = createMemo(
|
||||
() => {
|
||||
const revert = revertMessageID()
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
},
|
||||
emptyUserMessages,
|
||||
{ equals: same },
|
||||
)
|
||||
const lastUserMessage = createMemo(() => visibleUserMessages().at(-1))
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => lastUserMessage()?.id,
|
||||
() => {
|
||||
const msg = lastUserMessage()
|
||||
if (!msg) return
|
||||
if (msg.agent) local.agent.set(msg.agent)
|
||||
if (msg.model) local.model.set(msg.model)
|
||||
},
|
||||
),
|
||||
)
|
||||
const visibleUserMessages = createMemo(() => {
|
||||
const revert = revertMessageID()
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
})
|
||||
const lastUserMessage = createMemo(() => visibleUserMessages()?.at(-1))
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
clickTimer: undefined as number | undefined,
|
||||
@@ -159,16 +185,37 @@ export default function Page() {
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
params.id
|
||||
const status = sync.data.session_status[params.id ?? ""] ?? { type: "idle" }
|
||||
batch(() => {
|
||||
setStore("userInteracted", false)
|
||||
setStore("stepsExpanded", status.type !== "idle")
|
||||
})
|
||||
})
|
||||
const idle = { type: "idle" as const }
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => params.id,
|
||||
(id) => {
|
||||
const status = sync.data.session_status[id ?? ""] ?? idle
|
||||
batch(() => {
|
||||
setStore("userInteracted", false)
|
||||
setStore("stepsExpanded", status.type !== "idle")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => status().type,
|
||||
(type) => {
|
||||
if (type !== "idle") return
|
||||
batch(() => {
|
||||
setStore("userInteracted", false)
|
||||
setStore("stepsExpanded", false)
|
||||
})
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? { type: "idle" })
|
||||
const working = createMemo(() => status().type !== "idle" && activeMessage()?.id === lastUserMessage()?.id)
|
||||
|
||||
createRenderEffect((prev) => {
|
||||
@@ -304,6 +351,22 @@ export default function Page() {
|
||||
keybind: "shift+mod+.",
|
||||
onSelect: () => local.agent.move(-1),
|
||||
},
|
||||
{
|
||||
id: "permissions.autoaccept",
|
||||
title: params.id && permission.isAutoAccepting(params.id) ? "Stop auto-accepting edits" : "Auto-accept edits",
|
||||
category: "Permissions",
|
||||
disabled: !params.id,
|
||||
onSelect: () => {
|
||||
if (!params.id) return
|
||||
permission.toggleAutoAccept(params.id)
|
||||
showToast({
|
||||
title: permission.isAutoAccepting(params.id) ? "Auto-accepting edits" : "Stopped auto-accepting edits",
|
||||
description: permission.isAutoAccepting(params.id)
|
||||
? "Edit and write permissions will be automatically approved"
|
||||
: "Edit and write permissions will require approval",
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "session.undo",
|
||||
title: "Undo",
|
||||
@@ -575,6 +638,7 @@ export default function Page() {
|
||||
<SessionTurn
|
||||
sessionID={params.id!}
|
||||
messageID={message.id}
|
||||
lastUserMessageID={lastUserMessage()?.id}
|
||||
stepsExpanded={store.mobileStepsExpanded[message.id] ?? false}
|
||||
onStepsExpandedToggle={() => setStore("mobileStepsExpanded", message.id, (x) => !x)}
|
||||
onUserInteracted={() => setStore("userInteracted", true)}
|
||||
@@ -631,6 +695,7 @@ export default function Page() {
|
||||
<SessionTurn
|
||||
sessionID={params.id!}
|
||||
messageID={activeMessage()!.id}
|
||||
lastUserMessageID={lastUserMessage()?.id}
|
||||
stepsExpanded={store.stepsExpanded}
|
||||
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
|
||||
onUserInteracted={() => setStore("userInteracted", true)}
|
||||
@@ -793,7 +858,7 @@ export default function Page() {
|
||||
</Tabs.List>
|
||||
</div>
|
||||
<Show when={diffs().length}>
|
||||
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||
<SessionReview
|
||||
classes={{
|
||||
@@ -819,7 +884,7 @@ export default function Page() {
|
||||
},
|
||||
)
|
||||
return (
|
||||
<Tabs.Content value={tab} class="select-text mt-3">
|
||||
<Tabs.Content value={tab} class="mt-3">
|
||||
<Switch>
|
||||
<Match when={file()}>
|
||||
{(f) => (
|
||||
@@ -831,7 +896,7 @@ export default function Page() {
|
||||
cacheKey: checksum(f().content?.content ?? ""),
|
||||
}}
|
||||
overflow="scroll"
|
||||
class="pb-40"
|
||||
class="select-text pb-40"
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
|
||||
@@ -10,6 +10,6 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
target: "esnext",
|
||||
sourcemap: true,
|
||||
// sourcemap: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -124,6 +124,8 @@ export async function handler(
|
||||
res.status !== 200 &&
|
||||
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||
res.status !== 404 &&
|
||||
// ie. cannot change codex model providers mid-session
|
||||
!modelInfo.stickyProvider &&
|
||||
modelInfo.fallbackProvider &&
|
||||
providerInfo.id !== modelInfo.fallbackProvider
|
||||
) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
@@ -18,6 +18,7 @@
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-notification": "~2",
|
||||
"@tauri-apps/plugin-process": "~2",
|
||||
"@tauri-apps/plugin-shell": "~2",
|
||||
"@tauri-apps/plugin-store": "~2",
|
||||
|
||||
Generated
+58
@@ -2210,6 +2210,18 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"objc2 0.6.3",
|
||||
"objc2-foundation 0.3.2",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.14.1"
|
||||
@@ -2384,6 +2396,20 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.11.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400"
|
||||
dependencies = [
|
||||
"futures-lite",
|
||||
"log",
|
||||
"mac-notification-sys",
|
||||
"serde",
|
||||
"tauri-winrt-notification",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
@@ -2758,6 +2784,7 @@ dependencies = [
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-process",
|
||||
@@ -4519,6 +4546,25 @@ dependencies = [
|
||||
"urlpattern",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify-rust",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.5.2"
|
||||
@@ -4754,6 +4800,18 @@ dependencies = [
|
||||
"toml 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-winrt-notification"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
|
||||
dependencies = [
|
||||
"quick-xml 0.37.5",
|
||||
"thiserror 2.0.17",
|
||||
"windows",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.23.0"
|
||||
|
||||
@@ -28,6 +28,7 @@ tauri-plugin-store = "2"
|
||||
tauri-plugin-window-state = "2"
|
||||
tauri-plugin-clipboard-manager = "2"
|
||||
tauri-plugin-http = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
"opener:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:webview:allow-set-webview-zoom",
|
||||
"core:window:allow-is-focused",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-unminimize",
|
||||
"core:window:allow-set-focus",
|
||||
"shell:default",
|
||||
"updater:default",
|
||||
"dialog:default",
|
||||
@@ -15,6 +19,7 @@
|
||||
"store:default",
|
||||
"window-state:default",
|
||||
"os:default",
|
||||
"notification:default",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [{ "url": "http://*" }, { "url": "https://*" }, { "url": "http://*:*/*" }]
|
||||
|
||||
@@ -198,6 +198,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(PinchZoomDisablePlugin)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
kill_sidecar,
|
||||
|
||||
@@ -12,6 +12,8 @@ import { UPDATER_ENABLED } from "./updater"
|
||||
import { createMenu } from "./menu"
|
||||
import { check, Update } from "@tauri-apps/plugin-updater"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||
import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
|
||||
import { relaunch } from "@tauri-apps/plugin-process"
|
||||
import pkg from "../package.json"
|
||||
|
||||
@@ -94,6 +96,33 @@ const platform: Platform = {
|
||||
await relaunch()
|
||||
},
|
||||
|
||||
notify: async (title, description, href) => {
|
||||
const granted = await isPermissionGranted().catch(() => false)
|
||||
const permission = granted ? "granted" : await requestPermission().catch(() => "denied")
|
||||
if (permission !== "granted") return
|
||||
|
||||
const win = getCurrentWindow()
|
||||
const focused = await win.isFocused().catch(() => document.hasFocus())
|
||||
if (focused) return
|
||||
|
||||
await Promise.resolve()
|
||||
.then(() => {
|
||||
const notification = new Notification(title, { body: description ?? "" })
|
||||
notification.onclick = () => {
|
||||
const win = getCurrentWindow()
|
||||
void win.show().catch(() => undefined)
|
||||
void win.unminimize().catch(() => undefined)
|
||||
void win.setFocus().catch(() => undefined)
|
||||
if (href) {
|
||||
window.history.pushState(null, "", href)
|
||||
window.dispatchEvent(new PopStateEvent("popstate"))
|
||||
}
|
||||
notification.close()
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
},
|
||||
|
||||
// @ts-expect-error
|
||||
fetch: tauriFetch,
|
||||
}
|
||||
|
||||
@@ -10,9 +10,13 @@ export default defineConfig({
|
||||
//
|
||||
// 1. prevent Vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
build: {
|
||||
sourcemap: true,
|
||||
esbuild: {
|
||||
// Improves production stack traces
|
||||
keepNames: true,
|
||||
},
|
||||
// build: {
|
||||
// sourcemap: true,
|
||||
// },
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FileRoutes } from "@solidjs/start/router"
|
||||
import { Font } from "@opencode-ai/ui/font"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||
import { Suspense } from "solid-js"
|
||||
import "./app.css"
|
||||
import { Favicon } from "@opencode-ai/ui/favicon"
|
||||
@@ -12,11 +13,13 @@ export default function App() {
|
||||
<Router
|
||||
root={(props) => (
|
||||
<MetaProvider>
|
||||
<MarkedProvider>
|
||||
<Favicon />
|
||||
<Font />
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</MarkedProvider>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<Favicon />
|
||||
<Font />
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</MetaProvider>
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -162,11 +162,20 @@ export default function () {
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(e) => {
|
||||
fallback={(error) => {
|
||||
if (SessionDataMissingError.isInstance(error)) {
|
||||
return <NotFound />
|
||||
}
|
||||
console.error(error)
|
||||
const details = error instanceof Error ? (error.stack ?? error.message) : String(error)
|
||||
return (
|
||||
<Show when={e.message === "SessionDataMissingError"}>
|
||||
<NotFound />
|
||||
</Show>
|
||||
<div class="min-h-screen w-full bg-background-base text-text-base flex flex-col items-center justify-center gap-4 p-6 text-center">
|
||||
<p class="text-16-medium">Unable to render this share.</p>
|
||||
<p class="text-14-regular text-text-weaker">Check the console for more details.</p>
|
||||
<pre class="text-12-mono text-left whitespace-pre-wrap break-words w-full max-w-200 bg-background-stronger rounded-md p-4">
|
||||
{details}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.0.207"
|
||||
version = "1.0.217"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/sst/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.207/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -2,5 +2,6 @@ preload = ["@opentui/solid/preload"]
|
||||
|
||||
[test]
|
||||
preload = ["./test/preload.ts"]
|
||||
timeout = 10000 # 10 seconds (default is 5000ms)
|
||||
# Enable code coverage
|
||||
coverage = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -50,22 +50,21 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.5.1",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.57",
|
||||
"@ai-sdk/anthropic": "2.0.50",
|
||||
"@ai-sdk/azure": "2.0.73",
|
||||
"@ai-sdk/anthropic": "2.0.56",
|
||||
"@ai-sdk/azure": "2.0.82",
|
||||
"@ai-sdk/cerebras": "1.0.33",
|
||||
"@ai-sdk/cohere": "2.0.21",
|
||||
"@ai-sdk/deepinfra": "1.0.30",
|
||||
"@ai-sdk/gateway": "2.0.23",
|
||||
"@ai-sdk/google": "2.0.44",
|
||||
"@ai-sdk/google": "2.0.49",
|
||||
"@ai-sdk/google-vertex": "3.0.81",
|
||||
"@ai-sdk/groq": "2.0.33",
|
||||
"@ai-sdk/mcp": "0.0.8",
|
||||
"@ai-sdk/mistral": "2.0.26",
|
||||
"@ai-sdk/openai": "2.0.71",
|
||||
"@ai-sdk/openai-compatible": "1.0.27",
|
||||
"@ai-sdk/openai-compatible": "1.0.29",
|
||||
"@ai-sdk/perplexity": "2.0.22",
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.18",
|
||||
"@ai-sdk/provider-utils": "3.0.19",
|
||||
"@ai-sdk/togetherai": "1.0.30",
|
||||
"@ai-sdk/xai": "2.0.42",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
@@ -80,8 +79,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.63",
|
||||
"@opentui/solid": "0.1.63",
|
||||
"@opentui/core": "0.1.67",
|
||||
"@opentui/solid": "0.1.67",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
|
||||
@@ -235,5 +235,19 @@ export default {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
filetype: "nix",
|
||||
// TODO: Replace with official tree-sitter-nix WASM when published
|
||||
// See: https://github.com/nix-community/tree-sitter-nix/issues/66
|
||||
wasm: "https://github.com/ast-grep/ast-grep.github.io/raw/40b84530640aa83a0d34a20a2b0623d7b8e5ea97/website/public/parsers/tree-sitter-nix.wasm",
|
||||
queries: {
|
||||
highlights: [
|
||||
"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/nix/highlights.scm",
|
||||
],
|
||||
locals: [
|
||||
"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/nix/locals.scm",
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ Rules:
|
||||
- Do not explain what the user asked for
|
||||
- Write in first person (I added..., I fixed...)
|
||||
- Never ask questions or add new questions
|
||||
- Only exception: if the conversation ends with an unanswered question to the user, preserve that exact question
|
||||
- If the conversation ends with an unanswered question to the user, preserve that exact question
|
||||
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary
|
||||
|
||||
@@ -349,6 +349,12 @@ export const AuthLoginCommand = cmd({
|
||||
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
|
||||
}
|
||||
|
||||
if (["cloudflare", "cloudflare-ai-gateway"].includes(provider)) {
|
||||
prompts.log.info(
|
||||
"Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN environment variables. Read more: https://opencode.ai/docs/providers/#cloudflare-ai-gateway",
|
||||
)
|
||||
}
|
||||
|
||||
const key = await prompts.password({
|
||||
message: "Enter your API key",
|
||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||
|
||||
@@ -372,6 +372,15 @@ function App() {
|
||||
local.agent.move(1)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Variant cycle",
|
||||
value: "variant.cycle",
|
||||
keybind: "variant_cycle",
|
||||
category: "Agent",
|
||||
onSelect: () => {
|
||||
local.model.variant.cycle()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Agent cycle reverse",
|
||||
value: "agent.cycle.reverse",
|
||||
|
||||
@@ -150,7 +150,7 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
if (inFavorites) return false
|
||||
const inRecents = recents.some(
|
||||
const inRecents = recentList.some(
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
if (inRecents) return false
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { DialogPrompt } from "../ui/dialog-prompt"
|
||||
import { Link } from "../ui/link"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2"
|
||||
@@ -128,7 +129,7 @@ function AutoMethod(props: AutoMethodProps) {
|
||||
<text fg={theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<box gap={1}>
|
||||
<text fg={theme.primary}>{props.authorization.url}</text>
|
||||
<Link href={props.authorization.url} fg={theme.primary} />
|
||||
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
||||
</box>
|
||||
<text fg={theme.textMuted}>Waiting for authorization...</text>
|
||||
@@ -170,7 +171,7 @@ function CodeMethod(props: CodeMethodProps) {
|
||||
description={() => (
|
||||
<box gap={1}>
|
||||
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
||||
<text fg={theme.primary}>{props.authorization.url}</text>
|
||||
<Link href={props.authorization.url} fg={theme.primary} />
|
||||
<Show when={error()}>
|
||||
<text fg={theme.error}>Invalid code</text>
|
||||
</Show>
|
||||
|
||||
@@ -167,6 +167,13 @@ export function Prompt(props: PromptProps) {
|
||||
if (!props.disabled) input.cursorColor = theme.text
|
||||
})
|
||||
|
||||
const lastUserMessage = createMemo(() => {
|
||||
if (!props.sessionID) return undefined
|
||||
const messages = sync.data.message[props.sessionID]
|
||||
if (!messages) return undefined
|
||||
return messages.findLast((m) => m.role === "user")
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
prompt: PromptInfo
|
||||
mode: "normal" | "shell"
|
||||
@@ -184,6 +191,23 @@ export function Prompt(props: PromptProps) {
|
||||
interrupt: 0,
|
||||
})
|
||||
|
||||
// Initialize agent/model/variant from last user message when session changes
|
||||
let syncedSessionID: string | undefined
|
||||
createEffect(() => {
|
||||
const sessionID = props.sessionID
|
||||
const msg = lastUserMessage()
|
||||
|
||||
if (sessionID !== syncedSessionID) {
|
||||
if (!sessionID || !msg) return
|
||||
|
||||
syncedSessionID = sessionID
|
||||
|
||||
if (msg.agent) local.agent.set(msg.agent)
|
||||
if (msg.model) local.model.set(msg.model)
|
||||
if (msg.variant) local.model.variant.set(msg.variant)
|
||||
}
|
||||
})
|
||||
|
||||
command.register(() => {
|
||||
return [
|
||||
{
|
||||
@@ -562,6 +586,7 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
// Capture mode before it gets reset
|
||||
const currentMode = store.mode
|
||||
const variant = local.model.variant.current()
|
||||
|
||||
if (store.mode === "shell") {
|
||||
sdk.client.session.shell({
|
||||
@@ -590,6 +615,7 @@ export function Prompt(props: PromptProps) {
|
||||
agent: local.agent.current().name,
|
||||
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
|
||||
messageID,
|
||||
variant,
|
||||
})
|
||||
} else {
|
||||
sdk.client.session.prompt({
|
||||
@@ -598,6 +624,7 @@ export function Prompt(props: PromptProps) {
|
||||
messageID,
|
||||
agent: local.agent.current().name,
|
||||
model: selectedModel,
|
||||
variant,
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
@@ -718,6 +745,13 @@ export function Prompt(props: PromptProps) {
|
||||
return local.agent.color(local.agent.current().name)
|
||||
})
|
||||
|
||||
const showVariant = createMemo(() => {
|
||||
const variants = local.model.variant.list()
|
||||
if (variants.length === 0) return false
|
||||
const current = local.model.variant.current()
|
||||
return !!current
|
||||
})
|
||||
|
||||
const spinnerDef = createMemo(() => {
|
||||
const color = local.agent.color(local.agent.current().name)
|
||||
return {
|
||||
@@ -958,6 +992,12 @@ export function Prompt(props: PromptProps) {
|
||||
{local.model.parsed().model}
|
||||
</text>
|
||||
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
|
||||
<Show when={showVariant()}>
|
||||
<text fg={theme.textMuted}>·</text>
|
||||
<text>
|
||||
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
|
||||
@@ -33,24 +33,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically update model when agent changes
|
||||
createEffect(() => {
|
||||
const value = agent.current()
|
||||
if (value.model) {
|
||||
if (isModelValid(value.model))
|
||||
model.set({
|
||||
providerID: value.model.providerID,
|
||||
modelID: value.model.modelID,
|
||||
})
|
||||
else
|
||||
toast.show({
|
||||
variant: "warning",
|
||||
message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const agent = iife(() => {
|
||||
const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
|
||||
const [agentStore, setAgentStore] = createStore<{
|
||||
@@ -120,11 +102,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
providerID: string
|
||||
modelID: string
|
||||
}[]
|
||||
variant: Record<string, string | undefined>
|
||||
}>({
|
||||
ready: false,
|
||||
model: {},
|
||||
recent: [],
|
||||
favorite: [],
|
||||
variant: {},
|
||||
})
|
||||
|
||||
const file = Bun.file(path.join(Global.Path.state, "model.json"))
|
||||
@@ -135,6 +119,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
JSON.stringify({
|
||||
recent: modelStore.recent,
|
||||
favorite: modelStore.favorite,
|
||||
variant: modelStore.variant,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -144,6 +129,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
.then((x) => {
|
||||
if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
|
||||
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
|
||||
if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
@@ -218,6 +204,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
return {
|
||||
provider: "Connect a provider",
|
||||
model: "No provider selected",
|
||||
reasoning: false,
|
||||
}
|
||||
}
|
||||
const provider = sync.data.provider.find((x) => x.id === value.providerID)
|
||||
@@ -225,6 +212,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
return {
|
||||
provider: provider?.name ?? value.providerID,
|
||||
model: info?.name ?? value.modelID,
|
||||
reasoning: info?.capabilities?.reasoning ?? false,
|
||||
}
|
||||
}),
|
||||
cycle(direction: 1 | -1) {
|
||||
@@ -267,7 +255,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
setModelStore("model", agent.current().name, { ...next })
|
||||
const uniq = uniqueBy([next, ...modelStore.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 10) uniq.pop()
|
||||
setModelStore("recent", uniq)
|
||||
setModelStore(
|
||||
"recent",
|
||||
uniq.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
|
||||
)
|
||||
save()
|
||||
},
|
||||
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
||||
@@ -284,7 +275,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
if (options?.recent) {
|
||||
const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 10) uniq.pop()
|
||||
setModelStore("recent", uniq)
|
||||
setModelStore(
|
||||
"recent",
|
||||
uniq.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
|
||||
)
|
||||
save()
|
||||
}
|
||||
})
|
||||
@@ -305,10 +299,53 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const next = exists
|
||||
? modelStore.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
|
||||
: [model, ...modelStore.favorite]
|
||||
setModelStore("favorite", next)
|
||||
setModelStore(
|
||||
"favorite",
|
||||
next.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
|
||||
)
|
||||
save()
|
||||
})
|
||||
},
|
||||
variant: {
|
||||
current() {
|
||||
const m = currentModel()
|
||||
if (!m) return undefined
|
||||
const key = `${m.providerID}/${m.modelID}`
|
||||
return modelStore.variant[key]
|
||||
},
|
||||
list() {
|
||||
const m = currentModel()
|
||||
if (!m) return []
|
||||
const provider = sync.data.provider.find((x) => x.id === m.providerID)
|
||||
const info = provider?.models[m.modelID]
|
||||
if (!info?.variants) return []
|
||||
return Object.entries(info.variants)
|
||||
.filter(([_, v]) => !v.disabled)
|
||||
.map(([name]) => name)
|
||||
},
|
||||
set(value: string | undefined) {
|
||||
const m = currentModel()
|
||||
if (!m) return
|
||||
const key = `${m.providerID}/${m.modelID}`
|
||||
setModelStore("variant", key, value)
|
||||
save()
|
||||
},
|
||||
cycle() {
|
||||
const variants = this.list()
|
||||
if (variants.length === 0) return
|
||||
const current = this.current()
|
||||
if (!current) {
|
||||
this.set(variants[0])
|
||||
return
|
||||
}
|
||||
const index = variants.indexOf(current)
|
||||
if (index === -1 || index === variants.length - 1) {
|
||||
this.set(undefined)
|
||||
return
|
||||
}
|
||||
this.set(variants[index + 1])
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -329,6 +366,24 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
},
|
||||
}
|
||||
|
||||
// Automatically update model when agent changes
|
||||
createEffect(() => {
|
||||
const value = agent.current()
|
||||
if (value.model) {
|
||||
if (isModelValid(value.model))
|
||||
model.set({
|
||||
providerID: value.model.providerID,
|
||||
modelID: value.model.modelID,
|
||||
})
|
||||
else
|
||||
toast.show({
|
||||
variant: "warning",
|
||||
message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const result = {
|
||||
model,
|
||||
agent,
|
||||
|
||||
@@ -115,11 +115,12 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
setStore("selected", currentIndex)
|
||||
}
|
||||
}
|
||||
scroll.scrollTo(0)
|
||||
scroll?.scrollTo(0)
|
||||
}),
|
||||
)
|
||||
|
||||
function move(direction: number) {
|
||||
if (flat().length === 0) return
|
||||
let next = store.selected + direction
|
||||
if (next < 0) next = flat().length - 1
|
||||
if (next >= flat().length) next = 0
|
||||
@@ -129,6 +130,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
function moveTo(next: number) {
|
||||
setStore("selected", next)
|
||||
props.onMove?.(selected()!)
|
||||
if (!scroll) return
|
||||
const target = scroll.getChildren().find((child) => {
|
||||
return child.id === JSON.stringify(selected()?.value)
|
||||
})
|
||||
@@ -172,7 +174,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
}
|
||||
})
|
||||
|
||||
let scroll: ScrollBoxRenderable
|
||||
let scroll: ScrollBoxRenderable | undefined
|
||||
const ref: DialogSelectRef<T> = {
|
||||
get filter() {
|
||||
return store.filter
|
||||
@@ -213,61 +215,70 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
<scrollbox
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
scrollbarOptions={{ visible: false }}
|
||||
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
||||
maxHeight={height()}
|
||||
<Show
|
||||
when={grouped().length > 0}
|
||||
fallback={
|
||||
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
||||
<text fg={theme.textMuted}>No results found</text>
|
||||
</box>
|
||||
}
|
||||
>
|
||||
<For each={grouped()}>
|
||||
{([category, options], index) => (
|
||||
<>
|
||||
<Show when={category}>
|
||||
<box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}>
|
||||
<text fg={theme.accent} attributes={TextAttributes.BOLD}>
|
||||
{category}
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
<For each={options}>
|
||||
{(option) => {
|
||||
const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
|
||||
const current = createMemo(() => isDeepEqual(option.value, props.current))
|
||||
return (
|
||||
<box
|
||||
id={JSON.stringify(option.value)}
|
||||
flexDirection="row"
|
||||
onMouseUp={() => {
|
||||
option.onSelect?.(dialog)
|
||||
props.onSelect?.(option)
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
|
||||
if (index === -1) return
|
||||
moveTo(index)
|
||||
}}
|
||||
backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
|
||||
paddingLeft={current() || option.gutter ? 1 : 3}
|
||||
paddingRight={3}
|
||||
gap={1}
|
||||
>
|
||||
<Option
|
||||
title={option.title}
|
||||
footer={option.footer}
|
||||
description={option.description !== category ? option.description : undefined}
|
||||
active={active()}
|
||||
current={current()}
|
||||
gutter={option.gutter}
|
||||
/>
|
||||
</box>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</scrollbox>
|
||||
<scrollbox
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
scrollbarOptions={{ visible: false }}
|
||||
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
||||
maxHeight={height()}
|
||||
>
|
||||
<For each={grouped()}>
|
||||
{([category, options], index) => (
|
||||
<>
|
||||
<Show when={category}>
|
||||
<box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}>
|
||||
<text fg={theme.accent} attributes={TextAttributes.BOLD}>
|
||||
{category}
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
<For each={options}>
|
||||
{(option) => {
|
||||
const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
|
||||
const current = createMemo(() => isDeepEqual(option.value, props.current))
|
||||
return (
|
||||
<box
|
||||
id={JSON.stringify(option.value)}
|
||||
flexDirection="row"
|
||||
onMouseUp={() => {
|
||||
option.onSelect?.(dialog)
|
||||
props.onSelect?.(option)
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
|
||||
if (index === -1) return
|
||||
moveTo(index)
|
||||
}}
|
||||
backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
|
||||
paddingLeft={current() || option.gutter ? 1 : 3}
|
||||
paddingRight={3}
|
||||
gap={1}
|
||||
>
|
||||
<Option
|
||||
title={option.title}
|
||||
footer={option.footer}
|
||||
description={option.description !== category ? option.description : undefined}
|
||||
active={active()}
|
||||
current={current()}
|
||||
gutter={option.gutter}
|
||||
/>
|
||||
</box>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</scrollbox>
|
||||
</Show>
|
||||
<Show when={keybinds().length} fallback={<box flexShrink={0} />}>
|
||||
<box paddingRight={2} paddingLeft={4} flexDirection="row" gap={2} flexShrink={0} paddingTop={1}>
|
||||
<For each={keybinds()}>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { JSX } from "solid-js"
|
||||
import type { RGBA } from "@opentui/core"
|
||||
import open from "open"
|
||||
|
||||
export interface LinkProps {
|
||||
href: string
|
||||
children?: JSX.Element | string
|
||||
fg?: RGBA
|
||||
}
|
||||
|
||||
/**
|
||||
* Link component that renders clickable hyperlinks.
|
||||
* Clicking anywhere on the link text opens the URL in the default browser.
|
||||
*/
|
||||
export function Link(props: LinkProps) {
|
||||
const displayText = props.children ?? props.href
|
||||
|
||||
return (
|
||||
<text
|
||||
fg={props.fg}
|
||||
onMouseUp={() => {
|
||||
open(props.href).catch(() => {})
|
||||
}}
|
||||
>
|
||||
{displayText}
|
||||
</text>
|
||||
)
|
||||
}
|
||||
@@ -191,6 +191,10 @@ export namespace Config {
|
||||
cwd: dir,
|
||||
},
|
||||
).catch(() => {})
|
||||
|
||||
// Install any additional dependencies defined in the package.json
|
||||
// This allows local plugins and custom tools to use external packages
|
||||
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
|
||||
}
|
||||
|
||||
const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
|
||||
@@ -486,6 +490,7 @@ export namespace Config {
|
||||
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
|
||||
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
||||
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
||||
variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
|
||||
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
||||
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
||||
input_submit: z.string().optional().default("return").describe("Submit input"),
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Filesystem } from "../util/filesystem"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Ripgrep } from "./ripgrep"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { Global } from "../global"
|
||||
|
||||
export namespace File {
|
||||
const log = Log.create({ service: "file" })
|
||||
@@ -122,10 +123,49 @@ export namespace File {
|
||||
type Entry = { files: string[]; dirs: string[] }
|
||||
let cache: Entry = { files: [], dirs: [] }
|
||||
let fetching = false
|
||||
|
||||
const isGlobalHome = Instance.directory === Global.Path.home && Instance.project.id === "global"
|
||||
|
||||
const fn = async (result: Entry) => {
|
||||
// Disable scanning if in root of file system
|
||||
if (Instance.directory === path.parse(Instance.directory).root) return
|
||||
fetching = true
|
||||
|
||||
if (isGlobalHome) {
|
||||
const dirs = new Set<string>()
|
||||
const ignore = new Set<string>()
|
||||
|
||||
if (process.platform === "darwin") ignore.add("Library")
|
||||
if (process.platform === "win32") ignore.add("AppData")
|
||||
|
||||
const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"])
|
||||
const shouldIgnore = (name: string) => name.startsWith(".") || ignore.has(name)
|
||||
const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.has(name)
|
||||
|
||||
const top = await fs.promises
|
||||
.readdir(Instance.directory, { withFileTypes: true })
|
||||
.catch(() => [] as fs.Dirent[])
|
||||
|
||||
for (const entry of top) {
|
||||
if (!entry.isDirectory()) continue
|
||||
if (shouldIgnore(entry.name)) continue
|
||||
dirs.add(entry.name + "/")
|
||||
|
||||
const base = path.join(Instance.directory, entry.name)
|
||||
const children = await fs.promises.readdir(base, { withFileTypes: true }).catch(() => [] as fs.Dirent[])
|
||||
for (const child of children) {
|
||||
if (!child.isDirectory()) continue
|
||||
if (shouldIgnoreNested(child.name)) continue
|
||||
dirs.add(entry.name + "/" + child.name + "/")
|
||||
}
|
||||
}
|
||||
|
||||
result.dirs = Array.from(dirs).toSorted()
|
||||
cache = result
|
||||
fetching = false
|
||||
return
|
||||
}
|
||||
|
||||
const set = new Set<string>()
|
||||
for await (const file of Ripgrep.files({ cwd: Instance.directory })) {
|
||||
result.files.push(file)
|
||||
@@ -329,15 +369,43 @@ export namespace File {
|
||||
})
|
||||
}
|
||||
|
||||
export async function search(input: { query: string; limit?: number; dirs?: boolean }) {
|
||||
log.info("search", { query: input.query })
|
||||
export async function search(input: { query: string; limit?: number; dirs?: boolean; type?: "file" | "directory" }) {
|
||||
const query = input.query.trim()
|
||||
const limit = input.limit ?? 100
|
||||
const kind = input.type ?? (input.dirs === false ? "file" : "all")
|
||||
log.info("search", { query, kind })
|
||||
|
||||
const result = await state().then((x) => x.files())
|
||||
if (!input.query)
|
||||
return input.dirs !== false ? result.dirs.toSorted().slice(0, limit) : result.files.slice(0, limit)
|
||||
const items = input.dirs !== false ? [...result.files, ...result.dirs] : result.files
|
||||
const sorted = fuzzysort.go(input.query, items, { limit: limit }).map((r) => r.target)
|
||||
log.info("search", { query: input.query, results: sorted.length })
|
||||
return sorted
|
||||
|
||||
const hidden = (item: string) => {
|
||||
const normalized = item.replaceAll("\\", "/").replace(/\/+$/, "")
|
||||
return normalized.split("/").some((p) => p.startsWith(".") && p.length > 1)
|
||||
}
|
||||
const preferHidden = query.startsWith(".") || query.includes("/.")
|
||||
const sortHiddenLast = (items: string[]) => {
|
||||
if (preferHidden) return items
|
||||
const visible: string[] = []
|
||||
const hiddenItems: string[] = []
|
||||
for (const item of items) {
|
||||
const isHidden = hidden(item)
|
||||
if (isHidden) hiddenItems.push(item)
|
||||
if (!isHidden) visible.push(item)
|
||||
}
|
||||
return [...visible, ...hiddenItems]
|
||||
}
|
||||
if (!query) {
|
||||
if (kind === "file") return result.files.slice(0, limit)
|
||||
return sortHiddenLast(result.dirs.toSorted()).slice(0, limit)
|
||||
}
|
||||
|
||||
const items =
|
||||
kind === "file" ? result.files : kind === "directory" ? result.dirs : [...result.files, ...result.dirs]
|
||||
|
||||
const searchLimit = kind === "directory" && !preferHidden ? limit * 20 : limit
|
||||
const sorted = fuzzysort.go(query, items, { limit: searchLimit }).map((r) => r.target)
|
||||
const output = kind === "directory" ? sortHiddenLast(sorted).slice(0, limit) : sorted
|
||||
|
||||
log.info("search", { query, kind, results: output.length })
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,8 +205,17 @@ export namespace Ripgrep {
|
||||
return filepath
|
||||
}
|
||||
|
||||
export async function* files(input: { cwd: string; glob?: string[] }) {
|
||||
const args = [await filepath(), "--files", "--follow", "--hidden", "--glob=!.git/*"]
|
||||
export async function* files(input: {
|
||||
cwd: string
|
||||
glob?: string[]
|
||||
hidden?: boolean
|
||||
follow?: boolean
|
||||
maxDepth?: number
|
||||
}) {
|
||||
const args = [await filepath(), "--files", "--glob=!.git/*"]
|
||||
if (input.follow !== false) args.push("--follow")
|
||||
if (input.hidden !== false) args.push("--hidden")
|
||||
if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`)
|
||||
if (input.glob) {
|
||||
for (const g of input.glob) {
|
||||
args.push(`--glob=${g}`)
|
||||
|
||||
@@ -9,9 +9,13 @@ import path from "path"
|
||||
// @ts-ignore
|
||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { withTimeout } from "@/util/timeout"
|
||||
import type ParcelWatcher from "@parcel/watcher"
|
||||
import { $ } from "bun"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { readdir } from "fs/promises"
|
||||
|
||||
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
||||
|
||||
declare const OPENCODE_LIBC: string | undefined
|
||||
|
||||
@@ -63,12 +67,16 @@ export namespace FileWatcher {
|
||||
const cfgIgnores = cfg.watcher?.ignore ?? []
|
||||
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
|
||||
subs.push(
|
||||
await watcher().subscribe(Instance.directory, subscribe, {
|
||||
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
|
||||
backend,
|
||||
}),
|
||||
)
|
||||
const pending = watcher().subscribe(Instance.directory, subscribe, {
|
||||
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
|
||||
backend,
|
||||
})
|
||||
const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => {
|
||||
log.error("failed to subscribe to Instance.directory", { error: err })
|
||||
pending.then((s) => s.unsubscribe()).catch(() => {})
|
||||
return undefined
|
||||
})
|
||||
if (sub) subs.push(sub)
|
||||
}
|
||||
|
||||
const vcsDir = await $`git rev-parse --git-dir`
|
||||
@@ -78,12 +86,18 @@ export namespace FileWatcher {
|
||||
.text()
|
||||
.then((x) => path.resolve(Instance.worktree, x.trim()))
|
||||
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
|
||||
subs.push(
|
||||
await watcher().subscribe(vcsDir, subscribe, {
|
||||
ignore: ["hooks", "info", "logs", "objects", "refs", "worktrees", "modules", "lfs"],
|
||||
backend,
|
||||
}),
|
||||
)
|
||||
const gitDirContents = await readdir(vcsDir).catch(() => [])
|
||||
const ignoreList = gitDirContents.filter((entry) => entry !== "HEAD")
|
||||
const pending = watcher().subscribe(vcsDir, subscribe, {
|
||||
ignore: ignoreList,
|
||||
backend,
|
||||
})
|
||||
const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => {
|
||||
log.error("failed to subscribe to vcsDir", { error: err })
|
||||
pending.then((s) => s.unsubscribe()).catch(() => {})
|
||||
return undefined
|
||||
})
|
||||
if (sub) subs.push(sub)
|
||||
}
|
||||
|
||||
return { subs }
|
||||
|
||||
@@ -322,3 +322,12 @@ export const shfmt: Info = {
|
||||
return Bun.which("shfmt") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const nixfmt: Info = {
|
||||
name: "nixfmt",
|
||||
command: ["nixfmt", "$FILE"],
|
||||
extensions: [".nix"],
|
||||
async enabled() {
|
||||
return Bun.which("nixfmt") !== null
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,14 +12,17 @@ const state = path.join(xdgState!, app)
|
||||
|
||||
export namespace Global {
|
||||
export const Path = {
|
||||
home: os.homedir(),
|
||||
// Allow override via OPENCODE_TEST_HOME for test isolation
|
||||
get home() {
|
||||
return process.env.OPENCODE_TEST_HOME || os.homedir()
|
||||
},
|
||||
data,
|
||||
bin: path.join(data, "bin"),
|
||||
log: path.join(data, "log"),
|
||||
cache,
|
||||
config,
|
||||
state,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
|
||||
@@ -196,13 +196,14 @@ export namespace LSPServer {
|
||||
}
|
||||
await fs.rename(extractedPath, finalPath)
|
||||
|
||||
await $`npm install`.cwd(finalPath).quiet()
|
||||
await $`npm run compile`.cwd(finalPath).quiet()
|
||||
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
|
||||
await $`${npmCmd} install`.cwd(finalPath).quiet()
|
||||
await $`${npmCmd} run compile`.cwd(finalPath).quiet()
|
||||
|
||||
log.info("installed VS Code ESLint server", { serverPath })
|
||||
}
|
||||
|
||||
const proc = spawn(BunProc.which(), ["--max-old-space-size=8192", serverPath, "--stdio"], {
|
||||
const proc = spawn(BunProc.which(), [serverPath, "--stdio"], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
@@ -24,6 +24,7 @@ export namespace Plugin {
|
||||
project: Instance.project,
|
||||
worktree: Instance.worktree,
|
||||
directory: Instance.directory,
|
||||
serverUrl: Server.url(),
|
||||
$: Bun.$,
|
||||
}
|
||||
const plugins = [...(config.plugin ?? [])]
|
||||
|
||||
@@ -34,6 +34,7 @@ import { createCohere } from "@ai-sdk/cohere"
|
||||
import { createGateway } from "@ai-sdk/gateway"
|
||||
import { createTogetherAI } from "@ai-sdk/togetherai"
|
||||
import { createPerplexity } from "@ai-sdk/perplexity"
|
||||
import { ProviderTransform } from "./transform"
|
||||
|
||||
export namespace Provider {
|
||||
const log = Log.create({ service: "provider" })
|
||||
@@ -404,6 +405,16 @@ export namespace Provider {
|
||||
},
|
||||
}
|
||||
|
||||
export const Variant = z
|
||||
.object({
|
||||
disabled: z.boolean(),
|
||||
})
|
||||
.catchall(z.any())
|
||||
.meta({
|
||||
ref: "Variant",
|
||||
})
|
||||
export type Variant = z.infer<typeof Variant>
|
||||
|
||||
export const Model = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
@@ -467,6 +478,7 @@ export namespace Provider {
|
||||
options: z.record(z.string(), z.any()),
|
||||
headers: z.record(z.string(), z.string()),
|
||||
release_date: z.string(),
|
||||
variants: z.record(z.string(), Variant).optional(),
|
||||
})
|
||||
.meta({
|
||||
ref: "Model",
|
||||
@@ -489,7 +501,7 @@ export namespace Provider {
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
||||
return {
|
||||
const m: Model = {
|
||||
id: model.id,
|
||||
providerID: provider.id,
|
||||
name: model.name,
|
||||
@@ -546,7 +558,12 @@ export namespace Provider {
|
||||
interleaved: model.interleaved ?? false,
|
||||
},
|
||||
release_date: model.release_date,
|
||||
variants: {},
|
||||
}
|
||||
|
||||
m.variants = mapValues(ProviderTransform.variants(m), (v) => ({ disabled: false, ...v }))
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||
|
||||
@@ -124,7 +124,7 @@ export namespace ProviderTransform {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
openrouter: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
bedrock: {
|
||||
cachePoint: { type: "ephemeral" },
|
||||
@@ -243,6 +243,162 @@ export namespace ProviderTransform {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
|
||||
const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
|
||||
|
||||
export function variants(model: Provider.Model) {
|
||||
if (!model.capabilities.reasoning) return {}
|
||||
|
||||
const id = model.id.toLowerCase()
|
||||
if (id.includes("deepseek") || id.includes("minimax") || id.includes("glm") || id.includes("mistral")) return {}
|
||||
|
||||
switch (model.api.npm) {
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
if (!model.id.includes("gpt") && !model.id.includes("gemini-3") && !model.id.includes("grok-4")) return {}
|
||||
return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }]))
|
||||
|
||||
// TODO: YOU CANNOT SET max_tokens if this is set!!!
|
||||
case "@ai-sdk/gateway":
|
||||
return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
|
||||
case "@ai-sdk/cerebras":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/cerebras
|
||||
case "@ai-sdk/togetherai":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/togetherai
|
||||
case "@ai-sdk/xai":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
|
||||
case "@ai-sdk/deepinfra":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
|
||||
case "@ai-sdk/openai-compatible":
|
||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
|
||||
case "@ai-sdk/azure":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
|
||||
if (id === "o1-mini") return {}
|
||||
const azureEfforts = ["low", "medium", "high"]
|
||||
if (id.includes("gpt-5")) {
|
||||
azureEfforts.unshift("minimal")
|
||||
}
|
||||
return Object.fromEntries(
|
||||
azureEfforts.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
reasoningEffort: effort,
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
]),
|
||||
)
|
||||
case "@ai-sdk/openai":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
|
||||
if (id === "gpt-5-pro") return {}
|
||||
const openaiEfforts = ["minimal", ...WIDELY_SUPPORTED_EFFORTS]
|
||||
if (model.release_date >= "2025-11-13") {
|
||||
openaiEfforts.unshift("none")
|
||||
}
|
||||
if (model.release_date >= "2025-12-04") {
|
||||
openaiEfforts.push("xhigh")
|
||||
}
|
||||
return Object.fromEntries(
|
||||
openaiEfforts.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
reasoningEffort: effort,
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
case "@ai-sdk/anthropic":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
|
||||
return {
|
||||
high: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budgetTokens: 16000,
|
||||
},
|
||||
},
|
||||
max: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budgetTokens: 31999,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
case "@ai-sdk/amazon-bedrock":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
|
||||
return Object.fromEntries(
|
||||
WIDELY_SUPPORTED_EFFORTS.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
reasoningConfig: {
|
||||
type: "enabled",
|
||||
maxReasoningEffort: effort,
|
||||
},
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
case "@ai-sdk/google-vertex":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
|
||||
case "@ai-sdk/google":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
|
||||
if (id.includes("2.5")) {
|
||||
return {
|
||||
high: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: 16000,
|
||||
},
|
||||
},
|
||||
max: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: 24576,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return Object.fromEntries(
|
||||
["low", "high"].map((effort) => [
|
||||
effort,
|
||||
{
|
||||
includeThoughts: true,
|
||||
thinkingLevel: effort,
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
case "@ai-sdk/mistral":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
|
||||
return {}
|
||||
|
||||
case "@ai-sdk/cohere":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/cohere
|
||||
return {}
|
||||
|
||||
case "@ai-sdk/groq":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/groq
|
||||
const groqEffort = ["none", ...WIDELY_SUPPORTED_EFFORTS]
|
||||
return Object.fromEntries(
|
||||
groqEffort.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
includeThoughts: true,
|
||||
thinkingLevel: effort,
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
case "@ai-sdk/perplexity":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
|
||||
return {}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export function options(
|
||||
model: Provider.Model,
|
||||
sessionID: string,
|
||||
@@ -302,26 +458,27 @@ export namespace ProviderTransform {
|
||||
}
|
||||
|
||||
export function smallOptions(model: Provider.Model) {
|
||||
const options: Record<string, any> = {}
|
||||
|
||||
if (model.providerID === "openai" || model.api.id.includes("gpt-5")) {
|
||||
if (model.api.id.includes("5.")) {
|
||||
options["reasoningEffort"] = "low"
|
||||
} else {
|
||||
options["reasoningEffort"] = "minimal"
|
||||
return { reasoningEffort: "low" }
|
||||
}
|
||||
return { reasoningEffort: "minimal" }
|
||||
}
|
||||
if (model.providerID === "google") {
|
||||
options["thinkingConfig"] = {
|
||||
thinkingBudget: 0,
|
||||
}
|
||||
return { thinkingConfig: { thinkingBudget: 0 } }
|
||||
}
|
||||
|
||||
return options
|
||||
if (model.providerID === "openrouter") {
|
||||
if (model.api.id.includes("google")) {
|
||||
return { reasoning: { enabled: false } }
|
||||
}
|
||||
return { reasoningEffort: "minimal" }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
|
||||
switch (model.api.npm) {
|
||||
case "@ai-sdk/github-copilot":
|
||||
case "@ai-sdk/openai":
|
||||
case "@ai-sdk/azure":
|
||||
return {
|
||||
@@ -335,6 +492,7 @@ export namespace ProviderTransform {
|
||||
return {
|
||||
["anthropic" as string]: options,
|
||||
}
|
||||
case "@ai-sdk/google-vertex":
|
||||
case "@ai-sdk/google":
|
||||
return {
|
||||
["google" as string]: options,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Log } from "@/util/log"
|
||||
import Bonjour from "bonjour-service"
|
||||
import { Bonjour } from "bonjour-service"
|
||||
|
||||
const log = Log.create({ service: "mdns" })
|
||||
|
||||
|
||||
@@ -57,6 +57,12 @@ globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
export namespace Server {
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
let _url: URL | undefined
|
||||
|
||||
export function url(): URL {
|
||||
return _url ?? new URL("http://localhost:4096")
|
||||
}
|
||||
|
||||
export const Event = {
|
||||
Connected: BusEvent.define("server.connected", z.object({})),
|
||||
Disposed: BusEvent.define("global.disposed", z.object({})),
|
||||
@@ -98,7 +104,23 @@ export namespace Server {
|
||||
timer.stop()
|
||||
}
|
||||
})
|
||||
.use(cors())
|
||||
.use(
|
||||
cors({
|
||||
origin(input) {
|
||||
if (!input) return
|
||||
|
||||
if (input.startsWith("http://localhost:")) return input
|
||||
if (input.startsWith("http://127.0.0.1:")) return input
|
||||
if (input === "tauri://localhost") return input
|
||||
|
||||
// *.opencode.ai (https only, adjust if needed)
|
||||
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
|
||||
return input
|
||||
}
|
||||
return
|
||||
},
|
||||
}),
|
||||
)
|
||||
.get(
|
||||
"/global/health",
|
||||
describeRoute({
|
||||
@@ -1795,7 +1817,7 @@ export namespace Server {
|
||||
"/find/file",
|
||||
describeRoute({
|
||||
summary: "Find files",
|
||||
description: "Search for files by name or pattern in the project directory.",
|
||||
description: "Search for files or directories by name or pattern in the project directory.",
|
||||
operationId: "find.files",
|
||||
responses: {
|
||||
200: {
|
||||
@@ -1813,15 +1835,20 @@ export namespace Server {
|
||||
z.object({
|
||||
query: z.string(),
|
||||
dirs: z.enum(["true", "false"]).optional(),
|
||||
type: z.enum(["file", "directory"]).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).optional(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const query = c.req.valid("query").query
|
||||
const dirs = c.req.valid("query").dirs
|
||||
const type = c.req.valid("query").type
|
||||
const limit = c.req.valid("query").limit
|
||||
const results = await File.search({
|
||||
query,
|
||||
limit: 10,
|
||||
limit: limit ?? 10,
|
||||
dirs: dirs !== "false",
|
||||
type,
|
||||
})
|
||||
return c.json(results)
|
||||
},
|
||||
@@ -2666,6 +2693,8 @@ export namespace Server {
|
||||
const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port)
|
||||
if (!server) throw new Error(`Failed to start server on port ${opts.port}`)
|
||||
|
||||
_url = server.url
|
||||
|
||||
const shouldPublishMDNS =
|
||||
opts.mdns &&
|
||||
server.port &&
|
||||
|
||||
@@ -74,6 +74,14 @@ export namespace LLM {
|
||||
}
|
||||
|
||||
const provider = await Provider.getProvider(input.model.providerID)
|
||||
const variant = input.model.variants && input.user.variant ? input.model.variants[input.user.variant] : undefined
|
||||
const options = pipe(
|
||||
ProviderTransform.options(input.model, input.sessionID, provider.options),
|
||||
mergeDeep(input.small ? ProviderTransform.smallOptions(input.model) : {}),
|
||||
mergeDeep(input.model.options),
|
||||
mergeDeep(input.agent.options),
|
||||
mergeDeep(variant && !variant.disabled ? variant : {}),
|
||||
)
|
||||
|
||||
const params = await Plugin.trigger(
|
||||
"chat.params",
|
||||
@@ -90,13 +98,7 @@ export namespace LLM {
|
||||
: undefined,
|
||||
topP: input.agent.topP ?? ProviderTransform.topP(input.model),
|
||||
topK: ProviderTransform.topK(input.model),
|
||||
options: pipe(
|
||||
{},
|
||||
mergeDeep(ProviderTransform.options(input.model, input.sessionID, provider.options)),
|
||||
input.small ? mergeDeep(ProviderTransform.smallOptions(input.model)) : mergeDeep({}),
|
||||
mergeDeep(input.model.options),
|
||||
mergeDeep(input.agent.options),
|
||||
),
|
||||
options,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { Message } from "./message"
|
||||
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
|
||||
import { Identifier } from "../id/id"
|
||||
import { LSP } from "../lsp"
|
||||
@@ -308,6 +306,7 @@ export namespace MessageV2 {
|
||||
}),
|
||||
system: z.string().optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
variant: z.string().optional(),
|
||||
}).meta({
|
||||
ref: "UserMessage",
|
||||
})
|
||||
@@ -539,7 +538,7 @@ export namespace MessageV2 {
|
||||
}
|
||||
}
|
||||
|
||||
return convertToModelMessages(result.filter((msg) => msg.parts.length > 0))
|
||||
return convertToModelMessages(result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")))
|
||||
}
|
||||
|
||||
export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
|
||||
|
||||
@@ -90,6 +90,7 @@ export namespace SessionPrompt {
|
||||
noReply: z.boolean().optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
system: z.string().optional(),
|
||||
variant: z.string().optional(),
|
||||
parts: z.array(
|
||||
z.discriminatedUnion("type", [
|
||||
MessageV2.TextPart.omit({
|
||||
@@ -727,6 +728,7 @@ export namespace SessionPrompt {
|
||||
agent: agent.name,
|
||||
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
|
||||
system: input.system,
|
||||
variant: input.variant,
|
||||
}
|
||||
|
||||
const parts = await Promise.all(
|
||||
@@ -1267,6 +1269,7 @@ export namespace SessionPrompt {
|
||||
model: z.string().optional(),
|
||||
arguments: z.string(),
|
||||
command: z.string(),
|
||||
variant: z.string().optional(),
|
||||
})
|
||||
export type CommandInput = z.infer<typeof CommandInput>
|
||||
const bashRegex = /!`([^`]+)`/g
|
||||
@@ -1369,6 +1372,7 @@ export namespace SessionPrompt {
|
||||
model,
|
||||
agent: agentName,
|
||||
parts,
|
||||
variant: input.variant,
|
||||
})) as MessageV2.WithParts
|
||||
|
||||
Bus.publish(Command.Event.Executed, {
|
||||
|
||||
@@ -240,7 +240,7 @@ You are producing plain text that will later be styled by the CLI. Follow these
|
||||
- Choose descriptive names that fit the content
|
||||
- Keep headers short (1–3 words) and in `**Title Case**`. Always start headers with `**` and end with `**`
|
||||
- Leave no blank line before the first bullet under a header.
|
||||
- Section headers should only be used where they genuinely improve scanability; avoid fragmenting the answer.
|
||||
- Section headers should only be used where they genuinely improve scannability; avoid fragmenting the answer.
|
||||
|
||||
**Bullets**
|
||||
|
||||
@@ -289,7 +289,7 @@ When referencing files in your response, make sure to include the relevant start
|
||||
- Don’t nest bullets or create deep hierarchies.
|
||||
- Don’t output ANSI escape codes directly — the CLI renderer applies them.
|
||||
- Don’t cram unrelated keywords into a single bullet; split for clarity.
|
||||
- Don’t let keyword lists run long — wrap or reformat for scanability.
|
||||
- Don’t let keyword lists run long — wrap or reformat for scannability.
|
||||
|
||||
Generally, ensure your final answers adapt their shape and depth to the request. For example, answers to code explanations should have a precise, structured explanation with code references that answer the question directly. For tasks with a simple implementation, lead with the outcome and supplement only with what’s needed for clarity. Larger changes can be presented as a logical walkthrough of your approach, grouping related steps, explaining rationale where it adds value, and highlighting next actions to accelerate the user. Your answers should provide the right level of detail while being easily scannable.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ is a critical violation. ZERO exceptions.
|
||||
|
||||
## Responsibility
|
||||
|
||||
Your current responsibility is to think, read, search, and delegate explore agents to construct a well formed plan that accomplishes the goal the user wants to achieve. Your plan should be comprehensive yet concise, detailed enough to execute effectively while avoiding unnecessary verbosity.
|
||||
Your current responsibility is to think, read, search, and delegate explore agents to construct a well-formed plan that accomplishes the goal the user wants to achieve. Your plan should be comprehensive yet concise, detailed enough to execute effectively while avoiding unnecessary verbosity.
|
||||
|
||||
Ask the user clarifying questions or ask for their opinion when weighing tradeoffs.
|
||||
|
||||
@@ -22,5 +22,5 @@ Ask the user clarifying questions or ask for their opinion when weighing tradeof
|
||||
|
||||
## Important
|
||||
|
||||
The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
|
||||
The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
|
||||
</system-reminder>
|
||||
|
||||
@@ -4,6 +4,9 @@ import { Instance } from "../project/instance"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { ConfigMarkdown } from "../config/markdown"
|
||||
import { Log } from "../util/log"
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { exists } from "fs/promises"
|
||||
|
||||
export namespace Skill {
|
||||
const log = Log.create({ service: "skill" })
|
||||
@@ -33,10 +36,9 @@ export namespace Skill {
|
||||
)
|
||||
|
||||
const OPENCODE_SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
|
||||
const CLAUDE_SKILL_GLOB = new Bun.Glob(".claude/skills/**/SKILL.md")
|
||||
const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")
|
||||
|
||||
export const state = Instance.state(async () => {
|
||||
const directories = await Config.directories()
|
||||
const skills: Record<string, Info> = {}
|
||||
|
||||
const addSkill = async (match: string) => {
|
||||
@@ -64,7 +66,34 @@ export namespace Skill {
|
||||
}
|
||||
}
|
||||
|
||||
for (const dir of directories) {
|
||||
// Scan .claude/skills/ directories (project-level)
|
||||
const claudeDirs = await Array.fromAsync(
|
||||
Filesystem.up({
|
||||
targets: [".claude"],
|
||||
start: Instance.directory,
|
||||
stop: Instance.worktree,
|
||||
}),
|
||||
)
|
||||
// Also include global ~/.claude/skills/
|
||||
const globalClaude = `${Global.Path.home}/.claude`
|
||||
if (await exists(globalClaude)) {
|
||||
claudeDirs.push(globalClaude)
|
||||
}
|
||||
|
||||
for (const dir of claudeDirs) {
|
||||
for await (const match of CLAUDE_SKILL_GLOB.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
})) {
|
||||
await addSkill(match)
|
||||
}
|
||||
}
|
||||
|
||||
// Scan .opencode/skill/ directories
|
||||
for (const dir of await Config.directories()) {
|
||||
for await (const match of OPENCODE_SKILL_GLOB.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
@@ -75,16 +104,6 @@ export namespace Skill {
|
||||
}
|
||||
}
|
||||
|
||||
for await (const match of CLAUDE_SKILL_GLOB.scan({
|
||||
cwd: Instance.worktree,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
})) {
|
||||
await addSkill(match)
|
||||
}
|
||||
|
||||
return skills
|
||||
})
|
||||
|
||||
|
||||
@@ -280,7 +280,7 @@ export const BashTool = Tool.define("bash", async () => {
|
||||
}
|
||||
|
||||
if (timedOut) {
|
||||
resultMetadata.push(`bash tool terminated commmand after exceeding timeout ${timeout} ms`)
|
||||
resultMetadata.push(`bash tool terminated command after exceeding timeout ${timeout} ms`)
|
||||
}
|
||||
|
||||
if (aborted) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Launch a new agent to handle complex, multi-step tasks autonomously.
|
||||
Launch a new agent to handle complex, multistep tasks autonomously.
|
||||
|
||||
Available agent types and the tools they have access to:
|
||||
{agents}
|
||||
|
||||
@@ -4,7 +4,7 @@ It also helps the user understand the progress of the task and overall progress
|
||||
## When to Use This Tool
|
||||
Use this tool proactively in these scenarios:
|
||||
|
||||
1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions
|
||||
1. Complex multistep tasks - When a task requires 3 or more distinct steps or actions
|
||||
2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
|
||||
3. User explicitly requests todo list - When the user directly asks you to use the todo list
|
||||
4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
|
||||
|
||||
@@ -15,7 +15,8 @@ export const WebFetchTool = Tool.define("webfetch", {
|
||||
url: z.string().describe("The URL to fetch content from"),
|
||||
format: z
|
||||
.enum(["text", "markdown", "html"])
|
||||
.describe("The format to return the content in (text, markdown, or html)"),
|
||||
.default("markdown")
|
||||
.describe("The format to return the content in (text, markdown, or html). Defaults to markdown."),
|
||||
timeout: z.number().describe("Optional timeout in seconds (max 120)").optional(),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
- Fetches content from a specified URL
|
||||
- Takes a URL and a prompt as input
|
||||
- Fetches the URL content, converts HTML to markdown
|
||||
- Returns the model's response about the content
|
||||
- Takes a URL and optional format as input
|
||||
- Fetches the URL content, converts to requested format (markdown by default)
|
||||
- Returns the content in the specified format
|
||||
- Use this tool when you need to retrieve and analyze web content
|
||||
|
||||
Usage notes:
|
||||
- IMPORTANT: if another tool is present that offers better web fetching capabilities, is more targeted to the task, or has fewer restrictions, prefer using that tool instead of this one.
|
||||
- The URL must be a fully-formed valid URL
|
||||
- HTTP URLs will be automatically upgraded to HTTPS
|
||||
- The prompt should describe what information you want to extract from the page
|
||||
- Format options: "markdown" (default), "text", or "html"
|
||||
- This tool is read-only and does not modify any files
|
||||
- Results may be summarized if the content is very large
|
||||
|
||||
@@ -11,6 +11,12 @@ await fs.mkdir(dir, { recursive: true })
|
||||
afterAll(() => {
|
||||
fsSync.rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
// Set test home directory to isolate tests from user's actual home directory
|
||||
// This prevents tests from picking up real user configs/skills from ~/.claude/skills
|
||||
const testHome = path.join(dir, "home")
|
||||
await fs.mkdir(testHome, { recursive: true })
|
||||
process.env["OPENCODE_TEST_HOME"] = testHome
|
||||
|
||||
process.env["XDG_DATA_HOME"] = path.join(dir, "share")
|
||||
process.env["XDG_CACHE_HOME"] = path.join(dir, "cache")
|
||||
process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { test, expect } from "bun:test"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { SystemPrompt } from "../../src/session/system"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
|
||||
async function createGlobalSkill(homeDir: string) {
|
||||
const skillDir = path.join(homeDir, ".claude", "skills", "global-test-skill")
|
||||
await fs.mkdir(skillDir, { recursive: true })
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: global-test-skill
|
||||
description: A global skill from ~/.claude/skills for testing.
|
||||
---
|
||||
|
||||
# Global Test Skill
|
||||
|
||||
This skill is loaded from the global home directory.
|
||||
`,
|
||||
)
|
||||
}
|
||||
|
||||
test("discovers skills from .opencode/skill/ directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
@@ -30,9 +47,10 @@ Instructions here.
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("test-skill")
|
||||
expect(skills[0].description).toBe("A test skill for verification.")
|
||||
expect(skills[0].location).toContain("skill/test-skill/SKILL.md")
|
||||
const testSkill = skills.find((s) => s.name === "test-skill")
|
||||
expect(testSkill).toBeDefined()
|
||||
expect(testSkill!.description).toBe("A test skill for verification.")
|
||||
expect(testSkill!.location).toContain("skill/test-skill/SKILL.md")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -41,15 +59,26 @@ test("discovers multiple skills from .opencode/skill/ directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".opencode", "skill", "my-skill")
|
||||
const skillDir1 = path.join(dir, ".opencode", "skill", "skill-one")
|
||||
const skillDir2 = path.join(dir, ".opencode", "skill", "skill-two")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
path.join(skillDir1, "SKILL.md"),
|
||||
`---
|
||||
name: my-skill
|
||||
description: Another test skill.
|
||||
name: skill-one
|
||||
description: First test skill.
|
||||
---
|
||||
|
||||
# My Skill
|
||||
# Skill One
|
||||
`,
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(skillDir2, "SKILL.md"),
|
||||
`---
|
||||
name: skill-two
|
||||
description: Second test skill.
|
||||
---
|
||||
|
||||
# Skill Two
|
||||
`,
|
||||
)
|
||||
},
|
||||
@@ -59,8 +88,9 @@ description: Another test skill.
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("my-skill")
|
||||
expect(skills.length).toBe(2)
|
||||
expect(skills.find((s) => s.name === "skill-one")).toBeDefined()
|
||||
expect(skills.find((s) => s.name === "skill-two")).toBeDefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -89,18 +119,6 @@ Just some content without YAML frontmatter.
|
||||
})
|
||||
})
|
||||
|
||||
test("returns empty array when no skills exist", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills).toEqual([])
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("discovers skills from .claude/skills/ directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
@@ -124,8 +142,44 @@ description: A skill in the .claude/skills directory.
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("claude-skill")
|
||||
expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||
const claudeSkill = skills.find((s) => s.name === "claude-skill")
|
||||
expect(claudeSkill).toBeDefined()
|
||||
expect(claudeSkill!.location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("discovers global skills from ~/.claude/skills/ directory", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
const originalHome = process.env.OPENCODE_TEST_HOME
|
||||
process.env.OPENCODE_TEST_HOME = tmp.path
|
||||
|
||||
try {
|
||||
await createGlobalSkill(tmp.path)
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("global-test-skill")
|
||||
expect(skills[0].description).toBe("A global skill from ~/.claude/skills for testing.")
|
||||
expect(skills[0].location).toContain(".claude/skills/global-test-skill/SKILL.md")
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
process.env.OPENCODE_TEST_HOME = originalHome
|
||||
}
|
||||
})
|
||||
|
||||
test("returns empty array when no skills exist", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills).toEqual([])
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
@@ -24,4 +24,4 @@
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export type PluginInput = {
|
||||
project: Project
|
||||
directory: string
|
||||
worktree: string
|
||||
serverUrl: URL
|
||||
$: BunShell
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.207",
|
||||
"version": "1.0.217",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
@@ -29,4 +29,4 @@
|
||||
"publishConfig": {
|
||||
"directory": "dist"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1228,6 +1228,7 @@ export class Session extends HeyApiClient {
|
||||
[key: string]: boolean
|
||||
}
|
||||
system?: string
|
||||
variant?: string
|
||||
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
@@ -1245,6 +1246,7 @@ export class Session extends HeyApiClient {
|
||||
{ in: "body", key: "noReply" },
|
||||
{ in: "body", key: "tools" },
|
||||
{ in: "body", key: "system" },
|
||||
{ in: "body", key: "variant" },
|
||||
{ in: "body", key: "parts" },
|
||||
],
|
||||
},
|
||||
@@ -1314,6 +1316,7 @@ export class Session extends HeyApiClient {
|
||||
[key: string]: boolean
|
||||
}
|
||||
system?: string
|
||||
variant?: string
|
||||
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
@@ -1331,6 +1334,7 @@ export class Session extends HeyApiClient {
|
||||
{ in: "body", key: "noReply" },
|
||||
{ in: "body", key: "tools" },
|
||||
{ in: "body", key: "system" },
|
||||
{ in: "body", key: "variant" },
|
||||
{ in: "body", key: "parts" },
|
||||
],
|
||||
},
|
||||
@@ -1362,6 +1366,7 @@ export class Session extends HeyApiClient {
|
||||
model?: string
|
||||
arguments?: string
|
||||
command?: string
|
||||
variant?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
@@ -1377,6 +1382,7 @@ export class Session extends HeyApiClient {
|
||||
{ in: "body", key: "model" },
|
||||
{ in: "body", key: "arguments" },
|
||||
{ in: "body", key: "command" },
|
||||
{ in: "body", key: "variant" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -1823,13 +1829,15 @@ export class Find extends HeyApiClient {
|
||||
/**
|
||||
* Find files
|
||||
*
|
||||
* Search for files by name or pattern in the project directory.
|
||||
* Search for files or directories by name or pattern in the project directory.
|
||||
*/
|
||||
public files<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
directory?: string
|
||||
query: string
|
||||
dirs?: "true" | "false"
|
||||
type?: "file" | "directory"
|
||||
limit?: number
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
@@ -1841,6 +1849,8 @@ export class Find extends HeyApiClient {
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "query" },
|
||||
{ in: "query", key: "dirs" },
|
||||
{ in: "query", key: "type" },
|
||||
{ in: "query", key: "limit" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user